gpu: [compute] re-use layers that differ only in integer offsets

To re-use drawing operations common to two layers, every operation must
exactly match, including their transformations. However, layers that
differ only by an integer offset can be re-used because rendering does
not depend on the absolute integer offset. This is important in the very
common case of scrolling otherwise static UI content.

This change separate the integer offset from drawing operations and
relaxes the layer cache to match layers that differ only in integer
offsets.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-07-19 15:08:34 +02:00
parent 318ddd0644
commit 4ab872e36a
+30 -15
View File
@@ -169,6 +169,7 @@ type opsCollector struct {
type paintOp struct { type paintOp struct {
clipStack []clipCmd clipStack []clipCmd
offset image.Point
state paintKey state paintKey
intersect f32.Rectangle intersect f32.Rectangle
hash uint64 hash uint64
@@ -1383,7 +1384,6 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
panic("unexpected end of path operation") panic("unexpected end of path operation")
} }
pathData = encOp.Data[opconst.TypeAuxLen:] pathData = encOp.Data[opconst.TypeAuxLen:]
case opconst.TypeClip: case opconst.TypeClip:
var op clipOp var op clipOp
op.decode(encOp.Data) op.decode(encOp.Data)
@@ -1464,21 +1464,31 @@ func (c *collector) collect(root *op.Ops, viewport image.Point) {
// For each clip, cull rectangular clip regions that contain its // For each clip, cull rectangular clip regions that contain its
// (transformed) bounds. addClip already handled the converse case. // (transformed) bounds. addClip already handled the converse case.
// TODO: do better than O(n²) to efficiently deal with deep stacks. // TODO: do better than O(n²) to efficiently deal with deep stacks.
for i := 0; i < len(op.clipStack)-1; i++ { for j := 0; j < len(op.clipStack)-1; j++ {
cl := op.clipStack[i] cl := op.clipStack[j]
p := cl.state p := cl.state
r := transformBounds(p.relTrans, p.bounds) r := transformBounds(p.relTrans, p.bounds)
for j := i + 1; j < len(op.clipStack); j++ { for k := j + 1; k < len(op.clipStack); k++ {
cl2 := op.clipStack[j] cl2 := op.clipStack[k]
p2 := cl2.state p2 := cl2.state
if len(cl2.pathVerts) == 0 && r.In(cl2.state.bounds) { if len(cl2.pathVerts) == 0 && r.In(cl2.state.bounds) {
op.clipStack = append(op.clipStack[:j], op.clipStack[j+1:]...) op.clipStack = append(op.clipStack[:k], op.clipStack[k+1:]...)
j-- k--
op.clipStack[j].state.relTrans = p2.relTrans.Mul(op.clipStack[j].state.relTrans) op.clipStack[k].state.relTrans = p2.relTrans.Mul(op.clipStack[k].state.relTrans)
} }
r = transformRect(p2.relTrans, r) r = transformRect(p2.relTrans, r)
} }
} }
// Separate the integer offset from the first transform. Two ops that differ
// only in integer offsets may share backing storage.
if len(op.clipStack) > 0 {
c := &op.clipStack[len(op.clipStack)-1]
t := c.state.relTrans
t, off := separateTransform(t)
c.state.relTrans = t
op.offset = off
op.state.t = op.state.t.Offset(layout.FPt(off.Mul(-1)))
}
op.hash = c.hashOp(*op) op.hash = c.hashOp(*op)
} }
} }
@@ -1561,12 +1571,13 @@ outer:
match := prev[first.index:] match := prev[first.index:]
// Potential match found. Now find longest matching sequence. // Potential match found. Now find longest matching sequence.
end := 0 end := 0
layer := -1 layer := match[0].layer
off := match[0].offset.Sub(ops[0].offset)
for end < len(match) && end < len(ops) { for end < len(match) && end < len(ops) {
m := match[end] m := match[end]
o := ops[end] o := ops[end]
// End on layer boundaries. // End on layer boundaries.
if layer != -1 && m.layer != layer { if m.layer != layer {
break break
} }
// End layer when the next op doesn't match. // End layer when the next op doesn't match.
@@ -1578,10 +1589,9 @@ outer:
} }
break break
} }
if !opEqual(m, o) { if !opEqual(off, m, o) {
break break
} }
layer = m.layer
end++ end++
} }
if end > longest { if end > longest {
@@ -1605,13 +1615,16 @@ func searchOp(order []hashIndex, hash uint64) int {
return lo return lo
} }
func opEqual(o1 paintOp, o2 paintOp) bool { func opEqual(off image.Point, o1 paintOp, o2 paintOp) bool {
if len(o1.clipStack) != len(o2.clipStack) { if len(o1.clipStack) != len(o2.clipStack) {
return false return false
} }
if o1.state != o2.state { if o1.state != o2.state {
return false return false
} }
if o1.offset.Sub(o2.offset) != off {
return false
}
for i, cl1 := range o1.clipStack { for i, cl1 := range o1.clipStack {
cl2 := o2.clipStack[i] cl2 := o2.clipStack[i]
if len(cl1.pathVerts) != len(cl2.pathVerts) { if len(cl1.pathVerts) != len(cl2.pathVerts) {
@@ -1648,7 +1661,9 @@ func encodeOp(viewport image.Point, absOff f32.Point, enc *encoder, texOps *[]te
} }
fillMode := scene.FillModeNonzero fillMode := scene.FillModeNonzero
var inv f32.Affine2D opOff := layout.FPt(op.offset)
inv := f32.Affine2D{}.Offset(opOff)
enc.transform(inv)
for i := len(op.clipStack) - 1; i >= 0; i-- { for i := len(op.clipStack) - 1; i >= 0; i-- {
cl := op.clipStack[i] cl := op.clipStack[i]
if str := cl.state.stroke; str.Width > 0 { if str := cl.state.stroke; str.Width > 0 {
@@ -1681,7 +1696,7 @@ func encodeOp(viewport image.Point, absOff f32.Point, enc *encoder, texOps *[]te
idx := enc.fillImage(0) idx := enc.fillImage(0)
// Separate integer offset from transformation. TextureOps that have identical transforms // Separate integer offset from transformation. TextureOps that have identical transforms
// except for their integer offsets can share a transformed image. // except for their integer offsets can share a transformed image.
t := op.state.t.Offset(absOff) t := op.state.t.Offset(absOff.Add(opOff))
t, off := separateTransform(t) t, off := separateTransform(t)
*texOps = append(*texOps, textureOp{ *texOps = append(*texOps, textureOp{
sceneIdx: idx, sceneIdx: idx,