Files
gio-patched/gpu/internal/rendertest/render_test.go
T
Elias Naur a63e0cb44a all: [API] change op.Offset to take integer coordinates
op.Offset is a convenience function most often used by layouts. Layouts
usually operate in integer coordinates, and the float32 version of op.Offset
needlessly force conversions from int to float32. This change makes op.Offset
take integer coordinates, to better match its intended use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00

423 lines
10 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package rendertest
import (
"image"
"image/color"
"math"
"testing"
"golang.org/x/image/colornames"
"gioui.org/f32"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
func TestTransformMacro(t *testing.T) {
// testcase resulting from original bug when rendering layout.Stacked
// Build clip-path.
c := constSqPath()
run(t, func(o *op.Ops) {
// render the first Stacked item
m1 := op.Record(o)
dr := image.Rect(0, 0, 128, 50)
paint.FillShape(o, black, clip.Rect(dr).Op())
c1 := m1.Stop()
// Render the second stacked item
m2 := op.Record(o)
paint.ColorOp{Color: red}.Add(o)
// Simulate a draw text call
t := op.Offset(image.Pt(0, 10)).Push(o)
// Apply the clip-path.
cl := c.Push(o)
paint.PaintOp{}.Add(o)
cl.Pop()
t.Pop()
c2 := m2.Stop()
// Call each of them in a transform
t = op.Offset(image.Pt(0, 0)).Push(o)
c1.Add(o)
t.Pop()
t = op.Offset(image.Pt(0, 0)).Push(o)
c2.Add(o)
t.Pop()
}, func(r result) {
r.expect(5, 15, colornames.Red)
r.expect(15, 15, colornames.Black)
r.expect(11, 51, transparent)
})
}
func TestRepeatedPaintsZ(t *testing.T) {
run(t, func(o *op.Ops) {
// Draw a rectangle
paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op())
builder := clip.Path{}
builder.Begin(o)
builder.Move(f32.Pt(0, 0))
builder.Line(f32.Pt(10, 0))
builder.Line(f32.Pt(0, 10))
builder.Line(f32.Pt(-10, 0))
builder.Line(f32.Pt(0, -10))
p := builder.End()
defer clip.Outline{
Path: p,
}.Op().Push(o).Pop()
paint.Fill(o, red)
}, func(r result) {
r.expect(5, 5, colornames.Red)
r.expect(11, 15, colornames.Black)
r.expect(11, 51, transparent)
})
}
func TestNoClipFromPaint(t *testing.T) {
// ensure that a paint operation does not pollute the state
// by leaving any clip paths in place.
run(t, func(o *op.Ops) {
a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
defer op.Affine(a).Push(o).Pop()
paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op())
a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
defer op.Affine(a).Push(o).Pop()
paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
}, func(r result) {
r.expect(1, 1, colornames.Black)
r.expect(20, 20, colornames.Black)
r.expect(49, 49, colornames.Black)
r.expect(51, 51, transparent)
})
}
func TestDeferredPaint(t *testing.T) {
run(t, func(o *op.Ops) {
cl := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o)
paint.ColorOp{Color: color.NRGBA{A: 0x60, G: 0xff}}.Add(o)
paint.PaintOp{}.Add(o)
cl.Pop()
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Push(o)
m := op.Record(o)
cl2 := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o)
paint.ColorOp{Color: color.NRGBA{A: 0x60, R: 0xff, G: 0xff}}.Add(o)
paint.PaintOp{}.Add(o)
cl2.Pop()
paintMacro := m.Stop()
op.Defer(o, paintMacro)
t.Pop()
defer op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Push(o).Pop()
defer clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o).Pop()
paint.ColorOp{Color: color.NRGBA{A: 0x60, B: 0xff}}.Add(o)
paint.PaintOp{}.Add(o)
}, func(r result) {
})
}
func constSqPath() clip.Op {
innerOps := new(op.Ops)
builder := clip.Path{}
builder.Begin(innerOps)
builder.Move(f32.Pt(0, 0))
builder.Line(f32.Pt(10, 0))
builder.Line(f32.Pt(0, 10))
builder.Line(f32.Pt(-10, 0))
builder.Line(f32.Pt(0, -10))
p := builder.End()
return clip.Outline{Path: p}.Op()
}
func constSqCirc() clip.Op {
innerOps := new(op.Ops)
return clip.RRect{Rect: f32.Rect(0, 0, 40, 40),
NW: 20, NE: 20, SW: 20, SE: 20}.Op(innerOps)
}
func drawChild(ops *op.Ops, text clip.Op) op.CallOp {
r1 := op.Record(ops)
cl := text.Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
return r1.Stop()
}
func TestReuseStencil(t *testing.T) {
txt := constSqPath()
run(t, func(ops *op.Ops) {
c1 := drawChild(ops, txt)
c2 := drawChild(ops, txt)
// lay out the children
c1.Add(ops)
defer op.Offset(image.Pt(0, 50)).Push(ops).Pop()
c2.Add(ops)
}, func(r result) {
r.expect(5, 5, colornames.Black)
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 int, o *op.Ops) {
defer op.Offset(image.Pt(0, off)).Push(o).Pop()
defer txt.Push(o).Pop()
paint.PaintOp{}.Add(o)
}
multiRun(t,
frame(
func(ops *op.Ops) {
draw(-100, ops)
}, func(r result) {
r.expect(5, 5, transparent)
r.expect(20, 20, transparent)
}),
frame(
func(ops *op.Ops) {
draw(0, ops)
}, func(r result) {
r.expect(2, 2, transparent)
r.expect(20, 20, colornames.Black)
r.expect(38, 38, transparent)
}))
}
func TestNegativeOverlaps(t *testing.T) {
run(t, func(ops *op.Ops) {
defer clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Push(ops).Pop()
clip.Rect(image.Rect(0, 120, 100, 122)).Push(ops).Pop()
paint.PaintOp{}.Add(ops)
}, func(r result) {
r.expect(60, 60, transparent)
r.expect(60, 110, transparent)
r.expect(60, 120, transparent)
r.expect(60, 122, transparent)
})
}
func TestDepthOverlap(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op())
paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op())
}, func(r result) {
r.expect(96, 32, colornames.Red)
r.expect(32, 96, colornames.Green)
r.expect(32, 32, colornames.Green)
})
}
type Gradient struct {
From, To color.NRGBA
}
var gradients = []Gradient{
{From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
{From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
{From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
{From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
}
func TestLinearGradient(t *testing.T) {
t.Skip("linear gradients don't support transformations")
const gradienth = 8
// 0.5 offset from ends to ensure that the center of the pixel
// aligns with gradient from and to colors.
pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth)
samples := []int{0, 12, 32, 64, 96, 115, 127}
run(t, func(ops *op.Ops) {
gr := f32.Rect(0, 0, 128, gradienth)
for _, g := range gradients {
paint.LinearGradientOp{
Stop1: f32.Pt(gr.Min.X, gr.Min.Y),
Color1: g.From,
Stop2: f32.Pt(gr.Max.X, gr.Min.Y),
Color2: g.To,
}.Add(ops)
cl := clip.RRect{Rect: gr}.Push(ops)
t1 := op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Push(ops)
t2 := scale(pixelAligned.Dx()/128, 1).Push(ops)
paint.PaintOp{}.Add(ops)
t2.Pop()
t1.Pop()
cl.Pop()
gr = gr.Add(f32.Pt(0, gradienth))
}
}, func(r result) {
gr := pixelAligned
for _, g := range gradients {
from := f32color.LinearFromSRGB(g.From)
to := f32color.LinearFromSRGB(g.To)
for _, p := range samples {
exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1))
r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB()))
}
gr = gr.Add(f32.Pt(0, gradienth))
}
})
}
func TestLinearGradientAngled(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: black,
Stop2: f32.Pt(0, 0),
Color2: red,
}.Add(ops)
cl := clip.Rect(image.Rect(0, 0, 64, 64)).Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: white,
Stop2: f32.Pt(128, 0),
Color2: green,
}.Add(ops)
cl = clip.Rect(image.Rect(64, 0, 128, 64)).Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: black,
Stop2: f32.Pt(128, 128),
Color2: blue,
}.Add(ops)
cl = clip.Rect(image.Rect(64, 64, 128, 128)).Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: white,
Stop2: f32.Pt(0, 128),
Color2: magenta,
}.Add(ops)
cl = clip.Rect(image.Rect(0, 64, 64, 128)).Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
}, func(r result) {})
}
func TestZeroImage(t *testing.T) {
ops := new(op.Ops)
w := newWindow(t, 10, 10)
paint.ImageOp{}.Add(ops)
paint.PaintOp{}.Add(ops)
if err := w.Frame(ops); err != nil {
t.Error(err)
}
}
func TestImageRGBA(t *testing.T) {
run(t, func(o *op.Ops) {
w := newWindow(t, 10, 10)
im := image.NewRGBA(image.Rect(0, 0, 5, 5))
im.Set(3, 3, colornames.Red)
im.Set(4, 3, colornames.Red)
im.Set(3, 4, colornames.Red)
im.Set(4, 4, colornames.Red)
im = im.SubImage(image.Rect(2, 2, 5, 5)).(*image.RGBA)
paint.NewImageOp(im).Add(o)
paint.PaintOp{}.Add(o)
if err := w.Frame(o); err != nil {
t.Error(err)
}
}, func(r result) {
r.expect(1, 1, colornames.Red)
r.expect(2, 1, colornames.Red)
r.expect(1, 2, colornames.Red)
r.expect(2, 2, colornames.Red)
})
}
func TestGapsInPath(t *testing.T) {
ops := new(op.Ops)
var p clip.Path
p.Begin(ops)
// Unclosed square 1
p.MoveTo(f32.Point{X: 10})
p.LineTo(f32.Point{X: 40})
p.LineTo(f32.Point{X: 40, Y: 30})
p.LineTo(f32.Point{X: 10, Y: 30})
// Unclosed square 2
p.MoveTo(f32.Point{X: 50})
p.LineTo(f32.Point{X: 80})
p.LineTo(f32.Point{X: 80, Y: 30})
p.LineTo(f32.Point{X: 50, Y: 30})
spec := p.End()
t.Run("Stroke", func(t *testing.T) {
run(t,
func(ops *op.Ops) {
stack := clip.Stroke{
Path: spec,
Width: 2,
}.Op().Push(ops)
paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
paint.PaintOp{}.Add(ops)
stack.Pop()
},
func(r result) {
r.expect(10, 20, color.RGBA{})
r.expect(50, 20, color.RGBA{})
},
)
})
t.Run("Outline", func(t *testing.T) {
run(t,
func(ops *op.Ops) {
stack := clip.Outline{Path: spec}.Op().Push(ops)
paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops)
paint.PaintOp{}.Add(ops)
stack.Pop()
},
func(r result) {
r.expect(10, 20, colornames.Red)
r.expect(20, 20, colornames.Red)
r.expect(50, 20, colornames.Red)
r.expect(60, 20, colornames.Red)
},
)
})
}
// lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{
R: a.R*(1-p) + b.R*p,
G: a.G*(1-p) + b.G*p,
B: a.B*(1-p) + b.B*p,
A: a.A*(1-p) + b.A*p,
}
}