~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
15 2

[PATCH v2 01/15] internal/rendertest: create new standard benchmark

Details
Message ID
<20200620213001.137-1-viktor.ogeman@gmail.com>
DKIM signature
pass
Download raw message
Patch: +430 -0
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>
---

New try - it's a bit scare with these chagnes though since I do not
have so many test-cases. Anyway now all the examples run as well (apart
from the glfw example that doesnt run here on my windows machine,
getting compliation errors that seem unrelated).

Might be worthwile if you test on linux to before merging though, I
cannot say that I feel 100% confident that all edge cases are covered
since I do not have any "app-base" to test it out on.

This set of patches effectively fixes 3 things from the last set, and
adds new test cases:

1) A error in the uniform uploading/ sizes that lead to bad depth
testing, this was the reson the text was not rendered. This is merged
into the initial patch.

2) While fixing 1 I realized that there was an error in that the extra
clip paths Im adding due to rotated rectangles were not properly popped
after a paintCall - no test case or example apps exposed this since they
all use Push()/Pop() and this do that bookkeeping manually. Fixed and
test case added merged into original patch.

3) Another error I found while testing the gopher example, this fix is
added at the very end so you can check if you agree that its fine to
remove the clip.Empty() -> continue, or if we need do something more
sophisticated.


 internal/rendertest/bench_test.go | 293 ++++++++++++++++++++++++++++++
 internal/rendertest/util_test.go  | 137 ++++++++++++++
 2 files changed, 430 insertions(+)
 create mode 100644 internal/rendertest/bench_test.go
 create mode 100644 internal/rendertest/util_test.go

diff --git a/internal/rendertest/bench_test.go b/internal/rendertest/bench_test.go
new file mode 100644
index 0000000..fe9dfba
--- /dev/null
+++ b/internal/rendertest/bench_test.go
@@ -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",
}
diff --git a/internal/rendertest/util_test.go b/internal/rendertest/util_test.go
new file mode 100644
index 0000000..20755f0
--- /dev/null
+++ b/internal/rendertest/util_test.go
@@ -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)
}
-- 
2.25.1

[PATCH v2 02/15] op/clip, gpu: split complex curves in package gpu instead

Details
Message ID
<20200620213001.137-2-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +240 -173
This is a first step towards supporting affine drawing transforms.
The rendering algorithm relies on quadratic curves that do not cross
x = 0 more than once, thus curves must be split after any rotation/shear
transforms. Move this logic and the generation of vertices to package gpu.
Also close all curves and draw zero-width edges as preparation for
transform since the will no longer implicitly be vertical with no
effect.

This commit will severely affect performance since vertexes are now
transformed also for cached items, using cpu resources.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/clip.go           |  98 +++++++++++++++++++++++++++++
 gpu/gpu.go            |  67 ++++++++++++++------
 gpu/path.go           |  51 ++++++++++++---
 internal/ops/ops.go   |  27 ++++++++
 internal/path/path.go |  27 --------
 op/clip/clip.go       | 143 +++++++-----------------------------------
 6 files changed, 240 insertions(+), 173 deletions(-)
 create mode 100644 gpu/clip.go
 delete mode 100644 internal/path/path.go

diff --git a/gpu/clip.go b/gpu/clip.go
new file mode 100644
index 0000000..beddb49
--- /dev/null
+++ b/gpu/clip.go
@@ -0,0 +1,98 @@
package gpu

import (
	"gioui.org/f32"
	"gioui.org/internal/ops"
)

type quadSplitter struct {
	verts   []byte
	bounds  f32.Rectangle
	contour uint32
	d       *drawOps
}

func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
	// NW.
	encodeVertex(data, meta, -1, 1, from, ctrl, to)
	// NE.
	encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to)
	// SW.
	encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to)
	// SE.
	encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to)
}

func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) {
	var corner float32
	if cornerx == 1 {
		corner += .5
	}
	if cornery == 1 {
		corner += .25
	}
	v := vertex{
		Corner: corner,
		FromX:  from.X,
		FromY:  from.Y,
		CtrlX:  ctrl.X,
		CtrlY:  ctrl.Y,
		ToX:    to.X,
		ToY:    to.Y,
	}
	v.encode(data, meta)
}

func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
	data := qs.d.writeVertCache(vertStride * 4)
	encodeQuadTo(data, qs.contour, from, ctrl, to)
}

func (qs *quadSplitter) splitAndEncode(quad ops.Quad) {
	cbnd := f32.Rectangle{
		Min: quad.From,
		Max: quad.To,
	}.Canon()
	from, ctrl, to := quad.From, quad.Ctrl, quad.To

	// If the curve contain areas where a vertical line
	// intersects it twice, split the curve in two x monotone
	// lower and upper curves. The stencil fragment program
	// expects only one intersection per curve.

	// Find the t where the derivative in x is 0.
	v0 := ctrl.Sub(from)
	v1 := to.Sub(ctrl)
	d := v0.X - v1.X
	// t = v0 / d. Split if t is in ]0;1[.
	if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
		t := v0.X / d
		ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t))
		ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
		mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
		qs.encodeQuadTo(from, ctrl0, mid)
		qs.encodeQuadTo(mid, ctrl1, to)
		if mid.X > cbnd.Max.X {
			cbnd.Max.X = mid.X
		}
		if mid.X < cbnd.Min.X {
			cbnd.Min.X = mid.X
		}
	} else {
		qs.encodeQuadTo(from, ctrl, to)
	}
	// Find the y extremum, if any.
	d = v0.Y - v1.Y
	if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
		t := v0.Y / d
		y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
		if y > cbnd.Max.Y {
			cbnd.Max.Y = y
		}
		if y < cbnd.Min.Y {
			cbnd.Min.Y = y
		}
	}

	qs.bounds = qs.bounds.Union(cbnd)
}
diff --git a/gpu/gpu.go b/gpu/gpu.go
index 56d1886..334f03a 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -22,7 +22,6 @@ import (
	"gioui.org/internal/f32color"
	"gioui.org/internal/opconst"
	"gioui.org/internal/ops"
	"gioui.org/internal/path"
	gunsafe "gioui.org/internal/unsafe"
	"gioui.org/layout"
	"gioui.org/op"
@@ -55,6 +54,7 @@ type drawOps struct {
	profile    bool
	reader     ops.Reader
	cache      *resourceCache
	vertCache  []byte
	viewport   image.Point
	clearColor f32color.RGBA
	imageOps   []imageOp
@@ -64,6 +64,7 @@ type drawOps struct {
	zimageOps   []imageOp
	pathOps     []*pathOp
	pathOpCache []pathOp
	qs          quadSplitter
}

type drawState struct {
@@ -659,6 +660,7 @@ func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
	d.zimageOps = d.zimageOps[:0]
	d.pathOps = d.pathOps[:0]
	d.pathOpCache = d.pathOpCache[:0]
	d.vertCache = d.vertCache[:0]
}

func (d *drawOps) collect(cache *resourceCache, root *op.Ops, viewport image.Point) {
@@ -693,29 +695,29 @@ loop:
			state.t = state.t.Multiply(op.TransformOp(dop))
		case opconst.TypeAux:
			aux = encOp.Data[opconst.TypeAuxLen:]
			// The first data byte stores whether the MaxY
			// fields have been initialized.
			maxyFilled := aux[0] == 1
			aux[0] = 1
			aux = aux[1:]
			if !maxyFilled {
				fillMaxY(aux)
			}
			auxKey = encOp.Key
		case opconst.TypeClip:
			var op clipOp
			op.decode(encOp.Data)
			off := state.t.Transform(f32.Point{})
			state.clip = state.clip.Intersect(op.bounds.Add(off))

			bounds := op.bounds
			if len(aux) > 0 {
				// there is a clipping path, bounds is not filled before for performance
				aux, bounds = d.buildVerts(aux)
			}
			state.clip = state.clip.Intersect(bounds.Add(off))
			if state.clip.Empty() {
				continue
			}

			npath := d.newPathOp()
			*npath = pathOp{
				parent: state.cpath,
				off:    off,
			}
			state.cpath = npath

			if len(aux) > 0 {
				state.rect = false
				state.cpath.pathKey = auxKey
@@ -1002,17 +1004,17 @@ func fillMaxY(verts []byte) {
	for len(verts) > 0 {
		maxy := float32(math.Inf(-1))
		i := 0
		for ; i+path.VertStride*4 <= len(verts); i += path.VertStride * 4 {
			vert := verts[i : i+path.VertStride]
		for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
			vert := verts[i : i+vertStride]
			// MaxY contains the integer contour index.
			pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)):]))
			pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):]))
			if contour != pathContour {
				contour = pathContour
				break
			}
			fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).FromY)):]))
			ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).CtrlY)):]))
			toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).ToY)):]))
			fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):]))
			ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):]))
			toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):]))
			if fromy > maxy {
				maxy = fromy
			}
@@ -1030,8 +1032,37 @@ func fillMaxY(verts []byte) {

func fillContourMaxY(maxy float32, verts []byte) {
	bo := binary.LittleEndian
	for i := 0; i < len(verts); i += path.VertStride {
		off := int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY))
	for i := 0; i < len(verts); i += vertStride {
		off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY))
		bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
	}
}

func (d *drawOps) writeVertCache(n int) []byte {
	d.vertCache = append(d.vertCache, make([]byte, n)...)
	return d.vertCache[len(d.vertCache)-n:]
}

func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
	// split paths as needed, calculate maxY, bounds and create
	// vertices that will be sent to GPU.
	inf := float32(math.Inf(+1))
	d.qs.bounds = f32.Rectangle{
		Min: f32.Point{X: inf, Y: inf},
		Max: f32.Point{X: -inf, Y: -inf},
	}
	d.qs.d = d
	bo := binary.LittleEndian
	startLength := len(d.vertCache)
	for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
		d.qs.contour = bo.Uint32(aux)
		quad := ops.DecodeQuad(aux[4:])

		d.qs.splitAndEncode(quad)

		aux = aux[ops.QuadSize+4:]
	}

	fillMaxY(d.vertCache[startLength:])
	return d.vertCache[startLength:], d.qs.bounds
}
diff --git a/gpu/path.go b/gpu/path.go
index 73e72f5..b182f6c 100644
--- a/gpu/path.go
+++ b/gpu/path.go
@@ -6,13 +6,14 @@ package gpu
// Pathfinder (https://github.com/servo/pathfinder).

import (
	"encoding/binary"
	"image"
	"math"
	"unsafe"

	"gioui.org/f32"
	"gioui.org/gpu/backend"
	"gioui.org/internal/f32color"
	"gioui.org/internal/path"
	gunsafe "gioui.org/internal/unsafe"
)

@@ -104,9 +105,33 @@ type pathData struct {
	data    backend.Buffer
}

// vertex data suitable for passing to vertex programs.
type vertex struct {
	// Corner encodes the corner: +0.5 for south, +.25 for east.
	Corner       float32
	MaxY         float32
	FromX, FromY float32
	CtrlX, CtrlY float32
	ToX, ToY     float32
}

func (v vertex) encode(d []byte, maxy uint32) {
	bo := binary.LittleEndian
	bo.PutUint32(d[0:], math.Float32bits(v.Corner))
	bo.PutUint32(d[4:], maxy)
	bo.PutUint32(d[8:], math.Float32bits(v.FromX))
	bo.PutUint32(d[12:], math.Float32bits(v.FromY))
	bo.PutUint32(d[16:], math.Float32bits(v.CtrlX))
	bo.PutUint32(d[20:], math.Float32bits(v.CtrlY))
	bo.PutUint32(d[24:], math.Float32bits(v.ToX))
	bo.PutUint32(d[28:], math.Float32bits(v.ToY))
}

const (
	// Number of path quads per draw batch.
	pathBatchSize = 10000
	// Size of a vertex as sent to gpu
	vertStride = 7*4 + 2*2
)

func newPather(ctx backend.Device) *pather {
@@ -152,11 +177,11 @@ func newStenciler(ctx backend.Device) *stenciler {
		panic(err)
	}
	progLayout, err := ctx.NewInputLayout(shader_stencil_vert, []backend.InputDesc{
		{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).Corner))},
		{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).MaxY))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).FromX))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).CtrlX))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).ToX))},
		{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))},
		{Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))},
		{Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))},
	})
	if err != nil {
		panic(err)
@@ -269,7 +294,7 @@ func buildPath(ctx backend.Device, p []byte) *pathData {
		panic(err)
	}
	return &pathData{
		ncurves: len(p) / path.VertStride,
		ncurves: len(p) / vertStride,
		data:    buf,
	}
}
@@ -329,8 +354,8 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
		if max := pathBatchSize; batch > max {
			batch = max
		}
		off := path.VertStride * start * 4
		s.ctx.BindVertexBuffer(data.data, path.VertStride, off)
		off := vertStride * start * 4
		s.ctx.BindVertexBuffer(data.data, vertStride, off)
		s.ctx.DrawElements(backend.DrawModeTriangles, 0, batch*6)
		start += batch
	}
@@ -358,3 +383,11 @@ func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, o
	p.UploadUniforms()
	c.ctx.DrawArrays(backend.DrawModeTriangleStrip, 0, 4)
}

func init() {
	// Check that struct vertex has the expected size and
	// that it contains no padding.
	if unsafe.Sizeof(*(*vertex)(nil)) != vertStride {
		panic("unexpected struct size")
	}
}
diff --git a/internal/ops/ops.go b/internal/ops/ops.go
index c8a9f89..80994e1 100644
--- a/internal/ops/ops.go
+++ b/internal/ops/ops.go
@@ -11,6 +11,33 @@ import (
	"gioui.org/op"
)

const QuadSize = 4 * 2 * 3

type Quad struct {
	From, Ctrl, To f32.Point
}

func EncodeQuad(d []byte, q Quad) {
	bo := binary.LittleEndian
	bo.PutUint32(d[0:], math.Float32bits(q.From.X))
	bo.PutUint32(d[4:], math.Float32bits(q.From.Y))
	bo.PutUint32(d[8:], math.Float32bits(q.Ctrl.X))
	bo.PutUint32(d[12:], math.Float32bits(q.Ctrl.Y))
	bo.PutUint32(d[16:], math.Float32bits(q.To.X))
	bo.PutUint32(d[20:], math.Float32bits(q.To.Y))
}

func DecodeQuad(d []byte) (q Quad) {
	bo := binary.LittleEndian
	q.From.X = math.Float32frombits(bo.Uint32(d[0:]))
	q.From.Y = math.Float32frombits(bo.Uint32(d[4:]))
	q.Ctrl.X = math.Float32frombits(bo.Uint32(d[8:]))
	q.Ctrl.Y = math.Float32frombits(bo.Uint32(d[12:]))
	q.To.X = math.Float32frombits(bo.Uint32(d[16:]))
	q.To.Y = math.Float32frombits(bo.Uint32(d[20:]))
	return
}

func DecodeTransformOp(d []byte) op.TransformOp {
	bo := binary.LittleEndian
	if opconst.OpType(d[0]) != opconst.TypeTransform {
diff --git a/internal/path/path.go b/internal/path/path.go
deleted file mode 100644
index 04dfd6d..0000000
--- a/internal/path/path.go
@@ -1,27 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT

package path

import (
	"unsafe"
)

// The vertex data suitable for passing to vertex programs.
type Vertex struct {
	// Corner encodes the corner: +0.5 for south, +.25 for east.
	Corner       float32
	MaxY         float32
	FromX, FromY float32
	CtrlX, CtrlY float32
	ToX, ToY     float32
}

const VertStride = 7*4 + 2*2

func init() {
	// Check that struct vertex has the expected size and
	// that it contains no padding.
	if unsafe.Sizeof(*(*Vertex)(nil)) != VertStride {
		panic("unexpected struct size")
	}
}
diff --git a/op/clip/clip.go b/op/clip/clip.go
index f77d5a8..59554b5 100644
--- a/op/clip/clip.go
+++ b/op/clip/clip.go
@@ -9,7 +9,7 @@ import (

	"gioui.org/f32"
	"gioui.org/internal/opconst"
	"gioui.org/internal/path"
	"gioui.org/internal/ops"
	"gioui.org/op"
)

@@ -21,12 +21,11 @@ import (
// Path generates no garbage and can be used for dynamic paths; path
// data is stored directly in the Ops list supplied to Begin.
type Path struct {
	ops       *op.Ops
	contour   int
	pen       f32.Point
	bounds    f32.Rectangle
	hasBounds bool
	macro     op.MacroOp
	ops     *op.Ops
	contour int
	pen     f32.Point
	macro   op.MacroOp
	start   f32.Point
}

// Op sets the current clip to the intersection of
@@ -54,22 +53,24 @@ func (p Op) Add(o *op.Ops) {
func (p *Path) Begin(ops *op.Ops) {
	p.ops = ops
	p.macro = op.Record(ops)
	// Write the TypeAux opcode and a byte for marking whether the
	// path has had its MaxY filled out. If not, the gpu will fill it
	// before using it.
	data := ops.Write(2)
	// Write the TypeAux opcode
	data := ops.Write(opconst.TypeAuxLen)
	data[0] = byte(opconst.TypeAux)
}

// MoveTo moves the pen to the given position.
func (p *Path) Move(to f32.Point) {
	p.end()
	to = to.Add(p.pen)
	p.end()
	p.pen = to
	p.start = to
}

// end completes the current contour.
func (p *Path) end() {
	if p.pen != p.start {
		p.lineTo(p.start)
	}
	p.contour++
}

@@ -93,56 +94,15 @@ func (p *Path) Quad(ctrl, to f32.Point) {
}

func (p *Path) quadTo(ctrl, to f32.Point) {
	// Zero width curves don't contribute to stenciling.
	if p.pen.X == to.X && p.pen.X == ctrl.X {
		p.pen = to
		return
	}

	bounds := f32.Rectangle{
		Min: p.pen,
		Max: to,
	}.Canon()

	// If the curve contain areas where a vertical line
	// intersects it twice, split the curve in two x monotone
	// lower and upper curves. The stencil fragment program
	// expects only one intersection per curve.

	// Find the t where the derivative in x is 0.
	v0 := ctrl.Sub(p.pen)
	v1 := to.Sub(ctrl)
	d := v0.X - v1.X
	// t = v0 / d. Split if t is in ]0;1[.
	if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
		t := v0.X / d
		ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t))
		ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
		mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
		p.simpleQuadTo(ctrl0, mid)
		p.simpleQuadTo(ctrl1, to)
		if mid.X > bounds.Max.X {
			bounds.Max.X = mid.X
		}
		if mid.X < bounds.Min.X {
			bounds.Min.X = mid.X
		}
	} else {
		p.simpleQuadTo(ctrl, to)
	}
	// Find the y extremum, if any.
	d = v0.Y - v1.Y
	if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
		t := v0.Y / d
		y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
		if y > bounds.Max.Y {
			bounds.Max.Y = y
		}
		if y < bounds.Min.Y {
			bounds.Min.Y = y
		}
	}
	p.expand(bounds)
	data := p.ops.Write(ops.QuadSize + 4)
	bo := binary.LittleEndian
	bo.PutUint32(data[0:], uint32(p.contour))
	ops.EncodeQuad(data[4:], ops.Quad{
		From: p.pen,
		Ctrl: ctrl,
		To:   to,
	})
	p.pen = to
}

// Cube records a cubic Bézier from the pen through
@@ -223,68 +183,12 @@ func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Po
	return splits
}

func (p *Path) expand(b f32.Rectangle) {
	if !p.hasBounds {
		p.hasBounds = true
		inf := float32(math.Inf(+1))
		p.bounds = f32.Rectangle{
			Min: f32.Point{X: inf, Y: inf},
			Max: f32.Point{X: -inf, Y: -inf},
		}
	}
	p.bounds = p.bounds.Union(b)
}

func (p *Path) vertex(cornerx, cornery int16, ctrl, to f32.Point) {
	var corner float32
	// Encode corner.
	if cornerx == 1 {
		corner += .5
	}
	if cornery == 1 {
		corner += .25
	}
	v := path.Vertex{
		Corner: corner,
		FromX:  p.pen.X,
		FromY:  p.pen.Y,
		CtrlX:  ctrl.X,
		CtrlY:  ctrl.Y,
		ToX:    to.X,
		ToY:    to.Y,
	}
	data := p.ops.Write(path.VertStride)
	bo := binary.LittleEndian
	bo.PutUint32(data[0:], math.Float32bits(corner))
	// Put the contour index in MaxY.
	bo.PutUint32(data[4:], uint32(p.contour))
	bo.PutUint32(data[8:], math.Float32bits(v.FromX))
	bo.PutUint32(data[12:], math.Float32bits(v.FromY))
	bo.PutUint32(data[16:], math.Float32bits(v.CtrlX))
	bo.PutUint32(data[20:], math.Float32bits(v.CtrlY))
	bo.PutUint32(data[24:], math.Float32bits(v.ToX))
	bo.PutUint32(data[28:], math.Float32bits(v.ToY))
}

func (p *Path) simpleQuadTo(ctrl, to f32.Point) {
	// NW.
	p.vertex(-1, 1, ctrl, to)
	// NE.
	p.vertex(1, 1, ctrl, to)
	// SW.
	p.vertex(-1, -1, ctrl, to)
	// SE.
	p.vertex(1, -1, ctrl, to)
	p.pen = to
}

// End the path and return a clip operation that represents it.
func (p *Path) End() Op {
	p.end()
	c := p.macro.Stop()
	return Op{
		call:   c,
		bounds: p.bounds,
		call: c,
	}
}

@@ -332,6 +236,7 @@ func roundRect(ops *op.Ops, r f32.Rectangle, se, sw, nw, ne float32) Op {
	var p Path
	p.Begin(ops)
	p.Move(r.Min)

	p.Move(f32.Point{X: w, Y: h - se})
	p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE
	p.Line(f32.Point{X: sw - w + se, Y: 0})
-- 
2.25.1

[PATCH v2 03/15] f32: implement 2D affine transformations

Details
Message ID
<20200620213001.137-3-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +275 -0
Implements 2D affine transformations. This commit is a step
towards full affine transformations for drawing operations.

Heavily based on the work by Péter Szilágyi in patch 9212

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 f32/affine.go      | 154 +++++++++++++++++++++++++++++++++++++++++++++
 f32/affine_test.go | 121 +++++++++++++++++++++++++++++++++++
 2 files changed, 275 insertions(+)
 create mode 100644 f32/affine.go
 create mode 100644 f32/affine_test.go

diff --git a/f32/affine.go b/f32/affine.go
new file mode 100644
index 0000000..e1e7418
--- /dev/null
+++ b/f32/affine.go
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Unlicense OR MIT

package f32

import (
	"math"
)

// Affine2D represents an affine 2D transformation. The zero value if Affine2D
// represents the identity transform.
type Affine2D struct {
	// in order to make the zero value of Affine2D represent the identity
	// transform we store it with the identity matrix subtracted, that is
	// if the actual transformaiton matrix is:
	// [sx, hx, ox]
	// [hy, sy, oy]
	// [ 0,  0,  1]
	// we store a = sx-1 and e = sy-1
	a, b, c float32
	d, e, f float32
}

// NewAffine2D creates a new Affine2D transform from the matrix elements
// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D {
	return Affine2D{
		a: sx, b: hx, c: ox,
		d: hy, e: sy, f: oy,
	}.encode()
}

// Offset the transformation.
func (a Affine2D) Offset(offset Point) Affine2D {
	return Affine2D{
		a.a, a.b, a.c + offset.X,
		a.d, a.e, a.f + offset.Y,
	}
}

// Scale the transformation around the given origin.
func (a Affine2D) Scale(origin, factor Point) Affine2D {
	if origin == (Point{}) {
		return a.scale(factor)
	}
	a = a.Offset(origin.Mul(-1))
	a = a.scale(factor)
	return a.Offset(origin)
}

// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin.
func (a Affine2D) Rotate(origin Point, radians float32) Affine2D {
	if origin == (Point{}) {
		return a.rotate(radians)
	}
	a = a.Offset(origin.Mul(-1))
	a = a.rotate(radians)
	return a.Offset(origin)
}

// Shear the transformation by the given angle (in radians) around the given origin.
func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D {
	if origin == (Point{}) {
		return a.shear(radiansX, radiansY)
	}
	a = a.Offset(origin.Mul(-1))
	a = a.shear(radiansX, radiansY)
	return a.Offset(origin)
}

// Mul returns A*B.
func (A Affine2D) Mul(B Affine2D) (r Affine2D) {
	A, B = A.decode(), B.decode()
	r.a = A.a*B.a + A.b*B.d
	r.b = A.a*B.b + A.b*B.e
	r.c = A.a*B.c + A.b*B.f + A.c
	r.d = A.d*B.a + A.e*B.d
	r.e = A.d*B.b + A.e*B.e
	r.f = A.d*B.c + A.e*B.f + A.f
	return r.encode()
}

// Invert the transformation. Note that if the matrix is close to singular
// numerical errors may become large or infinity.
func (a Affine2D) Invert() Affine2D {
	if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 {
		return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f}
	}
	a = a.decode()
	det := a.a*a.e - a.b*a.d
	a.a, a.e = a.e/det, a.a/det
	a.b, a.d = -a.b/det, -a.d/det
	temp := a.c
	a.c = -a.a*a.c - a.b*a.f
	a.f = -a.d*temp - a.e*a.f
	return a.encode()
}

// Transform p by returning a*p.
func (a Affine2D) Transform(p Point) Point {
	a = a.decode()
	return Point{
		X: p.X*a.a + p.Y*a.b + a.c,
		Y: p.X*a.d + p.Y*a.e + a.f,
	}
}

// Elems returns the matrix elements of the transform in row-major order. The
// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) {
	a = a.decode()
	return a.a, a.b, a.c, a.d, a.e, a.f
}

func (a Affine2D) encode() Affine2D {
	// since we store with identity matrix subtracted
	a.a -= 1
	a.e -= 1
	return a
}

func (a Affine2D) decode() Affine2D {
	// since we store with identity matrix subtracted
	a.a += 1
	a.e += 1
	return a
}

func (a Affine2D) scale(factor Point) Affine2D {
	a = a.decode()
	return Affine2D{
		a.a * factor.X, a.b * factor.X, a.c * factor.X,
		a.d * factor.Y, a.e * factor.Y, a.f * factor.Y,
	}.encode()
}

func (a Affine2D) rotate(radians float32) Affine2D {
	sin, cos := math.Sincos(float64(radians))
	s, c := float32(sin), float32(cos)
	a = a.decode()
	return Affine2D{
		a.a*c - a.d*s, a.b*c - a.e*s, a.c*c - a.f*s,
		a.a*s + a.d*c, a.b*s + a.e*c, a.c*s + a.f*c,
	}.encode()
}

func (a Affine2D) shear(radiansX, radiansY float32) Affine2D {
	tx := float32(math.Tan(float64(radiansX)))
	ty := float32(math.Tan(float64(radiansY)))
	a = a.decode()
	return Affine2D{
		a.a + a.d*tx, a.b + a.e*tx, a.c + a.f*tx,
		a.a*ty + a.d, a.b*ty + a.e, a.f*ty + a.f,
	}.encode()
}
diff --git a/f32/affine_test.go b/f32/affine_test.go
new file mode 100644
index 0000000..38c159f
--- /dev/null
+++ b/f32/affine_test.go
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: Unlicense OR MIT

package f32

import (
	"math"
	"testing"
)

func eq(p1, p2 Point) bool {
	tol := 1e-5
	dx, dy := p2.X-p1.X, p2.Y-p1.Y
	return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol
}

func TestTransformOffset(t *testing.T) {
	p := Point{X: 1, Y: 2}
	o := Point{X: 2, Y: -3}

	r := Affine2D{}.Offset(o).Transform(p)
	if !eq(r, Pt(3, -1)) {
		t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r)
	}
	i := Affine2D{}.Offset(o).Invert().Transform(r)
	if !eq(i, p) {
		t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
	}
}

func TestTransformScale(t *testing.T) {
	p := Point{X: 1, Y: 2}
	s := Point{X: -1, Y: 2}

	r := Affine2D{}.Scale(Point{}, s).Transform(p)
	if !eq(r, Pt(-1, 4)) {
		t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r)
	}
	i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r)
	if !eq(i, p) {
		t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p)
	}
}

func TestTransformRotate(t *testing.T) {
	p := Point{X: 1, Y: 0}
	a := float32(math.Pi / 2)

	r := Affine2D{}.Rotate(Point{}, a).Transform(p)
	if !eq(r, Pt(0, 1)) {
		t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r)
	}
	i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r)
	if !eq(i, p) {
		t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p)
	}
}

func TestTransformShear(t *testing.T) {
	p := Point{X: 1, Y: 1}

	r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p)
	if !eq(r, Pt(2, 1)) {
		t.Errorf("shear transformation mismatch: have %v, want {2 1}", r)
	}
	i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
	if !eq(i, p) {
		t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p)
	}
}

func TestTransformMultiply(t *testing.T) {
	p := Point{X: 1, Y: 2}
	o := Point{X: 2, Y: -3}
	s := Point{X: -1, Y: 2}
	a := float32(-math.Pi / 2)

	r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p)
	if !eq(r, Pt(1, 3)) {
		t.Errorf("complex transformation mismatch: have %v, want {1 3}", r)
	}
	i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
	if !eq(i, p) {
		t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p)
	}
}

func TestTransformScaleAround(t *testing.T) {
	p := Pt(-1, -1)
	target := Pt(-6, -13)
	pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p)
	if !eq(pt, target) {
		t.Log(pt, "!=", target)
		t.Error("Scale not as expected")
	}
}

func TestTransformRotateAround(t *testing.T) {
	p := Pt(-1, -1)
	pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p)
	target := Pt(-1, 3)
	if !eq(pt, target) {
		t.Log(pt, "!=", target)
		t.Error("Rotate not as expected")
	}
}

func TestMulOrder(t *testing.T) {
	A := Affine2D{}.Offset(Pt(100, 100))
	B := Affine2D{}.Scale(Point{}, Pt(2, 2))
	_ = A
	_ = B

	T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2))
	T2 := B.Mul(A)

	if T1 != T2 {
		t.Log(T1)
		t.Log(T2)
		t.Error("multiplication / transform order not as expected")
	}
}
-- 
2.25.1

[PATCH v2 04/15] gpu, io/router, op: use f32.Affine2D instead of op.TransformOp for transforms

Details
Message ID
<20200620213001.137-4-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +32 -18
Encode TransformOp as an Affince2D matrix instead and use that in gpu and io transform handling.
There are no changes to user facing API and so far only the offset part of the matrix is used.

This patch is a step towards full affine transformations.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/gpu.go              |  6 +++---
 internal/opconst/ops.go |  2 +-
 internal/ops/ops.go     | 23 ++++++++++++++++-------
 io/router/pointer.go    | 10 +++++-----
 op/op.go                |  9 +++++++--
 5 files changed, 32 insertions(+), 18 deletions(-)

diff --git a/gpu/gpu.go b/gpu/gpu.go
index 334f03a..0cb78a4 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -69,7 +69,7 @@ type drawOps struct {

type drawState struct {
	clip  f32.Rectangle
	t     op.TransformOp
	t     f32.Affine2D
	cpath *pathOp
	rect  bool
	z     int
@@ -691,8 +691,8 @@ loop:
		case opconst.TypeProfile:
			d.profile = true
		case opconst.TypeTransform:
			dop := ops.DecodeTransformOp(encOp.Data)
			state.t = state.t.Multiply(op.TransformOp(dop))
			dop := ops.DecodeTransform(encOp.Data)
			state.t = state.t.Mul(dop)
		case opconst.TypeAux:
			aux = encOp.Data[opconst.TypeAuxLen:]
			auxKey = encOp.Key
diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go
index aca341f..62365b8 100644
--- a/internal/opconst/ops.go
+++ b/internal/opconst/ops.go
@@ -31,7 +31,7 @@ const (
const (
	TypeMacroLen        = 1 + 4 + 4
	TypeCallLen         = 1 + 4 + 4
	TypeTransformLen    = 1 + 4*2
	TypeTransformLen    = 1 + 4*6
	TypeLayerLen        = 1
	TypeRedrawLen       = 1 + 8
	TypeImageLen        = 1 + 4*4
diff --git a/internal/ops/ops.go b/internal/ops/ops.go
index 80994e1..721f462 100644
--- a/internal/ops/ops.go
+++ b/internal/ops/ops.go
@@ -8,7 +8,6 @@ import (

	"gioui.org/f32"
	"gioui.org/internal/opconst"
	"gioui.org/op"
)

const QuadSize = 4 * 2 * 3
@@ -38,13 +37,23 @@ func DecodeQuad(d []byte) (q Quad) {
	return
}

func DecodeTransformOp(d []byte) op.TransformOp {
	bo := binary.LittleEndian
func DecodeTransform(d []byte) (t f32.Affine2D) {
	if opconst.OpType(d[0]) != opconst.TypeTransform {
		panic("invalid op")
	}
	return op.TransformOp{}.Offset(f32.Point{
		X: math.Float32frombits(bo.Uint32(d[1:])),
		Y: math.Float32frombits(bo.Uint32(d[5:])),
	})
	if len(d) < 1+6*4 {
		panic("too short buffer")
	}
	return decodeAffine2D(d[1:])
}

func decodeAffine2D(data []byte) f32.Affine2D {
	bo := binary.LittleEndian
	a := math.Float32frombits(bo.Uint32(data))
	b := math.Float32frombits(bo.Uint32(data[4*1:]))
	c := math.Float32frombits(bo.Uint32(data[4*2:]))
	d := math.Float32frombits(bo.Uint32(data[4*3:]))
	e := math.Float32frombits(bo.Uint32(data[4*4:]))
	f := math.Float32frombits(bo.Uint32(data[4*5:]))
	return f32.NewAffine2D(a, b, c, d, e, f)
}
diff --git a/io/router/pointer.go b/io/router/pointer.go
index ec3bb39..0d0977f 100644
--- a/io/router/pointer.go
+++ b/io/router/pointer.go
@@ -56,7 +56,7 @@ type areaOp struct {
}

type areaNode struct {
	trans op.TransformOp
	trans f32.Affine2D
	next  int
	area  areaOp
}
@@ -68,7 +68,7 @@ const (
	areaEllipse
)

func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t op.TransformOp, area, node int, pass bool) {
func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t f32.Affine2D, area, node int, pass bool) {
	for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
		switch opconst.OpType(encOp.Data[0]) {
		case opconst.TypePush:
@@ -90,8 +90,8 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t o
			})
			node = len(q.hitTree) - 1
		case opconst.TypeTransform:
			dop := ops.DecodeTransformOp(encOp.Data)
			t = t.Multiply(op.TransformOp(dop))
			dop := ops.DecodeTransform(encOp.Data)
			t = t.Mul(dop)
		case opconst.TypePointerInput:
			op := decodePointerInputOp(encOp.Data, encOp.Refs)
			q.hitTree = append(q.hitTree, hitNode{
@@ -175,7 +175,7 @@ func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) {
	q.hitTree = q.hitTree[:0]
	q.areas = q.areas[:0]
	q.reader.Reset(root)
	q.collectHandlers(&q.reader, events, op.TransformOp{}, -1, -1, false)
	q.collectHandlers(&q.reader, events, f32.Affine2D{}, -1, -1, false)
	for k, h := range q.handlers {
		if !h.active {
			q.dropHandlers(events, k)
diff --git a/op/op.go b/op/op.go
index 82f1457..3aafbc3 100644
--- a/op/op.go
+++ b/op/op.go
@@ -290,8 +290,13 @@ func (t TransformOp) Add(o *Ops) {
	data := o.Write(opconst.TypeTransformLen)
	data[0] = byte(opconst.TypeTransform)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], math.Float32bits(t.offset.X))
	bo.PutUint32(data[5:], math.Float32bits(t.offset.Y))
	// write it out as an affine matrix although we only support offset yet
	bo.PutUint32(data[1:], math.Float32bits(1.0))
	bo.PutUint32(data[1+4*1:], math.Float32bits(0))
	bo.PutUint32(data[1+4*2:], math.Float32bits(t.offset.X))
	bo.PutUint32(data[1+4*3:], math.Float32bits(0))
	bo.PutUint32(data[1+4*4:], math.Float32bits(1))
	bo.PutUint32(data[1+4*5:], math.Float32bits(t.offset.Y))
}

func (s *stack) push() stackID {
-- 
2.25.1

[PATCH v2 06/15] internal/rendertest: create test suit for drawing operations

Details
Message ID
<20200620213001.137-6-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +460 -10
Uses app/headless to create a set of test cases for drawing operations, including clipping
textures and transforms. This commit tests for approximate pixel matches, if future changes affect
local drawing operations it will be easy to change the reference images, it thus becomes and
should be an intentional operation if changes lead to local changes in drawn results.

Ideally we should be able to make the tests check for exact pixel matches down the line to ensure
consistent results between platforms.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 internal/rendertest/bench_test.go             |  23 ++
 internal/rendertest/clip_test.go              |  73 +++++++
 internal/rendertest/doc.go                    |   2 +
 internal/rendertest/refs/TestClipOffset.png   | Bin 0 -> 383 bytes
 .../rendertest/refs/TestClipPaintOffset.png   | Bin 0 -> 383 bytes
 internal/rendertest/refs/TestClipRotate.png   | Bin 0 -> 546 bytes
 internal/rendertest/refs/TestClipScale.png    | Bin 0 -> 383 bytes
 .../refs/TestComplicatedTransform.png         | Bin 0 -> 1116 bytes
 .../rendertest/refs/TestNoClipFromPaint.png   | Bin 0 -> 374 bytes
 .../refs/TestOffsetScaleTexture.png           | Bin 0 -> 470 bytes
 .../rendertest/refs/TestOffsetTexture.png     | Bin 0 -> 539 bytes
 .../rendertest/refs/TestPaintClippedCirle.png | Bin 0 -> 571 bytes
 .../rendertest/refs/TestPaintClippedRect.png  | Bin 0 -> 382 bytes
 .../refs/TestPaintClippedTexture.png          | Bin 0 -> 392 bytes
 internal/rendertest/refs/TestPaintOffset.png  | Bin 0 -> 387 bytes
 internal/rendertest/refs/TestPaintRect.png    | Bin 0 -> 374 bytes
 internal/rendertest/refs/TestPaintRotate.png  | Bin 0 -> 1436 bytes
 internal/rendertest/refs/TestPaintShear.png   | Bin 0 -> 511 bytes
 internal/rendertest/refs/TestPaintTexture.png | Bin 0 -> 408 bytes
 .../rendertest/refs/TestRepeatedPaintsZ.png   | Bin 0 -> 376 bytes
 internal/rendertest/refs/TestReuseStencil.png | Bin 0 -> 383 bytes
 .../rendertest/refs/TestRotateClipTexture.png | Bin 0 -> 1122 bytes
 .../rendertest/refs/TestRotateTexture.png     | Bin 0 -> 864 bytes
 .../rendertest/refs/TestTransformMacro.png    | Bin 0 -> 375 bytes
 .../rendertest/refs/TestTransformOrder.png    | Bin 0 -> 384 bytes
 internal/rendertest/render_test.go            | 144 +++++++++++++
 internal/rendertest/transform_test.go         | 201 ++++++++++++++++++
 internal/rendertest/util_test.go              |  27 ++-
 28 files changed, 460 insertions(+), 10 deletions(-)
 create mode 100644 internal/rendertest/clip_test.go
 create mode 100644 internal/rendertest/doc.go
 create mode 100644 internal/rendertest/refs/TestClipOffset.png
 create mode 100644 internal/rendertest/refs/TestClipPaintOffset.png
 create mode 100644 internal/rendertest/refs/TestClipRotate.png
 create mode 100644 internal/rendertest/refs/TestClipScale.png
 create mode 100644 internal/rendertest/refs/TestComplicatedTransform.png
 create mode 100644 internal/rendertest/refs/TestNoClipFromPaint.png
 create mode 100644 internal/rendertest/refs/TestOffsetScaleTexture.png
 create mode 100644 internal/rendertest/refs/TestOffsetTexture.png
 create mode 100644 internal/rendertest/refs/TestPaintClippedCirle.png
 create mode 100644 internal/rendertest/refs/TestPaintClippedRect.png
 create mode 100644 internal/rendertest/refs/TestPaintClippedTexture.png
 create mode 100644 internal/rendertest/refs/TestPaintOffset.png
 create mode 100644 internal/rendertest/refs/TestPaintRect.png
 create mode 100644 internal/rendertest/refs/TestPaintRotate.png
 create mode 100644 internal/rendertest/refs/TestPaintShear.png
 create mode 100644 internal/rendertest/refs/TestPaintTexture.png
 create mode 100644 internal/rendertest/refs/TestRepeatedPaintsZ.png
 create mode 100644 internal/rendertest/refs/TestReuseStencil.png
 create mode 100644 internal/rendertest/refs/TestRotateClipTexture.png
 create mode 100644 internal/rendertest/refs/TestRotateTexture.png
 create mode 100644 internal/rendertest/refs/TestTransformMacro.png
 create mode 100644 internal/rendertest/refs/TestTransformOrder.png
 create mode 100644 internal/rendertest/render_test.go
 create mode 100644 internal/rendertest/transform_test.go

diff --git a/internal/rendertest/bench_test.go b/internal/rendertest/bench_test.go
index fe9dfba..7eab542 100644
--- a/internal/rendertest/bench_test.go
+++ b/internal/rendertest/bench_test.go
@@ -97,6 +97,29 @@ func BenchmarkDrawUI(b *testing.B) {
	finishBenchmark(b, w)
}

func BenchmarkDrawUITransformed(b *testing.B) {
	// Like BenchmarkDraw UI but transformed at every frame
	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)
		angle := float32(math.Mod(float64(i)/1000, 0.05))
		a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle)
		op.Affine(a).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
diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go
new file mode 100644
index 0000000..f0a9285
--- /dev/null
+++ b/internal/rendertest/clip_test.go
@@ -0,0 +1,73 @@
package rendertest

import (
	"testing"

	"gioui.org/f32"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"golang.org/x/image/colornames"
)

func TestPaintRect(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.Red)
		r.expect(49, 0, colornames.Red)
		r.expect(50, 0, colornames.White)
		r.expect(10, 50, colornames.White)
	})
}

func TestPaintClippedRect(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		clip.Rect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(24, 35, colornames.White)
		r.expect(25, 35, colornames.Red)
		r.expect(50, 0, colornames.White)
		r.expect(10, 50, colornames.White)
	})
}

func TestPaintClippedCirle(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		r := float32(10)
		clip.Rect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, NE: r}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 30, 50)}.Add(o)
	}, func(r result) {
		r.expect(21, 21, colornames.White)
		r.expect(25, 30, colornames.Red)
		r.expect(31, 30, colornames.White)
	})
}

func TestPaintTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 80, 80)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.Blue)
		r.expect(79, 10, colornames.Green)
		r.expect(80, 0, colornames.White)
		r.expect(10, 80, colornames.White)
	})
}

func TestPaintClippedTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)
		clip.Rect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 80, 80)}.Add(o)
	}, func(r result) {
		r.expect(40, 40, colornames.White)
		r.expect(25, 35, colornames.Blue)
	})
}
diff --git a/internal/rendertest/doc.go b/internal/rendertest/doc.go
new file mode 100644
index 0000000..1f6b99b
--- /dev/null
+++ b/internal/rendertest/doc.go
@@ -0,0 +1,2 @@
// Package rendertest is intended for testing of drawing ops only.
package rendertest
diff --git a/internal/rendertest/refs/TestClipOffset.png b/internal/rendertest/refs/TestClipOffset.png
new file mode 100644
index 0000000000000000000000000000000000000000..59371e421b591a7e1a4519e0c54c2f0bfe826af4
GIT binary patch
literal 383
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V080zaSW-L^XAq;P8LN0hJ&9C
z{!h6pQ#P^6=X$*0tjgZsb3S)}zRo{m9YX};2Brko16&QF4B8A~3~PuN_;z&n^{V=^
zGxOi75vldfGrx*6ao@X%RZoIr$Z$V?E$R#m|NqDR;9xq-9nl61SO!m5KbLh*2~7Zf
C$ZR|S

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestClipPaintOffset.png b/internal/rendertest/refs/TestClipPaintOffset.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9988440947dd18557f702afc1bf83e36afe2b56
GIT binary patch
literal 383
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V080zaSW-L^X8@@*8v3qhJzA+
z{!c&4@ARNc*XVMWf=;;1yW(@M&)3O+C}%pt(!nVptf1!L(=da<kTD6Xz}t&&x8C#n
zYgV7YgsEV&?~QfVX;>t%8b#Q$!Jxt)7?%J47rx|RT2rfP0pu`vy85}Sb4q9e0EJU&
ASO5S3

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestClipRotate.png b/internal/rendertest/refs/TestClipRotate.png
new file mode 100644
index 0000000000000000000000000000000000000000..e6c0e4e5791c995de879fb8be7e990f3e6641bc7
GIT binary patch
literal 546
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V0`K6;uumf=gpl!ug3ulZVyWe
zGT%AH#Fj|L_U|@uR#&!ItXaD&{^LA;`5F$1f0GkbZnWp#o@TNC3+I`23=xbQm=ahI
za5acBXfuQ{tRYpP%%Abx^QTW!XTFhlll#J1V=;gF@#FjN+uQ0c7q*)p`0g>^k3IiB
zfBy8#>U?bhJEPtD{j*m#@_i`%d)I_Fqhaya@?w($#&tl^1&mw;r+*a-Fy_VA$JYN*
ze))l6?f>-~8KeGpHv$Rk#Y|iNH7WvGHzqLT=bOu#-Tx(io8kTY+q?2k7ro7n)o(wW
zenPguDpv1psjW@Ix$OV_f%{w5o6M;AyRm=aeX|+We;?L&{Joy=>+4bNlY0;5{rak3
zJ;UGkPW_87uXj)X<on-o|D-u_6b1(wAxMmtH|4<S{{O$9?bzFl?Pdi)4uhwwpUXO@
GgeCx-(9jY9

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestClipScale.png b/internal/rendertest/refs/TestClipScale.png
new file mode 100644
index 0000000000000000000000000000000000000000..59371e421b591a7e1a4519e0c54c2f0bfe826af4
GIT binary patch
literal 383
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V080zaSW-L^XAq;P8LN0hJ&9C
z{!h6pQ#P^6=X$*0tjgZsb3S)}zRo{m9YX};2Brko16&QF4B8A~3~PuN_;z&n^{V=^
zGxOi75vldfGrx*6ao@X%RZoIr$Z$V?E$R#m|NqDR;9xq-9nl61SO!m5KbLh*2~7Zf
C$ZR|S

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestComplicatedTransform.png b/internal/rendertest/refs/TestComplicatedTransform.png
new file mode 100644
index 0000000000000000000000000000000000000000..33042c5e867b99532fa34e0de841d3c57dac3047
GIT binary patch
literal 1116
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U{UsTaSW-L^X9g%k8-Na@sIzd
z^zZeE+vLosauD{ksVXjyHYw~q_WGUKtQk3H)a1S;I63vS6nsgtnGj+q*i_Uo+fl3I
zlTyV4U6~IF6|)VeY|Gj>E$C9iwS-0A7;G+ov;RE1tnU7s)Q2zYcmI9Ey!reTckQWN
z3RZTO=l;#J{Xg}5+P@PiTr<A}T7~*7J^k`ZX4cQQPrq#|J0bjeXLcRalcG&qRcFsO
zitg+GXIAw!>33u9*2p?r(}+3-8JQEWR{8jxc#v?_{r5Uac?mXVenv0V$jFHw3ne9X
zq_FbZFr5%BGVZ>~f3Lo*_U;ywFKqoU`mA+i_<Z?G>#kI$FD?GN)AKX)uQPupx*R!a
zWp!S^V&&V0pq*1<{L0RkzyG)M_kZ`d>r-Bcer|T&!!N?1HTBdj|Lnb!_nut*gmwPC
z*;5|gex{jdnjxYPe@1<u-<Ll(imxh3#l$%?NK1eEuu$j78T~nLR8xPXe%|y!@(O1`
znObtPQ2+nm)#Caxyn1WqtzM86`0MxTxY}}Y<4;#|D~nI<(Mh@PYWlt*o}c@D-6R>$
zr}{tcywm;sc>ay)$G7ulYyIyrddu?raoOu%$<OQ!Z(L>D@c;C`zY~84A3gnFaraHG
zkCrbxW=-EIk$OAH^yl<NQVge-KV8Sadz#-gp4TywXT03c?9pLi+xnJ4{k4mb;AbnT
z9qYE`9}1hDYQN#^>pL+bwSrUM=7+kuDm^lJyZy<m^-U(ccKsI@%sU;B+blUX&^IgK
zdFF~$j^Q(SU)lF9YxLs1%O7BN<EL9{NU@|zm9yLBgeR^yXNE~$bz$QwKN7+l$2Gh0
zoUUgsvw3=%ZqkCv;CqGV{-1kx#~`P+{GYvPZy=-n?rVEA^COdX-(XwEr)8BSG(T8U
z^uy-tr{%6Iq+AXsEZ|v?U$Fit?-#}?yz}}_?X8>|c+lhni^J^)VcgOctPdIwHl13r
zK(c~kL2g0tyKM^^w=<k#JAY@&?)$5IxcVBOGu&b`k2-UJpMjr8EYywl-~n}p+-C77
ze%%s3ei<hzJ^J$}{K>JEm3O7r&%XX<kGj3hdZSZy&x2o|eElp??umKBr|Rf?vjkq7
zP7Pe~ecO-ksxS7NoL^^O9Xdxp)NSFpb&S>xq3kyp*H|$%C^J|LJ7ULvW(J1;|99Ma
WAlDx8RUTMsFnGH9xvX<aXaWG&_SzKy

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestNoClipFromPaint.png b/internal/rendertest/refs/TestNoClipFromPaint.png
new file mode 100644
index 0000000000000000000000000000000000000000..b382f8737ff9296da870a41d11d2fb0e9baa6b7f
GIT binary patch
literal 374
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V6^pgaSW-L^X5(<=K%*E)`LOk
z_CLMi`dq=9OHSwK*~)b$##M*><7^XlGbOMd;A#+M&}Il@Si=y(xPc}Do1dFsox6*M
h7EsI8`+!0C|9>h2+y5Q=lRg1C44$rjF6*2UngB2EWfA}Y

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestOffsetScaleTexture.png b/internal/rendertest/refs/TestOffsetScaleTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..42ebc4bc1b3aaf4649c07ae168ca11814c6705da
GIT binary patch
literal 470
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V4Ume;uumf=gqByzONlb90Gr9
zPyO5VqG3bJrWYz94(x6@k(Eo-V?EAAx<6hXbF+K-PMPByFa8YY|L}sPgHu3QLCwLZ
zVFrUCV-nL5guso|m)C#WeSh-$PUQ8Sy}#~+9RIv><sL>fRZwl$${cTBzI|iz{rqJ2
zzvot-yE*+`+dKX8eU8#|UOoB$N>~J|B{_wX<?p%YoLp%pzWE;$s`XHV#mXK3=ifHl
zqWiOEuI2OURs9<mvn{teesqnz`_36(6A;cta)0z~R>O1Bxcr5r5UZoG1O>TT;u#qj
a{{I)U(akqJ7On`4F9uInKbLh*2~7a`vZBKP

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestOffsetTexture.png b/internal/rendertest/refs/TestOffsetTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f9f029d8aaaae5de06af1f46a1ea5f48270d92e
GIT binary patch
literal 539
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V0`51;uumf=gl3%tSOEX4G$wd
zb(3Vju+4J_TJnKM&RJTw_vx}v5^+x4>k}7SHnf;%*7qBHK78o)qt935oll(pbM*KI
z@!Q*Yt^8lvKd4uC@M)OAV91!nbcCgYQ$Scj4I!Xc<}1&~?fgFP_~!o48)vtg{`h)#
zN0sH=TXu6ly4~ePR}Rr%cH^tb#}EI??7qA{TTx*rU;VYz?sJ7@-M)E#x%cDF^#2Nf
z8+}YS_om#vzW3b!C$4T?e5~;0isEO&A}Dsl9Fz0h=-lBo!Nttj9LUn~wnn_9)_i{D
z9+}4#vlh?$C;6S*eeUMDr+1v^zP&?!|Ma-vyE-zZ^)m5xKFiPjN@uTa!eP#B*21DZ
ogx6u#;|?4esrbvw!0`WnT8O2X4oBQ}V5Bp6y85}Sb4q9e06hG{;{X5v

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintClippedCirle.png b/internal/rendertest/refs/TestPaintClippedCirle.png
new file mode 100644
index 0000000000000000000000000000000000000000..9aa298e91a2e29d0f32ea857d01462be7a8e13c2
GIT binary patch
literal 571
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U}ExgaSW-L^X8V}sYd}K4i9HP
zynjw#?A9*Mzzc70>=0rt6&H(IVidJ&vPav?WiuqFls`PM=kwOa<3|sBA2N<VY|vt1
z|AH|qonZ!J0$YP5LmB}_{LJpJwKj5vdyhR8dR}ZZe`fdp<)4^JVso}{yuDO6qP>UV
zu)y+Qo#~w3SJfX&K3E^WK=ZHOiq}adzdoz(x2STy$@k*-jhstqD}!$GW!#?O_rL0@
z?1#2ZvKO{xHeXq#bD(a%naA2wd5*rH*RQ_UbnihR%Nm^nInzLbOl!g*YEmYFxjn}>
zMl3lSq}s2Pa5UfX{{2FmXJ>+z_FTX4(}u6TrF7L4!3CLhww!s}Bi0Ltt@in7{Gfb&
z_?`FvRDVvq!tiU3yPyH{0UicphBFL9mhoGXf#LuEH{F36y}?FPfQf>^)78&qol`;+
E06tdIp#T5?

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintClippedRect.png b/internal/rendertest/refs/TestPaintClippedRect.png
new file mode 100644
index 0000000000000000000000000000000000000000..4bd3e5cbbb6c7b40f9e49d9356b1652c33078dfa
GIT binary patch
literal 382
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V086#aSW-L^XAq;P8UT1)`O*n
z|EK)*<hhc#i%DSN%-g%)?~R|c|KEW!)&pD(q72#$VGL^+A{aL?B@hy@{&~YP*?;ZK
z?ovjg)FYdiQ!-;~ny={!Zc;5pHiSrbQmEw(Hv_}}|I;3GFeR!)T?B?JgQu&X%Q~lo
FCIEjNX%zqf

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintClippedTexture.png b/internal/rendertest/refs/TestPaintClippedTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee95e5b80e732f5de3bf515cb4c8a73208bffe80
GIT binary patch
literal 392
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^VD$HNaSW-L^XBG3PG>~{hCtqz
z)@6ALsxJ~1{_p+0$ly)p{p2@=IcFbV{JXQ_d;gwS{5EqL3>lM{j<9ra3J5ExIrucp
zz$#F@ru$`S-SxYEhwRPpC_^^r#+|;+n$mbpMOKd8-T_mwkCB1l|Nkc)+&`377gqp7
On8DN4&t;ucLK6VM;Big>

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintOffset.png b/internal/rendertest/refs/TestPaintOffset.png
new file mode 100644
index 0000000000000000000000000000000000000000..aa5d7d8f3a32a7c0814e4124d5d03027b3cbea96
GIT binary patch
literal 387
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^VD$2IaSW-L^X85r*AWE)hl`~j
z|Am{mPcW%G_~-g9o|<JsY3sJu75=MgSg*|x#;}GVf^h>=0_y>;22lpA0y%m2j#p$%
zi01osJdYW_Qo@E2sp9U@HO+a!dxNEMIf_Vg@OliN8L^BE4FCTxZsTBTIvH#S3|t0J
LS3j3^P6<r_@vdw}

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintRect.png b/internal/rendertest/refs/TestPaintRect.png
new file mode 100644
index 0000000000000000000000000000000000000000..0195a6be10a4f4e553e8c05b0128a0346c28b3da
GIT binary patch
literal 374
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V6^pgaSW-L^X5(<=K%*E)`LOk
z_CH<Sd5C4x;s*yO-JkQjr=<Peue+<+bHW+cFhnqJU`k*;z||nipv@3Q3xTq4f0oT!
l$xTZusN?Q8+zbr=|4YASV@T+@bpjZk44$rjF6*2UngCQ}Xf6N%

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintRotate.png b/internal/rendertest/refs/TestPaintRotate.png
new file mode 100644
index 0000000000000000000000000000000000000000..4cf9c851360266f95d97e57caddb4d79e2a7efa5
GIT binary patch
literal 1436
zcmchXeK^wz0LOm|8*ApRY|+h_B(cfcO&;%dOqocrYPHCQtZv@rHAmRdyyfMR&TC4X
zL~+OrEfJ0jXN%_Tu7<dVWNYXC@3}wkkMGO#{rmfTGEb0Pm7!Wt005MUZZ6(Cnf$L5
zckTG`a~q`qpb$WGarB8PT`Ki(^PwPlGMb}&W=?tT?Wz|}jRZ%9l7uY915+=dda4NZ
zlo2lLOsk<hLa4_j7+$}GWr22AseI4!K2hZm!YZ~RxI^>umHT5ZV8u=!`01N-)|*N6
z_{3(#$RVkDSKMN0+)y~JI*@!K<{=g0w^9q386{^y(UrSie5Gof2EDVeadb%a<cKX+
zrU?v_JYrM<+OHqJihE}<%Y?z+a?^C|_wDNz_RRY%DqT-*vxccuwrfkJvaMK86*#Jw
zwYu)*6;`Zm_F-nOTP`93o%>0u({U}0!A~LWHO@lixL=IlpODZR#mMaHh{lN&<oR+2
z2i1Noe?0ajKHBMg*&FNedUI5kE7}%)=XtMug;#S+Tb!^67xQ^fWMoZB8qi})jnW*x
zTUj?yHXVae@0M=+wYcN`f=n@-rqz{iz!2sFw5CS##Of&Pj!j@nn#w!&&s(wE()Y`K
z+1mb$>9+j7UGvvCJa&u(>HEv<!m$!?(qa!JRA4w0s<<jGe%zS+=t!wOd)B|yjxqlR
z652v$u~DYx0iIqB7kEAC-15Zg4gqR#;^C3Rat;$mWvX4Bo@Bcf;rL^{9HbWai?o}9
zH59<O{Z&%n_1F(*pTZ12Yjk$j);f^j!chaJKyEbn_wlKdVR~0*{0B5?fE*r`8Xo@J
z2-RvJ4+hVZLvmgrO>MK+MX#Q3*`%dq@N(}C52CYg2M0NVrufP<a^C$zao7S%YdpO6
z{*k@Cx)cie31!LKA^6aHkw+kt%K;1wia}M(Lui;Y)50B&W^F?rwOKDsuJJs2sh`A3
z1i~N?)_P(0nRd<k`tO?X2Q@K}oSrfNbEj^_tNKPn>D|(!t8(*`J#LQ(jPLeYV=zb^
zIyk3C6eiBp;5rIMCtTs^)QN?^e4r3G-sFm@i{8b}J{a$^ieKkcNU++_Y$33rJ2)5M
zObb)jwo>FV2}Gsy9gXzBk}S4Ke}&#eq+OG~Iub<#=Wu4qOgk?<Xtx<Fx*<_AJdD8&
zuWw>dbnt4nJ*cfMC`5f=R(aSmTF8l}j4T<uA5(ZaBU#-theC@%@o(F)hW0uL+{;UA
zgZT&N0lOT0k~hU<Btyq6*04~uG1gp5TVZOA)1SXFH@VE;2r4L1e0}4&f9=CPK|I~q
z*wzRoSGK=s{J6b~fj%`66}S155lmX*CQl!t&OBSPwhACJKh`K9CDALTmPbPpWLQo7
z{*1zV2*iNJy7|HaK29?o9(XzS*K;b3#@hyaI~KIH>&taTBH#X0p|dz+bP^f0{5Rk2
zV!4#+MPYb0G&Stm^mS1%Hh%j;?ZbfB(lRKd3G#8t$WCH2nl7&J+HbS?`4oduTjxwB
zFOvSW#h6fO-~$IpDp!`K87<B8KBs9OxopII`mLKct09w%G|?aCBL25qb#yK8kWyD-
z_A)<7R+p#V{G{T6(Qd1;YE$&iL+_EKGUUkwTP>|dx@XN*AuC|9N>b^X;gTrn>U>i+
zT~LVa0gWrs<n{CMiGpVfhmFjl>-3RX31ru!^-nuAYG;b8Q`gRF6z%&De0&`$|NAP@
a005Z`5lZT15yMS(s0$DYB$oy!YRW&pIfuOf

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintShear.png b/internal/rendertest/refs/TestPaintShear.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a7d553c420c4f289899b03b95cfe328a39d45c6
GIT binary patch
literal 511
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U_9pO;uumf=gsBVtlJI}4uRn%
z=^r;u-MW*7D|eQ~M4huS&gW|{oZnM(SmxRD+*tkVuT$$@S5;PWKC_EFeOq%)w!zP;
zy?get70+Mu^zFd~*+2iL32goIGbLclp5Ms^d!OECWh?&8a3CRXKBK{m8?p>+n|JXa
zNbrhhGLX1m&cMUAow4EIiEWGq64%lg*phECA4qVpW|Ww7w~m2r`8NTE=Fe3k4F^4C
z84?QIjx$Keyfa{EPM*tn;DLiWqd~=%&kPbaS5`9cd@DC&Xr9Z<kg#F7Ap?(@IZwmE
zZEj2kH!dVFu-!M6YdARFmocGWe<TCXI}^c%gXw2k6Fw|_$-r~(`{T0RSF3-l*UoGB
z$K@c?V8Kwx@QC3EV+XSUy8_>k5cn<1!0`XS(9|V=>|_(FfDy~!>FVdQ&MBb@05$Kk
Ap#T5?

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestPaintTexture.png b/internal/rendertest/refs/TestPaintTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..33feacd3819325766bb54ed0fe02e8528372c623
GIT binary patch
literal 408
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V2t;4aSW-L^X9f=SBnFW!^NTx
z_a2{J{r-aEyx#wBRS&7J4tO3_UHjIG?f$Htzv~UYv(^7%zqgJdf^h>=0_y>;22loW
zhA@UTSOt<}o7Z2AEjKE+f30$dErXD9h=FtWS6r7pJ9kg_K1XT1rX%dXW}Uc(mw;Pf
ys;$3W?pvaNnT??1VH$9IkAR9B>I@A3|1Ucy^|d$2;x{nF89ZJ6T-G@yGywpG_I=F&

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestRepeatedPaintsZ.png b/internal/rendertest/refs/TestRepeatedPaintsZ.png
new file mode 100644
index 0000000000000000000000000000000000000000..1da622452b0c735191ec80a978f0ff6d8936ad26
GIT binary patch
literal 376
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V6^viaSW-L^X85rSA&57Yv9y7
z|4)5nyWQb4@%t^4#)sQ?e>2^AJbYjEEB-lt4Ko-F8Izcfuyk+=2rH;Dq;EzOaqyv7
wVB5EO`Dq{Zxgl0i!9X=ynQ=p!f#LuEPqwTKGq1(I28Jkur>mdKI;Vst0I|(z*8l(j

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestReuseStencil.png b/internal/rendertest/refs/TestReuseStencil.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ea05a002ade8facefd7ef5a07cb1867c6c48d3e
GIT binary patch
literal 383
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V080zaSW-L^XAS*-XjVE3<vAG
z|3%xC7j&lPKA0)OwB#x)OF`Z0nK#WpTnLqa@S3HAQ$Scj&B3Q(27@7E64Q~s-Fc^3
zj$jB=D6sMWzBi&DZ~GwZff;zFKhjVb)d?^uxSQxJ@P?0p;s1Z%5}sei+cNI~1D3(l
L)z4*}Q$iB}NN#Kl

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestRotateClipTexture.png b/internal/rendertest/refs/TestRotateClipTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..8cd211d42908ce04e1bcb4eed2da7d7cce32d13a
GIT binary patch
literal 1122
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^VA1q+aSW-L^X8U+#^iFD;~(=?
zt^~Z+cr(GGDpD~lV4A0gpsj>03%^*|#|c__r&W6z7FAsAKIxk8)FQ{UDB8jH&4K`z
z0Ev$aUY~d~Yi;8{<u?ItMG`H#DVd(%d%oPXnzeO*uIm2hpR+$z%5JIs<vmaS{OsqI
zlj0w3)A8m==dWVg+sAl7f}w{z;><#ZKfmsl-8uK>*+b`*S5<^2K1@t6+mXFP=0{$}
zUcKY}-U~1MvYa>}Bc#dkV@1jPvho6@1FOS@`0{TFah;s1UG!?okD6WOc~)<~Gn}53
zv#mDSPXF=aq6ZBslZ}j(&u(4zE>pt$x8#N$8tnX9?meQ%{o?!OyXRXJB^4wDyiVa+
zVte53?xLTYid-(G{(pb>@m|3lh0k6{{-1FFX;J3fsg_^ZR)jshptv$*O7o<*$E}`q
z$J&$`nh3r6{^j-T*@|D-D!%!gn-|*m<M_q<0x!5@r1zHn`||UhowUilmf5~1Q_B8*
zIFz+;ThQxwi`Sbwd^~>W$pz-C9k1uFn6LOLamnFgqyA$I{`+c`?nS&-+I+V(?Y($c
zyLy|@;rFHgf2YNI3qGBHdgae6_xIoC-S@|}>-Yc1#xCxQXFW?3iDfLh|9snxs&}%M
z+y6OV3c6Dloj=uf;RBC=%XfMfJr%kVk-Ow`=GBgCrJi43y?*r~)IZ*Tjaa57^D^;e
z%j;$b^=|0j_xJt7_~`UwX%G1iS*fKd=rYRHetuEkqxWw5qPvgJcV9X?arVCQ>eQ+0
zJ~MjRPgR?y9-<s(cdu%9ty9gn2a7*0mYqL!qm}%Fr)N&rOBVhvJ}Q5Fn!W9(y7uKM
zoA~ax=$^i{y*z$_{)Wft9mkKpy>Pt5?ZPIpko+s3KmUAl{pRnfzu(pe@B8twDmBlK
ztvb^;>T%MX)JX5s-sksM*R3vC{pokS_M%e^FSo?(T9RqiR`f{uWY*eWRo_a}w2XgV
z&7EnpMq=j%g}VLkUd}k$$*}ENRPi_V@0XWP^2)VU5Sv{6eaY=```eb{3C=rpcFwu8
zQ~BqgWp8)RnPc<vL(B&T#+=@^J+<2X@~4mds%~>|xVLBGzjyyx*2wA{XbbX;+cTk&
zac2RewIELo!|c?rcdp(~J2kzy`tu8Ekp=073=+%@LyY(<$iVRb|Fy7h#U_D|O@U<x
NgQu&X%Q~loCIDYS`3L|2

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestRotateTexture.png b/internal/rendertest/refs/TestRotateTexture.png
new file mode 100644
index 0000000000000000000000000000000000000000..874e8384a0769a2073c102ab9772ee478a9b215f
GIT binary patch
literal 864
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U{?2ZaSW-L^JbQB-eLn8_p>Xe
zzo_fG7;1e<rb+2+poN!eyK~Q!va88kUf)xa6BIfWzBH00E_A=T*p-0RKk+^F68D!(
zX)5@$KwqI;gkdp*1nU6}1~*0nu7*H{9;O5$gm{z{mOiN3z~|uLwJ`IRFS}yP>|Xoz
ztLsAN{?j=fAR--q=z7v+IfIpTp*Q)>L?#4f#g)~5wT@!f-#tg1BVXk~#NTCAYn^QO
zJ-*`lHOf@qcN^a}fr6EF?(ZM$nY_6<>Fs*kwf~+U*&T6jx@%hVr(L!WEMAFSz5Moz
z!`xfBK3_|2o4DrPO>kwLer9tuNB-~Ajr+bhv*-WbP<_Hsd}*nLOi+(z%fW9u_)8_W
zmPE_ma&Dd`al8I}ODKyBUwh`)YWDxvD@rD`w!FUI<hS_U?;M@A-5)-OSJcRv+SzUI
z{rcs}tGy>w#A725Xnf{5ed|5HY{}L6a|-UASD77W+@_~~&g{dPDra-G?^pl-d6GZh
zw^p8^*q6~YaLV`8J>Aon>1FsS{WmQ1+x+s<$@~AF%U|nQn9FkOc<YhK_R2>#sh5{d
zer)$lprvP_!Rj8yw|iIc81_ZC&rCm|GyB+$y(@lRE1zea&3njI(2eictMkhiAHH#L
z=P&Kp>(i6(-n?+R<ih=%`TtGs^_~8+t$)A#rd9u5y>IFNtI&G8|5a&C!oL&&r$3(q
z?=AOvXJfnCo?R#9^3s<tbcCJ@McZ`v%AQ%ac=Ou2h<vN6b1#1;%(M7>;`jWGzqC(o
zd~<Qr6dvRAb9|pSsolKtidp-y6}xO$eRrr}cgg(BuZ?mS5?@x=$di>;h)YpaR`rjS
cf#LuE;7vPyxu>|h0&@<7r>mdKI;Vst0MVvvWB>pF

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestTransformMacro.png b/internal/rendertest/refs/TestTransformMacro.png
new file mode 100644
index 0000000000000000000000000000000000000000..07570593132d2989da68a6ba5826a9c922e89c19
GIT binary patch
literal 375
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V6^jeaSW-L^XB$O-Ub5!7DtD7
z`=2gnPT$a&9;7}kO#EN*L?Hz=2cL!+42FzJOh;HcI0b|iwx#{9`TuQVa3s2f8bkVK
zRHYbt@e8b-`FE+o{Y?~^0dgUJTd1L8FCzoP|NqZpSQ)r3Nk;+$l)=;0&t;ucLK6Ub
CjAq^d

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestTransformOrder.png b/internal/rendertest/refs/TestTransformOrder.png
new file mode 100644
index 0000000000000000000000000000000000000000..27c91fb0fdaef6bce02a091a66c5bb4031962e5e
GIT binary patch
literal 384
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V08C%aSW-L^XBeF&I1ZO3>TyB
z)Ti!tc@VhA+(Dr459iJ2bJWkRmM_@Nl)!p`t3i}On<0#04MPOu2D}0}{k~DzIal;~
z{w~jA#;=sHVf0av^KRaU3$=G|Z($_lIW+ftV`X6Y|NrGK4yNOcq04|F%i!ti=d#Wz
Gp$P!N)oXwN

literal 0
HcmV?d00001

diff --git a/internal/rendertest/render_test.go b/internal/rendertest/render_test.go
new file mode 100644
index 0000000..44099e2
--- /dev/null
+++ b/internal/rendertest/render_test.go
@@ -0,0 +1,144 @@
package rendertest

import (
	"math"
	"testing"

	"gioui.org/f32"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"golang.org/x/image/colornames"
)

func TestTransformMacro(t *testing.T) {
	// testcase resulting from original bug when rendering layout.Stacked

	// pre-build the text
	c := createText()

	run(t, func(o *op.Ops) {

		// render the first Stacked item
		m1 := op.Record(o)
		dr := f32.Rect(0, 0, 128, 50)
		paint.ColorOp{Color: colornames.Black}.Add(o)
		paint.PaintOp{Rect: dr}.Add(o)
		c1 := m1.Stop()

		// Render the second stacked item
		m2 := op.Record(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		// Simulate a draw text call
		stack := op.Push(o)
		op.TransformOp{}.Offset(f32.Pt(0, 10)).Add(o)

		// Actually create the text clip-path
		c.Add(o)

		paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(o)
		stack.Pop()

		c2 := m2.Stop()

		// Call each of them in a transform
		s1 := op.Push(o)
		op.TransformOp{}.Offset(f32.Pt(0, 0)).Add(o)
		c1.Add(o)
		s1.Pop()
		s2 := op.Push(o)
		op.TransformOp{}.Offset(f32.Pt(0, 0)).Add(o)
		c2.Add(o)
		s2.Pop()
	}, func(r result) {
		r.expect(5, 15, colornames.Red)
		r.expect(15, 15, colornames.Black)
		r.expect(11, 51, colornames.White)
	})
}

func TestRepeatedPaintsZ(t *testing.T) {
	run(t, func(o *op.Ops) {
		// Draw a rectangle
		paint.ColorOp{Color: colornames.Black}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 128, 50)}.Add(o)

		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))
		builder.End().Add(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(o)
	}, func(r result) {
		r.expect(5, 5, colornames.Red)
		r.expect(11, 15, colornames.Black)
		r.expect(11, 51, colornames.White)
	})
}

func TestNoClipFromPaint(t *testing.T) {
	// ensure that a paint operation does not polute the state
	// by leaving any clip paths i place.
	run(t, func(o *op.Ops) {
		a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
		op.Affine(a).Add(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
		a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
		op.Affine(a).Add(o)

		paint.ColorOp{Color: colornames.Black}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, 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, colornames.White)
	})
}

func createText() op.CallOp {
	innerOps := new(op.Ops)
	m := op.Record(innerOps)
	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))
	builder.End().Add(innerOps)
	return m.Stop()
}

func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
	r1 := op.Record(ops)
	text.Add(ops)
	paint.PaintOp{Rect: f32.Rect(0, 0, 10, 10)}.Add(ops)
	return r1.Stop()
}

func TestReuseStencil(t *testing.T) {
	txt := createText()
	run(t, func(ops *op.Ops) {
		c1 := drawChild(ops, txt)
		c2 := drawChild(ops, txt)

		// lay out the children
		stack1 := op.Push(ops)
		c1.Add(ops)
		stack1.Pop()

		stack2 := op.Push(ops)
		op.TransformOp{}.Offset(f32.Pt(0, 50)).Add(ops)
		c2.Add(ops)
		stack2.Pop()
	}, func(r result) {
		r.expect(5, 5, colornames.Black)
		r.expect(5, 55, colornames.Black)
	})
}
diff --git a/internal/rendertest/transform_test.go b/internal/rendertest/transform_test.go
new file mode 100644
index 0000000..fa55010
--- /dev/null
+++ b/internal/rendertest/transform_test.go
@@ -0,0 +1,201 @@
package rendertest

import (
	"math"
	"testing"

	"gioui.org/f32"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"golang.org/x/image/colornames"
)

func TestPaintOffset(t *testing.T) {
	run(t, func(o *op.Ops) {
		op.Offset(f32.Pt(10, 20)).Add(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(59, 30, colornames.Red)
		r.expect(60, 30, colornames.White)
		r.expect(10, 70, colornames.White)
	})
}

func TestPaintRotate(t *testing.T) {
	run(t, func(o *op.Ops) {
		a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8)
		op.Affine(a).Add(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(20, 20, 60, 60)}.Add(o)
	}, func(r result) {
		r.expect(40, 40, colornames.Red)
		r.expect(50, 19, colornames.Red)
		r.expect(59, 19, colornames.White)
		r.expect(21, 21, colornames.White)
	})
}

func TestPaintShear(t *testing.T) {
	run(t, func(o *op.Ops) {
		a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
		op.Affine(a).Add(o)
		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
	}, func(r result) {
		r.expect(10, 30, colornames.White)
	})
}

func TestClipPaintOffset(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		clip.Rect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
		op.Offset(f32.Pt(20, 20)).Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(19, 19, colornames.White)
		r.expect(20, 20, colornames.Red)
		r.expect(30, 30, colornames.White)
	})
}

func TestClipOffset(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		op.Offset(f32.Pt(20, 20)).Add(o)
		clip.Rect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(29, 29, colornames.White)
		r.expect(30, 30, colornames.Red)
		r.expect(49, 49, colornames.Red)
		r.expect(50, 50, colornames.White)
	})
}

func TestClipScale(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, 10))
		op.Affine(a).Add(o)
		clip.Rect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 1000, 1000)}.Add(o)
	}, func(r result) {
		r.expect(19+10, 19+10, colornames.White)
		r.expect(20+10, 20+10, colornames.Red)
		r.expect(39+10, 39+10, colornames.Red)
		r.expect(40+10, 40+10, colornames.White)
	})
}

func TestClipRotate(t *testing.T) {
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)
		op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o)
		clip.Rect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 40, 100, 100)}.Add(o)
	}, func(r result) {
		r.expect(39, 39, colornames.White)
		r.expect(41, 41, colornames.Red)
		r.expect(50, 50, colornames.White)
	})
}

func TestOffsetTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		op.Offset(f32.Pt(15, 15)).Add(o)
		squares.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(14, 20, colornames.White)
		r.expect(66, 20, colornames.White)
		r.expect(16, 64, colornames.Green)
		r.expect(64, 16, colornames.Green)
	})
}

func TestOffsetScaleTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		op.Offset(f32.Pt(15, 15)).Add(o)
		squares.Add(o)
		op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(114, 64, colornames.Blue)
		r.expect(116, 64, colornames.White)
	})
}

func TestRotateTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)
		a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/4)
		op.Affine(a).Add(o)
		paint.PaintOp{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
	}, func(r result) {
		r.expect(40, 40-12, colornames.Blue)
		r.expect(40+12, 40, colornames.Green)
	})
}

func TestRotateClipTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)
		a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8)
		op.Affine(a).Add(o)
		clip.Rect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
		paint.PaintOp{Rect: f32.Rect(10, 10, 70, 70)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(37, 39, colornames.Green)
		r.expect(36, 39, colornames.Green)
		r.expect(35, 39, colornames.Green)
		r.expect(34, 39, colornames.Green)
		r.expect(33, 39, colornames.Green)
	})
}

func TestComplicatedTransform(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)

		clip.Rect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, NE: 50}.Add(o)

		a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
		op.Affine(a).Add(o)
		clip.Rect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o)

		op.Offset(f32.Pt(-100, -100)).Add(o)
		paint.PaintOp{Rect: f32.Rect(100, 100, 150, 150)}.Add(o)
	}, func(r result) {
		r.expect(20, 5, colornames.White)
	})
}

func TestTransformOrder(t *testing.T) {
	// check the ordering of operations bot in affine and in gpu stack.
	run(t, func(o *op.Ops) {
		paint.ColorOp{Color: colornames.Red}.Add(o)

		a := f32.Affine2D{}.Offset(f32.Pt(64, 64))
		op.Affine(a).Add(o)

		b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8))
		op.Affine(b).Add(o)

		c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, f32.Pt(0.5, 0.5))
		op.Affine(c).Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 20, 20)}.Add(o)
	}, func(r result) {
		// centered and with radius 40
		r.expect(64-41, 64, colornames.White)
		r.expect(64-39, 64, colornames.Red)
		r.expect(64+39, 64, colornames.Red)
		r.expect(64+41, 64, colornames.White)
	})
}
diff --git a/internal/rendertest/util_test.go b/internal/rendertest/util_test.go
index 20755f0..bf0df12 100644
--- a/internal/rendertest/util_test.go
+++ b/internal/rendertest/util_test.go
@@ -38,33 +38,40 @@ func init() {
	squares = paint.NewImageOp(im)
}

func drawImage(size int, draw func(o *op.Ops)) (im *image.RGBA, err error) {
func drawImage(size int, ops *op.Ops, 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)
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.
	ok := true
	var img *image.RGBA
	var err error
	ops := new(op.Ops)
	for i := 0; i < 3; i++ {
		ops.Reset()
		img, err = drawImage(128, ops, f)
		if err != nil {
			t.Error("error rendering:", err)
		}
		// check for a reference image and make sure we are identical.
		ok = ok && verifyRef(t, img)
		c(result{t: t, img: img})
	}

	// 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) {
-- 
2.25.1

[PATCH v2 05/15] gpu, op, internal/ops: add affine transformations

Details
Message ID
<20200620213001.137-5-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +267 -142
Add support for affine transformations. The key changes are outlined
below.

- Painting/clipping with rectangles is handled by, for complex
  transforms, creating clipping paths representing the transformed
  rectangle and using a larger bounding box. Cover/Blit shaders updated
  correspondingly to correctly map texture cordinates from the new
  bounding boxes.
- Since path splitting must happen on CPU the transforms must happen CPU
  side as well - offsets removed from shaders.
- Complex transforms will lead to different path splitting which means
  that GPU arrays can no longer be cached if the transform has changed.
  Thus the current transform is added as a key to the cache.
- Add a public API to op for setting Affine transformations.

There are a number of optimizations that could be explored further but
which are left out now:
- Caching also of CPU operations (e.g path splitting & transforms) and
  not only caching the GPU arrays.
- Allow for re-use of cached GPU vertices if the transformation change
  is a pure offset / scaling since the splitting is then the same.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/gpu.go               | 194 +++++++++++++++++++++++++++++----------
 gpu/path.go              |  24 ++---
 gpu/shaders.go           |  98 +++++++++++---------
 gpu/shaders/blit.vert    |   5 +-
 gpu/shaders/cover.vert   |   5 +-
 gpu/shaders/stencil.vert |   9 +-
 internal/ops/ops.go      |   7 ++
 internal/ops/reader.go   |  21 ++++-
 op/op.go                 |  46 ++++------
 9 files changed, 267 insertions(+), 142 deletions(-)

diff --git a/gpu/gpu.go b/gpu/gpu.go
index 0cb78a4..245e8f6 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -61,10 +61,11 @@ type drawOps struct {
	// zimageOps are the rectangle clipped opaque images
	// that can use fast front-to-back rendering with z-test
	// and no blending.
	zimageOps   []imageOp
	pathOps     []*pathOp
	pathOpCache []pathOp
	qs          quadSplitter
	zimageOps        []imageOp
	pathOps          []*pathOp
	pathOpCache      []pathOp
	qs               quadSplitter
	uniqueKeyCounter int
}

type drawState struct {
@@ -82,7 +83,6 @@ type drawState struct {
}

type pathOp struct {
	off f32.Point
	// clip is the union of all
	// later clip rectangles.
	clip      image.Rectangle
@@ -96,7 +96,6 @@ type pathOp struct {
type imageOp struct {
	z        float32
	path     *pathOp
	off      f32.Point
	clip     image.Rectangle
	material material
	clipType clipType
@@ -109,9 +108,8 @@ type material struct {
	// For materialTypeColor.
	color f32color.RGBA
	// For materialTypeTexture.
	texture  *texture
	uvScale  f32.Point
	uvOffset f32.Point
	texture *texture
	uvTrans f32.Affine2D
}

// clipOp is the shadow of clip.Op.
@@ -227,7 +225,7 @@ type blitter struct {
type blitColUniforms struct {
	vert struct {
		blitUniforms
		_ [10]byte // Padding to a multiple of 16.
		_ [12]byte // Padding to a multiple of 16.
	}
	frag struct {
		colorUniforms
@@ -237,7 +235,7 @@ type blitColUniforms struct {
type blitTexUniforms struct {
	vert struct {
		blitUniforms
		_ [10]byte // Padding to a multiple of 16.
		_ [12]byte // Padding to a multiple of 16.
	}
}

@@ -253,9 +251,10 @@ type program struct {
}

type blitUniforms struct {
	transform   [4]float32
	uvTransform [4]float32
	z           float32
	transform     [4]float32
	uvTransformR1 [4]float32
	uvTransformR2 [4]float32
	z             float32
}

type colorUniforms struct {
@@ -507,7 +506,7 @@ func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
			r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
		}
		data, _ := pathCache.get(p.pathKey)
		r.pather.stencilPath(p.clip, p.off, p.place.Pos, data.(*pathData))
		r.pather.stencilPath(p.clip, p.place.Pos, data.(*pathData))
	}
}

@@ -682,6 +681,28 @@ func (d *drawOps) newPathOp() *pathOp {
	return &d.pathOpCache[len(d.pathOpCache)-1]
}

func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key) {
	npath := d.newPathOp()
	*npath = pathOp{
		parent: state.cpath,
	}
	state.cpath = npath
	if len(aux) > 0 {
		state.rect = false
		state.cpath.pathKey = auxKey
		state.cpath.path = true
		state.cpath.pathVerts = aux
		d.pathOps = append(d.pathOps, state.cpath)
	}
}

// noCacheKey creates a new key for caches, but one that is unique and
// thus will never lead to re-use.
func (d *drawOps) noCacheKey() ops.Key {
	d.uniqueKeyCounter--
	return d.reader.NewKey(d.uniqueKeyCounter)
}

func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
	var aux []byte
	var auxKey ops.Key
@@ -699,32 +720,23 @@ loop:
		case opconst.TypeClip:
			var op clipOp
			op.decode(encOp.Data)
			off := state.t.Transform(f32.Point{})

			bounds := op.bounds
			if len(aux) > 0 {
				// there is a clipping path, bounds is not filled before for performance
				aux, bounds = d.buildVerts(aux)
				// There is a clipping path, build the gpu data and update the
				// cache key such that it will be equal only if the transform is the
				// same also.
				aux, op.bounds = d.buildVerts(aux, state.t)
				auxKey = auxKey.SetTransform(state.t)
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, state.t)
				auxKey = d.noCacheKey()
			}
			state.clip = state.clip.Intersect(bounds.Add(off))
			state.clip = state.clip.Intersect(op.bounds)
			if state.clip.Empty() {
				continue
			}

			npath := d.newPathOp()
			*npath = pathOp{
				parent: state.cpath,
				off:    off,
			}
			state.cpath = npath

			if len(aux) > 0 {
				state.rect = false
				state.cpath.pathKey = auxKey
				state.cpath.path = true
				state.cpath.pathVerts = aux
				d.pathOps = append(d.pathOps, state.cpath)
			}
			d.addClipPath(&state, aux, auxKey)
			aux = nil
			auxKey = ops.Key{}
		case opconst.TypeColor:
@@ -735,13 +747,25 @@ loop:
			state.image = decodeImageOp(encOp.Data, encOp.Refs)
		case opconst.TypePaint:
			op := decodePaintOp(encOp.Data)
			off := state.t.Transform(f32.Point{})
			clip := state.clip.Intersect(op.Rect.Add(off))
			// Transform (if needed) the painting rectangle and if so generate a clip path,
			// for those cases also compute a partialTrans that maps texture coordinates between
			// the new bounding rectangle and the transformed original paint rectangle.
			clipData, bnd, partialTrans := d.boundsForTransformedRect(op.Rect, state.t)
			clip := state.clip.Intersect(bnd).Canon()
			if clip.Empty() {
				continue
			}

			wasrect := state.rect
			if clipData != nil {
				// The paint operation is sheared or rotated, add a clip path representing
				// this transformed rectangle.
				d.addClipPath(&state, clipData, d.noCacheKey())
			}

			bounds := boundRectF(clip)
			mat := state.materialFor(d.cache, op.Rect, off, bounds)
			mat := state.materialFor(d.cache, bnd, partialTrans, bounds)

			if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && mat.material == materialColor {
				// The image is a uniform opaque color and takes up the whole screen.
				// Scrap images up to and including this image and set clear color.
@@ -763,15 +787,20 @@ loop:
			img := imageOp{
				z:        zf,
				path:     state.cpath,
				off:      off,
				clip:     bounds,
				material: mat,
			}

			if state.rect && img.material.opaque {
				d.zimageOps = append(d.zimageOps, img)
			} else {
				d.imageOps = append(d.imageOps, img)
			}
			if clipData != nil {
				// we added a clip path that should not remain
				state.cpath = state.cpath.parent
				state.rect = wasrect
			}
		case opconst.TypePush:
			state.z = d.collectOps(r, state)
		case opconst.TypePop:
@@ -792,7 +821,7 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
	}
}

func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f32.Point, clip image.Rectangle) material {
func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, trans f32.Affine2D, clip image.Rectangle) material {
	var m material
	switch d.matType {
	case materialColor:
@@ -801,7 +830,7 @@ func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f3
		m.opaque = m.color.A == 1.0
	case materialTexture:
		m.material = materialTexture
		dr := boundRectF(rect.Add(off))
		dr := boundRectF(rect)
		sz := d.image.src.Bounds().Size()
		sr := layout.FRect(d.image.rect)
		if dx := float32(dr.Dx()); dx != 0 {
@@ -827,7 +856,8 @@ func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f3
			tex = t
		}
		m.texture = tex.(*texture)
		m.uvScale, m.uvOffset = texSpaceTransform(sr, sz)
		uvScale, uvOffset := texSpaceTransform(sr, sz)
		m.uvTrans = trans.Mul(f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset))
	}
	return m
}
@@ -846,7 +876,7 @@ func (r *renderer) drawZOps(ops []imageOp) {
		}
		drc := img.clip
		scale, off := clipSpaceTransform(drc, r.blitter.viewport)
		r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset)
		r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvTrans)
	}
	r.ctx.SetDepthTest(false)
}
@@ -865,11 +895,12 @@ func (r *renderer) drawOps(ops []imageOp) {
			r.ctx.BindTexture(0, r.texHandle(m.texture))
		}
		drc := img.clip

		scale, off := clipSpaceTransform(drc, r.blitter.viewport)
		var fbo stencilFBO
		switch img.clipType {
		case clipTypeNone:
			r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset)
			r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvTrans)
			continue
		case clipTypePath:
			fbo = r.pather.stenciler.cover(img.place.Idx)
@@ -885,13 +916,13 @@ func (r *renderer) drawOps(ops []imageOp) {
			Max: img.place.Pos.Add(drc.Size()),
		}
		coverScale, coverOff := texSpaceTransform(toRectF(uv), fbo.size)
		r.pather.cover(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset, coverScale, coverOff)
		r.pather.cover(img.z, m.material, m.color, scale, off, m.uvTrans, coverScale, coverOff)
	}
	r.ctx.DepthMask(true)
	r.ctx.SetDepthTest(false)
}

func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff f32.Point) {
func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
	p := b.prog[mat]
	b.ctx.BindProgram(p.prog)
	var uniforms *blitUniforms
@@ -900,7 +931,9 @@ func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, scale, of
		b.colUniforms.frag.color = col
		uniforms = &b.colUniforms.vert.blitUniforms
	case materialTexture:
		b.texUniforms.vert.uvTransform = [4]float32{uvScale.X, uvScale.Y, uvOff.X, uvOff.Y}
		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
		b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
		b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
		uniforms = &b.texUniforms.vert.blitUniforms
	}
	uniforms.z = z
@@ -994,6 +1027,7 @@ func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32
	// the rectangle at (x, y) and dimensions (w, h).
	scale := f32.Point{X: w * .5, Y: h * .5}
	offset := f32.Point{X: x + w*.5, Y: y - h*.5}

	return scale, offset
}

@@ -1043,9 +1077,8 @@ func (d *drawOps) writeVertCache(n int) []byte {
	return d.vertCache[len(d.vertCache)-n:]
}

func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
	// split paths as needed, calculate maxY, bounds and create
	// vertices that will be sent to GPU.
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D) (verts []byte, bounds f32.Rectangle) {
	inf := float32(math.Inf(+1))
	d.qs.bounds = f32.Rectangle{
		Min: f32.Point{X: inf, Y: inf},
@@ -1057,6 +1090,7 @@ func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
	for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ {
		d.qs.contour = bo.Uint32(aux)
		quad := ops.DecodeQuad(aux[4:])
		quad = quad.Transform(tr)

		d.qs.splitAndEncode(quad)

@@ -1066,3 +1100,65 @@ func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) {
	fillMaxY(d.vertCache[startLength:])
	return d.vertCache[startLength:], d.qs.bounds
}

// create GPU vertices for transformed r, find the bounds and establish texture transform.
func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
	if isPureOffset(tr) {
		// fast-path to allow blitting of pure rectangles
		_, _, ox, _, _, oy := tr.Elems()
		off := f32.Pt(ox, oy)
		bnd.Min = r.Min.Add(off)
		bnd.Max = r.Max.Add(off)
		return
	}

	// transform all corners, find new bounds
	corners := [4]f32.Point{
		tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)),
		tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)),
	}
	bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32)
	bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32)
	for _, c := range corners {
		if c.X < bnd.Min.X {
			bnd.Min.X = c.X
		}
		if c.Y < bnd.Min.Y {
			bnd.Min.Y = c.Y
		}
		if c.X > bnd.Max.X {
			bnd.Max.X = c.X
		}
		if c.Y > bnd.Max.Y {
			bnd.Max.Y = c.Y
		}
	}

	// build the GPU vertices
	l := len(d.vertCache)
	d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...)
	aux = d.vertCache[l:]
	encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1])
	encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2])
	encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3])
	encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0])
	fillMaxY(aux)

	// establish the transform mapping from bounds rectangle to transformed corners
	var P1, P2, P3 f32.Point
	P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
	P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
	P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
	P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
	P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
	P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
	sx, sy := P2.X-P3.X, P2.Y-P3.Y
	ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert()

	return
}

func isPureOffset(t f32.Affine2D) bool {
	a, b, _, d, e, _ := t.Elems()
	return a == 1 && b == 0 && d == 0 && e == 1
}
diff --git a/gpu/path.go b/gpu/path.go
index b182f6c..43a8660 100644
--- a/gpu/path.go
+++ b/gpu/path.go
@@ -54,7 +54,8 @@ type coverColUniforms struct {
type coverUniforms struct {
	transform        [4]float32
	uvCoverTransform [4]float32
	uvTransform      [4]float32
	uvTransformR1    [4]float32
	uvTransformR2    [4]float32
	z                float32
}

@@ -77,9 +78,7 @@ type stenciler struct {

type stencilUniforms struct {
	vert struct {
		transform  [4]float32
		pathOffset [2]float32
		_          [8]byte // Padding to multiple of 16.
		transform [4]float32
	}
}

@@ -307,8 +306,8 @@ func (p *pather) begin(sizes []image.Point) {
	p.stenciler.begin(sizes)
}

func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
	p.stenciler.stencilPath(bounds, offset, uv, data)
func (p *pather) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
	p.stenciler.stencilPath(bounds, uv, data)
}

func (s *stenciler) beginIntersect(sizes []image.Point) {
@@ -337,14 +336,13 @@ func (s *stenciler) begin(sizes []image.Point) {
	s.ctx.BindIndexBuffer(s.indexBuf)
}

func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
func (s *stenciler) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
	s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
	// Transform UI coordinates to OpenGL coordinates.
	texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
	scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
	orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
	s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y}
	s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
	s.prog.prog.UploadUniforms()
	// Draw in batches that fit in uint16 indices.
	start := 0
@@ -361,11 +359,11 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
	}
}

func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) {
	p.coverer.cover(z, mat, col, scale, off, uvScale, uvOff, coverScale, coverOff)
func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
	p.coverer.cover(z, mat, col, scale, off, uvTrans, coverScale, coverOff)
}

func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) {
func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
	p := c.prog[mat]
	c.ctx.BindProgram(p.prog)
	var uniforms *coverUniforms
@@ -374,7 +372,9 @@ func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, o
		c.colUniforms.frag.color = col
		uniforms = &c.colUniforms.vert.coverUniforms
	case materialTexture:
		c.texUniforms.vert.uvTransform = [4]float32{uvScale.X, uvScale.Y, uvOff.X, uvOff.Y}
		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
		c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
		c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
		uniforms = &c.texUniforms.vert.coverUniforms
	}
	uniforms.z = z
diff --git a/gpu/shaders.go b/gpu/shaders.go
index a118de4..f2d53ff 100644
--- a/gpu/shaders.go
+++ b/gpu/shaders.go
@@ -98,13 +98,13 @@ var (
		Inputs: []backend.InputLocation{{Name: "pos", Location: 0, Semantic: "POSITION", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "NORMAL", SemanticIndex: 0, Type: 0x0, Size: 2}},
		Uniforms: backend.UniformsReflection{
			Blocks:    []backend.UniformBlock{{Name: "Block", Binding: 0}},
			Locations: []backend.UniformLocation{{Name: "_24.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_24.uvTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_24.z", Type: 0x0, Size: 1, Offset: 32}},
			Size:      36,
			Locations: []backend.UniformLocation{{Name: "_52.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_52.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_52.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_52.z", Type: 0x0, Size: 1, Offset: 48}},
			Size:      52,
		},
		GLSL100ES: "\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _24;\n\nattribute vec2 pos;\nvarying vec2 vUV;\nattribute vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvoid main()\n{\n    vec2 p = (pos * _24.transform.xy) + _24.transform.zw;\n    vec4 param = vec4(p, _24.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _24.uvTransform.xy) + _24.uvTransform.zw;\n}\n\n",
		GLSL300ES: "#version 300 es\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec4 uvTransform;\n    float z;\n} _24;\n\nlayout(location = 0) in vec2 pos;\nout vec2 vUV;\nlayout(location = 1) in vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvoid main()\n{\n    vec2 p = (pos * _24.transform.xy) + _24.transform.zw;\n    vec4 param = vec4(p, _24.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _24.uvTransform.xy) + _24.uvTransform.zw;\n}\n\n",
		GLSL130:   "#version 130\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _24;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvoid main()\n{\n    vec2 p = (pos * _24.transform.xy) + _24.transform.zw;\n    vec4 param = vec4(p, _24.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _24.uvTransform.xy) + _24.uvTransform.zw;\n}\n\n",
		GLSL150:   "#version 150\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _24;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvoid main()\n{\n    vec2 p = (pos * _24.transform.xy) + _24.transform.zw;\n    vec4 param = vec4(p, _24.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _24.uvTransform.xy) + _24.uvTransform.zw;\n}\n\n",
		GLSL100ES: "\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _52;\n\nattribute vec2 pos;\nvarying vec2 vUV;\nattribute vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec2 p = (pos * _52.transform.xy) + _52.transform.zw;\n    vec4 param = vec4(p, _52.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_52.uvTransformR1.xyz, _52.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n}\n\n",
		GLSL300ES: "#version 300 es\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n} _52;\n\nlayout(location = 0) in vec2 pos;\nout vec2 vUV;\nlayout(location = 1) in vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec2 p = (pos * _52.transform.xy) + _52.transform.zw;\n    vec4 param = vec4(p, _52.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_52.uvTransformR1.xyz, _52.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n}\n\n",
		GLSL130:   "#version 130\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _52;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec2 p = (pos * _52.transform.xy) + _52.transform.zw;\n    vec4 param = vec4(p, _52.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_52.uvTransformR1.xyz, _52.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n}\n\n",
		GLSL150:   "#version 150\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _52;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec2 p = (pos * _52.transform.xy) + _52.transform.zw;\n    vec4 param = vec4(p, _52.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_52.uvTransformR1.xyz, _52.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n}\n\n",
		/*
		   struct m3x2
		   {
@@ -112,14 +112,15 @@ var (
		       float3 r1;
		   };

		   static const m3x2 _88 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, -1.0f, 1.0f) };
		   static const m3x2 _90 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, 1.0f, 0.0f) };
		   static const m3x2 _116 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, -1.0f, 1.0f) };
		   static const m3x2 _118 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, 1.0f, 0.0f) };

		   cbuffer Block : register(b0)
		   {
		       float4 _41_transform : packoffset(c0);
		       float4 _41_uvTransform : packoffset(c1);
		       float _41_z : packoffset(c2);
		       float4 _69_transform : packoffset(c0);
		       float4 _69_uvTransformR1 : packoffset(c1);
		       float4 _69_uvTransformR2 : packoffset(c2);
		       float _69_z : packoffset(c3);
		   };


@@ -145,12 +146,20 @@ var (
		       return float4(pos_1.xy, (pos_1.z + pos_1.w) * 0.5f, pos_1.w);
		   }

		   float3 transform3x2(m3x2 t, float3 v)
		   {
		       return float3(dot(t.r0, v), dot(t.r1, v), dot(float3(0.0f, 0.0f, 1.0f), v));
		   }

		   void vert_main()
		   {
		       float2 p = (pos * _41_transform.xy) + _41_transform.zw;
		       float4 param = float4(p, _41_z, 1.0f);
		       float2 p = (pos * _69_transform.xy) + _69_transform.zw;
		       float4 param = float4(p, _69_z, 1.0f);
		       gl_Position = toClipSpace(param);
		       vUV = (uv * _41_uvTransform.xy) + _41_uvTransform.zw;
		       m3x2 _103 = { _69_uvTransformR1.xyz, _69_uvTransformR2.xyz };
		       m3x2 param_1 = _103;
		       float3 param_2 = float3(uv, 1.0f);
		       vUV = transform3x2(param_1, param_2).xy;
		   }

		   SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input)
@@ -165,7 +174,7 @@ var (
		   }

		*/
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x6d, 0xa9, 0x3e, 0xfb, 0x86, 0x40, 0x22, 0x11, 0x11, 0xc1, 0x8, 0xa9, 0x4a, 0xab, 0x34, 0x3f, 0x1, 0x0, 0x0, 0x0, 0x30, 0x4, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x4, 0x1, 0x0, 0x0, 0xf0, 0x1, 0x0, 0x0, 0x6c, 0x2, 0x0, 0x0, 0x88, 0x3, 0x0, 0x0, 0xd8, 0x3, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xc4, 0x0, 0x0, 0x0, 0xc4, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x90, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x4, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x0, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0xe0, 0x1, 0x0, 0xe4, 0x90, 0x2, 0x0, 0xe4, 0xa0, 0x2, 0x0, 0xee, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x90, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x7, 0x80, 0x4, 0x0, 0xe4, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0xc, 0xc0, 0x3, 0x0, 0x0, 0xa0, 0x0, 0x0, 0x44, 0x80, 0x0, 0x0, 0x84, 0x80, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0xe4, 0x0, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x39, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xa, 0x42, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x82, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0x14, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0xec, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc4, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xb4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd4, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xdc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x34, 0x31, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x34, 0x31, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0x5f, 0x34, 0x31, 0x5f, 0x7a, 0x0, 0xab, 0xab, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x48, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x4f, 0x53, 0x47, 0x4e, 0x50, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x57, 0x0, 0x34, 0xa, 0xf6, 0x3b, 0x1b, 0xc4, 0x28, 0xa1, 0x26, 0x75, 0x95, 0x95, 0xc4, 0x27, 0x1, 0x0, 0x0, 0x0, 0xc0, 0x4, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x24, 0x1, 0x0, 0x0, 0x54, 0x2, 0x0, 0x0, 0xd0, 0x2, 0x0, 0x0, 0x18, 0x4, 0x0, 0x0, 0x68, 0x4, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xe4, 0x0, 0x0, 0x0, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xb0, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x5, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x7, 0x80, 0x1, 0x0, 0xc4, 0x90, 0x5, 0x0, 0xd0, 0xa0, 0x5, 0x0, 0xc5, 0xa0, 0x8, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0xe0, 0x2, 0x0, 0xe4, 0xa0, 0x0, 0x0, 0xe4, 0x80, 0x8, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0xe0, 0x3, 0x0, 0xe4, 0xa0, 0x0, 0x0, 0xe4, 0x80, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x90, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x7, 0x80, 0x5, 0x0, 0xe4, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0xc, 0xc0, 0x4, 0x0, 0x0, 0xa0, 0x0, 0x0, 0x64, 0x80, 0x0, 0x0, 0x24, 0x80, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0x28, 0x1, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x4a, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x32, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x10, 0x0, 0x0, 0x8, 0x12, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x82, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x2, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x8, 0x22, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x82, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x2, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xa, 0x42, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x82, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0x40, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0x18, 0x1, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xdc, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xee, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x36, 0x39, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x36, 0x39, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x31, 0x0, 0x5f, 0x36, 0x39, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x32, 0x0, 0x5f, 0x36, 0x39, 0x5f, 0x7a, 0x0, 0xab, 0xab, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x48, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x4f, 0x53, 0x47, 0x4e, 0x50, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
	}
	shader_cover_frag = [...]backend.ShaderSources{
		{
@@ -275,13 +284,13 @@ var (
		Inputs: []backend.InputLocation{{Name: "pos", Location: 0, Semantic: "POSITION", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "NORMAL", SemanticIndex: 0, Type: 0x0, Size: 2}},
		Uniforms: backend.UniformsReflection{
			Blocks:    []backend.UniformBlock{{Name: "Block", Binding: 0}},
			Locations: []backend.UniformLocation{{Name: "_53.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_53.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_53.uvTransform", Type: 0x0, Size: 4, Offset: 32}, {Name: "_53.z", Type: 0x0, Size: 1, Offset: 48}},
			Size:      52,
			Locations: []backend.UniformLocation{{Name: "_53.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_53.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_53.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_53.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_53.z", Type: 0x0, Size: 1, Offset: 64}},
			Size:      68,
		},
		GLSL100ES: "\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _53;\n\nattribute vec2 pos;\nvarying vec2 vUV;\nattribute vec2 uv;\nvarying vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _53.uvTransform.xy) + _53.uvTransform.zw;\n    m3x2 param_1 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_2 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_1, param_2);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL300ES: "#version 300 es\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransform;\n    float z;\n} _53;\n\nlayout(location = 0) in vec2 pos;\nout vec2 vUV;\nlayout(location = 1) in vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _53.uvTransform.xy) + _53.uvTransform.zw;\n    m3x2 param_1 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_2 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_1, param_2);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL130:   "#version 130\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _53;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _53.uvTransform.xy) + _53.uvTransform.zw;\n    m3x2 param_1 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_2 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_1, param_2);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL150:   "#version 150\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransform;\n    float z;\n};\n\nuniform Block _53;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    vUV = (uv * _53.uvTransform.xy) + _53.uvTransform.zw;\n    m3x2 param_1 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_2 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_1, param_2);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL100ES: "\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _53;\n\nattribute vec2 pos;\nvarying vec2 vUV;\nattribute vec2 uv;\nvarying vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_53.uvTransformR1.xyz, _53.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n    m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_4 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_3, param_4);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL300ES: "#version 300 es\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n} _53;\n\nlayout(location = 0) in vec2 pos;\nout vec2 vUV;\nlayout(location = 1) in vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_53.uvTransformR1.xyz, _53.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n    m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_4 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_3, param_4);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL130:   "#version 130\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _53;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_53.uvTransformR1.xyz, _53.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n    m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_4 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_3, param_4);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		GLSL150:   "#version 150\n\nstruct m3x2\n{\n    vec3 r0;\n    vec3 r1;\n};\n\nstruct Block\n{\n    vec4 transform;\n    vec4 uvCoverTransform;\n    vec4 uvTransformR1;\n    vec4 uvTransformR2;\n    float z;\n};\n\nuniform Block _53;\n\nin vec2 pos;\nout vec2 vUV;\nin vec2 uv;\nout vec2 vCoverUV;\n\nvec4 toClipSpace(vec4 pos_1)\n{\n    return pos_1;\n}\n\nvec3 transform3x2(m3x2 t, vec3 v)\n{\n    return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));\n}\n\nvoid main()\n{\n    vec4 param = vec4((pos * _53.transform.xy) + _53.transform.zw, _53.z, 1.0);\n    gl_Position = toClipSpace(param);\n    m3x2 param_1 = m3x2(_53.uvTransformR1.xyz, _53.uvTransformR2.xyz);\n    vec3 param_2 = vec3(uv, 1.0);\n    vUV = transform3x2(param_1, param_2).xy;\n    m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));\n    vec3 param_4 = vec3(uv, 1.0);\n    vec3 uv3 = transform3x2(param_3, param_4);\n    vCoverUV = ((uv3 * vec3(_53.uvCoverTransform.xy, 1.0)) + vec3(_53.uvCoverTransform.zw, 0.0)).xy;\n}\n\n",
		/*
		   struct m3x2
		   {
@@ -289,15 +298,16 @@ var (
		       float3 r1;
		   };

		   static const m3x2 _108 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, -1.0f, 1.0f) };
		   static const m3x2 _134 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, 1.0f, 0.0f) };
		   static const m3x2 _115 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, -1.0f, 1.0f) };
		   static const m3x2 _141 = { float3(1.0f, 0.0f, 0.0f), float3(0.0f, 1.0f, 0.0f) };

		   cbuffer Block : register(b0)
		   {
		       float4 _70_transform : packoffset(c0);
		       float4 _70_uvCoverTransform : packoffset(c1);
		       float4 _70_uvTransform : packoffset(c2);
		       float _70_z : packoffset(c3);
		       float4 _70_uvTransformR1 : packoffset(c2);
		       float4 _70_uvTransformR2 : packoffset(c3);
		       float _70_z : packoffset(c4);
		   };


@@ -334,10 +344,13 @@ var (
		   {
		       float4 param = float4((pos * _70_transform.xy) + _70_transform.zw, _70_z, 1.0f);
		       gl_Position = toClipSpace(param);
		       vUV = (uv * _70_uvTransform.xy) + _70_uvTransform.zw;
		       m3x2 param_1 = _108;
		       m3x2 _101 = { _70_uvTransformR1.xyz, _70_uvTransformR2.xyz };
		       m3x2 param_1 = _101;
		       float3 param_2 = float3(uv, 1.0f);
		       float3 uv3 = transform3x2(param_1, param_2);
		       vUV = transform3x2(param_1, param_2).xy;
		       m3x2 param_3 = _115;
		       float3 param_4 = float3(uv, 1.0f);
		       float3 uv3 = transform3x2(param_3, param_4);
		       vCoverUV = ((uv3 * float3(_70_uvCoverTransform.xy, 1.0f)) + float3(_70_uvCoverTransform.zw, 0.0f)).xy;
		   }

@@ -354,7 +367,7 @@ var (
		   }

		*/
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x1, 0x37, 0x92, 0x86, 0xba, 0xc2, 0x35, 0xd4, 0x8, 0x93, 0xeb, 0xba, 0x83, 0xea, 0x93, 0xb, 0x1, 0x0, 0x0, 0x0, 0x58, 0x5, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x58, 0x1, 0x0, 0x0, 0xd4, 0x2, 0x0, 0x0, 0x50, 0x3, 0x0, 0x0, 0x98, 0x4, 0x0, 0x0, 0xe8, 0x4, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0x18, 0x1, 0x0, 0x0, 0x18, 0x1, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xe4, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x5, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x3f, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0xc, 0xe0, 0x1, 0x0, 0x14, 0x90, 0x3, 0x0, 0x14, 0xa0, 0x3, 0x0, 0xb4, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x1, 0x0, 0x55, 0x90, 0x5, 0x0, 0xe4, 0xa0, 0x5, 0x0, 0xe1, 0xa0, 0x5, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x5, 0x0, 0xe2, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x55, 0x80, 0x0, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x1, 0x80, 0x1, 0x0, 0x0, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0xe0, 0x0, 0x0, 0xe4, 0x80, 0x2, 0x0, 0xe4, 0xa0, 0x2, 0x0, 0xee, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x90, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xb, 0x80, 0x5, 0x0, 0xe4, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0xc, 0xc0, 0x4, 0x0, 0x0, 0xa0, 0x0, 0x0, 0x74, 0x80, 0x0, 0x0, 0x34, 0x80, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0x74, 0x1, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x5d, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x52, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0xa, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0xa, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x6, 0x84, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa6, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xa, 0x42, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x82, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0x40, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0x18, 0x1, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xdc, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf1, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x75, 0x76, 0x43, 0x6f, 0x76, 0x65, 0x72, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x7a, 0x0, 0xab, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x48, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x4f, 0x53, 0x47, 0x4e, 0x68, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0xb3, 0x71, 0x8c, 0x9f, 0x3e, 0x4f, 0x8b, 0x50, 0xb7, 0xa2, 0x4b, 0xfb, 0xad, 0x7d, 0xc8, 0x71, 0x1, 0x0, 0x0, 0x0, 0xcc, 0x5, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x78, 0x1, 0x0, 0x0, 0x1c, 0x3, 0x0, 0x0, 0x98, 0x3, 0x0, 0x0, 0xc, 0x5, 0x0, 0x0, 0x5c, 0x5, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0x38, 0x1, 0x0, 0x0, 0x38, 0x1, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x4, 0x1, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x6, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x3f, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x7, 0x80, 0x1, 0x0, 0xc4, 0x90, 0x6, 0x0, 0xd0, 0xa0, 0x6, 0x0, 0xc5, 0xa0, 0x8, 0x0, 0x0, 0x3, 0x0, 0x0, 0x8, 0xe0, 0x3, 0x0, 0xe4, 0xa0, 0x0, 0x0, 0xe4, 0x80, 0x8, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0xe0, 0x4, 0x0, 0xe4, 0xa0, 0x0, 0x0, 0xe4, 0x80, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x1, 0x0, 0xe1, 0x90, 0x6, 0x0, 0xe4, 0xa0, 0x6, 0x0, 0xe1, 0xa0, 0x5, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x6, 0x0, 0xe2, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x55, 0x80, 0x0, 0x0, 0x0, 0x80, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x1, 0x80, 0x1, 0x0, 0x0, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0xe0, 0x0, 0x0, 0xe4, 0x80, 0x2, 0x0, 0xe4, 0xa0, 0x2, 0x0, 0xee, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x90, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xb, 0x80, 0x6, 0x0, 0xe4, 0xa0, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0xc, 0xc0, 0x5, 0x0, 0x0, 0xa0, 0x0, 0x0, 0x74, 0x80, 0x0, 0x0, 0x34, 0x80, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0x9c, 0x1, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0x67, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x2, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x32, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0xf, 0x0, 0x0, 0xa, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x96, 0x5, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x8, 0x42, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x82, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x2, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x8, 0x82, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x82, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x46, 0x2, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xa, 0x42, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x82, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0x6c, 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0x44, 0x1, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf4, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x1, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x1, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2d, 0x1, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x34, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x75, 0x76, 0x43, 0x6f, 0x76, 0x65, 0x72, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x31, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x75, 0x76, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x32, 0x0, 0x5f, 0x37, 0x30, 0x5f, 0x7a, 0x0, 0xab, 0x0, 0x0, 0x3, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x48, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x4f, 0x53, 0x47, 0x4e, 0x68, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x59, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
	}
	shader_intersect_frag = backend.ShaderSources{
		Textures:  []backend.TextureBinding{{Name: "cover", Binding: 0}},
@@ -548,18 +561,17 @@ var (
		Inputs: []backend.InputLocation{{Name: "corner", Location: 0, Semantic: "POSITION", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "NORMAL", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}},
		Uniforms: backend.UniformsReflection{
			Blocks:    []backend.UniformBlock{{Name: "Block", Binding: 0}},
			Locations: []backend.UniformLocation{{Name: "_16.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_16.pathOffset", Type: 0x0, Size: 2, Offset: 16}},
			Size:      24,
			Locations: []backend.UniformLocation{{Name: "_98.transform", Type: 0x0, Size: 4, Offset: 0}},
			Size:      16,
		},
		GLSL100ES: "\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nattribute vec2 from;\nattribute vec2 ctrl;\nattribute vec2 to;\nattribute float maxy;\nattribute float corner;\nvarying vec2 vFrom;\nvarying vec2 vCtrl;\nvarying vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL300ES: "#version 300 es\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n} _16;\n\nlayout(location = 2) in vec2 from;\nlayout(location = 3) in vec2 ctrl;\nlayout(location = 4) in vec2 to;\nlayout(location = 1) in float maxy;\nlayout(location = 0) in float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL130:   "#version 130\n\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL150:   "#version 150\n\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL100ES: "\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nattribute vec2 from;\nattribute vec2 ctrl;\nattribute vec2 to;\nattribute float maxy;\nattribute float corner;\nvarying vec2 vFrom;\nvarying vec2 vCtrl;\nvarying vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL300ES: "#version 300 es\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n} _98;\n\nlayout(location = 2) in vec2 from;\nlayout(location = 3) in vec2 ctrl;\nlayout(location = 4) in vec2 to;\nlayout(location = 1) in float maxy;\nlayout(location = 0) in float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL130:   "#version 130\n\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL150:   "#version 150\n\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		/*
		   cbuffer Block : register(b0)
		   {
		       float4 _16_transform : packoffset(c0);
		       float2 _16_pathOffset : packoffset(c1);
		       float4 _98_transform : packoffset(c0);
		   };


@@ -592,10 +604,10 @@ var (

		   void vert_main()
		   {
		       float2 from_1 = from + _16_pathOffset;
		       float2 ctrl_1 = ctrl + _16_pathOffset;
		       float2 to_1 = to + _16_pathOffset;
		       float maxy_1 = maxy + _16_pathOffset.y;
		       float2 from_1 = from;
		       float2 ctrl_1 = ctrl;
		       float2 to_1 = to;
		       float maxy_1 = maxy;
		       float c = corner;
		       float2 pos;
		       if (c >= 0.375f)
@@ -618,7 +630,7 @@ var (
		       vFrom = from_1 - pos;
		       vCtrl = ctrl_1 - pos;
		       vTo = to_1 - pos;
		       pos = (pos * _16_transform.xy) + _16_transform.zw;
		       pos = (pos * _98_transform.xy) + _98_transform.zw;
		       gl_Position = float4(pos, 1.0f, 1.0f);
		   }

@@ -639,6 +651,6 @@ var (
		   }

		*/
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x99, 0xea, 0x97, 0xb5, 0xa8, 0xd5, 0x84, 0x5e, 0x4b, 0x12, 0x14, 0x56, 0xdd, 0xee, 0xfc, 0x44, 0x1, 0x0, 0x0, 0x0, 0x18, 0x8, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x4c, 0x2, 0x0, 0x0, 0x74, 0x5, 0x0, 0x0, 0xf0, 0x5, 0x0, 0x0, 0xec, 0x6, 0x0, 0x0, 0x90, 0x7, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xc, 0x2, 0x0, 0x0, 0xc, 0x2, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xd8, 0x1, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x3, 0x0, 0xf, 0xa0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0xbf, 0x51, 0x0, 0x0, 0x5, 0x4, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x0, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x2, 0x80, 0x2, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x3, 0x80, 0x3, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x4, 0x80, 0x4, 0x0, 0xf, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x1, 0x0, 0x0, 0x90, 0x2, 0x0, 0x55, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x80, 0x3, 0x0, 0x55, 0xa0, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x90, 0x3, 0x0, 0x0, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x1, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x0, 0x90, 0x3, 0x0, 0xff, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x3, 0x80, 0x2, 0x0, 0xe4, 0x90, 0x2, 0x0, 0xe4, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0xc, 0x80, 0x3, 0x0, 0x14, 0x90, 0x2, 0x0, 0x14, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x3, 0x0, 0x3, 0x80, 0x2, 0x0, 0xee, 0x80, 0x2, 0x0, 0xe1, 0x80, 0x2, 0x0, 0x0, 0x3, 0x3, 0x0, 0xc, 0x80, 0x4, 0x0, 0x44, 0x90, 0x2, 0x0, 0x44, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x3, 0x0, 0x3, 0x80, 0x3, 0x0, 0xeb, 0x80, 0x3, 0x0, 0xe4, 0x80, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0x80, 0x3, 0x0, 0xe4, 0x80, 0x3, 0x0, 0xaa, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x4, 0x0, 0x6, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x1, 0x0, 0xc8, 0x80, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x4, 0x0, 0x55, 0x80, 0x4, 0x0, 0x0, 0xa0, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x2, 0x0, 0xff, 0x80, 0x2, 0x0, 0x0, 0x80, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x3, 0x0, 0xaa, 0x80, 0x0, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x55, 0x80, 0x3, 0x0, 0x55, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x4, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x55, 0x80, 0x1, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0xf, 0xe0, 0x2, 0x0, 0xe4, 0x80, 0x4, 0x0, 0x28, 0x81, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xee, 0x80, 0x4, 0x0, 0xe8, 0x81, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x4, 0x0, 0xe8, 0x80, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xc, 0xc0, 0x3, 0x0, 0x55, 0xa0, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0x20, 0x3, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xc8, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1a, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x42, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x1d, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x6, 0x84, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x32, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0xb6, 0xf, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x5, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6, 0x84, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x32, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0xb6, 0xf, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x32, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x9, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa6, 0x8, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e, 0x34, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x37, 0x0, 0x0, 0x9, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xf2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0x8, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0xe, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0xa, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0xf4, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x9c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xac, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xbc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x31, 0x36, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x31, 0x36, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x0, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x9c, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x89, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0xab, 0xab, 0xab, 0x4f, 0x53, 0x47, 0x4e, 0x80, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x26, 0xf8, 0x65, 0xa8, 0xa8, 0x7, 0x5e, 0xd2, 0x7e, 0x3a, 0x31, 0x9, 0xd1, 0xc8, 0xe5, 0x5, 0x1, 0x0, 0x0, 0x0, 0x5c, 0x7, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x28, 0x2, 0x0, 0x0, 0xf0, 0x4, 0x0, 0x0, 0x6c, 0x5, 0x0, 0x0, 0x30, 0x6, 0x0, 0x0, 0xd4, 0x6, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xe8, 0x1, 0x0, 0x0, 0xe8, 0x1, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xb4, 0x1, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x2, 0x0, 0xf, 0xa0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0xbf, 0x0, 0x0, 0x80, 0x3f, 0x51, 0x0, 0x0, 0x5, 0x3, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x0, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x2, 0x80, 0x2, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x3, 0x80, 0x3, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x4, 0x80, 0x4, 0x0, 0xf, 0x90, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x3, 0x80, 0x2, 0x0, 0xe4, 0x90, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x80, 0x3, 0x0, 0x0, 0x90, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x4, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x2, 0x0, 0xff, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe1, 0x80, 0x3, 0x0, 0xe1, 0x90, 0xa, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x4, 0x0, 0xe1, 0x90, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x2, 0x0, 0x55, 0xa0, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x1, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x2, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0xaa, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x4, 0x80, 0x1, 0x0, 0x0, 0x90, 0x2, 0x0, 0xff, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x3, 0x0, 0x6, 0x80, 0x0, 0x0, 0x0, 0x80, 0x2, 0x0, 0xe4, 0x80, 0x1, 0x0, 0xc8, 0x80, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x3, 0x0, 0x55, 0x80, 0x3, 0x0, 0x0, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x3, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x1, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xe8, 0x81, 0x2, 0x0, 0xe4, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0xc, 0xe0, 0x3, 0x0, 0x24, 0x81, 0x3, 0x0, 0x14, 0x90, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xe8, 0x81, 0x4, 0x0, 0xe4, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x3, 0x0, 0xe8, 0x80, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xc, 0xc0, 0x2, 0x0, 0xff, 0xa0, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0xc0, 0x2, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xb0, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x33, 0x0, 0x0, 0x7, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x32, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x96, 0x5, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0x0, 0x0, 0x0, 0x7, 0x42, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x9, 0x62, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x56, 0x5, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa6, 0x8, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e, 0x37, 0x0, 0x0, 0x9, 0x12, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x8, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0xe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0xbc, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x84, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x39, 0x38, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x9c, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x89, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0xab, 0xab, 0xab, 0x4f, 0x53, 0x47, 0x4e, 0x80, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
	}
)
diff --git a/gpu/shaders/blit.vert b/gpu/shaders/blit.vert
index 8ef9058..c9b260c 100644
--- a/gpu/shaders/blit.vert
+++ b/gpu/shaders/blit.vert
@@ -8,7 +8,8 @@ precision highp float;

layout(binding = 0) uniform Block {
	vec4 transform;
	vec4 uvTransform;
	vec4 uvTransformR1;
	vec4 uvTransformR2;
	float z;
};

@@ -21,5 +22,5 @@ layout(location = 0) out vec2 vUV;
void main() {
	vec2 p = pos*transform.xy + transform.zw;
	gl_Position = toClipSpace(vec4(p, z, 1));
	vUV = uv*uvTransform.xy + uvTransform.zw;
	vUV = transform3x2(m3x2(uvTransformR1.xyz, uvTransformR2.xyz), vec3(uv,1)).xy;
}
diff --git a/gpu/shaders/cover.vert b/gpu/shaders/cover.vert
index 7eca280..ffc1c78 100644
--- a/gpu/shaders/cover.vert
+++ b/gpu/shaders/cover.vert
@@ -9,7 +9,8 @@ precision highp float;
layout(binding = 0) uniform Block {
	vec4 transform;
	vec4 uvCoverTransform;
	vec4 uvTransform;
	vec4 uvTransformR1;
	vec4 uvTransformR2;
	float z;
};

@@ -22,7 +23,7 @@ layout(location = 1) out vec2 vUV;

void main() {
    gl_Position = toClipSpace(vec4(pos*transform.xy + transform.zw, z, 1));
	vUV = uv*uvTransform.xy + uvTransform.zw;
	vUV = transform3x2(m3x2(uvTransformR1.xyz, uvTransformR2.xyz), vec3(uv,1)).xy;
	vec3 uv3 = transform3x2(fboTextureTransform, vec3(uv, 1.0));
	vCoverUV = (uv3*vec3(uvCoverTransform.xy, 1.0)+vec3(uvCoverTransform.zw, 0.0)).xy;
}
diff --git a/gpu/shaders/stencil.vert b/gpu/shaders/stencil.vert
index 0fe65f5..a13b6c4 100644
--- a/gpu/shaders/stencil.vert
+++ b/gpu/shaders/stencil.vert
@@ -6,7 +6,6 @@ precision highp float;

layout(binding = 0) uniform Block {
	vec4 transform;
	vec2 pathOffset;
};

layout(location=0) in float corner;
@@ -23,10 +22,10 @@ void main() {
	// Add a one pixel overlap so curve quads cover their
	// entire curves. Could use conservative rasterization
	// if available.
	vec2 from = from + pathOffset;
	vec2 ctrl = ctrl + pathOffset;
	vec2 to = to + pathOffset;
	float maxy = maxy + pathOffset.y;
	vec2 from = from;
	vec2 ctrl = ctrl;
	vec2 to = to;
	float maxy = maxy;
	vec2 pos;
	float c = corner;
	if (c >= 0.375) {
diff --git a/internal/ops/ops.go b/internal/ops/ops.go
index 721f462..c56b9d6 100644
--- a/internal/ops/ops.go
+++ b/internal/ops/ops.go
@@ -16,6 +16,13 @@ type Quad struct {
	From, Ctrl, To f32.Point
}

func (q Quad) Transform(t f32.Affine2D) Quad {
	q.From = t.Transform(q.From)
	q.Ctrl = t.Transform(q.Ctrl)
	q.To = t.Transform(q.To)
	return q
}

func EncodeQuad(d []byte, q Quad) {
	bo := binary.LittleEndian
	bo.PutUint32(d[0:], math.Float32bits(q.From.X))
diff --git a/internal/ops/reader.go b/internal/ops/reader.go
index 077b9d8..7e60728 100644
--- a/internal/ops/reader.go
+++ b/internal/ops/reader.go
@@ -5,6 +5,7 @@ package ops
import (
	"encoding/binary"

	"gioui.org/f32"
	"gioui.org/internal/opconst"
	"gioui.org/op"
)
@@ -26,9 +27,10 @@ type EncodedOp struct {

// Key is a unique key for a given op.
type Key struct {
	ops     *op.Ops
	pc      int
	version int
	ops            *op.Ops
	pc             int
	version        int
	sx, hx, sy, hy float32
}

// Shadow of op.MacroOp.
@@ -52,6 +54,10 @@ type opMacroDef struct {
	endpc pc
}

func (r *Reader) NewKey(pc int) Key {
	return Key{ops: r.ops, pc: pc, version: r.ops.Version()}
}

// Reset start reading from the op list.
func (r *Reader) Reset(ops *op.Ops) {
	r.stack = r.stack[:0]
@@ -59,6 +65,15 @@ func (r *Reader) Reset(ops *op.Ops) {
	r.ops = ops
}

func (k Key) SetTransform(t f32.Affine2D) Key {
	sx, hx, sy, hy, _, _ := t.Elems()
	k.sx = sx
	k.hx = hx
	k.sy = sy
	k.hy = hy
	return k
}

func (r *Reader) Decode() (EncodedOp, bool) {
	if r.ops == nil {
		return EncodedOp{}, false
diff --git a/op/op.go b/op/op.go
index 3aafbc3..7d09885 100644
--- a/op/op.go
+++ b/op/op.go
@@ -117,10 +117,10 @@ type InvalidateOp struct {
	At time.Time
}

// TransformOp applies a transform to the current transform.
// TransformOp applies a transform to the current transform. The zero value
// for TransformOp represents the identity transform.
type TransformOp struct {
	// TODO: general transformations.
	offset f32.Point
	t f32.Affine2D
}

// stack tracks the integer identities of StackOp and MacroOp
@@ -264,39 +264,33 @@ func (r InvalidateOp) Add(o *Ops) {
	}
}

// Offset the transformation.
func (t TransformOp) Offset(o f32.Point) TransformOp {
	return t.Multiply(TransformOp{o})
}

// Invert the transformation.
func (t TransformOp) Invert() TransformOp {
	return TransformOp{offset: t.offset.Mul(-1)}
// Offset creates a TransformOp with the offset o.
func Offset(o f32.Point) TransformOp {
	return TransformOp{t: f32.Affine2D{}.Offset(o)}
}

// Transform a point.
func (t TransformOp) Transform(p f32.Point) f32.Point {
	return p.Add(t.offset)
// Affine creates a TransformOp representing the transformation a.
func Affine(a f32.Affine2D) TransformOp {
	return TransformOp{t: a}
}

// Multiply by a transformation.
func (t TransformOp) Multiply(t2 TransformOp) TransformOp {
	return TransformOp{
		offset: t.offset.Add(t2.offset),
	}
// Offset the transfomraiton.
func (t TransformOp) Offset(o f32.Point) TransformOp {
	t.t = t.t.Offset(o)
	return t
}

func (t TransformOp) Add(o *Ops) {
	data := o.Write(opconst.TypeTransformLen)
	data[0] = byte(opconst.TypeTransform)
	bo := binary.LittleEndian
	// write it out as an affine matrix although we only support offset yet
	bo.PutUint32(data[1:], math.Float32bits(1.0))
	bo.PutUint32(data[1+4*1:], math.Float32bits(0))
	bo.PutUint32(data[1+4*2:], math.Float32bits(t.offset.X))
	bo.PutUint32(data[1+4*3:], math.Float32bits(0))
	bo.PutUint32(data[1+4*4:], math.Float32bits(1))
	bo.PutUint32(data[1+4*5:], math.Float32bits(t.offset.Y))
	a, b, c, d, e, f := t.t.Elems()
	bo.PutUint32(data[1:], math.Float32bits(a))
	bo.PutUint32(data[1+4*1:], math.Float32bits(b))
	bo.PutUint32(data[1+4*2:], math.Float32bits(c))
	bo.PutUint32(data[1+4*3:], math.Float32bits(d))
	bo.PutUint32(data[1+4*4:], math.Float32bits(e))
	bo.PutUint32(data[1+4*5:], math.Float32bits(f))
}

func (s *stack) push() stackID {
-- 
2.25.1

[PATCH v2 07/15] example/kitchen: include example of affine transforms

Details
Message ID
<20200620213001.137-7-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +24 -2
Include an example of transforming the entire UI in the example.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 example/kitchen/kitchen.go | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)

diff --git a/example/kitchen/kitchen.go b/example/kitchen/kitchen.go
index 86e6385..1c54bbe 100644
--- a/example/kitchen/kitchen.go
+++ b/example/kitchen/kitchen.go
@@ -18,6 +18,7 @@ import (

	"gioui.org/app"
	"gioui.org/app/headless"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/io/router"
	"gioui.org/io/system"
@@ -127,7 +128,15 @@ func loop(w *app.Window) error {
				if *disable {
					gtx = gtx.Disabled()
				}
				kitchen(gtx, th)
				if checkbox.Changed() {
					if checkbox.Value {
						transformValue = 1
					} else {
						transformValue = 0
					}
				}

				transformedKitchen(gtx, th)
				e.Frame(gtx.Ops)
			}
		case p := <-progressIncrementer:
@@ -140,6 +149,18 @@ func loop(w *app.Window) error {
	}
}

func transformedKitchen(gtx layout.Context, th *material.Theme) layout.Dimensions {
	defer op.Push(gtx.Ops).Pop()
	tr := f32.Affine2D{}
	tr = tr.Rotate(f32.Pt(300, 20), -transformValue)
	s := 1 - transformValue*0.5
	tr = tr.Scale(f32.Pt(300, 20), f32.Pt(s, s))
	tr = tr.Offset(f32.Pt(0, transformValue*200))
	op.Affine(tr).Add(gtx.Ops)

	return kitchen(gtx, th)
}

var (
	editor     = new(widget.Editor)
	lineEditor = &widget.Editor{
@@ -163,6 +184,7 @@ var (
	icon                *widget.Icon
	checkbox            = new(widget.Bool)
	swtch               = new(widget.Bool)
	transformValue      float32
)

type (
@@ -268,7 +290,7 @@ func kitchen(gtx layout.Context, th *material.Theme) layout.Dimensions {
		func(gtx C) D {
			return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
				layout.Rigid(
					material.CheckBox(th, checkbox, "Checkbox").Layout,
					material.CheckBox(th, checkbox, "Transform").Layout,
				),
				layout.Rigid(func(gtx C) D {
					return layout.Inset{Left: unit.Dp(16)}.Layout(gtx,
-- 
2.25.1

[PATCH v2 08/15] gpu: cache quad splitting and transform

Details
Message ID
<20200620213001.137-8-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +42 -27
Cache also CPU operations by moving pathCache into
drawOps and use it in collectOps to avoid splitting and
transformation of quads if in cache. In order to support
this use a concrete type in opCache instead of interface.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/caches.go | 25 +++++++++++++++----------
 gpu/gpu.go    | 44 +++++++++++++++++++++++++++-----------------
 2 files changed, 42 insertions(+), 27 deletions(-)

diff --git a/gpu/caches.go b/gpu/caches.go
index 14eb8f7..4217b04 100644
--- a/gpu/caches.go
+++ b/gpu/caches.go
@@ -5,6 +5,7 @@ package gpu
import (
	"fmt"

	"gioui.org/f32"
	"gioui.org/internal/ops"
)

@@ -13,11 +14,15 @@ type resourceCache struct {
	newRes map[interface{}]resource
}

// opCache is like a resourceCache using the concrete Key
// key type to avoid allocations.
// opCache is like a resourceCache but using concrete types.
type opCache struct {
	res    map[ops.Key]resource
	newRes map[ops.Key]resource
	res    map[ops.Key]opCacheValue
	newRes map[ops.Key]opCacheValue
}

type opCacheValue struct {
	data   *pathData
	bounds f32.Rectangle
}

func newResourceCache() *resourceCache {
@@ -66,12 +71,12 @@ func (r *resourceCache) release() {

func newOpCache() *opCache {
	return &opCache{
		res:    make(map[ops.Key]resource),
		newRes: make(map[ops.Key]resource),
		res:    make(map[ops.Key]opCacheValue),
		newRes: make(map[ops.Key]opCacheValue),
	}
}

func (r *opCache) get(key ops.Key) (resource, bool) {
func (r *opCache) get(key ops.Key) (opCacheValue, bool) {
	v, exists := r.res[key]
	if exists {
		r.newRes[key] = v
@@ -79,7 +84,7 @@ func (r *opCache) get(key ops.Key) (resource, bool) {
	return v, exists
}

func (r *opCache) put(key ops.Key, val resource) {
func (r *opCache) put(key ops.Key, val opCacheValue) {
	if _, exists := r.newRes[key]; exists {
		panic(fmt.Errorf("key exists, %#v", key))
	}
@@ -91,7 +96,7 @@ func (r *opCache) frame() {
	for k, v := range r.res {
		if _, exists := r.newRes[k]; !exists {
			delete(r.res, k)
			v.release()
			v.data.release()
		}
	}
	for k, v := range r.newRes {
@@ -102,7 +107,7 @@ func (r *opCache) frame() {

func (r *opCache) release() {
	for _, v := range r.newRes {
		v.release()
		v.data.release()
	}
	r.newRes = nil
	r.res = nil
diff --git a/gpu/gpu.go b/gpu/gpu.go
index 245e8f6..b7d2388 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -29,8 +29,7 @@ import (
)

type GPU struct {
	pathCache *opCache
	cache     *resourceCache
	cache *resourceCache

	defFBO                                            backend.Framebuffer
	profile                                           string
@@ -65,6 +64,7 @@ type drawOps struct {
	pathOps          []*pathOp
	pathOpCache      []pathOp
	qs               quadSplitter
	pathCache        *opCache
	uniqueKeyCounter int
}

@@ -86,6 +86,7 @@ type pathOp struct {
	// clip is the union of all
	// later clip rectangles.
	clip      image.Rectangle
	bounds    f32.Rectangle
	pathKey   ops.Key
	path      bool
	pathVerts []byte
@@ -277,10 +278,10 @@ const (
func New(ctx backend.Device) (*GPU, error) {
	defFBO := ctx.CurrentFramebuffer()
	g := &GPU{
		defFBO:    defFBO,
		pathCache: newOpCache(),
		cache:     newResourceCache(),
		defFBO: defFBO,
		cache:  newResourceCache(),
	}
	g.drawOps.pathCache = newOpCache()
	if err := g.init(ctx); err != nil {
		return nil, err
	}
@@ -295,7 +296,7 @@ func (g *GPU) init(ctx backend.Device) error {

func (g *GPU) Release() {
	g.renderer.release()
	g.pathCache.release()
	g.drawOps.pathCache.release()
	g.cache.release()
	if g.timers != nil {
		g.timers.release()
@@ -316,9 +317,12 @@ func (g *GPU) Collect(viewport image.Point, frameOps *op.Ops) {
		g.cleanupTimer = g.timers.newTimer()
	}
	for _, p := range g.drawOps.pathOps {
		if _, exists := g.pathCache.get(p.pathKey); !exists {
		if _, exists := g.drawOps.pathCache.get(p.pathKey); !exists {
			data := buildPath(g.ctx, p.pathVerts)
			g.pathCache.put(p.pathKey, data)
			g.drawOps.pathCache.put(p.pathKey, opCacheValue{
				data:   data,
				bounds: p.bounds,
			})
		}
		p.pathVerts = nil
	}
@@ -344,7 +348,7 @@ func (g *GPU) BeginFrame() {
	g.stencilTimer.begin()
	g.ctx.SetBlend(true)
	g.renderer.packStencils(&g.drawOps.pathOps)
	g.renderer.stencilClips(g.pathCache, g.drawOps.pathOps)
	g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
	g.renderer.packIntersections(g.drawOps.imageOps)
	g.renderer.intersect(g.drawOps.imageOps)
	g.stencilTimer.end()
@@ -361,7 +365,7 @@ func (g *GPU) BeginFrame() {
func (g *GPU) EndFrame() {
	g.cleanupTimer.begin()
	g.cache.frame()
	g.pathCache.frame()
	g.drawOps.pathCache.frame()
	g.cleanupTimer.end()
	if g.drawOps.profile && g.timers.ready() {
		zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
@@ -505,8 +509,8 @@ func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
			r.ctx.BindFramebuffer(f.fbo)
			r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
		}
		data, _ := pathCache.get(p.pathKey)
		r.pather.stencilPath(p.clip, p.place.Pos, data.(*pathData))
		v, _ := pathCache.get(p.pathKey)
		r.pather.stencilPath(p.clip, p.place.Pos, v.data)
	}
}

@@ -681,10 +685,11 @@ func (d *drawOps) newPathOp() *pathOp {
	return &d.pathOpCache[len(d.pathOpCache)-1]
}

func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key) {
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle) {
	npath := d.newPathOp()
	*npath = pathOp{
		parent: state.cpath,
		bounds: bounds,
	}
	state.cpath = npath
	if len(aux) > 0 {
@@ -724,9 +729,14 @@ loop:
			if len(aux) > 0 {
				// There is a clipping path, build the gpu data and update the
				// cache key such that it will be equal only if the transform is the
				// same also.
				aux, op.bounds = d.buildVerts(aux, state.t)
				// same also. Use cached data if we have it.
				auxKey = auxKey.SetTransform(state.t)
				if v, ok := d.pathCache.get(auxKey); ok {
					// Since the GPU data exists in the cache aux will not be used.
					op.bounds = v.bounds
				} else {
					aux, op.bounds = d.buildVerts(aux, state.t)
				}
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, state.t)
				auxKey = d.noCacheKey()
@@ -736,7 +746,7 @@ loop:
				continue
			}

			d.addClipPath(&state, aux, auxKey)
			d.addClipPath(&state, aux, auxKey, op.bounds)
			aux = nil
			auxKey = ops.Key{}
		case opconst.TypeColor:
@@ -760,7 +770,7 @@ loop:
			if clipData != nil {
				// The paint operation is sheared or rotated, add a clip path representing
				// this transformed rectangle.
				d.addClipPath(&state, clipData, d.noCacheKey())
				d.addClipPath(&state, clipData, d.noCacheKey(), bnd)
			}

			bounds := boundRectF(clip)
-- 
2.25.1

[PATCH v2 10/15] gpu: exploit pathCache in collectOps

Details
Message ID
<20200620213001.137-10-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +5 -5
Previously the cache was only filled during gpu-buffer creation,
resulting in extra work on the CPU to transform vertices if the same
shape was used multiple times in the same frame. Cases such as font
rendering was cached already before this change as it is drawn in it's
own op.Ops that is never reset - and thus re-used from one frame
to the next.

Since we are now calling put() twice per frame an update should no
longer panic.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/caches.go | 3 ---
 gpu/gpu.go    | 7 +++++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/gpu/caches.go b/gpu/caches.go
index 4217b04..b962263 100644
--- a/gpu/caches.go
+++ b/gpu/caches.go
@@ -85,9 +85,6 @@ func (r *opCache) get(key ops.Key) (opCacheValue, bool) {
}

func (r *opCache) put(key ops.Key, val opCacheValue) {
	if _, exists := r.newRes[key]; exists {
		panic(fmt.Errorf("key exists, %#v", key))
	}
	r.res[key] = val
	r.newRes[key] = val
}
diff --git a/gpu/gpu.go b/gpu/gpu.go
index fccb0fc..6a6abe4 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -319,7 +319,7 @@ func (g *GPU) Collect(viewport image.Point, frameOps *op.Ops) {
		g.cleanupTimer = g.timers.newTimer()
	}
	for _, p := range g.drawOps.pathOps {
		if _, exists := g.drawOps.pathCache.get(p.pathKey); !exists {
		if v, exists := g.drawOps.pathCache.get(p.pathKey); !exists || v.data == nil {
			data := buildPath(g.ctx, p.pathVerts)
			g.drawOps.pathCache.put(p.pathKey, opCacheValue{
				data:   data,
@@ -746,10 +746,13 @@ loop:
				auxKey = auxKey.SetTransform(trans)
				if v, ok := d.pathCache.get(auxKey); ok {
					// Since the GPU data exists in the cache aux will not be used.
					// Why is this not used for the offset shapes?
					op.bounds = v.bounds
				} else {
					aux, op.bounds = d.buildVerts(aux, trans)
					// this will be added to the cache when building the paths later
					// add it to the cache, without GPU data, so the transform can be
					// reused.
					d.pathCache.put(auxKey, opCacheValue{bounds: op.bounds})
				}
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
-- 
2.25.1

[PATCH v2 09/15] gpu: reintroduce reuse of offset-only stenciling

Details
Message ID
<20200620213001.137-9-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +55 -35
Reintroduce support for offset in stencil vertex so we can reuse
cached values if the only difference in transform is offset. Split
current transform into a pure-offset part and the rest and use
only the complex part as cache key.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/gpu.go               | 43 +++++++++++++++++++++++++++-------------
 gpu/path.go              | 11 ++++++----
 gpu/shaders.go           | 27 +++++++++++++------------
 gpu/shaders/stencil.vert |  9 +++++----
 4 files changed, 55 insertions(+), 35 deletions(-)

diff --git a/gpu/gpu.go b/gpu/gpu.go
index b7d2388..fccb0fc 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -83,6 +83,7 @@ type drawState struct {
}

type pathOp struct {
	off f32.Point
	// clip is the union of all
	// later clip rectangles.
	clip      image.Rectangle
@@ -97,6 +98,7 @@ type pathOp struct {
type imageOp struct {
	z        float32
	path     *pathOp
	off      f32.Point
	clip     image.Rectangle
	material material
	clipType clipType
@@ -510,7 +512,7 @@ func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
			r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
		}
		v, _ := pathCache.get(p.pathKey)
		r.pather.stencilPath(p.clip, p.place.Pos, v.data)
		r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
	}
}

@@ -685,11 +687,12 @@ func (d *drawOps) newPathOp() *pathOp {
	return &d.pathOpCache[len(d.pathOpCache)-1]
}

func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle) {
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point) {
	npath := d.newPathOp()
	*npath = pathOp{
		parent: state.cpath,
		bounds: bounds,
		off:    off,
	}
	state.cpath = npath
	if len(aux) > 0 {
@@ -708,6 +711,15 @@ func (d *drawOps) noCacheKey() ops.Key {
	return d.reader.NewKey(d.uniqueKeyCounter)
}

// 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()
	offset = f32.Point{X: ox, Y: oy}
	srs = f32.NewAffine2D(sx, hx, 0, sy, hy, 0)
	return
}

func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
	var aux []byte
	var auxKey ops.Key
@@ -726,27 +738,28 @@ loop:
			var op clipOp
			op.decode(encOp.Data)
			bounds := op.bounds
			trans, off := splitTransform(state.t)
			if len(aux) > 0 {
				// There is a clipping path, build the gpu data and update the
				// cache key such that it will be equal only if the transform is the
				// same also. Use cached data if we have it.
				auxKey = auxKey.SetTransform(state.t)
				auxKey = auxKey.SetTransform(trans)
				if v, ok := d.pathCache.get(auxKey); ok {
					// Since the GPU data exists in the cache aux will not be used.
					op.bounds = v.bounds
				} else {
					aux, op.bounds = d.buildVerts(aux, state.t)
					aux, op.bounds = d.buildVerts(aux, trans)
					// this will be added to the cache when building the paths later
				}
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, state.t)
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
				auxKey = d.noCacheKey()
			}
			state.clip = state.clip.Intersect(op.bounds)
			state.clip = state.clip.Intersect(op.bounds.Add(off))
			if state.clip.Empty() {
				continue
			}

			d.addClipPath(&state, aux, auxKey, op.bounds)
			d.addClipPath(&state, aux, auxKey, op.bounds, off)
			aux = nil
			auxKey = ops.Key{}
		case opconst.TypeColor:
@@ -760,8 +773,9 @@ loop:
			// Transform (if needed) the painting rectangle and if so generate a clip path,
			// for those cases also compute a partialTrans that maps texture coordinates between
			// the new bounding rectangle and the transformed original paint rectangle.
			clipData, bnd, partialTrans := d.boundsForTransformedRect(op.Rect, state.t)
			clip := state.clip.Intersect(bnd).Canon()
			trans, off := splitTransform(state.t)
			clipData, bnd, partialTrans := d.boundsForTransformedRect(op.Rect, trans)
			clip := state.clip.Intersect(bnd.Add(off)).Canon()
			if clip.Empty() {
				continue
			}
@@ -770,11 +784,11 @@ loop:
			if clipData != nil {
				// The paint operation is sheared or rotated, add a clip path representing
				// this transformed rectangle.
				d.addClipPath(&state, clipData, d.noCacheKey(), bnd)
				d.addClipPath(&state, clipData, d.noCacheKey(), bnd, off)
			}

			bounds := boundRectF(clip)
			mat := state.materialFor(d.cache, bnd, partialTrans, bounds)
			mat := state.materialFor(d.cache, bnd, off, partialTrans, bounds)

			if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && mat.material == materialColor {
				// The image is a uniform opaque color and takes up the whole screen.
@@ -797,6 +811,7 @@ loop:
			img := imageOp{
				z:        zf,
				path:     state.cpath,
				off:      off,
				clip:     bounds,
				material: mat,
			}
@@ -831,7 +846,7 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
	}
}

func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, trans f32.Affine2D, clip image.Rectangle) material {
func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, off f32.Point, trans f32.Affine2D, clip image.Rectangle) material {
	var m material
	switch d.matType {
	case materialColor:
@@ -840,7 +855,7 @@ func (d *drawState) materialFor(cache *resourceCache, rect f32.Rectangle, trans
		m.opaque = m.color.A == 1.0
	case materialTexture:
		m.material = materialTexture
		dr := boundRectF(rect)
		dr := boundRectF(rect.Add(off))
		sz := d.image.src.Bounds().Size()
		sr := layout.FRect(d.image.rect)
		if dx := float32(dr.Dx()); dx != 0 {
diff --git a/gpu/path.go b/gpu/path.go
index 43a8660..14d0b9f 100644
--- a/gpu/path.go
+++ b/gpu/path.go
@@ -78,7 +78,9 @@ type stenciler struct {

type stencilUniforms struct {
	vert struct {
		transform [4]float32
		transform  [4]float32
		pathOffset [2]float32
		_          [8]byte // Padding to multiple of 16.
	}
}

@@ -306,8 +308,8 @@ func (p *pather) begin(sizes []image.Point) {
	p.stenciler.begin(sizes)
}

func (p *pather) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
	p.stenciler.stencilPath(bounds, uv, data)
func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
	p.stenciler.stencilPath(bounds, offset, uv, data)
}

func (s *stenciler) beginIntersect(sizes []image.Point) {
@@ -336,13 +338,14 @@ func (s *stenciler) begin(sizes []image.Point) {
	s.ctx.BindIndexBuffer(s.indexBuf)
}

func (s *stenciler) stencilPath(bounds image.Rectangle, uv image.Point, data *pathData) {
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
	s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
	// Transform UI coordinates to OpenGL coordinates.
	texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
	scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
	orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
	s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y}
	s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
	s.prog.prog.UploadUniforms()
	// Draw in batches that fit in uint16 indices.
	start := 0
diff --git a/gpu/shaders.go b/gpu/shaders.go
index f2d53ff..d8e8435 100644
--- a/gpu/shaders.go
+++ b/gpu/shaders.go
@@ -561,17 +561,18 @@ var (
		Inputs: []backend.InputLocation{{Name: "corner", Location: 0, Semantic: "POSITION", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "NORMAL", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}},
		Uniforms: backend.UniformsReflection{
			Blocks:    []backend.UniformBlock{{Name: "Block", Binding: 0}},
			Locations: []backend.UniformLocation{{Name: "_98.transform", Type: 0x0, Size: 4, Offset: 0}},
			Size:      16,
			Locations: []backend.UniformLocation{{Name: "_16.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_16.pathOffset", Type: 0x0, Size: 2, Offset: 16}},
			Size:      24,
		},
		GLSL100ES: "\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nattribute vec2 from;\nattribute vec2 ctrl;\nattribute vec2 to;\nattribute float maxy;\nattribute float corner;\nvarying vec2 vFrom;\nvarying vec2 vCtrl;\nvarying vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL300ES: "#version 300 es\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n} _98;\n\nlayout(location = 2) in vec2 from;\nlayout(location = 3) in vec2 ctrl;\nlayout(location = 4) in vec2 to;\nlayout(location = 1) in float maxy;\nlayout(location = 0) in float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL130:   "#version 130\n\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL150:   "#version 150\n\nstruct Block\n{\n    vec4 transform;\n};\n\nuniform Block _98;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from;\n    vec2 ctrl_1 = ctrl;\n    vec2 to_1 = to;\n    float maxy_1 = maxy;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _98.transform.xy) + _98.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL100ES: "\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nattribute vec2 from;\nattribute vec2 ctrl;\nattribute vec2 to;\nattribute float maxy;\nattribute float corner;\nvarying vec2 vFrom;\nvarying vec2 vCtrl;\nvarying vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL300ES: "#version 300 es\n\nlayout(std140) uniform Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n} _16;\n\nlayout(location = 2) in vec2 from;\nlayout(location = 3) in vec2 ctrl;\nlayout(location = 4) in vec2 to;\nlayout(location = 1) in float maxy;\nlayout(location = 0) in float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL130:   "#version 130\n\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		GLSL150:   "#version 150\n\nstruct Block\n{\n    vec4 transform;\n    vec2 pathOffset;\n};\n\nuniform Block _16;\n\nin vec2 from;\nin vec2 ctrl;\nin vec2 to;\nin float maxy;\nin float corner;\nout vec2 vFrom;\nout vec2 vCtrl;\nout vec2 vTo;\n\nvoid main()\n{\n    vec2 from_1 = from + _16.pathOffset;\n    vec2 ctrl_1 = ctrl + _16.pathOffset;\n    vec2 to_1 = to + _16.pathOffset;\n    float maxy_1 = maxy + _16.pathOffset.y;\n    float c = corner;\n    vec2 pos;\n    if (c >= 0.375)\n    {\n        c -= 0.5;\n        pos.y = maxy_1 + 1.0;\n    }\n    else\n    {\n        pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;\n    }\n    if (c >= 0.125)\n    {\n        pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;\n    }\n    else\n    {\n        pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;\n    }\n    vFrom = from_1 - pos;\n    vCtrl = ctrl_1 - pos;\n    vTo = to_1 - pos;\n    pos = (pos * _16.transform.xy) + _16.transform.zw;\n    gl_Position = vec4(pos, 1.0, 1.0);\n}\n\n",
		/*
		   cbuffer Block : register(b0)
		   {
		       float4 _98_transform : packoffset(c0);
		       float4 _16_transform : packoffset(c0);
		       float2 _16_pathOffset : packoffset(c1);
		   };


@@ -604,10 +605,10 @@ var (

		   void vert_main()
		   {
		       float2 from_1 = from;
		       float2 ctrl_1 = ctrl;
		       float2 to_1 = to;
		       float maxy_1 = maxy;
		       float2 from_1 = from + _16_pathOffset;
		       float2 ctrl_1 = ctrl + _16_pathOffset;
		       float2 to_1 = to + _16_pathOffset;
		       float maxy_1 = maxy + _16_pathOffset.y;
		       float c = corner;
		       float2 pos;
		       if (c >= 0.375f)
@@ -630,7 +631,7 @@ var (
		       vFrom = from_1 - pos;
		       vCtrl = ctrl_1 - pos;
		       vTo = to_1 - pos;
		       pos = (pos * _98_transform.xy) + _98_transform.zw;
		       pos = (pos * _16_transform.xy) + _16_transform.zw;
		       gl_Position = float4(pos, 1.0f, 1.0f);
		   }

@@ -651,6 +652,6 @@ var (
		   }

		*/
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x26, 0xf8, 0x65, 0xa8, 0xa8, 0x7, 0x5e, 0xd2, 0x7e, 0x3a, 0x31, 0x9, 0xd1, 0xc8, 0xe5, 0x5, 0x1, 0x0, 0x0, 0x0, 0x5c, 0x7, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x28, 0x2, 0x0, 0x0, 0xf0, 0x4, 0x0, 0x0, 0x6c, 0x5, 0x0, 0x0, 0x30, 0x6, 0x0, 0x0, 0xd4, 0x6, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xe8, 0x1, 0x0, 0x0, 0xe8, 0x1, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xb4, 0x1, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x2, 0x0, 0xf, 0xa0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0xbf, 0x0, 0x0, 0x80, 0x3f, 0x51, 0x0, 0x0, 0x5, 0x3, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x0, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x2, 0x80, 0x2, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x3, 0x80, 0x3, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x4, 0x80, 0x4, 0x0, 0xf, 0x90, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0x3, 0x80, 0x2, 0x0, 0xe4, 0x90, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x80, 0x3, 0x0, 0x0, 0x90, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x4, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x2, 0x0, 0xff, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe1, 0x80, 0x3, 0x0, 0xe1, 0x90, 0xa, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x4, 0x0, 0xe1, 0x90, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x2, 0x0, 0x55, 0xa0, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x1, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x2, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0xaa, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x4, 0x80, 0x1, 0x0, 0x0, 0x90, 0x2, 0x0, 0xff, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x3, 0x0, 0x6, 0x80, 0x0, 0x0, 0x0, 0x80, 0x2, 0x0, 0xe4, 0x80, 0x1, 0x0, 0xc8, 0x80, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x3, 0x0, 0x55, 0x80, 0x3, 0x0, 0x0, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x3, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0xaa, 0x80, 0x1, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xe8, 0x81, 0x2, 0x0, 0xe4, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0xc, 0xe0, 0x3, 0x0, 0x24, 0x81, 0x3, 0x0, 0x14, 0x90, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xe8, 0x81, 0x4, 0x0, 0xe4, 0x90, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x3, 0x0, 0xe8, 0x80, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xc, 0xc0, 0x2, 0x0, 0xff, 0xa0, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0xc0, 0x2, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xb0, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x33, 0x0, 0x0, 0x7, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x14, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x32, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x96, 0x5, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0x0, 0x0, 0x0, 0x7, 0x42, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x9, 0x62, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x56, 0x5, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa6, 0x8, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e, 0x37, 0x0, 0x0, 0x9, 0x12, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x8, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x13, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0xe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0xbc, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0x94, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x74, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x84, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x39, 0x38, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x9c, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x89, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0xab, 0xab, 0xab, 0x4f, 0x53, 0x47, 0x4e, 0x80, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
		HLSL: []byte{0x44, 0x58, 0x42, 0x43, 0x99, 0xea, 0x97, 0xb5, 0xa8, 0xd5, 0x84, 0x5e, 0x4b, 0x12, 0x14, 0x56, 0xdd, 0xee, 0xfc, 0x44, 0x1, 0x0, 0x0, 0x0, 0x18, 0x8, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x4c, 0x2, 0x0, 0x0, 0x74, 0x5, 0x0, 0x0, 0xf0, 0x5, 0x0, 0x0, 0xec, 0x6, 0x0, 0x0, 0x90, 0x7, 0x0, 0x0, 0x41, 0x6f, 0x6e, 0x39, 0xc, 0x2, 0x0, 0x0, 0xc, 0x2, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0xd8, 0x1, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, 0x1, 0x0, 0x24, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x30, 0x0, 0x0, 0x0, 0x24, 0x0, 0x1, 0x0, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0xff, 0x51, 0x0, 0x0, 0x5, 0x3, 0x0, 0xf, 0xa0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0xbf, 0x51, 0x0, 0x0, 0x5, 0x4, 0x0, 0xf, 0xa0, 0x0, 0x0, 0x0, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x0, 0x80, 0x0, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x1, 0x80, 0x1, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x2, 0x80, 0x2, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x3, 0x80, 0x3, 0x0, 0xf, 0x90, 0x1f, 0x0, 0x0, 0x2, 0x5, 0x0, 0x4, 0x80, 0x4, 0x0, 0xf, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x1, 0x0, 0x0, 0x90, 0x2, 0x0, 0x55, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x80, 0x3, 0x0, 0x55, 0xa0, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x90, 0x3, 0x0, 0x0, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x1, 0x0, 0x4, 0x80, 0x0, 0x0, 0x0, 0x90, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x0, 0x90, 0x3, 0x0, 0xff, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0x3, 0x80, 0x2, 0x0, 0xe4, 0x90, 0x2, 0x0, 0xe4, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x2, 0x0, 0xc, 0x80, 0x3, 0x0, 0x14, 0x90, 0x2, 0x0, 0x14, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x3, 0x0, 0x3, 0x80, 0x2, 0x0, 0xee, 0x80, 0x2, 0x0, 0xe1, 0x80, 0x2, 0x0, 0x0, 0x3, 0x3, 0x0, 0xc, 0x80, 0x4, 0x0, 0x44, 0x90, 0x2, 0x0, 0x44, 0xa0, 0xa, 0x0, 0x0, 0x3, 0x3, 0x0, 0x3, 0x80, 0x3, 0x0, 0xeb, 0x80, 0x3, 0x0, 0xe4, 0x80, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0x80, 0x3, 0x0, 0xe4, 0x80, 0x3, 0x0, 0xaa, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x4, 0x0, 0x6, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0xe4, 0x80, 0x1, 0x0, 0xc8, 0x80, 0xd, 0x0, 0x0, 0x3, 0x0, 0x0, 0x1, 0x80, 0x4, 0x0, 0x55, 0x80, 0x4, 0x0, 0x0, 0xa0, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x2, 0x0, 0xff, 0x80, 0x2, 0x0, 0x0, 0x80, 0xb, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x3, 0x0, 0xaa, 0x80, 0x0, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x2, 0x80, 0x0, 0x0, 0x55, 0x80, 0x3, 0x0, 0x55, 0xa0, 0x12, 0x0, 0x0, 0x4, 0x4, 0x0, 0x1, 0x80, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x55, 0x80, 0x1, 0x0, 0x55, 0x80, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0xf, 0xe0, 0x2, 0x0, 0xe4, 0x80, 0x4, 0x0, 0x28, 0x81, 0x2, 0x0, 0x0, 0x3, 0x1, 0x0, 0x3, 0xe0, 0x3, 0x0, 0xee, 0x80, 0x4, 0x0, 0xe8, 0x81, 0x4, 0x0, 0x0, 0x4, 0x0, 0x0, 0x3, 0x80, 0x4, 0x0, 0xe8, 0x80, 0x1, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0xee, 0xa0, 0x2, 0x0, 0x0, 0x3, 0x0, 0x0, 0x3, 0xc0, 0x0, 0x0, 0xe4, 0x80, 0x0, 0x0, 0xe4, 0xa0, 0x1, 0x0, 0x0, 0x2, 0x0, 0x0, 0xc, 0xc0, 0x3, 0x0, 0x55, 0xa0, 0xff, 0xff, 0x0, 0x0, 0x53, 0x48, 0x44, 0x52, 0x20, 0x3, 0x0, 0x0, 0x40, 0x0, 0x1, 0x0, 0xc8, 0x0, 0x0, 0x0, 0x59, 0x0, 0x0, 0x4, 0x46, 0x8e, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x12, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x5f, 0x0, 0x0, 0x3, 0x32, 0x10, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0xc2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x65, 0x0, 0x0, 0x3, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x67, 0x0, 0x0, 0x4, 0xf2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x2, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1a, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x42, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x1d, 0x0, 0x0, 0x7, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x3e, 0x0, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0xbf, 0x36, 0x0, 0x0, 0x5, 0x42, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0xa, 0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x10, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x6, 0x84, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x32, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0xb6, 0xf, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x16, 0x5, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xc2, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x6, 0x14, 0x10, 0x0, 0x4, 0x0, 0x0, 0x0, 0x6, 0x84, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x33, 0x0, 0x0, 0x7, 0x32, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0xb6, 0xf, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x32, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x46, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x80, 0xbf, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x37, 0x0, 0x0, 0x9, 0x62, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56, 0x6, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa6, 0x8, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x7, 0x22, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3e, 0x34, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0xa, 0x0, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2a, 0x0, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x82, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x40, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x37, 0x0, 0x0, 0x9, 0x12, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3a, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0x0, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0xf2, 0x20, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x86, 0x8, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0xe, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x32, 0x20, 0x10, 0x0, 0x1, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x80, 0x41, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0xa, 0x10, 0x0, 0x3, 0x0, 0x0, 0x0, 0x32, 0x0, 0x0, 0xb, 0x32, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x86, 0x0, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe6, 0x8a, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x36, 0x0, 0x0, 0x8, 0xc2, 0x20, 0x10, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x3f, 0x0, 0x0, 0x80, 0x3f, 0x3e, 0x0, 0x0, 0x1, 0x53, 0x54, 0x41, 0x54, 0x74, 0x0, 0x0, 0x0, 0x16, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x9, 0x0, 0x0, 0x0, 0x11, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x52, 0x44, 0x45, 0x46, 0xf4, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x44, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1c, 0x0, 0x0, 0x0, 0x0, 0x4, 0xfe, 0xff, 0x0, 0x1, 0x0, 0x0, 0xcc, 0x0, 0x0, 0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x0, 0xab, 0xab, 0x3c, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5c, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x9c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xac, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xbc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x31, 0x36, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x0, 0xab, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5f, 0x31, 0x36, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x0, 0xab, 0x1, 0x0, 0x3, 0x0, 0x1, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x28, 0x52, 0x29, 0x20, 0x48, 0x4c, 0x53, 0x4c, 0x20, 0x53, 0x68, 0x61, 0x64, 0x65, 0x72, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x72, 0x20, 0x31, 0x30, 0x2e, 0x31, 0x0, 0x49, 0x53, 0x47, 0x4e, 0x9c, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x89, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x3, 0x3, 0x0, 0x0, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x0, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0xab, 0xab, 0xab, 0x4f, 0x53, 0x47, 0x4e, 0x80, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x3, 0x0, 0x0, 0x68, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0xc, 0x0, 0x0, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0xf, 0x0, 0x0, 0x0, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x0, 0x53, 0x56, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x0, 0xab, 0xab, 0xab},
	}
)
diff --git a/gpu/shaders/stencil.vert b/gpu/shaders/stencil.vert
index a13b6c4..0fe65f5 100644
--- a/gpu/shaders/stencil.vert
+++ b/gpu/shaders/stencil.vert
@@ -6,6 +6,7 @@ precision highp float;

layout(binding = 0) uniform Block {
	vec4 transform;
	vec2 pathOffset;
};

layout(location=0) in float corner;
@@ -22,10 +23,10 @@ void main() {
	// Add a one pixel overlap so curve quads cover their
	// entire curves. Could use conservative rasterization
	// if available.
	vec2 from = from;
	vec2 ctrl = ctrl;
	vec2 to = to;
	float maxy = maxy;
	vec2 from = from + pathOffset;
	vec2 ctrl = ctrl + pathOffset;
	vec2 to = to + pathOffset;
	float maxy = maxy + pathOffset.y;
	vec2 pos;
	float c = corner;
	if (c >= 0.375) {
-- 
2.25.1

[PATCH v2 11/15] gpu: optimize opCache to avoid expensive map lookups

Details
Message ID
<20200620213001.137-11-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +52 -23
Benchmarking showed that the double map access calls
were a bottleneck. Rework the cache to avoid half of them.

The simplest, naive approach would have been to store a
pointer to a struct with a keep field in the map, allowing cheap
update and frame() operation. Benchmarking showed that the
increased GC pressure of that approach decreased performance
however.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/caches.go | 75 +++++++++++++++++++++++++++++++++++----------------
 1 file changed, 52 insertions(+), 23 deletions(-)

diff --git a/gpu/caches.go b/gpu/caches.go
index b962263..4ed461b 100644
--- a/gpu/caches.go
+++ b/gpu/caches.go
@@ -14,15 +14,22 @@ type resourceCache struct {
	newRes map[interface{}]resource
}

// opCache is like a resourceCache but using concrete types.
// opCache is like a resourceCache but using concrete types and a
// freelist instead of two maps to avoid runtime.mapaccess2 calls
// since benchmarking showed them as a bottleneck.
type opCache struct {
	res    map[ops.Key]opCacheValue
	newRes map[ops.Key]opCacheValue
	// store the index + 1 in cache this key is stored in
	index map[ops.Key]int
	// list of indexes in cache that are free and can be used
	freelist []int
	cache    []opCacheValue
}

type opCacheValue struct {
	data   *pathData
	bounds f32.Rectangle
	key    ops.Key
	keep   bool
}

func newResourceCache() *resourceCache {
@@ -71,41 +78,63 @@ func (r *resourceCache) release() {

func newOpCache() *opCache {
	return &opCache{
		res:    make(map[ops.Key]opCacheValue),
		newRes: make(map[ops.Key]opCacheValue),
		index:    make(map[ops.Key]int),
		freelist: make([]int, 0),
		cache:    make([]opCacheValue, 0),
	}
}

func (r *opCache) get(key ops.Key) (opCacheValue, bool) {
	v, exists := r.res[key]
	if exists {
		r.newRes[key] = v
func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) {
	v := r.index[key]
	if v == 0 {
		return
	}
	return v, exists
	r.cache[v-1].keep = true
	return r.cache[v-1], true
}

func (r *opCache) put(key ops.Key, val opCacheValue) {
	r.res[key] = val
	r.newRes[key] = val
	v := r.index[key]
	val.keep = true
	val.key = key
	if v == 0 {
		// not in cache
		i := len(r.cache)
		if len(r.freelist) > 0 {
			i = r.freelist[len(r.freelist)-1]
			r.freelist = r.freelist[:len(r.freelist)-1]
			r.cache[i] = val
		} else {
			r.cache = append(r.cache, val)
		}
		r.index[key] = i + 1
	} else {
		r.cache[v-1] = val
	}
}

func (r *opCache) frame() {
	for k, v := range r.res {
		if _, exists := r.newRes[k]; !exists {
			delete(r.res, k)
	r.freelist = r.freelist[:0]
	for i, v := range r.cache {
		r.cache[i].keep = false
		if v.keep {
			continue
		}
		if v.data != nil {
			v.data.release()
			r.cache[i].data = nil
		}
	}
	for k, v := range r.newRes {
		delete(r.newRes, k)
		r.res[k] = v
		delete(r.index, v.key)
		r.freelist = append(r.freelist, i)
	}
}

func (r *opCache) release() {
	for _, v := range r.newRes {
		v.data.release()
	for i := range r.cache {
		r.cache[i].keep = false
	}
	r.newRes = nil
	r.res = nil
	r.frame()
	r.index = nil
	r.freelist = nil
	r.cache = nil
}
-- 
2.25.1

[PATCH v2 12/15] gpu: cache transformed bounds

Details
Message ID
<20200620213001.137-12-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +9 -19
To avoid duplicate work when using macros and non-offset transforms,
cache also the new bounding boxes set up for them. The ops.Reader
already generates Keys for all operations, so use them in the cache.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/gpu.go             | 24 +++++++++---------------
 internal/ops/reader.go |  4 ----
 2 files changed, 9 insertions(+), 19 deletions(-)

diff --git a/gpu/gpu.go b/gpu/gpu.go
index 6a6abe4..cd87f2a 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -60,12 +60,11 @@ type drawOps struct {
	// zimageOps are the rectangle clipped opaque images
	// that can use fast front-to-back rendering with z-test
	// and no blending.
	zimageOps        []imageOp
	pathOps          []*pathOp
	pathOpCache      []pathOp
	qs               quadSplitter
	pathCache        *opCache
	uniqueKeyCounter int
	zimageOps   []imageOp
	pathOps     []*pathOp
	pathOpCache []pathOp
	qs          quadSplitter
	pathCache   *opCache
}

type drawState struct {
@@ -704,13 +703,6 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, boun
	}
}

// noCacheKey creates a new key for caches, but one that is unique and
// thus will never lead to re-use.
func (d *drawOps) noCacheKey() ops.Key {
	d.uniqueKeyCounter--
	return d.reader.NewKey(d.uniqueKeyCounter)
}

// 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) {
@@ -756,7 +748,8 @@ loop:
				}
			} else {
				aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
				auxKey = d.noCacheKey()
				auxKey = encOp.Key
				auxKey.SetTransform(trans)
			}
			state.clip = state.clip.Intersect(op.bounds.Add(off))
			if state.clip.Empty() {
@@ -787,7 +780,8 @@ loop:
			if clipData != nil {
				// The paint operation is sheared or rotated, add a clip path representing
				// this transformed rectangle.
				d.addClipPath(&state, clipData, d.noCacheKey(), bnd, off)
				encOp.Key.SetTransform(trans)
				d.addClipPath(&state, clipData, encOp.Key, bnd, off)
			}

			bounds := boundRectF(clip)
diff --git a/internal/ops/reader.go b/internal/ops/reader.go
index 7e60728..cf768c0 100644
--- a/internal/ops/reader.go
+++ b/internal/ops/reader.go
@@ -54,10 +54,6 @@ type opMacroDef struct {
	endpc pc
}

func (r *Reader) NewKey(pc int) Key {
	return Key{ops: r.ops, pc: pc, version: r.ops.Version()}
}

// Reset start reading from the op list.
func (r *Reader) Reset(ops *op.Ops) {
	r.stack = r.stack[:0]
-- 
2.25.1

[PATCH v2 13/15] internal/f32color: use explicit type to avoid allocation

Details
Message ID
<20200620213001.137-13-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +2 -2
f32color.RGBAFromSRGB is used extensively in package gpu, avoid an
interface type to save allocations.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 internal/f32color/rgba.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/internal/f32color/rgba.go b/internal/f32color/rgba.go
index 03377ac..efaa1fb 100644
--- a/internal/f32color/rgba.go
+++ b/internal/f32color/rgba.go
@@ -38,8 +38,8 @@ func (col RGBA) Opaque() RGBA {
	return col
}

// RGBAFromSRGB converts color.Color to RGBA.
func RGBAFromSRGB(col color.Color) RGBA {
// RGBAFromSRGB converts from SRGBA to RGBA.
func RGBAFromSRGB(col color.RGBA) RGBA {
	r, g, b, a := col.RGBA()
	return RGBA{
		R: sRGBToLinear(float32(r) / 0xffff),
-- 
2.25.1

[PATCH v2 14/15] gpu: avoid pointers of pathData

Details
Message ID
<20200620213001.137-14-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +9 -9
Save allocations by using pathData instead of *pathData.

Signed-off-by: Viktor <viktor.ogeman@gmail.com>
---
 gpu/caches.go |  6 +++---
 gpu/gpu.go    |  2 +-
 gpu/path.go   | 10 +++++-----
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/gpu/caches.go b/gpu/caches.go
index 4ed461b..7a383c2 100644
--- a/gpu/caches.go
+++ b/gpu/caches.go
@@ -26,7 +26,7 @@ type opCache struct {
}

type opCacheValue struct {
	data   *pathData
	data   pathData
	bounds f32.Rectangle
	key    ops.Key
	keep   bool
@@ -120,9 +120,9 @@ func (r *opCache) frame() {
		if v.keep {
			continue
		}
		if v.data != nil {
		if v.data.data != nil {
			v.data.release()
			r.cache[i].data = nil
			r.cache[i].data.data = nil
		}
		delete(r.index, v.key)
		r.freelist = append(r.freelist, i)
diff --git a/gpu/gpu.go b/gpu/gpu.go
index cd87f2a..0f5874d 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -318,7 +318,7 @@ func (g *GPU) Collect(viewport image.Point, frameOps *op.Ops) {
		g.cleanupTimer = g.timers.newTimer()
	}
	for _, p := range g.drawOps.pathOps {
		if v, exists := g.drawOps.pathCache.get(p.pathKey); !exists || v.data == nil {
		if v, exists := g.drawOps.pathCache.get(p.pathKey); !exists || v.data.data == nil {
			data := buildPath(g.ctx, p.pathVerts)
			g.drawOps.pathCache.put(p.pathKey, opCacheValue{
				data:   data,
diff --git a/gpu/path.go b/gpu/path.go
index 14d0b9f..2dd5875 100644
--- a/gpu/path.go
+++ b/gpu/path.go
@@ -289,18 +289,18 @@ func (c *coverer) release() {
	c.layout.Release()
}

func buildPath(ctx backend.Device, p []byte) *pathData {
func buildPath(ctx backend.Device, p []byte) pathData {
	buf, err := ctx.NewImmutableBuffer(backend.BufferBindingVertices, p)
	if err != nil {
		panic(err)
	}
	return &pathData{
	return pathData{
		ncurves: len(p) / vertStride,
		data:    buf,
	}
}

func (p *pathData) release() {
func (p pathData) release() {
	p.data.Release()
}

@@ -308,7 +308,7 @@ func (p *pather) begin(sizes []image.Point) {
	p.stenciler.begin(sizes)
}

func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
	p.stenciler.stencilPath(bounds, offset, uv, data)
}

@@ -338,7 +338,7 @@ func (s *stenciler) begin(sizes []image.Point) {
	s.ctx.BindIndexBuffer(s.indexBuf)
}

func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) {
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
	s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
	// Transform UI coordinates to OpenGL coordinates.
	texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
-- 
2.25.1

[PATCH v2 15/15] gpu: build gpu data also when outside window

Details
Message ID
<20200620213001.137-15-viktor.ogeman@gmail.com>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Patch: +108 -14
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>
---
 gpu/caches.go                                 |   5 +-
 gpu/gpu.go                                    |   7 +-
 internal/ops/reader.go                        |   4 +-
 .../rendertest/refs/TestBuildOffscreen.png    | Bin 0 -> 364 bytes
 .../rendertest/refs/TestBuildOffscreen_1.png  | Bin 0 -> 929 bytes
 internal/rendertest/render_test.go            |  46 +++++++++++++-
 internal/rendertest/util_test.go              |  60 +++++++++++++++++-
 7 files changed, 108 insertions(+), 14 deletions(-)
 create mode 100644 internal/rendertest/refs/TestBuildOffscreen.png
 create mode 100644 internal/rendertest/refs/TestBuildOffscreen_1.png

diff --git a/gpu/caches.go b/gpu/caches.go
index 7a383c2..cefba83 100644
--- a/gpu/caches.go
+++ b/gpu/caches.go
@@ -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 {
diff --git a/gpu/gpu.go b/gpu/gpu.go
index 0f5874d..48a6334 100644
--- a/gpu/gpu.go
+++ b/gpu/gpu.go
@@ -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{}
diff --git a/internal/ops/reader.go b/internal/ops/reader.go
index cf768c0..5d713db 100644
--- a/internal/ops/reader.go
+++ b/internal/ops/reader.go
@@ -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
}

diff --git a/internal/rendertest/refs/TestBuildOffscreen.png b/internal/rendertest/refs/TestBuildOffscreen.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ed91358c5102f5423f4aee5bc3e241445ea011c
GIT binary patch
literal 364
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^U^Mo0aSW-L^X6h8=K%wTqX!IM
z-T$<@=XZvmqeA*^@#ohW*Jm?aV_3r&!MuSjf%kw^gDJxZ7RUnz-2eZJSXdY~s>`MV
OISihzelF{r5}E+9Ep1l-

literal 0
HcmV?d00001

diff --git a/internal/rendertest/refs/TestBuildOffscreen_1.png b/internal/rendertest/refs/TestBuildOffscreen_1.png
new file mode 100644
index 0000000000000000000000000000000000000000..881341152ee24a98dacf18b20250b4c27284210e
GIT binary patch
literal 929
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V9xM#aSW-L^X9gF-lHfP_rk~D
zT~0H3bSb#{cd_U&2P*FpS<@8SaNQw&_AJ>Aj6RDz`~y84#T-=bNO;Ssy8JYs*`oMk
z-s_p~m(T3GA2Z)js%HKFvzyM`bexu2US59v`t|?zyLRoGb^dwZV-u;~&6_v>mzz6p
zo}8@gRIjD6b^-HJ;v?6FO+LBg_S<)#GVkV<8_ir3wp!mlY?~do!0|$fwyjZb%NJkE
z+G@b_*urebghjtn8#KCZz5Nzvesyx$ZduofH=l}}XJ@=*^0oHA_4I$H_rG5@QJogX
z^PXA7(@2Jozx3<&w!?{&R4y>(ephptwQSyV*>A7%E9Wi${lGb&iN$fk@vHZ_ni`^)
z?e#jppZVG6%FQ>==r=sJ2oy1u>Wy(1>D&ERx$kk^{`ucG)a{o)UU)B<E3I0&B{3r{
zMxyPDZ~pZ5^$ULg{#~_qUyL3vi-n!tz8yPOglMs)i5@x}exbz5wD_y+@kqVtmtS5v
zv&N}lvWjG{o0@x{+hP?@AHOY;nHwYCeBAPUwpjPklP5bLE-Wj`&(A;W@7i|j<la4d
zOzcG^vzFIy(%HRpr=gAM=}n(M7aw+h+wwN<?GEAZ$BSR?KXBiVF@cMrIC|~0pDRxU
zSns@l=f_=sqbq)Sd*jSOy4A%iRTexKIDaQ=>#dh1K7DUWtgdEVed02E=hLE{*I&<+
zx%v9*si&JxcA8yo<9YM2++k^uBoABKGhR04*uUo6tS7(zdh5Xdv(am>Rqb6?p7{1t
zkzR(`?5DB`X0sP|Z@qYWaYx$b$^*&^?LXJrzb|%g{j#gAPi@xse+BQ-Hd|IOpZ{z&
zn|F)D?b;ffzYXm!%?cOx|Mh>t%6mh6y+-fx$A9AjrTULoxn_!2dGd!{%3if5mf6E^
zi#ks*!wjYaA`HtI3|Jd98GJx~9I`@h+=036|Nnh^Zb$#U`7#5@VeoYIb6Mw<&;$Tx
Cj*xEv

literal 0
HcmV?d00001

diff --git a/internal/rendertest/render_test.go b/internal/rendertest/render_test.go
index 44099e2..dc086f6 100644
--- a/internal/rendertest/render_test.go
+++ b/internal/rendertest/render_test.go
@@ -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)
			}))
}
diff --git a/internal/rendertest/util_test.go b/internal/rendertest/util_test.go
index bf0df12..e594935 100644
--- a/internal/rendertest/util_test.go
+++ b/internal/rendertest/util_test.go
@@ -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
			}
		}
	}
-- 
2.25.1
Details
Message ID
<C3MOIM0VXUUH.PSHL942Z6DYU@themachine>
In-Reply-To
<20200620213001.137-1-viktor.ogeman@gmail.com> (view parent)
DKIM signature
pass
Download raw message
Hi Viktor,

All merged. Thank you again for the excellent work! I took the liberty
to spice up your example a bit; I think your work deserves animation :)

Would you like to summarize your work in a post to the gio mailing list?
If so, I'd like to link to it from the Gio twitter account, along with
a video clip.

-- elias
Review patch Export thread (mbox)