From 88fb798ccaed8a1c57f915281a18d60e34c0d2c5 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 19 Jul 2021 17:00:06 +0200 Subject: [PATCH] gpu: [compute] speed up path comparisons with op keys To re-use previously cached layers, the compute renderer must know whether two drawing operations are equal. In the case two operations are not equal, a fast hash comparison will most likely fail. In the case two equal operations with complicated clipping paths, the comparison of the path data is expensive. This change adds support for fast ops.Key comparisons, where two paths are equal if their ops.Key are. This is an optimization that kicks in for text rendering, where glyph clipping shapes are re-used across frames. Signed-off-by: Elias Naur --- gpu/compute.go | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/gpu/compute.go b/gpu/compute.go index 28f93e8e..de3be0f6 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -182,7 +182,8 @@ type clipCmd struct { // union of the bounds of the operations that are clipped. union f32.Rectangle state clipKey - pathVerts []byte + path []byte + pathKey ops.Key absBounds f32.Rectangle } @@ -222,7 +223,8 @@ type paintKey struct { type clipState struct { absBounds f32.Rectangle parent *clipState - pathVerts []byte + path []byte + pathKey ops.Key clipKey } @@ -1319,7 +1321,7 @@ func (c *opsCollector) reset() { c.layers = c.layers[:0] } -func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, stroke clip.StrokeStyle) { +func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, stroke clip.StrokeStyle) { // Rectangle clip regions. if len(path) == 0 { // If the rectangular clip region contains a previous path it can be discarded. @@ -1340,7 +1342,8 @@ func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, c.clipStates = append(c.clipStates, clipState{ parent: state.clip, absBounds: absBounds, - pathVerts: path, + path: path, + pathKey: key, clipKey: clipKey{ bounds: bounds, relTrans: state.relTrans, @@ -1363,11 +1366,14 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { } r := &c.reader var ( - pathData []byte - str clip.StrokeStyle + pathData struct { + data []byte + key ops.Key + } + str clip.StrokeStyle ) c.save(opconst.InitialStateID, state) - c.addClip(&state, fview, fview, nil, clip.StrokeStyle{}) + c.addClip(&state, fview, fview, nil, ops.Key{}, clip.StrokeStyle{}) for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch opconst.OpType(encOp.Data[0]) { case opconst.TypeProfile: @@ -1383,12 +1389,13 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { if !ok { panic("unexpected end of path operation") } - pathData = encOp.Data[opconst.TypeAuxLen:] + pathData.data = encOp.Data[opconst.TypeAuxLen:] + pathData.key = encOp.Key case opconst.TypeClip: var op clipOp op.decode(encOp.Data) - c.addClip(&state, fview, op.bounds, pathData, str) - pathData = nil + c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, str) + pathData.data = nil str = clip.StrokeStyle{} case opconst.TypeColor: state.matType = materialColor @@ -1408,7 +1415,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { if paintState.matType == materialTexture { // Clip to the bounds of the image, to hide other images in the atlas. bounds := paintState.image.src.Bounds() - c.addClip(&paintState, fview, layout.FRect(bounds), nil, clip.StrokeStyle{}) + c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, clip.StrokeStyle{}) } if paintState.intersect.Empty() { break @@ -1429,12 +1436,13 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { startIdx := len(c.frame.clipCmds) for p != nil { idx := len(c.frame.paths) - c.frame.paths = append(c.frame.paths, make([]byte, len(p.pathVerts))...) + c.frame.paths = append(c.frame.paths, make([]byte, len(p.path))...) path := c.frame.paths[idx:] - copy(path, p.pathVerts) + copy(path, p.path) c.frame.clipCmds = append(c.frame.clipCmds, clipCmd{ state: p.clipKey, - pathVerts: path, + path: path, + pathKey: p.pathKey, absBounds: p.absBounds, }) p = p.parent @@ -1471,7 +1479,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { for k := j + 1; k < len(op.clipStack); k++ { cl2 := op.clipStack[k] p2 := cl2.state - if len(cl2.pathVerts) == 0 && r.In(cl2.state.bounds) { + if len(cl2.path) == 0 && r.In(cl2.state.bounds) { op.clipStack = append(op.clipStack[:k], op.clipStack[k+1:]...) k-- op.clipStack[k].state.relTrans = p2.relTrans.Mul(op.clipStack[k].state.relTrans) @@ -1496,7 +1504,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { func (c *collector) hashOp(op paintOp) uint64 { c.hasher.Reset() for _, cl := range op.clipStack { - c.hasher.Write(cl.pathVerts) + c.hasher.Write(cl.path) k := cl.state keyBytes := (*[unsafe.Sizeof(k)]byte)(unsafe.Pointer(unsafe.Pointer(&k))) c.hasher.Write(keyBytes[:]) @@ -1627,13 +1635,13 @@ func opEqual(off image.Point, o1 paintOp, o2 paintOp) bool { } for i, cl1 := range o1.clipStack { cl2 := o2.clipStack[i] - if len(cl1.pathVerts) != len(cl2.pathVerts) { + if len(cl1.path) != len(cl2.path) { return false } if cl1.state != cl2.state { return false } - if !bytes.Equal(cl1.pathVerts, cl2.pathVerts) { + if cl1.pathKey != cl2.pathKey && !bytes.Equal(cl1.path, cl2.path) { return false } } @@ -1676,10 +1684,10 @@ func encodeOp(viewport image.Point, absOff f32.Point, enc *encoder, texOps *[]te } enc.transform(cl.state.relTrans) inv = inv.Mul(cl.state.relTrans) - if len(cl.pathVerts) == 0 { + if len(cl.path) == 0 { enc.rect(cl.state.bounds) } else { - enc.encodePath(cl.pathVerts) + enc.encodePath(cl.path) } if i != 0 { enc.beginClip(cl.union.Add(absOff))