mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
internal/rendertest: create new standard benchmark
Create a standard, representative set of benchmarks for the rendering pipeline to allow for measurement of performance improvement/regressions due to changes. The benchmarks are intended to be representative of the types of drawing different gio uses should encounter. BenchmarkDrawUI: Draw text, instanced shaped and unique shapes in a mix that is reasonable for a simple UI. BenchmarkDrawUICached: Same as BenchmarkDrawUI but not reset between iterations to benchmark the rendering pipeline when using maximum caching. Benchmark1000Circles: Draw 1000 circles individually to benchmark the rendering performance with no caching. Represents usages such as animating shapes or drawing complex shapes. Benchmark1000CirclesInstanced: Draw 1000 circles by calling a Macro op, each one with an offset transform. Represents cases such as drawing spirits etc. Signed-off-by: Viktor <viktor.ogeman@gmail.com>
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"gioui.org/app/headless"
|
||||
"gioui.org/f32"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
// use some global variables for benchmarking so as to not pollute
|
||||
// the reported allocs with allocations that we do not want to count.
|
||||
var (
|
||||
c1, c2, c3 = make(chan op.CallOp, 0), make(chan op.CallOp, 0), make(chan op.CallOp, 0)
|
||||
op1, op2, op3 op.Ops
|
||||
)
|
||||
|
||||
func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.Theme) {
|
||||
sz := image.Point{X: 1024, Y: 1200}
|
||||
w, err := headless.NewWindow(sz.X, sz.Y)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
ops := new(op.Ops)
|
||||
gtx := layout.Context{
|
||||
Ops: ops,
|
||||
Constraints: layout.Exact(sz),
|
||||
}
|
||||
th := material.NewTheme(gofont.Collection())
|
||||
return gtx, w, th
|
||||
}
|
||||
|
||||
func resetOps(gtx layout.Context) {
|
||||
gtx.Ops.Reset()
|
||||
op1.Reset()
|
||||
op2.Reset()
|
||||
op3.Reset()
|
||||
}
|
||||
|
||||
func finishBenchmark(b *testing.B, w *headless.Window) {
|
||||
b.StopTimer()
|
||||
if *dumpImages {
|
||||
img, err := w.Screenshot()
|
||||
w.Release()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if err := saveImage(b.Name()+".png", img); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDrawUICached(b *testing.B) {
|
||||
// As BecnhmarkDraw but the same op.Ops every time that is not reset - this
|
||||
// should thus allow for maximal cache usage.
|
||||
gtx, w, th := setupBenchmark(b)
|
||||
drawCore(gtx, th)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func BenchmarkDrawUI(b *testing.B) {
|
||||
// BenchmarkDraw is intended as a reasonable overall benchmark for
|
||||
// the drawing performance of the full drawing pipeline, in each iteration
|
||||
// resetting the ops and drawing, similar to how a typical UI would function.
|
||||
// This will allow font caching across frames.
|
||||
gtx, w, th := setupBenchmark(b)
|
||||
drawCore(gtx, th)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
|
||||
p := op.Push(gtx.Ops)
|
||||
off := float32(math.Mod(float64(i)/10, 10))
|
||||
op.TransformOp{}.Offset(f32.Pt(off, off)).Add(gtx.Ops)
|
||||
|
||||
drawCore(gtx, th)
|
||||
|
||||
p.Pop()
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func Benchmark1000Circles(b *testing.B) {
|
||||
// Benchmark1000Shapes draws 1000 individual shapes such that no caching between
|
||||
// shapes will be possible and resets buffers on each operation to prevent caching
|
||||
// between frames.
|
||||
gtx, w, _ := setupBenchmark(b)
|
||||
draw1000Circles(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
draw1000Circles(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func Benchmark1000CirclesInstanced(b *testing.B) {
|
||||
// Like Benchmark1000Circles but will record them and thus allow for caching between
|
||||
// them.
|
||||
gtx, w, _ := setupBenchmark(b)
|
||||
draw1000CirclesInstanced(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
draw1000CirclesInstanced(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func draw1000Circles(gtx layout.Context) {
|
||||
ops := gtx.Ops
|
||||
for x := 0; x < 100; x++ {
|
||||
p := op.Push(ops)
|
||||
op.TransformOp{}.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
|
||||
for y := 0; y < 10; y++ {
|
||||
pi := op.Push(ops)
|
||||
paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
|
||||
clip.Rect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops)
|
||||
paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(ops)
|
||||
pi.Pop()
|
||||
op.TransformOp{}.Offset(f32.Pt(0, float32(100))).Add(ops)
|
||||
}
|
||||
p.Pop()
|
||||
}
|
||||
}
|
||||
|
||||
func draw1000CirclesInstanced(gtx layout.Context) {
|
||||
ops := gtx.Ops
|
||||
|
||||
r := op.Record(ops)
|
||||
clip.Rect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops)
|
||||
paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(ops)
|
||||
c := r.Stop()
|
||||
|
||||
for x := 0; x < 100; x++ {
|
||||
p := op.Push(ops)
|
||||
op.TransformOp{}.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
|
||||
for y := 0; y < 10; y++ {
|
||||
pi := op.Push(ops)
|
||||
paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
|
||||
c.Add(ops)
|
||||
pi.Pop()
|
||||
op.TransformOp{}.Offset(f32.Pt(0, float32(100))).Add(ops)
|
||||
}
|
||||
p.Pop()
|
||||
}
|
||||
}
|
||||
|
||||
func drawCore(gtx layout.Context, th *material.Theme) {
|
||||
c1 := drawIndividualShapes(gtx, th)
|
||||
c2 := drawShapeInstances(gtx, th)
|
||||
c3 := drawText(gtx, th)
|
||||
|
||||
(<-c1).Add(gtx.Ops)
|
||||
(<-c2).Add(gtx.Ops)
|
||||
(<-c3).Add(gtx.Ops)
|
||||
}
|
||||
|
||||
func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 81 rounded rectangles of different solid colors - each one individually
|
||||
go func() {
|
||||
ops := &op1
|
||||
c := op.Record(ops)
|
||||
for x := 0; x < 9; x++ {
|
||||
p := op.Push(ops)
|
||||
op.TransformOp{}.Offset(f32.Pt(float32(x*50), 0)).Add(ops)
|
||||
for y := 0; y < 9; y++ {
|
||||
pi := op.Push(ops)
|
||||
paint.ColorOp{Color: color.RGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
|
||||
clip.Rect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops)
|
||||
paint.PaintOp{Rect: f32.Rect(0, 0, 25, 25)}.Add(ops)
|
||||
pi.Pop()
|
||||
op.TransformOp{}.Offset(f32.Pt(0, float32(50))).Add(ops)
|
||||
}
|
||||
p.Pop()
|
||||
}
|
||||
c1 <- c.Stop()
|
||||
}()
|
||||
return c1
|
||||
}
|
||||
|
||||
func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 400 textured circle instances, each with individual transform
|
||||
go func() {
|
||||
ops := &op2
|
||||
co := op.Record(ops)
|
||||
|
||||
r := op.Record(ops)
|
||||
clip.Rect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops)
|
||||
paint.PaintOp{Rect: f32.Rect(0, 0, 25, 25)}.Add(ops)
|
||||
c := r.Stop()
|
||||
|
||||
squares.Add(ops)
|
||||
rad := float32(0)
|
||||
for x := 0; x < 20; x++ {
|
||||
for y := 0; y < 20; y++ {
|
||||
p := op.Push(ops)
|
||||
op.TransformOp{}.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops)
|
||||
c.Add(ops)
|
||||
p.Pop()
|
||||
rad += math.Pi * 2 / 400
|
||||
}
|
||||
}
|
||||
c2 <- co.Stop()
|
||||
}()
|
||||
return c2
|
||||
}
|
||||
|
||||
func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 40 lines of text with different transforms.
|
||||
go func() {
|
||||
ops := &op3
|
||||
c := op.Record(ops)
|
||||
|
||||
txt := material.H6(th, "")
|
||||
for x := 0; x < 40; x++ {
|
||||
txt.Text = textRows[x]
|
||||
p := op.Push(ops)
|
||||
op.TransformOp{}.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops)
|
||||
gtx.Ops = ops
|
||||
txt.Layout(gtx)
|
||||
p.Pop()
|
||||
}
|
||||
c3 <- c.Stop()
|
||||
}()
|
||||
return c3
|
||||
}
|
||||
|
||||
var textRows = []string{
|
||||
"1. I learned from my grandfather, Verus, to use good manners, and to",
|
||||
"put restraint on anger. 2. In the famous memory of my father I had a",
|
||||
"pattern of modesty and manliness. 3. Of my mother I learned to be",
|
||||
"pious and generous; to keep myself not only from evil deeds, but even",
|
||||
"from evil thoughts; and to live with a simplicity which is far from",
|
||||
"customary among the rich. 4. I owe it to my great-grandfather that I",
|
||||
"did not attend public lectures and discussions, but had good and able",
|
||||
"teachers at home; and I owe him also the knowledge that for things of",
|
||||
"this nature a man should count no expense too great.",
|
||||
"5. My tutor taught me not to favour either green or blue at the",
|
||||
"chariot races, nor, in the contests of gladiators, to be a supporter",
|
||||
"either of light or heavy armed. He taught me also to endure labour;",
|
||||
"not to need many things; to serve myself without troubling others; not",
|
||||
"to intermeddle in the affairs of others, and not easily to listen to",
|
||||
"slanders against them.",
|
||||
"6. Of Diognetus I had the lesson not to busy myself about vain things;",
|
||||
"not to credit the great professions of such as pretend to work",
|
||||
"wonders, or of sorcerers about their charms, and their expelling of",
|
||||
"Demons and the like; not to keep quails (for fighting or divination),",
|
||||
"nor to run after such things; to suffer freedom of speech in others,",
|
||||
"and to apply myself heartily to philosophy. Him also I must thank for",
|
||||
"my hearing first Bacchius, then Tandasis and Marcianus; that I wrote",
|
||||
"dialogues in my youth, and took a liking to the philosopher's pallet",
|
||||
"and skins, and to the other things which, by the Grecian discipline,",
|
||||
"belong to that profession.",
|
||||
"7. To Rusticus I owe my first apprehensions that my nature needed",
|
||||
"reform and cure; and that I did not fall into the ambition of the",
|
||||
"common Sophists, either by composing speculative writings or by",
|
||||
"declaiming harangues of exhortation in public; further, that I never",
|
||||
"strove to be admired by ostentation of great patience in an ascetic",
|
||||
"life, or by display of activity and application; that I gave over the",
|
||||
"study of rhetoric, poetry, and the graces of language; and that I did",
|
||||
"not pace my house in my senatorial robes, or practise any similar",
|
||||
"affectation. I observed also the simplicity of style in his letters,",
|
||||
"particularly in that which he wrote to my mother from Sinuessa. I",
|
||||
"learned from him to be easily appeased, and to be readily reconciled",
|
||||
"with those who had displeased me or given cause of offence, so soon as",
|
||||
"they inclined to make their peace; to read with care; not to rest",
|
||||
"satisfied with a slight and superficial knowledge; nor quickly to",
|
||||
"assent to great talkers. I have him to thank that I met with the",
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gioui.org/app/headless"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/paint"
|
||||
"golang.org/x/image/colornames"
|
||||
)
|
||||
|
||||
var (
|
||||
dumpImages = flag.Bool("saveimages", false, "save test images")
|
||||
squares paint.ImageOp
|
||||
)
|
||||
|
||||
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(size int, draw func(o *op.Ops)) (im *image.RGBA, err error) {
|
||||
sz := image.Point{X: size, Y: size}
|
||||
w, err := headless.NewWindow(sz.X, sz.Y)
|
||||
if err != nil {
|
||||
return im, err
|
||||
}
|
||||
ops := new(op.Ops)
|
||||
draw(ops)
|
||||
w.Frame(ops)
|
||||
return w.Screenshot()
|
||||
}
|
||||
|
||||
func run(t *testing.T, f func(o *op.Ops)) result {
|
||||
img, err := drawImage(128, f)
|
||||
if err != nil {
|
||||
t.Error("error rendering:", err)
|
||||
}
|
||||
|
||||
// check for a reference image and make sure we are identical.
|
||||
ok := verifyRef(t, img)
|
||||
|
||||
if *dumpImages || !ok {
|
||||
if err := saveImage(t.Name()+".png", img); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
return result{t: t, img: img}
|
||||
}
|
||||
|
||||
func verifyRef(t *testing.T, img *image.RGBA) (ok bool) {
|
||||
// ensure identical to ref data
|
||||
path := filepath.Join("refs", t.Name()+".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.Error("ref image note RGBA")
|
||||
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 true
|
||||
}
|
||||
|
||||
func colorsClose(c1, c2 color.RGBA) bool {
|
||||
return close(c1.A, c2.A) && close(c1.R, c2.R) && close(c1.G, c2.G) && close(c1.B, c2.B)
|
||||
}
|
||||
|
||||
func close(b1, b2 uint8) bool {
|
||||
if b1 > b2 {
|
||||
b1, b2 = b2, b1
|
||||
}
|
||||
diff := b2 - b1
|
||||
return diff < 20
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user