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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-07-19 17:19:26 +02:00
parent 88fb798cca
commit 5197f637a7
3 changed files with 31 additions and 10 deletions
+9 -5
View File
@@ -201,6 +201,7 @@ type clipKey struct {
bounds f32.Rectangle bounds f32.Rectangle
stroke clip.StrokeStyle stroke clip.StrokeStyle
relTrans f32.Affine2D relTrans f32.Affine2D
pathHash uint64
} }
// paintKey completely defines a paint operation. It is suitable for hashing and // 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] 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. // Rectangle clip regions.
if len(path) == 0 { if len(path) == 0 {
// If the rectangular clip region contains a previous path it can be discarded. // 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, bounds: bounds,
relTrans: state.relTrans, relTrans: state.relTrans,
stroke: stroke, stroke: stroke,
pathHash: hash,
}, },
}) })
state.intersect = state.intersect.Intersect(absBounds) state.intersect = state.intersect.Intersect(absBounds)
@@ -1369,11 +1371,12 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
pathData struct { pathData struct {
data []byte data []byte
key ops.Key key ops.Key
hash uint64
} }
str clip.StrokeStyle str clip.StrokeStyle
) )
c.save(opconst.InitialStateID, state) 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() { for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch opconst.OpType(encOp.Data[0]) { switch opconst.OpType(encOp.Data[0]) {
case opconst.TypeProfile: case opconst.TypeProfile:
@@ -1385,16 +1388,18 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
case opconst.TypeStroke: case opconst.TypeStroke:
str = decodeStrokeOp(encOp.Data) str = decodeStrokeOp(encOp.Data)
case opconst.TypePath: case opconst.TypePath:
hash := bo.Uint64(encOp.Data[1:])
encOp, ok = r.Decode() encOp, ok = r.Decode()
if !ok { if !ok {
panic("unexpected end of path operation") panic("unexpected end of path operation")
} }
pathData.data = encOp.Data[opconst.TypeAuxLen:] pathData.data = encOp.Data[opconst.TypeAuxLen:]
pathData.key = encOp.Key pathData.key = encOp.Key
pathData.hash = hash
case opconst.TypeClip: case opconst.TypeClip:
var op clipOp var op clipOp
op.decode(encOp.Data) 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 pathData.data = nil
str = clip.StrokeStyle{} str = clip.StrokeStyle{}
case opconst.TypeColor: case opconst.TypeColor:
@@ -1415,7 +1420,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
if paintState.matType == materialTexture { if paintState.matType == materialTexture {
// Clip to the bounds of the image, to hide other images in the atlas. // Clip to the bounds of the image, to hide other images in the atlas.
bounds := paintState.image.src.Bounds() 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() { if paintState.intersect.Empty() {
break break
@@ -1504,7 +1509,6 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
func (c *collector) hashOp(op paintOp) uint64 { func (c *collector) hashOp(op paintOp) uint64 {
c.hasher.Reset() c.hasher.Reset()
for _, cl := range op.clipStack { for _, cl := range op.clipStack {
c.hasher.Write(cl.path)
k := cl.state k := cl.state
keyBytes := (*[unsafe.Sizeof(k)]byte)(unsafe.Pointer(unsafe.Pointer(&k))) keyBytes := (*[unsafe.Sizeof(k)]byte)(unsafe.Pointer(unsafe.Pointer(&k)))
c.hasher.Write(keyBytes[:]) c.hasher.Write(keyBytes[:])
+1 -1
View File
@@ -59,7 +59,7 @@ const (
TypeClipLen = 1 + 4*4 + 1 TypeClipLen = 1 + 4*4 + 1
TypeProfileLen = 1 TypeProfileLen = 1
TypeCursorLen = 1 + 1 TypeCursorLen = 1 + 1
TypePathLen = 1 TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4 TypeStrokeLen = 1 + 4
) )
+21 -4
View File
@@ -4,6 +4,7 @@ package clip
import ( import (
"encoding/binary" "encoding/binary"
"hash/maphash"
"image" "image"
"math" "math"
@@ -15,6 +16,12 @@ import (
"gioui.org/op" "gioui.org/op"
) )
var pathSeed maphash.Seed
func init() {
pathSeed = maphash.MakeSeed()
}
// Op represents a clip area. Op intersects the current clip area with // Op represents a clip area. Op intersects the current clip area with
// itself. // itself.
type Op struct { type Op struct {
@@ -38,9 +45,11 @@ func (p Op) Add(o *op.Ops) {
outline = true outline = true
} }
bo := binary.LittleEndian
if path.hasSegments { if path.hasSegments {
data := o.Write(opconst.TypePathLen) data := o.Write(opconst.TypePathLen)
data[0] = byte(opconst.TypePath) data[0] = byte(opconst.TypePath)
bo.PutUint64(data[1:], path.hash)
path.spec.Add(o) path.spec.Add(o)
} }
@@ -60,7 +69,6 @@ func (p Op) Add(o *op.Ops) {
data := o.Write(opconst.TypeClipLen) data := o.Write(opconst.TypeClipLen)
data[0] = byte(opconst.TypeClip) data[0] = byte(opconst.TypeClip)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(bounds.Min.X)) bo.PutUint32(data[1:], uint32(bounds.Min.X))
bo.PutUint32(data[5:], uint32(bounds.Min.Y)) bo.PutUint32(data[5:], uint32(bounds.Min.Y))
bo.PutUint32(data[9:], uint32(bounds.Max.X)) 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 tracks whether there are any segments in the path.
hasSegments bool hasSegments bool
bounds image.Rectangle bounds image.Rectangle
hash uint64
} }
// Path constructs a Op clip path described by lines and // Path constructs a Op clip path described by lines and
@@ -156,6 +165,7 @@ type Path struct {
start f32.Point start f32.Point
hasSegments bool hasSegments bool
bounds f32.Rectangle bounds f32.Rectangle
hash maphash.Hash
} }
// Pos returns the current pen position. // 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. // Begin the path, storing the path data and final Op into ops.
func (p *Path) Begin(ops *op.Ops) { func (p *Path) Begin(ops *op.Ops) {
p.hash.SetSeed(pathSeed)
p.ops = ops p.ops = ops
p.macro = op.Record(ops) p.macro = op.Record(ops)
// Write the TypeAux opcode // Write the TypeAux opcode
@@ -178,6 +189,7 @@ func (p *Path) End() PathSpec {
open: p.open || p.pen != p.start, open: p.open || p.pen != p.start,
hasSegments: p.hasSegments, hasSegments: p.hasSegments,
bounds: boundRectF(p.bounds), 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) data := p.ops.Write(scene.CommandSize + 4)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour)) 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.pen = to
p.expand(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) { func (p *Path) expand(pt f32.Point) {
if !p.hasSegments { if !p.hasSegments {
p.hasSegments = true p.hasSegments = true
@@ -274,7 +291,7 @@ func (p *Path) QuadTo(ctrl, to f32.Point) {
data := p.ops.Write(scene.CommandSize + 4) data := p.ops.Write(scene.CommandSize + 4)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour)) 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.pen = to
p.expand(ctrl) p.expand(ctrl)
p.expand(to) p.expand(to)
@@ -315,7 +332,7 @@ func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
data := p.ops.Write(scene.CommandSize + 4) data := p.ops.Write(scene.CommandSize + 4)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour)) 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.pen = to
p.expand(ctrl0) p.expand(ctrl0)
p.expand(ctrl1) p.expand(ctrl1)