Files
gio-patched/internal/rendertest/util_test.go
T
Elias Naur 84b586ae6c gpu: don't automatically clear screen before rendering
Gio UI may be overlaid on top of custom graphics such as in the glfw example.
That will only work if Gio doesn't clear the screen (to white).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-01-22 18:33:34 +01:00

263 lines
6.2 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package rendertest
import (
"bytes"
"flag"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"io/ioutil"
"path/filepath"
"strconv"
"testing"
"golang.org/x/image/colornames"
"gioui.org/app/headless"
"gioui.org/f32"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/op/paint"
)
var (
dumpImages = flag.Bool("saveimages", false, "save test images")
squares paint.ImageOp
)
var (
red = f32color.RGBAToNRGBA(colornames.Red)
green = f32color.RGBAToNRGBA(colornames.Green)
blue = f32color.RGBAToNRGBA(colornames.Blue)
magenta = f32color.RGBAToNRGBA(colornames.Magenta)
black = f32color.RGBAToNRGBA(colornames.Black)
white = f32color.RGBAToNRGBA(colornames.White)
)
func init() {
// build the texture we use for testing
size := 512
sub := size / 4
im := image.NewNRGBA(image.Rect(0, 0, size, size))
c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue)
for r := 0; r < 4; r++ {
for c := 0; c < 4; c++ {
c1, c2 = c2, c1
draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over)
}
c1, c2 = c2, c1
}
squares = paint.NewImageOp(im)
}
func drawImage(t *testing.T, size int, ops *op.Ops, draw func(o *op.Ops)) (im *image.RGBA, err error) {
sz := image.Point{X: size, Y: size}
w := newWindow(t, sz.X, sz.Y)
draw(ops)
if err := w.Frame(ops); err != nil {
return nil, err
}
return w.Screenshot()
}
func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
// 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
ops := new(op.Ops)
for i := 0; i < 3; i++ {
ops.Reset()
img, err = drawImage(t, 128, ops, f)
if err != nil {
t.Error("error rendering:", err)
return
}
// check for a reference image and make sure we are identical.
if !verifyRef(t, img, 0) {
name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
if err := saveImage(name, img); err != nil {
t.Error(err)
}
}
c(result{t: t, img: img})
}
if *dumpImages {
if err := saveImage(t.Name()+".png", img); err != nil {
t.Error(err)
}
}
}
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 := newWindow(t, sz.X, sz.Y)
ops := new(op.Ops)
for i := range frames {
ops.Reset()
frames[i].f(ops)
if err := w.Frame(ops); err != nil {
t.Errorf("rendering failed: %v", err)
continue
}
img, err = w.Screenshot()
if err != nil {
t.Errorf("screenshot failed: %v", err)
continue
}
// Check for a reference image and make sure they 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)
return
}
r, err := png.Decode(bytes.NewReader(b))
if err != nil {
t.Error("could not decode ref:", err)
return
}
ref, ok := r.(*image.RGBA)
if !ok {
t.Errorf("image is a %T, expected *image.RGBA", r)
return
}
if len(ref.Pix) != len(img.Pix) {
t.Error("not equal to ref (len)")
return false
}
bnd := img.Bounds()
for x := bnd.Min.X; x < bnd.Max.X; x++ {
for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
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
}
}
}
return true
}
func colorsClose(c1, c2 color.RGBA) bool {
const delta = 0.01 // magic value obtained from experimentation.
return yiqEqApprox(c1, c2, delta)
}
// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
// as described in:
//
// Measuring perceived color difference using YIQ NTSC
// transmission color space in mobile applications.
// Yuriy Kotsarenko, Fernando Ramos.
//
// An electronic version is available at:
//
// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
const max = 35215.0 // difference between 2 maximally different pixels.
var (
r1 = float64(c1.R)
g1 = float64(c1.G)
b1 = float64(c1.B)
r2 = float64(c2.R)
g2 = float64(c2.G)
b2 = float64(c2.B)
y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
y = y1 - y2
i = i1 - i2
q = q1 - q2
diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
)
return diff <= max*d2
}
func (r result) expect(x, y int, col color.RGBA) {
if r.img == nil {
return
}
c := r.img.RGBAAt(x, y)
if !colorsClose(c, col) {
r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c)
}
}
type result struct {
t *testing.T
img *image.RGBA
}
func saveImage(file string, img image.Image) error {
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return err
}
return ioutil.WriteFile(file, buf.Bytes(), 0666)
}
func newWindow(t testing.TB, width, height int) *headless.Window {
w, err := headless.NewWindow(width, height)
if err != nil {
t.Skipf("failed to create headless window, skipping: %v", err)
}
t.Cleanup(w.Release)
return w
}
func scale(sx, sy float32) op.TransformOp {
return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
}