gpu: build gpu data also when outside window

This commit fixes a bug where a shape first drawn off-screen
and later moved into screen would not display properly. Since we
cache CPU operations (vertex transform / construction) we need to
upload the constructed data to the GPU after it was build, or a later
frame will use non-initialized memory for it's draw call.

Note that this fix removes the optimization of not processing clip
paths outside the screen - but this is assumed to be uncommon except
when it is first drawn off screen to later be moved in (e.g. in a scrolling list)
in which case we do want to upload the data and prepare for that later
call.

This commit also does a few minor clean ups and adds a test case.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
This commit is contained in:
Viktor
2020-06-20 23:30:01 +02:00
committed by Elias Naur
parent 901478d102
commit cee045bf92
7 changed files with 108 additions and 14 deletions
+3 -2
View File
@@ -28,8 +28,9 @@ type opCache struct {
type opCacheValue struct {
data pathData
bounds f32.Rectangle
key ops.Key
keep bool
// the fields below are handled by opCache
key ops.Key
keep bool
}
func newResourceCache() *resourceCache {
+2 -5
View File
@@ -706,9 +706,9 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, boun
// split a transform into two parts, one which is pur offset and the
// other representing the scaling, shearing and rotation part
func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
sx, hx, ox, sy, hy, oy := t.Elems()
sx, hx, ox, hy, sy, oy := t.Elems()
offset = f32.Point{X: ox, Y: oy}
srs = f32.NewAffine2D(sx, hx, 0, sy, hy, 0)
srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0)
return
}
@@ -752,9 +752,6 @@ loop:
auxKey.SetTransform(trans)
}
state.clip = state.clip.Intersect(op.bounds.Add(off))
if state.clip.Empty() {
continue
}
d.addClipPath(&state, aux, auxKey, op.bounds, off)
aux = nil
auxKey = ops.Key{}
+2 -2
View File
@@ -62,11 +62,11 @@ func (r *Reader) Reset(ops *op.Ops) {
}
func (k Key) SetTransform(t f32.Affine2D) Key {
sx, hx, sy, hy, _, _ := t.Elems()
sx, hx, _, hy, sy, _ := t.Elems()
k.sx = sx
k.hx = hx
k.sy = sy
k.hy = hy
k.sy = sy
return k
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

+43 -3
View File
@@ -15,7 +15,7 @@ func TestTransformMacro(t *testing.T) {
// testcase resulting from original bug when rendering layout.Stacked
// pre-build the text
c := createText()
c := constSqPath()
run(t, func(o *op.Ops) {
@@ -101,7 +101,7 @@ func TestNoClipFromPaint(t *testing.T) {
})
}
func createText() op.CallOp {
func constSqPath() op.CallOp {
innerOps := new(op.Ops)
m := op.Record(innerOps)
builder := clip.Path{}
@@ -115,6 +115,14 @@ func createText() op.CallOp {
return m.Stop()
}
func constSqCirc() op.CallOp {
innerOps := new(op.Ops)
m := op.Record(innerOps)
clip.Rect{Rect: f32.Rect(0, 0, 40, 40),
NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps)
return m.Stop()
}
func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
r1 := op.Record(ops)
text.Add(ops)
@@ -123,7 +131,7 @@ func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
}
func TestReuseStencil(t *testing.T) {
txt := createText()
txt := constSqPath()
run(t, func(ops *op.Ops) {
c1 := drawChild(ops, txt)
c2 := drawChild(ops, txt)
@@ -142,3 +150,35 @@ func TestReuseStencil(t *testing.T) {
r.expect(5, 55, colornames.Black)
})
}
func TestBuildOffscreen(t *testing.T) {
// Check that something we in one frame build outside the screen
// still is rendered correctly if moved into the screen in a later
// frame.
txt := constSqCirc()
draw := func(off float32, o *op.Ops) {
s := op.Push(o)
op.TransformOp{}.Offset(f32.Pt(0, off)).Add(o)
txt.Add(o)
paint.PaintOp{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
s.Pop()
}
multiRun(t,
frame(
func(ops *op.Ops) {
draw(-100, ops)
}, func(r result) {
r.expect(5, 5, colornames.White)
r.expect(20, 20, colornames.White)
}),
frame(
func(ops *op.Ops) {
draw(0, ops)
}, func(r result) {
r.expect(2, 2, colornames.White)
r.expect(20, 20, colornames.Black)
r.expect(38, 38, colornames.White)
}))
}
+58 -2
View File
@@ -9,6 +9,7 @@ import (
"image/png"
"io/ioutil"
"path/filepath"
"strconv"
"testing"
"gioui.org/app/headless"
@@ -61,9 +62,10 @@ func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
img, err = drawImage(128, ops, f)
if err != nil {
t.Error("error rendering:", err)
return
}
// check for a reference image and make sure we are identical.
ok = ok && verifyRef(t, img)
ok = ok && verifyRef(t, img, 0)
c(result{t: t, img: img})
}
@@ -74,9 +76,62 @@ func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
}
}
func verifyRef(t *testing.T, img *image.RGBA) (ok bool) {
func frame(f func(o *op.Ops), c func(r result)) frameT {
return frameT{f: f, c: c}
}
type frameT struct {
f func(o *op.Ops)
c func(r result)
}
// multiRun is used to run test cases over multiple frames, typically
// to test caching interactions.
func multiRun(t *testing.T, frames ...frameT) {
// draw a few times and check that it is correct each time, to
// ensure any caching effects still generate the correct images.
var img *image.RGBA
var err error
sz := image.Point{X: 128, Y: 128}
w, err := headless.NewWindow(sz.X, sz.Y)
if err != nil {
t.Error("error creating window:", err)
t.FailNow()
}
ops := new(op.Ops)
for i := range frames {
ops.Reset()
frames[i].f(ops)
w.Frame(ops)
img, err = w.Screenshot()
if err != nil {
t.Error("error rendering:", err)
return
}
// check for a reference image and make sure we are identical.
ok := verifyRef(t, img, i)
if frames[i].c != nil {
frames[i].c(result{t: t, img: img})
}
if *dumpImages || !ok {
name := t.Name() + ".png"
if i != 0 {
name = t.Name() + "_" + strconv.Itoa(i) + ".png"
}
if err := saveImage(name, img); err != nil {
t.Error(err)
}
}
}
}
func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
// ensure identical to ref data
path := filepath.Join("refs", t.Name()+".png")
if frame != 0 {
path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png")
}
b, err := ioutil.ReadFile(path)
if err != nil {
t.Error("could not open ref:", err)
@@ -102,6 +157,7 @@ func verifyRef(t *testing.T, img *image.RGBA) (ok bool) {
c1, c2 := ref.RGBAAt(x, y), img.RGBAAt(x, y)
if !colorsClose(c1, c2) {
t.Error("not equal to ref at", x, y, " ", c1, c2)
return false
}
}
}