// SPDX-License-Identifier: Unlicense OR MIT

/*
Package gpu implements the rendering of Gio drawing operations. It
is used by package app and package app/headless and is otherwise not
useful except for integrating with external window implementations.
*/
package gpu

import (
	"encoding/binary"
	"errors"
	"fmt"
	"image"
	"image/color"
	"math"
	"reflect"
	"time"
	"unsafe"

	"gioui.org/gpu/internal/driver"
	"gioui.org/internal/byteslice"
	"gioui.org/internal/f32"
	"gioui.org/internal/f32color"
	"gioui.org/internal/ops"
	"gioui.org/internal/scene"
	"gioui.org/internal/stroke"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/shader"
	"gioui.org/shader/gio"

	// Register backends.
	_ "gioui.org/gpu/internal/d3d11"
	_ "gioui.org/gpu/internal/metal"
	_ "gioui.org/gpu/internal/opengl"
	_ "gioui.org/gpu/internal/vulkan"
)

type GPU interface {
	// Release non-Go resources. The GPU is no longer valid after Release.
	Release()
	// Clear sets the clear color for the next Frame.
	Clear(color color.NRGBA)
	// Frame draws the graphics operations from op into a viewport of target.
	Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
}

type gpu struct {
	cache *textureCache

	profile                                string
	timers                                 *timers
	frameStart                             time.Time
	stencilTimer, coverTimer, cleanupTimer *timer
	drawOps                                drawOps
	ctx                                    driver.Device
	renderer                               *renderer
}

type renderer struct {
	ctx           driver.Device
	blitter       *blitter
	pather        *pather
	packer        packer
	intersections packer
	layers        packer
	layerFBOs     fboSet
}

type drawOps struct {
	reader       ops.Reader
	states       []f32.Affine2D
	transStack   []f32.Affine2D
	layers       []opacityLayer
	opacityStack []int
	vertCache    []byte
	viewport     image.Point
	clear        bool
	clearColor   f32color.RGBA
	imageOps     []imageOp
	pathOps      []*pathOp
	pathOpCache  []pathOp
	qs           quadSplitter
	pathCache    *opCache
}

type opacityLayer struct {
	opacity float32
	parent  int
	// depth of the opacity stack. Layers of equal depth are
	// independent and may be packed into one atlas.
	depth int
	// opStart and opEnd denote the range of drawOps.imageOps
	// that belong to the layer.
	opStart, opEnd int
	// clip of the layer operations.
	clip  image.Rectangle
	place placement
}

type drawState struct {
	t     f32.Affine2D
	cpath *pathOp

	matType materialType
	// Current paint.ImageOp
	image imageOpData
	// Current paint.ColorOp, if any.
	color color.NRGBA

	// Current paint.LinearGradientOp.
	stop1  f32.Point
	stop2  f32.Point
	color1 color.NRGBA
	color2 color.NRGBA
}

type pathOp struct {
	off f32.Point
	// rect tracks whether the clip stack can be represented by a
	// pixel-aligned rectangle.
	rect bool
	// clip is the union of all
	// later clip rectangles.
	clip   image.Rectangle
	bounds f32.Rectangle
	// intersect is the intersection of bounds and all
	// previous clip bounds.
	intersect f32.Rectangle
	pathKey   opKey
	path      bool
	pathVerts []byte
	parent    *pathOp
	place     placement
}

type imageOp struct {
	path     *pathOp
	clip     image.Rectangle
	material material
	clipType clipType
	// place is either a placement in the path fbos or intersection fbos,
	// depending on clipType.
	place placement
	// layerOps is the number of operations this
	// operation replaces.
	layerOps int
}

func decodeStrokeOp(data []byte) float32 {
	_ = data[4]
	bo := binary.LittleEndian
	return math.Float32frombits(bo.Uint32(data[1:]))
}

type quadsOp struct {
	key opKey
	aux []byte
}

type opKey struct {
	outline        bool
	strokeWidth    float32
	sx, hx, sy, hy float32
	ops.Key
}

type material struct {
	material materialType
	opaque   bool
	// For materialTypeColor.
	color f32color.RGBA
	// For materialTypeLinearGradient.
	color1  f32color.RGBA
	color2  f32color.RGBA
	opacity float32
	// For materialTypeTexture.
	data    imageOpData
	tex     driver.Texture
	uvTrans f32.Affine2D
}

const (
	filterLinear  = 0
	filterNearest = 1
)

// imageOpData is the shadow of paint.ImageOp.
type imageOpData struct {
	src    *image.RGBA
	handle interface{}
	filter byte
}

type linearGradientOpData struct {
	stop1  f32.Point
	color1 color.NRGBA
	stop2  f32.Point
	color2 color.NRGBA
}

func decodeImageOp(data []byte, refs []interface{}) imageOpData {
	handle := refs[1]
	if handle == nil {
		return imageOpData{}
	}
	return imageOpData{
		src:    refs[0].(*image.RGBA),
		handle: handle,
		filter: data[1],
	}
}

func decodeColorOp(data []byte) color.NRGBA {
	data = data[:ops.TypeColorLen]
	return color.NRGBA{
		R: data[1],
		G: data[2],
		B: data[3],
		A: data[4],
	}
}

func decodeLinearGradientOp(data []byte) linearGradientOpData {
	data = data[:ops.TypeLinearGradientLen]
	bo := binary.LittleEndian
	return linearGradientOpData{
		stop1: f32.Point{
			X: math.Float32frombits(bo.Uint32(data[1:])),
			Y: math.Float32frombits(bo.Uint32(data[5:])),
		},
		stop2: f32.Point{
			X: math.Float32frombits(bo.Uint32(data[9:])),
			Y: math.Float32frombits(bo.Uint32(data[13:])),
		},
		color1: color.NRGBA{
			R: data[17+0],
			G: data[17+1],
			B: data[17+2],
			A: data[17+3],
		},
		color2: color.NRGBA{
			R: data[21+0],
			G: data[21+1],
			B: data[21+2],
			A: data[21+3],
		},
	}
}

type resource interface {
	release()
}

type texture struct {
	src *image.RGBA
	tex driver.Texture
}

type blitter struct {
	ctx                    driver.Device
	viewport               image.Point
	pipelines              [2][3]*pipeline
	colUniforms            *blitColUniforms
	texUniforms            *blitTexUniforms
	linearGradientUniforms *blitLinearGradientUniforms
	quadVerts              driver.Buffer
}

type blitColUniforms struct {
	blitUniforms
	_ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(colorUniforms{})]byte // Padding to 128 bytes.
	colorUniforms
}

type blitTexUniforms struct {
	blitUniforms
}

type blitLinearGradientUniforms struct {
	blitUniforms
	_ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(gradientUniforms{})]byte // Padding to 128 bytes.
	gradientUniforms
}

type uniformBuffer struct {
	buf driver.Buffer
	ptr []byte
}

type pipeline struct {
	pipeline driver.Pipeline
	uniforms *uniformBuffer
}

type blitUniforms struct {
	transform     [4]float32
	uvTransformR1 [4]float32
	uvTransformR2 [4]float32
	opacity       float32
	fbo           float32
	_             [2]float32
}

type colorUniforms struct {
	color f32color.RGBA
}

type gradientUniforms struct {
	color1 f32color.RGBA
	color2 f32color.RGBA
}

type clipType uint8

const (
	clipTypeNone clipType = iota
	clipTypePath
	clipTypeIntersection
)

type materialType uint8

const (
	materialColor materialType = iota
	materialLinearGradient
	materialTexture
)

// New creates a GPU for the given API.
func New(api API) (GPU, error) {
	d, err := driver.NewDevice(api)
	if err != nil {
		return nil, err
	}
	return NewWithDevice(d)
}

// NewWithDevice creates a GPU with a pre-existing device.
//
// Note: for internal use only.
func NewWithDevice(d driver.Device) (GPU, error) {
	d.BeginFrame(nil, false, image.Point{})
	defer d.EndFrame()
	feats := d.Caps().Features
	switch {
	case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
		return newGPU(d)
	}
	return nil, errors.New("no available GPU driver")
}

func newGPU(ctx driver.Device) (*gpu, error) {
	g := &gpu{
		cache: newTextureCache(),
	}
	g.drawOps.pathCache = newOpCache()
	if err := g.init(ctx); err != nil {
		return nil, err
	}
	return g, nil
}

func (g *gpu) init(ctx driver.Device) error {
	g.ctx = ctx
	g.renderer = newRenderer(ctx)
	return nil
}

func (g *gpu) Clear(col color.NRGBA) {
	g.drawOps.clear = true
	g.drawOps.clearColor = f32color.LinearFromSRGB(col)
}

func (g *gpu) Release() {
	g.renderer.release()
	g.drawOps.pathCache.release()
	g.cache.release()
	if g.timers != nil {
		g.timers.Release()
	}
	g.ctx.Release()
}

func (g *gpu) Frame(frameOps *op.Ops, target RenderTarget, viewport image.Point) error {
	g.collect(viewport, frameOps)
	return g.frame(target)
}

func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
	g.renderer.blitter.viewport = viewport
	g.renderer.pather.viewport = viewport
	g.drawOps.reset(viewport)
	g.drawOps.collect(frameOps, viewport)
	if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
		g.frameStart = time.Now()
		g.timers = newTimers(g.ctx)
		g.stencilTimer = g.timers.newTimer()
		g.coverTimer = g.timers.newTimer()
		g.cleanupTimer = g.timers.newTimer()
	}
}

func (g *gpu) frame(target RenderTarget) error {
	viewport := g.renderer.blitter.viewport
	defFBO := g.ctx.BeginFrame(target, g.drawOps.clear, viewport)
	defer g.ctx.EndFrame()
	g.drawOps.buildPaths(g.ctx)
	for _, img := range g.drawOps.imageOps {
		expandPathOp(img.path, img.clip)
	}
	g.stencilTimer.begin()
	g.renderer.packStencils(&g.drawOps.pathOps)
	g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
	g.renderer.packIntersections(g.drawOps.imageOps)
	g.renderer.prepareIntersections(g.drawOps.imageOps)
	g.renderer.intersect(g.drawOps.imageOps)
	g.stencilTimer.end()
	g.coverTimer.begin()
	g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
	g.renderer.prepareDrawOps(g.drawOps.imageOps)
	g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
	g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
	d := driver.LoadDesc{
		ClearColor: g.drawOps.clearColor,
	}
	if g.drawOps.clear {
		g.drawOps.clear = false
		d.Action = driver.LoadActionClear
	}
	g.ctx.BeginRenderPass(defFBO, d)
	g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
	g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
	g.coverTimer.end()
	g.ctx.EndRenderPass()
	g.cleanupTimer.begin()
	g.cache.frame()
	g.drawOps.pathCache.frame()
	g.cleanupTimer.end()
	if false && g.timers.ready() {
		st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
		ft := st + covt + cleant
		q := 100 * time.Microsecond
		st, covt = st.Round(q), covt.Round(q)
		frameDur := time.Since(g.frameStart).Round(q)
		ft = ft.Round(q)
		g.profile = fmt.Sprintf("draw:%7s gpu:%7s st:%7s cov:%7s", frameDur, ft, st, covt)
	}
	return nil
}

func (g *gpu) Profile() string {
	return g.profile
}

func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture {
	key := textureCacheKey{
		filter: data.filter,
		handle: data.handle,
	}

	var tex *texture
	t, exists := cache.get(key)
	if !exists {
		t = &texture{
			src: data.src,
		}
		cache.put(key, t)
	}
	tex = t.(*texture)
	if tex.tex != nil {
		return tex.tex
	}

	var minFilter, magFilter driver.TextureFilter
	switch data.filter {
	case filterLinear:
		minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
	case filterNearest:
		minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
	}

	handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
		data.src.Bounds().Dx(), data.src.Bounds().Dy(),
		minFilter, magFilter,
		driver.BufferBindingTexture,
	)
	if err != nil {
		panic(err)
	}
	driver.UploadImage(handle, image.Pt(0, 0), data.src)
	tex.tex = handle
	return tex.tex
}

func (t *texture) release() {
	if t.tex != nil {
		t.tex.Release()
	}
}

func newRenderer(ctx driver.Device) *renderer {
	r := &renderer{
		ctx:     ctx,
		blitter: newBlitter(ctx),
		pather:  newPather(ctx),
	}

	maxDim := ctx.Caps().MaxTextureSize
	// Large atlas textures cause artifacts due to precision loss in
	// shaders.
	if cap := 8192; maxDim > cap {
		maxDim = cap
	}
	d := image.Pt(maxDim, maxDim)

	r.packer.maxDims = d
	r.intersections.maxDims = d
	r.layers.maxDims = d
	return r
}

func (r *renderer) release() {
	r.pather.release()
	r.blitter.release()
	r.layerFBOs.delete(r.ctx, 0)
}

func newBlitter(ctx driver.Device) *blitter {
	quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices,
		byteslice.Slice([]float32{
			-1, -1, 0, 0,
			+1, -1, 1, 0,
			-1, +1, 0, 1,
			+1, +1, 1, 1,
		}),
	)
	if err != nil {
		panic(err)
	}
	b := &blitter{
		ctx:       ctx,
		quadVerts: quadVerts,
	}
	b.colUniforms = new(blitColUniforms)
	b.texUniforms = new(blitTexUniforms)
	b.linearGradientUniforms = new(blitLinearGradientUniforms)
	pipelines, err := createColorPrograms(ctx, gio.Shader_blit_vert, gio.Shader_blit_frag,
		[3]interface{}{b.colUniforms, b.linearGradientUniforms, b.texUniforms},
	)
	if err != nil {
		panic(err)
	}
	b.pipelines = pipelines
	return b
}

func (b *blitter) release() {
	b.quadVerts.Release()
	for _, p := range b.pipelines {
		for _, p := range p {
			p.Release()
		}
	}
}

func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) (pipelines [2][3]*pipeline, err error) {
	defer func() {
		if err != nil {
			for _, p := range pipelines {
				for _, p := range p {
					if p != nil {
						p.Release()
					}
				}
			}
		}
	}()
	blend := driver.BlendDesc{
		Enable:    true,
		SrcFactor: driver.BlendFactorOne,
		DstFactor: driver.BlendFactorOneMinusSrcAlpha,
	}
	layout := driver.VertexLayout{
		Inputs: []driver.InputDesc{
			{Type: shader.DataTypeFloat, Size: 2, Offset: 0},
			{Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2},
		},
		Stride: 4 * 4,
	}
	vsh, err := b.NewVertexShader(vsSrc)
	if err != nil {
		return pipelines, err
	}
	defer vsh.Release()
	for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
		{
			fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
			if err != nil {
				return pipelines, err
			}
			defer fsh.Release()
			pipe, err := b.NewPipeline(driver.PipelineDesc{
				VertexShader:   vsh,
				FragmentShader: fsh,
				BlendDesc:      blend,
				VertexLayout:   layout,
				PixelFormat:    format,
				Topology:       driver.TopologyTriangleStrip,
			})
			if err != nil {
				return pipelines, err
			}
			var vertBuffer *uniformBuffer
			if u := uniforms[materialTexture]; u != nil {
				vertBuffer = newUniformBuffer(b, u)
			}
			pipelines[i][materialTexture] = &pipeline{pipe, vertBuffer}
		}
		{
			var vertBuffer *uniformBuffer
			fsh, err := b.NewFragmentShader(fsSrc[materialColor])
			if err != nil {
				return pipelines, err
			}
			defer fsh.Release()
			pipe, err := b.NewPipeline(driver.PipelineDesc{
				VertexShader:   vsh,
				FragmentShader: fsh,
				BlendDesc:      blend,
				VertexLayout:   layout,
				PixelFormat:    format,
				Topology:       driver.TopologyTriangleStrip,
			})
			if err != nil {
				return pipelines, err
			}
			if u := uniforms[materialColor]; u != nil {
				vertBuffer = newUniformBuffer(b, u)
			}
			pipelines[i][materialColor] = &pipeline{pipe, vertBuffer}
		}
		{
			var vertBuffer *uniformBuffer
			fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
			if err != nil {
				return pipelines, err
			}
			defer fsh.Release()
			pipe, err := b.NewPipeline(driver.PipelineDesc{
				VertexShader:   vsh,
				FragmentShader: fsh,
				BlendDesc:      blend,
				VertexLayout:   layout,
				PixelFormat:    format,
				Topology:       driver.TopologyTriangleStrip,
			})
			if err != nil {
				return pipelines, err
			}
			if u := uniforms[materialLinearGradient]; u != nil {
				vertBuffer = newUniformBuffer(b, u)
			}
			pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer}
		}
	}
	return pipelines, nil
}

func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
	if len(r.packer.sizes) == 0 {
		return
	}
	fbo := -1
	r.pather.begin(r.packer.sizes)
	for _, p := range ops {
		if fbo != p.place.Idx {
			if fbo != -1 {
				r.ctx.EndRenderPass()
			}
			fbo = p.place.Idx
			f := r.pather.stenciler.cover(fbo)
			r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
			r.ctx.BindPipeline(r.pather.stenciler.pipeline.pipeline.pipeline)
			r.ctx.BindIndexBuffer(r.pather.stenciler.indexBuf)
		}
		v, _ := pathCache.get(p.pathKey)
		r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
	}
	if fbo != -1 {
		r.ctx.EndRenderPass()
	}
}

func (r *renderer) prepareIntersections(ops []imageOp) {
	for _, img := range ops {
		if img.clipType != clipTypeIntersection {
			continue
		}
		fbo := r.pather.stenciler.cover(img.path.place.Idx)
		r.ctx.PrepareTexture(fbo.tex)
	}
}

func (r *renderer) intersect(ops []imageOp) {
	if len(r.intersections.sizes) == 0 {
		return
	}
	fbo := -1
	r.pather.stenciler.beginIntersect(r.intersections.sizes)
	for _, img := range ops {
		if img.clipType != clipTypeIntersection {
			continue
		}
		if fbo != img.place.Idx {
			if fbo != -1 {
				r.ctx.EndRenderPass()
			}
			fbo = img.place.Idx
			f := r.pather.stenciler.intersections.fbos[fbo]
			d := driver.LoadDesc{Action: driver.LoadActionClear}
			d.ClearColor.R = 1.0
			r.ctx.BeginRenderPass(f.tex, d)
			r.ctx.BindPipeline(r.pather.stenciler.ipipeline.pipeline.pipeline)
			r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
		}
		r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
		r.intersectPath(img.path, img.clip)
	}
	if fbo != -1 {
		r.ctx.EndRenderPass()
	}
}

func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
	if p.parent != nil {
		r.intersectPath(p.parent, clip)
	}
	if !p.path {
		return
	}
	uv := image.Rectangle{
		Min: p.place.Pos,
		Max: p.place.Pos.Add(p.clip.Size()),
	}
	o := clip.Min.Sub(p.clip.Min)
	sub := image.Rectangle{
		Min: o,
		Max: o.Add(clip.Size()),
	}
	fbo := r.pather.stenciler.cover(p.place.Idx)
	r.ctx.BindTexture(0, fbo.tex)
	coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
	subScale, subOff := texSpaceTransform(f32.FRect(sub), p.clip.Size())
	r.pather.stenciler.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
	r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y}
	r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx)
	r.ctx.DrawArrays(0, 4)
}

func (r *renderer) packIntersections(ops []imageOp) {
	r.intersections.clear()
	for i, img := range ops {
		var npaths int
		var onePath *pathOp
		for p := img.path; p != nil; p = p.parent {
			if p.path {
				onePath = p
				npaths++
			}
		}
		switch npaths {
		case 0:
		case 1:
			place := onePath.place
			place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
			ops[i].place = place
			ops[i].clipType = clipTypePath
		default:
			sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
			place, ok := r.intersections.add(sz)
			if !ok {
				panic("internal error: if the intersection fit, the intersection should fit as well")
			}
			ops[i].clipType = clipTypeIntersection
			ops[i].place = place
		}
	}
}

func (r *renderer) packStencils(pops *[]*pathOp) {
	r.packer.clear()
	ops := *pops
	// Allocate atlas space for cover textures.
	var i int
	for i < len(ops) {
		p := ops[i]
		if p.clip.Empty() {
			ops[i] = ops[len(ops)-1]
			ops = ops[:len(ops)-1]
			continue
		}
		place, ok := r.packer.add(p.clip.Size())
		if !ok {
			// The clip area is at most the entire screen. Hopefully no
			// screen is larger than GL_MAX_TEXTURE_SIZE.
			panic(fmt.Errorf("clip area %v is larger than maximum texture size %v", p.clip, r.packer.maxDims))
		}
		p.place = place
		i++
	}
	*pops = ops
}

func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
	// Make every layer bounds contain nested layers; cull empty layers.
	for i := len(layers) - 1; i >= 0; i-- {
		l := layers[i]
		if l.parent != -1 {
			b := layers[l.parent].clip
			layers[l.parent].clip = b.Union(l.clip)
		}
		if l.clip.Empty() {
			layers = append(layers[:i], layers[i+1:]...)
		}
	}
	// Pack layers.
	r.layers.clear()
	depth := 0
	for i := range layers {
		l := &layers[i]
		// Only layers of the same depth may be packed together.
		if l.depth != depth {
			r.layers.newPage()
		}
		place, ok := r.layers.add(l.clip.Size())
		if !ok {
			// The layer area is at most the entire screen. Hopefully no
			// screen is larger than GL_MAX_TEXTURE_SIZE.
			panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
		}
		l.place = place
	}
	return layers
}

func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
	if len(r.layers.sizes) == 0 {
		return
	}
	fbo := -1
	r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes)
	for i := len(layers) - 1; i >= 0; i-- {
		l := layers[i]
		if fbo != l.place.Idx {
			if fbo != -1 {
				r.ctx.EndRenderPass()
				r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
			}
			fbo = l.place.Idx
			f := r.layerFBOs.fbos[fbo]
			r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
		}
		v := image.Rectangle{
			Min: l.place.Pos,
			Max: l.place.Pos.Add(l.clip.Size()),
		}
		r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
		f := r.layerFBOs.fbos[fbo]
		r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
		sr := f32.FRect(v)
		uvScale, uvOffset := texSpaceTransform(sr, f.size)
		uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
		// Replace layer ops with one textured op.
		ops[l.opStart] = imageOp{
			clip: l.clip,
			material: material{
				material: materialTexture,
				tex:      f.tex,
				uvTrans:  uvTrans,
				opacity:  l.opacity,
			},
			layerOps: l.opEnd - l.opStart - 1,
		}
	}
	if fbo != -1 {
		r.ctx.EndRenderPass()
		r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
	}
}

func (d *drawOps) reset(viewport image.Point) {
	d.viewport = viewport
	d.imageOps = d.imageOps[:0]
	d.pathOps = d.pathOps[:0]
	d.pathOpCache = d.pathOpCache[:0]
	d.vertCache = d.vertCache[:0]
	d.transStack = d.transStack[:0]
	d.layers = d.layers[:0]
	d.opacityStack = d.opacityStack[:0]
}

func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
	viewf := f32.Rectangle{
		Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
	}
	var ops *ops.Ops
	if root != nil {
		ops = &root.Internal
	}
	d.reader.Reset(ops)
	d.collectOps(&d.reader, viewf)
}

func (d *drawOps) buildPaths(ctx driver.Device) {
	for _, p := range d.pathOps {
		if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
			data := buildPath(ctx, p.pathVerts)
			d.pathCache.put(p.pathKey, opCacheValue{
				data:   data,
				bounds: p.bounds,
			})
		}
		p.pathVerts = nil
	}
}

func (d *drawOps) newPathOp() *pathOp {
	d.pathOpCache = append(d.pathOpCache, pathOp{})
	return &d.pathOpCache[len(d.pathOpCache)-1]
}

func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point) {
	npath := d.newPathOp()
	*npath = pathOp{
		parent:    state.cpath,
		bounds:    bounds,
		off:       off,
		intersect: bounds.Add(off),
		rect:      true,
	}
	if npath.parent != nil {
		npath.rect = npath.parent.rect
		npath.intersect = npath.parent.intersect.Intersect(npath.intersect)
	}
	if len(aux) > 0 {
		npath.rect = false
		npath.pathKey = auxKey
		npath.path = true
		npath.pathVerts = aux
		d.pathOps = append(d.pathOps, npath)
	}
	state.cpath = npath
}

func (d *drawOps) save(id int, state f32.Affine2D) {
	if extra := id - len(d.states) + 1; extra > 0 {
		d.states = append(d.states, make([]f32.Affine2D, extra)...)
	}
	d.states[id] = state
}

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

func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
	var (
		quads quadsOp
		state drawState
	)
	reset := func() {
		state = drawState{
			color: color.NRGBA{A: 0xff},
		}
	}
	reset()
loop:
	for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
		switch ops.OpType(encOp.Data[0]) {
		case ops.TypeTransform:
			dop, push := ops.DecodeTransform(encOp.Data)
			if push {
				d.transStack = append(d.transStack, state.t)
			}
			state.t = state.t.Mul(dop)
		case ops.TypePopTransform:
			n := len(d.transStack)
			state.t = d.transStack[n-1]
			d.transStack = d.transStack[:n-1]

		case ops.TypePushOpacity:
			opacity := ops.DecodeOpacity(encOp.Data)
			parent := -1
			depth := len(d.opacityStack)
			if depth > 0 {
				parent = d.opacityStack[depth-1]
			}
			lidx := len(d.layers)
			d.layers = append(d.layers, opacityLayer{
				opacity: opacity,
				parent:  parent,
				depth:   depth,
				opStart: len(d.imageOps),
			})
			d.opacityStack = append(d.opacityStack, lidx)
		case ops.TypePopOpacity:
			n := len(d.opacityStack)
			idx := d.opacityStack[n-1]
			d.layers[idx].opEnd = len(d.imageOps)
			d.opacityStack = d.opacityStack[:n-1]

		case ops.TypeStroke:
			quads.key.strokeWidth = decodeStrokeOp(encOp.Data)

		case ops.TypePath:
			encOp, ok = r.Decode()
			if !ok {
				break loop
			}
			quads.aux = encOp.Data[ops.TypeAuxLen:]
			quads.key.Key = encOp.Key

		case ops.TypeClip:
			var op ops.ClipOp
			op.Decode(encOp.Data)
			quads.key.outline = op.Outline
			bounds := f32.FRect(op.Bounds)
			trans, off := state.t.Split()
			if len(quads.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.
				quads.key = quads.key.SetTransform(trans)
				if v, ok := d.pathCache.get(quads.key); ok {
					// Since the GPU data exists in the cache aux will not be used.
					// Why is this not used for the offset shapes?
					bounds = v.bounds
				} else {
					var pathData []byte
					pathData, bounds = d.buildVerts(
						quads.aux, trans, quads.key.outline, quads.key.strokeWidth,
					)
					quads.aux = pathData
					// add it to the cache, without GPU data, so the transform can be
					// reused.
					d.pathCache.put(quads.key, opCacheValue{bounds: bounds})
				}
			} else {
				quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
				quads.key = opKey{Key: encOp.Key}
			}
			d.addClipPath(&state, quads.aux, quads.key, bounds, off)
			quads = quadsOp{}
		case ops.TypePopClip:
			state.cpath = state.cpath.parent

		case ops.TypeColor:
			state.matType = materialColor
			state.color = decodeColorOp(encOp.Data)
		case ops.TypeLinearGradient:
			state.matType = materialLinearGradient
			op := decodeLinearGradientOp(encOp.Data)
			state.stop1 = op.stop1
			state.stop2 = op.stop2
			state.color1 = op.color1
			state.color2 = op.color2
		case ops.TypeImage:
			state.matType = materialTexture
			state.image = decodeImageOp(encOp.Data, encOp.Refs)
		case ops.TypePaint:
			// 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.
			t, off := state.t.Split()
			// Fill the clip area, unless the material is a (bounded) image.
			// TODO: Find a tighter bound.
			inf := float32(1e6)
			dst := f32.Rect(-inf, -inf, inf, inf)
			if state.matType == materialTexture {
				sz := state.image.src.Rect.Size()
				dst = f32.Rectangle{Max: layout.FPt(sz)}
			}
			clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, t)
			cl := viewport.Intersect(bnd.Add(off))
			if state.cpath != nil {
				cl = state.cpath.intersect.Intersect(cl)
			}
			if cl.Empty() {
				continue
			}

			if clipData != nil {
				// The paint operation is sheared or rotated, add a clip path representing
				// this transformed rectangle.
				k := opKey{Key: encOp.Key}
				k.SetTransform(t) // TODO: This call has no effect.
				d.addClipPath(&state, clipData, k, bnd, off)
			}

			bounds := cl.Round()
			mat := state.materialFor(bnd, off, partialTrans, bounds)

			rect := state.cpath == nil || state.cpath.rect
			if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
				// 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.
				d.imageOps = d.imageOps[:0]
				d.clearColor = mat.color.Opaque()
				d.clear = true
				continue
			}
			img := imageOp{
				path:     state.cpath,
				clip:     bounds,
				material: mat,
			}
			if n := len(d.opacityStack); n > 0 {
				idx := d.opacityStack[n-1]
				lb := d.layers[idx].clip
				if lb.Empty() {
					d.layers[idx].clip = img.clip
				} else {
					d.layers[idx].clip = lb.Union(img.clip)
				}
			}

			d.imageOps = append(d.imageOps, img)
			if clipData != nil {
				// we added a clip path that should not remain
				state.cpath = state.cpath.parent
			}
		case ops.TypeSave:
			id := ops.DecodeSave(encOp.Data)
			d.save(id, state.t)
		case ops.TypeLoad:
			reset()
			id := ops.DecodeLoad(encOp.Data)
			state.t = d.states[id]
		}
	}
}

func expandPathOp(p *pathOp, clip image.Rectangle) {
	for p != nil {
		pclip := p.clip
		if !pclip.Empty() {
			clip = clip.Union(pclip)
		}
		p.clip = clip
		p = p.parent
	}
}

func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
	m := material{
		opacity: 1.,
	}
	switch d.matType {
	case materialColor:
		m.material = materialColor
		m.color = f32color.LinearFromSRGB(d.color)
		m.opaque = m.color.A == 1.0
	case materialLinearGradient:
		m.material = materialLinearGradient

		m.color1 = f32color.LinearFromSRGB(d.color1)
		m.color2 = f32color.LinearFromSRGB(d.color2)
		m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0

		m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2))
	case materialTexture:
		m.material = materialTexture
		dr := rect.Add(off).Round()
		sz := d.image.src.Bounds().Size()
		sr := f32.Rectangle{
			Max: f32.Point{
				X: float32(sz.X),
				Y: float32(sz.Y),
			},
		}
		dx := float32(dr.Dx())
		sdx := sr.Dx()
		sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx
		sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx
		dy := float32(dr.Dy())
		sdy := sr.Dy()
		sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
		sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
		uvScale, uvOffset := texSpaceTransform(sr, sz)
		m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset))
		m.data = d.image
	}
	return m
}

func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
	for i := range ops {
		img := &ops[i]
		m := img.material
		if m.material == materialTexture {
			img.material.tex = r.texHandle(cache, m.data)
		}
	}
}

func (r *renderer) prepareDrawOps(ops []imageOp) {
	for _, img := range ops {
		m := img.material
		switch m.material {
		case materialTexture:
			r.ctx.PrepareTexture(m.tex)
		}

		var fbo FBO
		switch img.clipType {
		case clipTypeNone:
			continue
		case clipTypePath:
			fbo = r.pather.stenciler.cover(img.place.Idx)
		case clipTypeIntersection:
			fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
		}
		r.ctx.PrepareTexture(fbo.tex)
	}
}

func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
	var coverTex driver.Texture
	for i := 0; i < len(ops); i++ {
		img := ops[i]
		i += img.layerOps
		m := img.material
		switch m.material {
		case materialTexture:
			r.ctx.BindTexture(0, m.tex)
		}
		drc := img.clip.Add(opOff)

		scale, off := clipSpaceTransform(drc, viewport)
		var fbo FBO
		fboIdx := 0
		if isFBO {
			fboIdx = 1
		}
		switch img.clipType {
		case clipTypeNone:
			p := r.blitter.pipelines[fboIdx][m.material]
			r.ctx.BindPipeline(p.pipeline)
			r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
			r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
			continue
		case clipTypePath:
			fbo = r.pather.stenciler.cover(img.place.Idx)
		case clipTypeIntersection:
			fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
		}
		if coverTex != fbo.tex {
			coverTex = fbo.tex
			r.ctx.BindTexture(1, coverTex)
		}
		uv := image.Rectangle{
			Min: img.place.Pos,
			Max: img.place.Pos.Add(drc.Size()),
		}
		coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
		p := r.pather.coverer.pipelines[fboIdx][m.material]
		r.ctx.BindPipeline(p.pipeline)
		r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
		r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
	}
}

func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
	fboIdx := 0
	if fbo {
		fboIdx = 1
	}
	p := b.pipelines[fboIdx][mat]
	b.ctx.BindPipeline(p.pipeline)
	var uniforms *blitUniforms
	switch mat {
	case materialColor:
		b.colUniforms.color = col
		uniforms = &b.colUniforms.blitUniforms
	case materialTexture:
		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
		uniforms = &b.texUniforms.blitUniforms
		uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
		uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
	case materialLinearGradient:
		b.linearGradientUniforms.color1 = col1
		b.linearGradientUniforms.color2 = col2

		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
		uniforms = &b.linearGradientUniforms.blitUniforms
		uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
		uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
	}
	uniforms.fbo = 0
	if fbo {
		uniforms.fbo = 1
	}
	uniforms.opacity = opacity
	uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
	p.UploadUniforms(b.ctx)
	b.ctx.DrawArrays(0, 4)
}

// newUniformBuffer creates a new GPU uniform buffer backed by the
// structure uniformBlock points to.
func newUniformBuffer(b driver.Device, uniformBlock interface{}) *uniformBuffer {
	ref := reflect.ValueOf(uniformBlock)
	// Determine the size of the uniforms structure, *uniforms.
	size := ref.Elem().Type().Size()
	// Map the uniforms structure as a byte slice.
	ptr := unsafe.Slice((*byte)(unsafe.Pointer(ref.Pointer())), size)
	ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr))
	if err != nil {
		panic(err)
	}
	return &uniformBuffer{buf: ubuf, ptr: ptr}
}

func (u *uniformBuffer) Upload() {
	u.buf.Upload(u.ptr)
}

func (u *uniformBuffer) Release() {
	u.buf.Release()
	u.buf = nil
}

func (p *pipeline) UploadUniforms(ctx driver.Device) {
	if p.uniforms != nil {
		p.uniforms.Upload()
		ctx.BindUniforms(p.uniforms.buf)
	}
}

func (p *pipeline) Release() {
	p.pipeline.Release()
	if p.uniforms != nil {
		p.uniforms.Release()
	}
	*p = pipeline{}
}

// texSpaceTransform return the scale and offset that transforms the given subimage
// into quad texture coordinates.
func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, f32.Point) {
	size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)}
	scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y}
	offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y}
	return scale, offset
}

// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)].
func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f32.Point) f32.Affine2D {
	d := stop2.Sub(stop1)
	l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
	a := float32(math.Atan2(float64(-d.Y), float64(d.X)))

	// TODO: optimize
	zp := f32.Point{}
	return f32.Affine2D{}.
		Scale(zp, layout.FPt(clip.Size())).            // scale to pixel space
		Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
		Offset(zp.Sub(stop1)).                         // offset to first stop point
		Rotate(zp, a).                                 // rotate to align gradient
		Scale(zp, f32.Pt(1/l, 1/l))                    // scale gradient to right size
}

// clipSpaceTransform returns the scale and offset that transforms the given
// rectangle from a viewport into GPU driver device coordinates.
func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) {
	// First, transform UI coordinates to device coordinates:
	//
	//	[(-1, -1) (+1, -1)]
	//	[(-1, +1) (+1, +1)]
	//
	x, y := float32(r.Min.X), float32(r.Min.Y)
	w, h := float32(r.Dx()), float32(r.Dy())
	vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y)
	x = x*vx - 1
	y = y*vy - 1
	w *= vx
	h *= vy

	// Then, compute the transformation from the fullscreen quad to
	// 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
}

// Fill in maximal Y coordinates of the NW and NE corners.
func fillMaxY(verts []byte) {
	contour := 0
	bo := binary.LittleEndian
	for len(verts) > 0 {
		maxy := float32(math.Inf(-1))
		i := 0
		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(((*vertex)(nil)).MaxY)):]))
			if contour != pathContour {
				contour = pathContour
				break
			}
			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
			}
			if ctrly > maxy {
				maxy = ctrly
			}
			if toy > maxy {
				maxy = toy
			}
		}
		fillContourMaxY(maxy, verts[:i])
		verts = verts[i:]
	}
}

func fillContourMaxY(maxy float32, verts []byte) {
	bo := binary.LittleEndian
	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:]
}

// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, strWidth float32) (verts []byte, bounds f32.Rectangle) {
	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
	startLength := len(d.vertCache)

	switch {
	case strWidth > 0:
		// Stroke path.
		ss := stroke.StrokeStyle{
			Width: strWidth,
		}
		quads := stroke.StrokePathCommands(ss, pathData)
		for _, quad := range quads {
			d.qs.contour = quad.Contour
			quad.Quad = quad.Quad.Transform(tr)

			d.qs.splitAndEncode(quad.Quad)
		}

	case outline:
		decodeToOutlineQuads(&d.qs, tr, pathData)
	}

	fillMaxY(d.vertCache[startLength:])
	return d.vertCache[startLength:], d.qs.bounds
}

// decodeOutlineQuads decodes scene commands, splits them into quadratic béziers
// as needed and feeds them to the supplied splitter.
func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
	for len(pathData) >= scene.CommandSize+4 {
		qs.contour = binary.LittleEndian.Uint32(pathData)
		cmd := ops.DecodeCommand(pathData[4:])
		switch cmd.Op() {
		case scene.OpLine:
			var q stroke.QuadSegment
			q.From, q.To = scene.DecodeLine(cmd)
			q.Ctrl = q.From.Add(q.To).Mul(.5)
			q = q.Transform(tr)
			qs.splitAndEncode(q)
		case scene.OpGap:
			var q stroke.QuadSegment
			q.From, q.To = scene.DecodeGap(cmd)
			q.Ctrl = q.From.Add(q.To).Mul(.5)
			q = q.Transform(tr)
			qs.splitAndEncode(q)
		case scene.OpQuad:
			var q stroke.QuadSegment
			q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
			q = q.Transform(tr)
			qs.splitAndEncode(q)
		case scene.OpCubic:
			from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd)
			qs.scratch = stroke.SplitCubic(from, ctrl0, ctrl1, to, qs.scratch[:0])
			for _, q := range qs.scratch {
				q = q.Transform(tr)
				qs.splitAndEncode(q)
			}
		default:
			panic("unsupported scene command")
		}
		pathData = pathData[scene.CommandSize+4:]
	}
}

// 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
}

func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.VertexShader, frag driver.FragmentShader, err error) {
	vert, err = ctx.NewVertexShader(vsrc)
	if err != nil {
		return
	}
	frag, err = ctx.NewFragmentShader(fsrc)
	if err != nil {
		vert.Release()
	}
	return
}
