From 5197f637a79f8b94cef2e71f167f8df67938bb73 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 19 Jul 2021 17:19:26 +0200 Subject: [PATCH] gpu: [compute] compute and store clipping path hashes during construction The hash of the clipping paths that affect drawing operations are computed and used to quickly determine that two operations are not equal, the most likely outcome of a comparison. However, for paths that are constructed once and cached computing the hash at every frame is wasteful. This is especially true for text, which is both cached and also among the largest paths in a frame. This change moves the hashing to op/clip.Path construction time, and stores the hash in the ops list so it won't be re-computed at every use. Signed-off-by: Elias Naur --- gpu/compute.go | 14 +++++++++----- internal/opconst/ops.go | 2 +- op/clip/clip.go | 25 +++++++++++++++++++++---- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/gpu/compute.go b/gpu/compute.go index de3be0f6..a80c217c 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -201,6 +201,7 @@ type clipKey struct { bounds f32.Rectangle stroke clip.StrokeStyle relTrans f32.Affine2D + pathHash uint64 } // paintKey completely defines a paint operation. It is suitable for hashing and @@ -1321,7 +1322,7 @@ func (c *opsCollector) reset() { c.layers = c.layers[:0] } -func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, stroke clip.StrokeStyle) { +func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, hash uint64, stroke clip.StrokeStyle) { // Rectangle clip regions. if len(path) == 0 { // If the rectangular clip region contains a previous path it can be discarded. @@ -1348,6 +1349,7 @@ func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, bounds: bounds, relTrans: state.relTrans, stroke: stroke, + pathHash: hash, }, }) state.intersect = state.intersect.Intersect(absBounds) @@ -1369,11 +1371,12 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { pathData struct { data []byte key ops.Key + hash uint64 } str clip.StrokeStyle ) c.save(opconst.InitialStateID, state) - c.addClip(&state, fview, fview, nil, ops.Key{}, clip.StrokeStyle{}) + c.addClip(&state, fview, fview, nil, ops.Key{}, 0, clip.StrokeStyle{}) for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch opconst.OpType(encOp.Data[0]) { case opconst.TypeProfile: @@ -1385,16 +1388,18 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) { case opconst.TypeStroke: str = decodeStrokeOp(encOp.Data) case opconst.TypePath: + hash := bo.Uint64(encOp.Data[1:]) encOp, ok = r.Decode() if !ok { panic("unexpected end of path operation") } pathData.data = encOp.Data[opconst.TypeAuxLen:] pathData.key = encOp.Key + pathData.hash = hash case opconst.TypeClip: var op clipOp op.decode(encOp.Data) - c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, str) + c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, pathData.hash, str) pathData.data = nil str = clip.StrokeStyle{} case opconst.TypeColor: @@ -1415,7 +1420,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, ops.Key{}, clip.StrokeStyle{}) + c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, 0, clip.StrokeStyle{}) } if paintState.intersect.Empty() { break @@ -1504,7 +1509,6 @@ 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.path) k := cl.state keyBytes := (*[unsafe.Sizeof(k)]byte)(unsafe.Pointer(unsafe.Pointer(&k))) c.hasher.Write(keyBytes[:]) diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 13c4ddf2..1fb1d825 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -59,7 +59,7 @@ const ( TypeClipLen = 1 + 4*4 + 1 TypeProfileLen = 1 TypeCursorLen = 1 + 1 - TypePathLen = 1 + TypePathLen = 8 + 1 TypeStrokeLen = 1 + 4 ) diff --git a/op/clip/clip.go b/op/clip/clip.go index f9d094ee..aec3a536 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -4,6 +4,7 @@ package clip import ( "encoding/binary" + "hash/maphash" "image" "math" @@ -15,6 +16,12 @@ import ( "gioui.org/op" ) +var pathSeed maphash.Seed + +func init() { + pathSeed = maphash.MakeSeed() +} + // Op represents a clip area. Op intersects the current clip area with // itself. type Op struct { @@ -38,9 +45,11 @@ func (p Op) Add(o *op.Ops) { outline = true } + bo := binary.LittleEndian if path.hasSegments { data := o.Write(opconst.TypePathLen) data[0] = byte(opconst.TypePath) + bo.PutUint64(data[1:], path.hash) path.spec.Add(o) } @@ -60,7 +69,6 @@ func (p Op) Add(o *op.Ops) { data := o.Write(opconst.TypeClipLen) data[0] = byte(opconst.TypeClip) - bo := binary.LittleEndian bo.PutUint32(data[1:], uint32(bounds.Min.X)) bo.PutUint32(data[5:], uint32(bounds.Min.Y)) bo.PutUint32(data[9:], uint32(bounds.Max.X)) @@ -138,6 +146,7 @@ type PathSpec struct { // hasSegments tracks whether there are any segments in the path. hasSegments bool bounds image.Rectangle + hash uint64 } // Path constructs a Op clip path described by lines and @@ -156,6 +165,7 @@ type Path struct { start f32.Point hasSegments bool bounds f32.Rectangle + hash maphash.Hash } // Pos returns the current pen position. @@ -163,6 +173,7 @@ func (p *Path) Pos() f32.Point { return p.pen } // Begin the path, storing the path data and final Op into ops. func (p *Path) Begin(ops *op.Ops) { + p.hash.SetSeed(pathSeed) p.ops = ops p.macro = op.Record(ops) // Write the TypeAux opcode @@ -178,6 +189,7 @@ func (p *Path) End() PathSpec { open: p.open || p.pen != p.start, hasSegments: p.hasSegments, bounds: boundRectF(p.bounds), + hash: p.hash.Sum64(), } } @@ -211,11 +223,16 @@ func (p *Path) LineTo(to f32.Point) { data := p.ops.Write(scene.CommandSize + 4) bo := binary.LittleEndian bo.PutUint32(data[0:], uint32(p.contour)) - ops.EncodeCommand(data[4:], scene.Line(p.pen, to)) + p.cmd(data[4:], scene.Line(p.pen, to)) p.pen = to p.expand(to) } +func (p *Path) cmd(data []byte, c scene.Command) { + ops.EncodeCommand(data, c) + p.hash.Write(data) +} + func (p *Path) expand(pt f32.Point) { if !p.hasSegments { p.hasSegments = true @@ -274,7 +291,7 @@ func (p *Path) QuadTo(ctrl, to f32.Point) { data := p.ops.Write(scene.CommandSize + 4) bo := binary.LittleEndian bo.PutUint32(data[0:], uint32(p.contour)) - ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to)) + p.cmd(data[4:], scene.Quad(p.pen, ctrl, to)) p.pen = to p.expand(ctrl) p.expand(to) @@ -315,7 +332,7 @@ func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { data := p.ops.Write(scene.CommandSize + 4) bo := binary.LittleEndian bo.PutUint32(data[0:], uint32(p.contour)) - ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) + p.cmd(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) p.pen = to p.expand(ctrl0) p.expand(ctrl1)