gpu.mx raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  // GPU rendering pipeline. Ported from gioui.org/gpu.
   4  // Merges gpu.go, path.go, clip.go, caches.go, pack.go.
   5  // Timer support dropped. Compute shader path dropped (stencil-and-cover only).
   6  
   7  package gio
   8  
   9  import (
  10  	"encoding/binary"
  11  	"fmt"
  12  	"image"
  13  	"image/color"
  14  	"math"
  15  	"slices"
  16  	"unsafe"
  17  )
  18  
  19  // GPU is the interface for rendering Gio operations.
  20  type GPU interface {
  21  	Release()
  22  	Clear(color color.NRGBA)
  23  	Frame(frame *OpList, target RenderTarget, viewport image.Point) error
  24  }
  25  
  26  type gpu struct {
  27  	cache      *textureCache
  28  	drawOps    drawOps
  29  	ctx        Device
  30  	renderer   *renderer
  31  	tess       tessGenCache
  32  	generation uint32
  33  	// Async tess channels (non-nil when spawn worker is running).
  34  	// In sync mode (WASM / moxie_sync_tess build tag), both are nil.
  35  	tessReqCh chan TessRequest
  36  	tessResCh chan TessResult
  37  }
  38  
  39  type renderer struct {
  40  	ctx           Device
  41  	blitter       *blitter
  42  	pather        *pather
  43  	packer        packer
  44  	intersections packer
  45  	layers        packer
  46  	layerFBOs     fboSet
  47  }
  48  
  49  type drawOps struct {
  50  	reader       Reader
  51  	states       []Affine2D
  52  	transStack   []Affine2D
  53  	layers       []opacityLayer
  54  	opacityStack []int
  55  	vertCache    []byte
  56  	viewport     image.Point
  57  	clear        bool
  58  	clearColor   RGBA
  59  	imageOps     []imageOp
  60  	pathOps      []*pathOp
  61  	pathOpCache  []pathOp
  62  	tessCache    *tessGenCache // set by gpu each frame; nil disables caching
  63  	generation   uint32       // current frame generation (for tessCache lastGen)
  64  	qs           quadSplitter
  65  	pathCache    *opCache
  66  }
  67  
  68  type opacityLayer struct {
  69  	opacity    float32
  70  	parent     int
  71  	depth      int
  72  	opStart    int
  73  	opEnd      int
  74  	clip       image.Rectangle
  75  	place      placement
  76  }
  77  
  78  type drawState struct {
  79  	t     Affine2D
  80  	cpath *pathOp
  81  
  82  	matType materialType
  83  	image   imageOpData
  84  	color   color.NRGBA
  85  
  86  	stop1  Point
  87  	stop2  Point
  88  	color1 color.NRGBA
  89  	color2 color.NRGBA
  90  }
  91  
  92  type pathOp struct {
  93  	off     Point
  94  	rect    bool
  95  	clip    image.Rectangle
  96  	bounds  Rectangle
  97  	intersect Rectangle
  98  	pathKey   opKey
  99  	pathHash  uint64 // geometry hash for tessGenCache; 0 = not set
 100  	path    bool
 101  	pathVerts []byte
 102  	parent  *pathOp
 103  	place   placement
 104  }
 105  
 106  type imageOp struct {
 107  	path     *pathOp
 108  	clip     image.Rectangle
 109  	material material
 110  	clipType clipType
 111  	place    placement
 112  	layerOps int
 113  }
 114  
 115  func decodeStrokeOp(data []byte) float32 {
 116  	_ = data[4]
 117  	bo := binary.LittleEndian()
 118  	return math.Float32frombits(bo.Uint32(data[1:]))
 119  }
 120  
 121  type quadsOp struct {
 122  	key opKey
 123  	aux []byte
 124  }
 125  
 126  type opKey struct {
 127  	outline     bool
 128  	strokeWidth float32
 129  	sx, hx, sy, hy float32
 130  	Key
 131  }
 132  
 133  type material struct {
 134  	material materialType
 135  	opaque   bool
 136  	color    RGBA
 137  	color1   RGBA
 138  	color2   RGBA
 139  	opacity  float32
 140  	data     imageOpData
 141  	tex      Texture
 142  	uvTrans  Affine2D
 143  }
 144  
 145  const (
 146  	filterLinear  = 0
 147  	filterNearest = 1
 148  )
 149  
 150  type imageOpData struct {
 151  	src    *image.RGBA
 152  	handle any
 153  	filter byte
 154  }
 155  
 156  type linearGradientOpData struct {
 157  	stop1  Point
 158  	color1 color.NRGBA
 159  	stop2  Point
 160  	color2 color.NRGBA
 161  }
 162  
 163  func decodeImageOp(data []byte, refs []any) imageOpData {
 164  	handle := refs[1]
 165  	if handle == nil {
 166  		return imageOpData{}
 167  	}
 168  	return imageOpData{
 169  		src:    refs[0].(*image.RGBA),
 170  		handle: handle,
 171  		filter: data[1],
 172  	}
 173  }
 174  
 175  func decodeColorOp(data []byte) color.NRGBA {
 176  	data = data[:TypeColorLen]
 177  	return color.NRGBA{
 178  		R: data[1],
 179  		G: data[2],
 180  		B: data[3],
 181  		A: data[4],
 182  	}
 183  }
 184  
 185  func decodeLinearGradientOp(data []byte) linearGradientOpData {
 186  	data = data[:TypeLinearGradientLen]
 187  	bo := binary.LittleEndian()
 188  	return linearGradientOpData{
 189  		stop1: Point{
 190  			X: math.Float32frombits(bo.Uint32(data[1:])),
 191  			Y: math.Float32frombits(bo.Uint32(data[5:])),
 192  		},
 193  		stop2: Point{
 194  			X: math.Float32frombits(bo.Uint32(data[9:])),
 195  			Y: math.Float32frombits(bo.Uint32(data[13:])),
 196  		},
 197  		color1: color.NRGBA{
 198  			R: data[17+0], G: data[17+1], B: data[17+2], A: data[17+3],
 199  		},
 200  		color2: color.NRGBA{
 201  			R: data[21+0], G: data[21+1], B: data[21+2], A: data[21+3],
 202  		},
 203  	}
 204  }
 205  
 206  type gpuResource interface {
 207  	release()
 208  }
 209  
 210  type gpuTexture struct {
 211  	src *image.RGBA
 212  	tex Texture
 213  }
 214  
 215  type blitter struct {
 216  	ctx                    Device
 217  	viewport               image.Point
 218  	pipelines              [2][3]*blitPipeline
 219  	colUniforms            *blitColUniforms
 220  	texUniforms            *blitTexUniforms
 221  	linearGradientUniforms *blitLinearGradientUniforms
 222  	quadVerts              Buffer
 223  }
 224  
 225  type blitColUniforms struct {
 226  	blitUniforms
 227  	_ [48]byte // 128 - 64 (blitUniforms) - 16 (colorUniforms)
 228  	colorUniforms
 229  }
 230  
 231  type blitTexUniforms struct {
 232  	blitUniforms
 233  }
 234  
 235  type blitLinearGradientUniforms struct {
 236  	blitUniforms
 237  	_ [32]byte // 128 - 64 (blitUniforms) - 32 (gradientUniforms)
 238  	gradientUniforms
 239  }
 240  
 241  type gpuUniformBuffer struct {
 242  	buf Buffer
 243  	ptr []byte
 244  }
 245  
 246  type blitPipeline struct {
 247  	pipeline Pipeline
 248  	uniforms *gpuUniformBuffer
 249  }
 250  
 251  type blitUniforms struct {
 252  	transform     [4]float32
 253  	uvTransformR1 [4]float32
 254  	uvTransformR2 [4]float32
 255  	opacity       float32
 256  	fbo           float32
 257  	_             [2]float32
 258  }
 259  
 260  type colorUniforms struct {
 261  	color RGBA
 262  }
 263  
 264  type gradientUniforms struct {
 265  	color1 RGBA
 266  	color2 RGBA
 267  }
 268  
 269  type clipType uint8
 270  
 271  const (
 272  	clipTypeNone        clipType = iota
 273  	clipTypePath
 274  	clipTypeIntersection
 275  )
 276  
 277  type materialType uint8
 278  
 279  const (
 280  	materialColor materialType = iota
 281  	materialLinearGradient
 282  	materialTexture
 283  )
 284  
 285  // NewGPU creates a GPU for the given API.
 286  func NewGPU(api API) (GPU, error) {
 287  	d, err := NewDevice(api)
 288  	if err != nil {
 289  		return nil, err
 290  	}
 291  	return NewGPUWithDevice(d)
 292  }
 293  
 294  // NewGPUWithDevice creates a GPU with a pre-existing device.
 295  func NewGPUWithDevice(d Device) (GPU, error) {
 296  	d.BeginFrame(nil, false, image.Point{})
 297  	defer d.EndFrame()
 298  	feats := d.Caps().Features
 299  	switch {
 300  	case feats.Has(FeatureFloatRenderTargets) && feats.Has(FeatureSRGB):
 301  		return newGPU(d)
 302  	}
 303  	return nil, errorf("gio: no available GPU driver")
 304  }
 305  
 306  func newGPU(ctx Device) (*gpu, error) {
 307  	g := &gpu{
 308  		cache: newTextureCache(),
 309  		tess:  newTessGenCache(),
 310  	}
 311  	g.drawOps.pathCache = newOpCache()
 312  	// Pre-size slices that grow during frame collection to avoid per-frame
 313  	// allocations with the leaking GC. Size for typical UI frame (16 ops).
 314  	g.drawOps.imageOps = []imageOp{:0:16}
 315  	g.drawOps.pathOpCache = []pathOp{:0:16}
 316  	g.drawOps.transStack = []Affine2D{:0:8}
 317  	g.drawOps.states = []Affine2D{:0:8}
 318  	g.drawOps.layers = []opacityLayer{:0:4}
 319  	g.drawOps.opacityStack = []int{:0:4}
 320  	g.ctx = ctx
 321  	g.renderer = newRenderer(ctx)
 322  	return g, nil
 323  }
 324  
 325  func (g *gpu) Clear(col color.NRGBA) {
 326  	g.drawOps.clear = true
 327  	g.drawOps.clearColor = LinearFromSRGB(col)
 328  }
 329  
 330  func (g *gpu) Release() {
 331  	g.renderer.release()
 332  	g.drawOps.pathCache.release()
 333  	g.cache.release()
 334  	g.ctx.Release()
 335  	// tessGenCache has no GPU resources; just drop the map.
 336  	g.tess = tessGenCache{}
 337  }
 338  
 339  func (g *gpu) Frame(frameOps *OpList, target RenderTarget, viewport image.Point) error {
 340  	g.collect(viewport, frameOps)
 341  	return g.frame(target)
 342  }
 343  
 344  func (g *gpu) collect(viewport image.Point, frameOps *OpList) {
 345  	g.renderer.blitter.viewport = viewport
 346  	g.renderer.pather.viewport = viewport
 347  	g.generation++
 348  	g.drawOps.tessCache = &g.tess
 349  	g.drawOps.generation = g.generation
 350  	g.drawOps.reset(viewport)
 351  	g.drawOps.collect(frameOps, viewport)
 352  }
 353  
 354  func (g *gpu) frame(target RenderTarget) error {
 355  	viewport := g.renderer.blitter.viewport
 356  	defFBO := g.ctx.BeginFrame(target, g.drawOps.clear, viewport)
 357  	defer g.ctx.EndFrame()
 358  	g.buildPathsTess()
 359  	for _, img := range g.drawOps.imageOps {
 360  		expandPathOp(img.path, img.clip)
 361  	}
 362  	g.renderer.packStencils(&g.drawOps.pathOps)
 363  	g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
 364  	g.renderer.packIntersections(g.drawOps.imageOps)
 365  	g.renderer.prepareIntersections(g.drawOps.imageOps)
 366  	g.renderer.intersect(g.drawOps.imageOps)
 367  	g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
 368  	g.renderer.prepareDrawOps(g.drawOps.imageOps)
 369  	g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
 370  	g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
 371  	d := LoadDesc{
 372  		ClearColor: g.drawOps.clearColor,
 373  	}
 374  	if g.drawOps.clear {
 375  		g.drawOps.clear = false
 376  		d.Action = LoadActionClear
 377  	}
 378  	g.ctx.BeginRenderPass(defFBO, d)
 379  	g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
 380  	g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
 381  	g.ctx.EndRenderPass()
 382  	g.cache.frame()
 383  	g.drawOps.pathCache.frame()
 384  	return nil
 385  }
 386  
 387  func (r *renderer) texHandle(cache *textureCache, data imageOpData) Texture {
 388  	key := textureCacheKey{filter: data.filter, handle: data.handle}
 389  	var tex *gpuTexture
 390  	t, exists := cache.get(key)
 391  	if !exists {
 392  		t = &gpuTexture{src: data.src}
 393  		cache.put(key, t)
 394  	}
 395  	tex = t.(*gpuTexture)
 396  	if tex.tex != nil {
 397  		return tex.tex
 398  	}
 399  	var minFilter, magFilter TextureFilter
 400  	switch data.filter {
 401  	case filterLinear:
 402  		minFilter, magFilter = FilterLinearMipmapLinear, FilterLinear
 403  	case filterNearest:
 404  		minFilter, magFilter = FilterNearest, FilterNearest
 405  	}
 406  	handle, err := r.ctx.NewTexture(TextureFormatSRGBA,
 407  		data.src.Bounds().Dx(), data.src.Bounds().Dy(),
 408  		minFilter, magFilter,
 409  		BufferBindingTexture,
 410  	)
 411  	if err != nil {
 412  		panic(err)
 413  	}
 414  	UploadImage(handle, image.Pt(0, 0), data.src)
 415  	tex.tex = handle
 416  	return tex.tex
 417  }
 418  
 419  func (t *gpuTexture) release() {
 420  	if t.tex != nil {
 421  		t.tex.Release()
 422  	}
 423  }
 424  
 425  func newRenderer(ctx Device) *renderer {
 426  	r := &renderer{
 427  		ctx:     ctx,
 428  		blitter: newBlitter(ctx),
 429  		pather:  newPather(ctx),
 430  	}
 431  	maxDim := ctx.Caps().MaxTextureSize
 432  	if cap := 8192; maxDim > cap {
 433  		maxDim = cap
 434  	}
 435  	d := image.Pt(maxDim, maxDim)
 436  	r.packer.maxDims = d
 437  	r.intersections.maxDims = d
 438  	r.layers.maxDims = d
 439  	return r
 440  }
 441  
 442  func (r *renderer) release() {
 443  	r.pather.release()
 444  	r.blitter.release()
 445  	r.layerFBOs.delete(r.ctx, 0)
 446  }
 447  
 448  func newGPUUniformBuffer(b Device, ptr unsafe.Pointer, size int) *gpuUniformBuffer {
 449  	ubuf, err := b.NewBuffer(BufferBindingUniforms, size)
 450  	if err != nil {
 451  		panic(err)
 452  	}
 453  	data := unsafe.Slice((*byte)(ptr), size)
 454  	return &gpuUniformBuffer{buf: ubuf, ptr: data}
 455  }
 456  
 457  func (u *gpuUniformBuffer) Upload() {
 458  	u.buf.Upload(u.ptr)
 459  }
 460  
 461  func (u *gpuUniformBuffer) Release() {
 462  	u.buf.Release()
 463  	u.buf = nil
 464  }
 465  
 466  func (p *blitPipeline) UploadUniforms(ctx Device) {
 467  	if p.uniforms != nil {
 468  		p.uniforms.Upload()
 469  		ctx.BindUniforms(p.uniforms.buf)
 470  	}
 471  }
 472  
 473  func (p *blitPipeline) Release() {
 474  	p.pipeline.Release()
 475  	if p.uniforms != nil {
 476  		p.uniforms.Release()
 477  	}
 478  	*p = blitPipeline{}
 479  }
 480  
 481  func newBlitter(ctx Device) *blitter {
 482  	quadVerts, err := ctx.NewImmutableBuffer(BufferBindingVertices,
 483  		Float32sToBytes([]float32{
 484  			-1, -1, 0, 0,
 485  			+1, -1, 1, 0,
 486  			-1, +1, 0, 1,
 487  			+1, +1, 1, 1,
 488  		}),
 489  	)
 490  	if err != nil {
 491  		panic(err)
 492  	}
 493  	b := &blitter{
 494  		ctx:       ctx,
 495  		quadVerts: quadVerts,
 496  	}
 497  	b.colUniforms = &blitColUniforms{}
 498  	b.texUniforms = &blitTexUniforms{}
 499  	b.linearGradientUniforms = &blitLinearGradientUniforms{}
 500  	pipelines, err := createColorPrograms(ctx, ShaderBlitVert(), ShaderBlitFrag(),
 501  		[3]unsafe.Pointer{
 502  			unsafe.Pointer(b.colUniforms),
 503  			unsafe.Pointer(b.linearGradientUniforms),
 504  			unsafe.Pointer(b.texUniforms),
 505  		},
 506  		[3]int{
 507  			int(unsafe.Sizeof(*b.colUniforms)),
 508  			int(unsafe.Sizeof(*b.linearGradientUniforms)),
 509  			int(unsafe.Sizeof(*b.texUniforms)),
 510  		},
 511  	)
 512  	if err != nil {
 513  		panic(err)
 514  	}
 515  	b.pipelines = pipelines
 516  	return b
 517  }
 518  
 519  func (b *blitter) release() {
 520  	b.quadVerts.Release()
 521  	for _, p := range b.pipelines {
 522  		for _, p := range p {
 523  			p.Release()
 524  		}
 525  	}
 526  }
 527  
 528  func createColorPrograms(b Device, vsSrc ShaderSources, fsSrc [3]ShaderSources, uniformPtrs [3]unsafe.Pointer, uniformSizes [3]int) (pipelines [2][3]*blitPipeline, err error) {
 529  	defer func() {
 530  		if err != nil {
 531  			for _, p := range pipelines {
 532  				for _, p := range p {
 533  					if p != nil {
 534  						p.Release()
 535  					}
 536  				}
 537  			}
 538  		}
 539  	}()
 540  	blend := BlendDesc{
 541  		Enable:    true,
 542  		SrcFactor: BlendFactorOne,
 543  		DstFactor: BlendFactorOneMinusSrcAlpha,
 544  	}
 545  	layout := VertexLayout{
 546  		Inputs: []InputDesc{
 547  			{Type: ShaderDataTypeFloat, Size: 2, Offset: 0},
 548  			{Type: ShaderDataTypeFloat, Size: 2, Offset: 4 * 2},
 549  		},
 550  		Stride: 4 * 4,
 551  	}
 552  	vsh, err := b.NewVertexShader(vsSrc)
 553  	if err != nil {
 554  		return pipelines, err
 555  	}
 556  	defer vsh.Release()
 557  	for i, format := range []TextureFormat{TextureFormatOutput, TextureFormatSRGBA} {
 558  		for mat := 0; mat < 3; mat++ {
 559  			fsh, err := b.NewFragmentShader(fsSrc[mat])
 560  			if err != nil {
 561  				return pipelines, err
 562  			}
 563  			defer fsh.Release()
 564  			pipe, err := b.NewPipeline(PipelineDesc{
 565  				VertexShader:   vsh,
 566  				FragmentShader: fsh,
 567  				BlendDesc:      blend,
 568  				VertexLayout:   layout,
 569  				PixelFormat:    format,
 570  				Topology:       TopologyTriangleStrip,
 571  			})
 572  			if err != nil {
 573  				return pipelines, err
 574  			}
 575  			var vertBuffer *gpuUniformBuffer
 576  			if uniformPtrs[mat] != nil {
 577  				vertBuffer = newGPUUniformBuffer(b, uniformPtrs[mat], uniformSizes[mat])
 578  			}
 579  			pipelines[i][mat] = &blitPipeline{pipe, vertBuffer}
 580  		}
 581  	}
 582  	return pipelines, nil
 583  }
 584  
 585  func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
 586  	if len(r.packer.sizes) == 0 {
 587  		return
 588  	}
 589  	fbo := -1
 590  	r.pather.begin(r.packer.sizes)
 591  	for _, p := range ops {
 592  		if fbo != p.place.Idx {
 593  			if fbo != -1 {
 594  				r.ctx.EndRenderPass()
 595  			}
 596  			fbo = p.place.Idx
 597  			f := r.pather.stenciler.cover(fbo)
 598  			r.ctx.BeginRenderPass(f.tex, LoadDesc{Action: LoadActionClear})
 599  			r.ctx.BindPipeline(r.pather.stenciler.pipeline.pipeline.pipeline)
 600  			r.ctx.BindIndexBuffer(r.pather.stenciler.indexBuf)
 601  		}
 602  		v, _ := pathCache.get(p.pathKey)
 603  		r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
 604  	}
 605  	if fbo != -1 {
 606  		r.ctx.EndRenderPass()
 607  	}
 608  }
 609  
 610  func (r *renderer) prepareIntersections(ops []imageOp) {
 611  	for _, img := range ops {
 612  		if img.clipType != clipTypeIntersection {
 613  			continue
 614  		}
 615  		fbo := r.pather.stenciler.cover(img.path.place.Idx)
 616  		r.ctx.PrepareTexture(fbo.tex)
 617  	}
 618  }
 619  
 620  func (r *renderer) intersect(ops []imageOp) {
 621  	if len(r.intersections.sizes) == 0 {
 622  		return
 623  	}
 624  	fbo := -1
 625  	r.pather.stenciler.beginIntersect(r.intersections.sizes)
 626  	for _, img := range ops {
 627  		if img.clipType != clipTypeIntersection {
 628  			continue
 629  		}
 630  		if fbo != img.place.Idx {
 631  			if fbo != -1 {
 632  				r.ctx.EndRenderPass()
 633  			}
 634  			fbo = img.place.Idx
 635  			f := r.pather.stenciler.intersections.fbos[fbo]
 636  			d := LoadDesc{Action: LoadActionClear}
 637  			d.ClearColor.R = 1.0
 638  			r.ctx.BeginRenderPass(f.tex, d)
 639  			r.ctx.BindPipeline(r.pather.stenciler.ipipeline.pipeline.pipeline)
 640  			r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
 641  		}
 642  		r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
 643  		r.intersectPath(img.path, img.clip)
 644  	}
 645  	if fbo != -1 {
 646  		r.ctx.EndRenderPass()
 647  	}
 648  }
 649  
 650  func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
 651  	if p.parent != nil {
 652  		r.intersectPath(p.parent, clip)
 653  	}
 654  	if !p.path {
 655  		return
 656  	}
 657  	uv := image.Rectangle{
 658  		Min: p.place.Pos,
 659  		Max: p.place.Pos.Add(p.clip.Size()),
 660  	}
 661  	o := clip.Min.Sub(p.clip.Min)
 662  	sub := image.Rectangle{
 663  		Min: o,
 664  		Max: o.Add(clip.Size()),
 665  	}
 666  	fbo := r.pather.stenciler.cover(p.place.Idx)
 667  	r.ctx.BindTexture(0, fbo.tex)
 668  	coverScale, coverOff := texSpaceTransform(FRect(uv), fbo.size)
 669  	subScale, subOff := texSpaceTransform(FRect(sub), p.clip.Size())
 670  	r.pather.stenciler.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
 671  	r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y}
 672  	r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx)
 673  	r.ctx.DrawArrays(0, 4)
 674  }
 675  
 676  func (r *renderer) packIntersections(ops []imageOp) {
 677  	r.intersections.clear()
 678  	for i, img := range ops {
 679  		var npaths int
 680  		var onePath *pathOp
 681  		for p := img.path; p != nil; p = p.parent {
 682  			if p.path {
 683  				onePath = p
 684  				npaths++
 685  			}
 686  		}
 687  		switch npaths {
 688  		case 0:
 689  		case 1:
 690  			place := onePath.place
 691  			place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
 692  			ops[i].place = place
 693  			ops[i].clipType = clipTypePath
 694  		default:
 695  			sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
 696  			place, ok := r.intersections.add(sz)
 697  			if !ok {
 698  				panic("internal error: intersection should fit")
 699  			}
 700  			ops[i].clipType = clipTypeIntersection
 701  			ops[i].place = place
 702  		}
 703  	}
 704  }
 705  
 706  func (r *renderer) packStencils(pops *[]*pathOp) {
 707  	r.packer.clear()
 708  	ops := *pops
 709  	var i int
 710  	for i < len(ops) {
 711  		p := ops[i]
 712  		if p.clip.Empty() {
 713  			ops[i] = ops[len(ops)-1]
 714  			ops = ops[:len(ops)-1]
 715  			continue
 716  		}
 717  		place, ok := r.packer.add(p.clip.Size())
 718  		if !ok {
 719  			panic(errorf("clip area %v is larger than maximum texture size %v", p.clip, r.packer.maxDims))
 720  		}
 721  		p.place = place
 722  		i++
 723  	}
 724  	*pops = ops
 725  }
 726  
 727  func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
 728  	for i := len(layers) - 1; i >= 0; i-- {
 729  		l := layers[i]
 730  		if l.parent != -1 {
 731  			b := layers[l.parent].clip
 732  			layers[l.parent].clip = b.Union(l.clip)
 733  		}
 734  		if l.clip.Empty() {
 735  			layers = slices.Delete(layers, i, i+1)
 736  		}
 737  	}
 738  	r.layers.clear()
 739  	depth := 0
 740  	for i := range layers {
 741  		l := &layers[i]
 742  		if l.depth != depth {
 743  			r.layers.newPage()
 744  		}
 745  		place, ok := r.layers.add(l.clip.Size())
 746  		if !ok {
 747  			panic(errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
 748  		}
 749  		l.place = place
 750  	}
 751  	return layers
 752  }
 753  
 754  func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
 755  	if len(r.layers.sizes) == 0 {
 756  		return
 757  	}
 758  	fbo := -1
 759  	r.layerFBOs.resize(r.ctx, TextureFormatSRGBA, r.layers.sizes)
 760  	for i := len(layers) - 1; i >= 0; i-- {
 761  		l := layers[i]
 762  		if fbo != l.place.Idx {
 763  			if fbo != -1 {
 764  				r.ctx.EndRenderPass()
 765  				r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
 766  			}
 767  			fbo = l.place.Idx
 768  			f := r.layerFBOs.fbos[fbo]
 769  			r.ctx.BeginRenderPass(f.tex, LoadDesc{Action: LoadActionClear})
 770  		}
 771  		v := image.Rectangle{
 772  			Min: l.place.Pos,
 773  			Max: l.place.Pos.Add(l.clip.Size()),
 774  		}
 775  		r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
 776  		f := r.layerFBOs.fbos[fbo]
 777  		r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
 778  		sr := FRect(v)
 779  		uvScale, uvOffset := texSpaceTransform(sr, f.size)
 780  		uvTrans := AffineId().Scale(Point{}, uvScale).Offset(uvOffset)
 781  		ops[l.opStart] = imageOp{
 782  			clip: l.clip,
 783  			material: material{
 784  				material: materialTexture,
 785  				tex:      f.tex,
 786  				uvTrans:  uvTrans,
 787  				opacity:  l.opacity,
 788  			},
 789  			layerOps: l.opEnd - l.opStart - 1,
 790  		}
 791  	}
 792  	if fbo != -1 {
 793  		r.ctx.EndRenderPass()
 794  		r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
 795  	}
 796  }
 797  
 798  func (d *drawOps) reset(viewport image.Point) {
 799  	d.viewport = viewport
 800  	d.imageOps = d.imageOps[:0]
 801  	d.pathOps = d.pathOps[:0]
 802  	d.pathOpCache = d.pathOpCache[:0]
 803  	d.vertCache = d.vertCache[:0]
 804  	d.transStack = d.transStack[:0]
 805  	d.layers = d.layers[:0]
 806  	d.opacityStack = d.opacityStack[:0]
 807  	d.states = d.states[:0]  // must reset; TypeLoad uses index from current frame's TypeSave
 808  }
 809  
 810  func (d *drawOps) collect(root *OpList, viewport image.Point) {
 811  	viewf := Rectangle{
 812  		Max: Point{X: float32(viewport.X), Y: float32(viewport.Y)},
 813  	}
 814  	var ops *Ops
 815  	if root != nil {
 816  		ops = &root.Internal
 817  	}
 818  	d.reader.ResetReader(ops)
 819  	d.collectOps(&d.reader, viewf)
 820  }
 821  
 822  func (d *drawOps) buildPaths(ctx Device) {
 823  	for _, p := range d.pathOps {
 824  		if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
 825  			data := buildPath(ctx, p.pathVerts)
 826  			d.pathCache.put(p.pathKey, opCacheValue{
 827  				data:   data,
 828  				bounds: p.bounds,
 829  			})
 830  		}
 831  		p.pathVerts = nil
 832  	}
 833  }
 834  
 835  // buildPathsTess uploads GPU vertex buffers for all path ops that need them.
 836  // Tessellation itself already happened in collectOps (via buildVerts), with
 837  // results cached in tessGenCache. This function just handles the GPU upload
 838  // step and drains any async tessellation results from the worker channel.
 839  func (g *gpu) buildPathsTess() {
 840  	// Drain async results if worker is running.
 841  	if g.tessResCh != nil {
 842  	drainLoop:
 843  		for {
 844  			select {
 845  			case res := <-g.tessResCh:
 846  				entry := &tessEntry{
 847  					key:   res.Key,
 848  					verts: []byte(res.Verts),
 849  					bounds: Rectangle{
 850  						Min: Point{X: res.BoundsMinX, Y: res.BoundsMinY},
 851  						Max: Point{X: res.BoundsMaxX, Y: res.BoundsMaxY},
 852  					},
 853  					lastGen: res.Gen,
 854  				}
 855  				g.tess.put(entry)
 856  			default:
 857  				break drainLoop
 858  			}
 859  		}
 860  	}
 861  
 862  	d := &g.drawOps
 863  	for _, p := range d.pathOps {
 864  		// If opCache already has a GPU buffer, no upload needed.
 865  		if v, exists := d.pathCache.get(p.pathKey); exists && v.data.data != nil {
 866  			p.pathVerts = nil
 867  			continue
 868  		}
 869  		// Upload tessellated vertex bytes to GPU.
 870  		if len(p.pathVerts) > 0 {
 871  			data := buildPath(g.ctx, p.pathVerts)
 872  			d.pathCache.put(p.pathKey, opCacheValue{data: data, bounds: p.bounds})
 873  		}
 874  		p.pathVerts = nil
 875  	}
 876  
 877  	// Evict tessGenCache entries older than tessEvictAge frames.
 878  	if g.generation >= tessEvictAge {
 879  		g.tess.evict(g.generation - tessEvictAge)
 880  	}
 881  }
 882  
 883  func (d *drawOps) newPathOp() *pathOp {
 884  	d.pathOpCache = append(d.pathOpCache, pathOp{})
 885  	return &d.pathOpCache[len(d.pathOpCache)-1]
 886  }
 887  
 888  func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds Rectangle, off Point) {
 889  	npath := d.newPathOp()
 890  	*npath = pathOp{
 891  		parent:    state.cpath,
 892  		bounds:    bounds,
 893  		off:       off,
 894  		intersect: bounds.Add(off),
 895  		rect:      true,
 896  	}
 897  	if npath.parent != nil {
 898  		npath.rect = npath.parent.rect
 899  		npath.intersect = npath.parent.intersect.Intersect(npath.intersect)
 900  	}
 901  	if len(aux) > 0 {
 902  		npath.rect = false
 903  		npath.pathKey = auxKey
 904  		npath.path = true
 905  		npath.pathVerts = aux
 906  		d.pathOps = append(d.pathOps, npath)
 907  	}
 908  	state.cpath = npath
 909  }
 910  
 911  func (d *drawOps) save(id int, state Affine2D) {
 912  	for extra := id - len(d.states) + 1; extra > 0; extra-- {
 913  		d.states = append(d.states, AffineId())
 914  	}
 915  	d.states[id] = state
 916  }
 917  
 918  func (k opKey) SetTransform(t Affine2D) opKey {
 919  	sx, hx, _, hy, sy, _ := t.Elems()
 920  	k.sx = sx
 921  	k.hx = hx
 922  	k.hy = hy
 923  	k.sy = sy
 924  	return k
 925  }
 926  
 927  // toAffine reconstructs an Affine2D from the transform components stored in
 928  // opKey. The translation (ox, oy) is not stored in opKey (it was split off as
 929  // a separate integer offset), so this returns a transform with zero offset.
 930  func (k opKey) toAffine() Affine2D {
 931  	return NewAffine2D(k.sx, k.hx, 0, k.hy, k.sy, 0)
 932  }
 933  
 934  func (d *drawOps) collectOps(r *Reader, viewport Rectangle) {
 935  	var quads quadsOp
 936  	state := drawState{t: AffineId()}
 937  	reset := func() {
 938  		state = drawState{
 939  			t:     AffineId(),
 940  			color: color.NRGBA{A: 0xff},
 941  		}
 942  	}
 943  	reset()
 944  loop:
 945  	for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
 946  		switch OpType(encOp.Data[0]) {
 947  		case TypeTransform:
 948  			dop, push := DecodeTransform(encOp.Data)
 949  			if push {
 950  				d.transStack = append(d.transStack, state.t)
 951  			}
 952  			state.t = state.t.Mul(dop)
 953  		case TypePopTransform:
 954  			n := len(d.transStack)
 955  			state.t = d.transStack[n-1]
 956  			d.transStack = d.transStack[:n-1]
 957  
 958  		case TypePushOpacity:
 959  			opacity := DecodeOpacity(encOp.Data)
 960  			parent := -1
 961  			depth := len(d.opacityStack)
 962  			if depth > 0 {
 963  				parent = d.opacityStack[depth-1]
 964  			}
 965  			lidx := len(d.layers)
 966  			d.layers = append(d.layers, opacityLayer{
 967  				opacity: opacity,
 968  				parent:  parent,
 969  				depth:   depth,
 970  				opStart: len(d.imageOps),
 971  			})
 972  			d.opacityStack = append(d.opacityStack, lidx)
 973  		case TypePopOpacity:
 974  			n := len(d.opacityStack)
 975  			idx := d.opacityStack[n-1]
 976  			d.layers[idx].opEnd = len(d.imageOps)
 977  			d.opacityStack = d.opacityStack[:n-1]
 978  
 979  		case TypeStroke:
 980  			quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
 981  
 982  		case TypePath:
 983  			encOp, ok = r.Decode()
 984  			if !ok {
 985  				break loop
 986  			}
 987  			quads.aux = encOp.Data[TypeAuxLen:]
 988  			quads.key.Key = encOp.Key
 989  
 990  		case TypeClip:
 991  			var op DecodedClipOp
 992  			op.Decode(encOp.Data)
 993  			quads.key.outline = op.Outline
 994  			bounds := FRect(op.Bounds)
 995  			trans, off := transformOffset(state.t)
 996  			if len(quads.aux) > 0 {
 997  				quads.key = quads.key.SetTransform(trans)
 998  				if v, ok := d.pathCache.get(quads.key); ok {
 999  					bounds = v.bounds
1000  				} else {
1001  					var pathData []byte
1002  					// Check tessGenCache before tessellating.
1003  					var tessHash uint64
1004  					if d.tessCache != nil {
1005  						tessHash = hashPathData(quads.aux, trans, quads.key.outline, quads.key.strokeWidth)
1006  						pk := PathKey{Hash: tessHash, Outline: quads.key.outline,
1007  							Stroke: quads.key.strokeWidth > 0, Width: quads.key.strokeWidth}
1008  						if entry, ok := d.tessCache.get(pk, d.generation); ok {
1009  							pathData = entry.verts
1010  							bounds = entry.bounds
1011  						}
1012  					}
1013  					if pathData == nil {
1014  						pathData, bounds = d.buildVerts(quads.aux, trans, quads.key.outline, quads.key.strokeWidth)
1015  						// Store in tessGenCache (copy: pathData points into vertCache).
1016  						if d.tessCache != nil && tessHash != 0 {
1017  							pk := PathKey{Hash: tessHash, Outline: quads.key.outline,
1018  								Stroke: quads.key.strokeWidth > 0, Width: quads.key.strokeWidth}
1019  							copyVerts := []byte{:len(pathData)}
1020  							copy(copyVerts, pathData)
1021  							d.tessCache.put(&tessEntry{key: pk, verts: copyVerts, bounds: bounds, lastGen: d.generation})
1022  						}
1023  					}
1024  					quads.aux = pathData
1025  					d.pathCache.put(quads.key, opCacheValue{bounds: bounds})
1026  				}
1027  			} else {
1028  				quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
1029  				quads.key = opKey{Key: encOp.Key}
1030  				quads.key = quads.key.SetTransform(trans)
1031  			}
1032  			d.addClipPath(&state, quads.aux, quads.key, bounds, off)
1033  			quads = quadsOp{}
1034  		case TypePopClip:
1035  			state.cpath = state.cpath.parent
1036  
1037  		case TypeColor:
1038  			state.matType = materialColor
1039  			state.color = decodeColorOp(encOp.Data)
1040  		case TypeLinearGradient:
1041  			state.matType = materialLinearGradient
1042  			op := decodeLinearGradientOp(encOp.Data)
1043  			state.stop1 = op.stop1
1044  			state.stop2 = op.stop2
1045  			state.color1 = op.color1
1046  			state.color2 = op.color2
1047  		case TypeImage:
1048  			state.matType = materialTexture
1049  			state.image = decodeImageOp(encOp.Data, encOp.Refs)
1050  		case TypePaint:
1051  			t, off := transformOffset(state.t)
1052  			inf := float32(1e6)
1053  			dst := Rect(-inf, -inf, inf, inf)
1054  			if state.matType == materialTexture {
1055  				sz := state.image.src.Rect.Size()
1056  				dst = Rectangle{Max: FPt(sz)}
1057  			}
1058  			clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, t)
1059  			cl := viewport.Intersect(bnd.Add(off))
1060  			if state.cpath != nil {
1061  				cl = state.cpath.intersect.Intersect(cl)
1062  			}
1063  			if cl.Empty() {
1064  				continue
1065  			}
1066  			if clipData != nil {
1067  				k := opKey{Key: encOp.Key}
1068  				k = k.SetTransform(t)
1069  				d.addClipPath(&state, clipData, k, bnd, off)
1070  			}
1071  			bounds := cl.Round()
1072  			mat := state.materialFor(bnd, off, partialTrans, bounds)
1073  			rect := state.cpath == nil || state.cpath.rect
1074  			if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
1075  				d.imageOps = d.imageOps[:0]
1076  				d.clearColor = mat.color.Opaque()
1077  				d.clear = true
1078  				continue
1079  			}
1080  			img := imageOp{
1081  				path:     state.cpath,
1082  				clip:     bounds,
1083  				material: mat,
1084  			}
1085  			if n := len(d.opacityStack); n > 0 {
1086  				idx := d.opacityStack[n-1]
1087  				lb := d.layers[idx].clip
1088  				if lb.Empty() {
1089  					d.layers[idx].clip = img.clip
1090  				} else {
1091  					d.layers[idx].clip = lb.Union(img.clip)
1092  				}
1093  			}
1094  			d.imageOps = append(d.imageOps, img)
1095  			if clipData != nil {
1096  				state.cpath = state.cpath.parent
1097  			}
1098  		case TypeSave:
1099  			id := DecodeSave(encOp.Data)
1100  			d.save(id, state.t)
1101  		case TypeLoad:
1102  			reset()
1103  			id := DecodeLoad(encOp.Data)
1104  			state.t = d.states[id]
1105  		}
1106  	}
1107  }
1108  
1109  func expandPathOp(p *pathOp, clip image.Rectangle) {
1110  	for p != nil {
1111  		pclip := p.clip
1112  		if !pclip.Empty() {
1113  			clip = clip.Union(pclip)
1114  		}
1115  		p.clip = clip
1116  		p = p.parent
1117  	}
1118  }
1119  
1120  func (d *drawState) materialFor(rect Rectangle, off Point, partTrans Affine2D, clip image.Rectangle) material {
1121  	m := material{
1122  		opacity: 1.,
1123  		uvTrans: AffineId(),
1124  	}
1125  	switch d.matType {
1126  	case materialColor:
1127  		m.material = materialColor
1128  		m.color = LinearFromSRGB(d.color)
1129  		m.opaque = m.color.A == 1.0
1130  	case materialLinearGradient:
1131  		m.material = materialLinearGradient
1132  		m.color1 = LinearFromSRGB(d.color1)
1133  		m.color2 = LinearFromSRGB(d.color2)
1134  		m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0
1135  		m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2))
1136  	case materialTexture:
1137  		m.material = materialTexture
1138  		dr := rect.Add(off).Round()
1139  		sz := d.image.src.Bounds().Size()
1140  		sr := Rectangle{Max: Point{X: float32(sz.X), Y: float32(sz.Y)}}
1141  		dx := float32(dr.Dx())
1142  		sdx := sr.Dx()
1143  		sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx
1144  		sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx
1145  		dy := float32(dr.Dy())
1146  		sdy := sr.Dy()
1147  		sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
1148  		sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
1149  		uvScale, uvOffset := texSpaceTransform(sr, sz)
1150  		m.uvTrans = partTrans.Mul(AffineId().Scale(Point{}, uvScale).Offset(uvOffset))
1151  		m.data = d.image
1152  	}
1153  	return m
1154  }
1155  
1156  func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
1157  	for i := range ops {
1158  		img := &ops[i]
1159  		m := img.material
1160  		if m.material == materialTexture {
1161  			img.material.tex = r.texHandle(cache, m.data)
1162  		}
1163  	}
1164  }
1165  
1166  func (r *renderer) prepareDrawOps(ops []imageOp) {
1167  	for _, img := range ops {
1168  		m := img.material
1169  		switch m.material {
1170  		case materialTexture:
1171  			r.ctx.PrepareTexture(m.tex)
1172  		}
1173  		var fbo FBO
1174  		switch img.clipType {
1175  		case clipTypeNone:
1176  			continue
1177  		case clipTypePath:
1178  			fbo = r.pather.stenciler.cover(img.place.Idx)
1179  		case clipTypeIntersection:
1180  			fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
1181  		}
1182  		r.ctx.PrepareTexture(fbo.tex)
1183  	}
1184  }
1185  
1186  func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
1187  	var coverTex Texture
1188  	for i := 0; i < len(ops); i++ {
1189  		img := ops[i]
1190  		i += img.layerOps
1191  		m := img.material
1192  		switch m.material {
1193  		case materialTexture:
1194  			r.ctx.BindTexture(0, m.tex)
1195  		}
1196  		drc := img.clip.Add(opOff)
1197  		scale, off := clipSpaceTransform(drc, viewport)
1198  		var fbo FBO
1199  		fboIdx := 0
1200  		if isFBO {
1201  			fboIdx = 1
1202  		}
1203  		switch img.clipType {
1204  		case clipTypeNone:
1205  			p := r.blitter.pipelines[fboIdx][m.material]
1206  			r.ctx.BindPipeline(p.pipeline)
1207  			r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
1208  			r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
1209  			continue
1210  		case clipTypePath:
1211  			fbo = r.pather.stenciler.cover(img.place.Idx)
1212  		case clipTypeIntersection:
1213  			fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
1214  		}
1215  		if coverTex != fbo.tex {
1216  			coverTex = fbo.tex
1217  			r.ctx.BindTexture(1, coverTex)
1218  		}
1219  		uv := image.Rectangle{
1220  			Min: img.place.Pos,
1221  			Max: img.place.Pos.Add(drc.Size()),
1222  		}
1223  		coverScale, coverOff := texSpaceTransform(FRect(uv), fbo.size)
1224  		p := r.pather.coverer.pipelines[fboIdx][m.material]
1225  		r.ctx.BindPipeline(p.pipeline)
1226  		r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
1227  		r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
1228  	}
1229  }
1230  
1231  func (b *blitter) blit(mat materialType, fbo bool, col RGBA, col1, col2 RGBA, scale, off Point, opacity float32, uvTrans Affine2D) {
1232  	fboIdx := 0
1233  	if fbo {
1234  		fboIdx = 1
1235  	}
1236  	p := b.pipelines[fboIdx][mat]
1237  	b.ctx.BindPipeline(p.pipeline)
1238  	var uniforms *blitUniforms
1239  	switch mat {
1240  	case materialColor:
1241  		b.colUniforms.color = col
1242  		uniforms = &b.colUniforms.blitUniforms
1243  	case materialTexture:
1244  		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
1245  		uniforms = &b.texUniforms.blitUniforms
1246  		uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
1247  		uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
1248  	case materialLinearGradient:
1249  		b.linearGradientUniforms.color1 = col1
1250  		b.linearGradientUniforms.color2 = col2
1251  		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
1252  		uniforms = &b.linearGradientUniforms.blitUniforms
1253  		uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
1254  		uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
1255  	}
1256  	uniforms.fbo = 0
1257  	if fbo {
1258  		uniforms.fbo = 1
1259  	}
1260  	uniforms.opacity = opacity
1261  	uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
1262  	p.UploadUniforms(b.ctx)
1263  	b.ctx.DrawArrays(0, 4)
1264  }
1265  
1266  func texSpaceTransform(r Rectangle, bounds image.Point) (Point, Point) {
1267  	size := Point{X: float32(bounds.X), Y: float32(bounds.Y)}
1268  	scale := Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y}
1269  	offset := Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y}
1270  	return scale, offset
1271  }
1272  
1273  func gradientSpaceTransform(clip image.Rectangle, off Point, stop1, stop2 Point) Affine2D {
1274  	d := stop2.Sub(stop1)
1275  	l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
1276  	a := float32(math.Atan2(float64(-d.Y), float64(d.X)))
1277  	zp := Point{}
1278  	return AffineId().
1279  		Scale(zp, FPt(clip.Size())).
1280  		Offset(zp.Sub(off).Add(FPt(clip.Min))).
1281  		Offset(zp.Sub(stop1)).
1282  		Rotate(zp, a).
1283  		Scale(zp, Pt(1/l, 1/l))
1284  }
1285  
1286  func clipSpaceTransform(r image.Rectangle, viewport image.Point) (Point, Point) {
1287  	x, y := float32(r.Min.X), float32(r.Min.Y)
1288  	w, h := float32(r.Dx()), float32(r.Dy())
1289  	vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y)
1290  	x = x*vx - 1
1291  	y = y*vy - 1
1292  	w *= vx
1293  	h *= vy
1294  	scale := Point{X: w * .5, Y: h * .5}
1295  	offset := Point{X: x + w*.5, Y: y + h*.5}
1296  	return scale, offset
1297  }
1298  
1299  func fillMaxY(verts []byte) {
1300  	contour := 0
1301  	bo := binary.LittleEndian()
1302  	for len(verts) > 0 {
1303  		maxy := float32(math.Inf(-1))
1304  		i := 0
1305  		for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
1306  			vert := verts[i : i+vertStride]
1307  			pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof((*vertex)(nil).MaxY)):]))
1308  			if contour != pathContour {
1309  				contour = pathContour
1310  				break
1311  			}
1312  			fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof((*vertex)(nil).FromY)):]))
1313  			ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof((*vertex)(nil).CtrlY)):]))
1314  			toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof((*vertex)(nil).ToY)):]))
1315  			if fromy > maxy {
1316  				maxy = fromy
1317  			}
1318  			if ctrly > maxy {
1319  				maxy = ctrly
1320  			}
1321  			if toy > maxy {
1322  				maxy = toy
1323  			}
1324  		}
1325  		fillContourMaxY(maxy, verts[:i])
1326  		verts = verts[i:]
1327  	}
1328  }
1329  
1330  func fillContourMaxY(maxy float32, verts []byte) {
1331  	bo := binary.LittleEndian()
1332  	for i := 0; i < len(verts); i += vertStride {
1333  		off := int(unsafe.Offsetof((*vertex)(nil).MaxY))
1334  		bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
1335  	}
1336  }
1337  
1338  func (d *drawOps) writeVertCache(n int) []byte {
1339  	d.vertCache = append(d.vertCache, []byte{:n}...)
1340  	return d.vertCache[len(d.vertCache)-n:]
1341  }
1342  
1343  func (d *drawOps) buildVerts(pathData []byte, tr Affine2D, outline bool, strWidth float32) (verts []byte, bounds Rectangle) {
1344  	inf := float32(math.Inf(+1))
1345  	d.qs.bounds = Rectangle{
1346  		Min: Point{X: inf, Y: inf},
1347  		Max: Point{X: -inf, Y: -inf},
1348  	}
1349  	d.qs.d = d
1350  	startLength := len(d.vertCache)
1351  	switch {
1352  	case strWidth > 0:
1353  		ss := StrokeStyle{Width: strWidth}
1354  		quads := StrokePathCommands(ss, pathData)
1355  		for _, quad := range quads {
1356  			d.qs.contour = quad.Contour
1357  			quad.Quad = quad.Quad.Transform(tr)
1358  			d.qs.splitAndEncode(quad.Quad)
1359  		}
1360  	case outline:
1361  		decodeToOutlineQuads(&d.qs, tr, pathData)
1362  	}
1363  	fillMaxY(d.vertCache[startLength:])
1364  	return d.vertCache[startLength:], d.qs.bounds
1365  }
1366  
1367  func decodeToOutlineQuads(qs *quadSplitter, tr Affine2D, pathData []byte) {
1368  	for len(pathData) >= sceneElemSize+4 {
1369  		qs.contour = binary.LittleEndian().Uint32(pathData)
1370  		cmd := DecodeCommand(pathData[4:])
1371  		switch cmd.Op() {
1372  		case OpLine:
1373  			var q QuadSegment
1374  			q.From, q.To = DecodeLine(cmd)
1375  			q.Ctrl = q.From.Add(q.To).Mul(.5)
1376  			q = q.Transform(tr)
1377  			qs.splitAndEncode(q)
1378  		case OpGap:
1379  			var q QuadSegment
1380  			q.From, q.To = DecodeGap(cmd)
1381  			q.Ctrl = q.From.Add(q.To).Mul(.5)
1382  			q = q.Transform(tr)
1383  			qs.splitAndEncode(q)
1384  		case OpQuad:
1385  			var q QuadSegment
1386  			q.From, q.Ctrl, q.To = DecodeQuad(cmd)
1387  			q = q.Transform(tr)
1388  			qs.splitAndEncode(q)
1389  		case OpCubic:
1390  			from, ctrl0, ctrl1, to := DecodeCubic(cmd)
1391  			qs.scratch = SplitCubic(from, ctrl0, ctrl1, to, qs.scratch[:0])
1392  			for _, q := range qs.scratch {
1393  				q = q.Transform(tr)
1394  				qs.splitAndEncode(q)
1395  			}
1396  		default:
1397  			panic("unsupported scene command")
1398  		}
1399  		pathData = pathData[sceneElemSize+4:]
1400  	}
1401  }
1402  
1403  func (d *drawOps) boundsForTransformedRect(r Rectangle, tr Affine2D) (aux []byte, bnd Rectangle, ptr Affine2D) {
1404  	ptr = AffineId()
1405  	if tr == AffineId() {
1406  		bnd = r
1407  		return
1408  	}
1409  	corners := [4]Point{
1410  		tr.Transform(r.Min), tr.Transform(Pt(r.Max.X, r.Min.Y)),
1411  		tr.Transform(r.Max), tr.Transform(Pt(r.Min.X, r.Max.Y)),
1412  	}
1413  	bnd.Min = Pt(math.MaxFloat32, math.MaxFloat32)
1414  	bnd.Max = Pt(-math.MaxFloat32, -math.MaxFloat32)
1415  	for _, c := range corners {
1416  		if c.X < bnd.Min.X {
1417  			bnd.Min.X = c.X
1418  		}
1419  		if c.Y < bnd.Min.Y {
1420  			bnd.Min.Y = c.Y
1421  		}
1422  		if c.X > bnd.Max.X {
1423  			bnd.Max.X = c.X
1424  		}
1425  		if c.Y > bnd.Max.Y {
1426  			bnd.Max.Y = c.Y
1427  		}
1428  	}
1429  	l := len(d.vertCache)
1430  	d.vertCache = append(d.vertCache, []byte{:vertStride * 4 * 4}...)
1431  	aux = d.vertCache[l:]
1432  	encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1])
1433  	encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2])
1434  	encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3])
1435  	encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0])
1436  	fillMaxY(aux)
1437  	var P1, P2, P3 Point
1438  	P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
1439  	P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
1440  	P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
1441  	P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
1442  	P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
1443  	P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
1444  	sx, sy := P2.X-P3.X, P2.Y-P3.Y
1445  	ptr = NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert()
1446  	return aux, bnd, ptr
1447  }
1448  
1449  func transformOffset(t Affine2D) (Affine2D, Point) {
1450  	sx, hx, ox, hy, sy, oy := t.Elems()
1451  	iox, fox := math.Modf(float64(ox))
1452  	ioy, foy := math.Modf(float64(oy))
1453  	ft := NewAffine2D(sx, hx, float32(fox), hy, sy, float32(foy))
1454  	ip := Pt(float32(iox), float32(ioy))
1455  	return ft, ip
1456  }
1457  
1458  func newShaders(ctx Device, vsrc, fsrc ShaderSources) (VertexShader, FragmentShader, error) {
1459  	vert, err := ctx.NewVertexShader(vsrc)
1460  	if err != nil {
1461  		return nil, nil, err
1462  	}
1463  	frag, err := ctx.NewFragmentShader(fsrc)
1464  	if err != nil {
1465  		vert.Release()
1466  		return nil, nil, err
1467  	}
1468  	return vert, frag, nil
1469  }
1470  
1471  // --- Path rendering (from path.go) ---
1472  
1473  type pather struct {
1474  	ctx      Device
1475  	viewport image.Point
1476  	stenciler *stenciler
1477  	coverer   *coverer
1478  }
1479  
1480  type coverer struct {
1481  	ctx                    Device
1482  	pipelines              [2][3]*blitPipeline
1483  	texUniforms            *coverTexUniforms
1484  	colUniforms            *coverColUniforms
1485  	linearGradientUniforms *coverLinearGradientUniforms
1486  }
1487  
1488  type coverTexUniforms struct {
1489  	coverUniforms
1490  	_ [12]byte
1491  }
1492  
1493  type coverColUniforms struct {
1494  	coverUniforms
1495  	_ [44]byte // 128 - 68 (coverUniforms) - 16 (colorUniforms)
1496  	colorUniforms
1497  }
1498  
1499  type coverLinearGradientUniforms struct {
1500  	coverUniforms
1501  	_ [28]byte // 128 - 68 (coverUniforms) - 32 (gradientUniforms)
1502  	gradientUniforms
1503  }
1504  
1505  type coverUniforms struct {
1506  	transform        [4]float32
1507  	uvCoverTransform [4]float32
1508  	uvTransformR1    [4]float32
1509  	uvTransformR2    [4]float32
1510  	fbo              float32
1511  }
1512  
1513  type stenciler struct {
1514  	ctx      Device
1515  	pipeline struct {
1516  		pipeline *blitPipeline
1517  		uniforms *stencilUniforms
1518  	}
1519  	ipipeline struct {
1520  		pipeline *blitPipeline
1521  		uniforms *intersectUniforms
1522  	}
1523  	fbos          fboSet
1524  	intersections fboSet
1525  	indexBuf      Buffer
1526  }
1527  
1528  type stencilUniforms struct {
1529  	transform  [4]float32
1530  	pathOffset [2]float32
1531  	_          [8]byte
1532  }
1533  
1534  type intersectUniforms struct {
1535  	vert struct {
1536  		uvTransform    [4]float32
1537  		subUVTransform [4]float32
1538  	}
1539  }
1540  
1541  type fboSet struct {
1542  	fbos []FBO
1543  }
1544  
1545  type FBO struct {
1546  	size image.Point
1547  	tex  Texture
1548  }
1549  
1550  type pathData struct {
1551  	ncurves int
1552  	data    Buffer
1553  }
1554  
1555  type vertex struct {
1556  	Corner       float32
1557  	MaxY         float32
1558  	FromX, FromY float32
1559  	CtrlX, CtrlY float32
1560  	ToX, ToY     float32
1561  }
1562  
1563  func (v vertex) encode(d []byte, maxy uint32) {
1564  	d = d[0:32]
1565  	bo := binary.LittleEndian()
1566  	bo.PutUint32(d[0:4], math.Float32bits(v.Corner))
1567  	bo.PutUint32(d[4:8], maxy)
1568  	bo.PutUint32(d[8:12], math.Float32bits(v.FromX))
1569  	bo.PutUint32(d[12:16], math.Float32bits(v.FromY))
1570  	bo.PutUint32(d[16:20], math.Float32bits(v.CtrlX))
1571  	bo.PutUint32(d[20:24], math.Float32bits(v.CtrlY))
1572  	bo.PutUint32(d[24:28], math.Float32bits(v.ToX))
1573  	bo.PutUint32(d[28:32], math.Float32bits(v.ToY))
1574  }
1575  
1576  const (
1577  	// 10000 costs 120KB WASM heap (60000 uint16 = 120000 bytes); 1000 costs 12KB.
1578  	pathBatchSize = 1000
1579  	vertStride    = 8 * 4
1580  )
1581  
1582  func newPather(ctx Device) *pather {
1583  	return &pather{
1584  		ctx:       ctx,
1585  		stenciler: newStenciler(ctx),
1586  		coverer:   newCoverer(ctx),
1587  	}
1588  }
1589  
1590  func newCoverer(ctx Device) *coverer {
1591  	c := &coverer{ctx: ctx}
1592  	c.colUniforms = &coverColUniforms{}
1593  	c.texUniforms = &coverTexUniforms{}
1594  	c.linearGradientUniforms = &coverLinearGradientUniforms{}
1595  	pipelines, err := createColorPrograms(ctx, ShaderCoverVert(), ShaderCoverFrag(),
1596  		[3]unsafe.Pointer{
1597  			unsafe.Pointer(c.colUniforms),
1598  			unsafe.Pointer(c.linearGradientUniforms),
1599  			unsafe.Pointer(c.texUniforms),
1600  		},
1601  		[3]int{
1602  			int(unsafe.Sizeof(*c.colUniforms)),
1603  			int(unsafe.Sizeof(*c.linearGradientUniforms)),
1604  			int(unsafe.Sizeof(*c.texUniforms)),
1605  		},
1606  	)
1607  	if err != nil {
1608  		panic(err)
1609  	}
1610  	c.pipelines = pipelines
1611  	return c
1612  }
1613  
1614  func newStenciler(ctx Device) *stenciler {
1615  	indices := []uint16{:pathBatchSize * 6}
1616  	for i := int32(0); i < pathBatchSize; i++ {
1617  		idx := uint16(i)
1618  		indices[idx*6+0] = idx*4 + 0
1619  		indices[idx*6+1] = idx*4 + 1
1620  		indices[idx*6+2] = idx*4 + 2
1621  		indices[idx*6+3] = idx*4 + 2
1622  		indices[idx*6+4] = idx*4 + 1
1623  		indices[idx*6+5] = idx*4 + 3
1624  	}
1625  	indexBuf, err := ctx.NewImmutableBuffer(BufferBindingIndices, Uint16sToBytes(indices))
1626  	if err != nil {
1627  		panic(err)
1628  	}
1629  	progLayout := VertexLayout{
1630  		Inputs: []InputDesc{
1631  			{Type: ShaderDataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*vertex)(nil).Corner))},
1632  			{Type: ShaderDataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*vertex)(nil).MaxY))},
1633  			{Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).FromX))},
1634  			{Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).CtrlX))},
1635  			{Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).ToX))},
1636  		},
1637  		Stride: vertStride,
1638  	}
1639  	iprogLayout := VertexLayout{
1640  		Inputs: []InputDesc{
1641  			{Type: ShaderDataTypeFloat, Size: 2, Offset: 0},
1642  			{Type: ShaderDataTypeFloat, Size: 2, Offset: 4 * 2},
1643  		},
1644  		Stride: 4 * 4,
1645  	}
1646  	st := &stenciler{ctx: ctx, indexBuf: indexBuf}
1647  	vsh, fsh, err := newShaders(ctx, ShaderStencilVert(), ShaderStencilFrag())
1648  	if err != nil {
1649  		panic(err)
1650  	}
1651  	defer vsh.Release()
1652  	defer fsh.Release()
1653  	st.pipeline.uniforms = &stencilUniforms{}
1654  	vertUniforms := newGPUUniformBuffer(ctx, unsafe.Pointer(st.pipeline.uniforms), int(unsafe.Sizeof(*st.pipeline.uniforms)))
1655  	pipe, err := st.ctx.NewPipeline(PipelineDesc{
1656  		VertexShader:   vsh,
1657  		FragmentShader: fsh,
1658  		VertexLayout:   progLayout,
1659  		BlendDesc: BlendDesc{
1660  			Enable:    true,
1661  			SrcFactor: BlendFactorOne,
1662  			DstFactor: BlendFactorOne,
1663  		},
1664  		PixelFormat: TextureFormatFloat,
1665  		Topology:    TopologyTriangles,
1666  	})
1667  	if err != nil {
1668  		panic(err)
1669  	}
1670  	st.pipeline.pipeline = &blitPipeline{pipe, vertUniforms}
1671  	vsh2, fsh2, err := newShaders(ctx, ShaderIntersectVert(), ShaderIntersectFrag())
1672  	if err != nil {
1673  		panic(err)
1674  	}
1675  	defer vsh2.Release()
1676  	defer fsh2.Release()
1677  	st.ipipeline.uniforms = &intersectUniforms{}
1678  	vertUniforms2 := newGPUUniformBuffer(ctx, unsafe.Pointer(&st.ipipeline.uniforms.vert), int(unsafe.Sizeof(st.ipipeline.uniforms.vert)))
1679  	ipipe, err := st.ctx.NewPipeline(PipelineDesc{
1680  		VertexShader:   vsh2,
1681  		FragmentShader: fsh2,
1682  		VertexLayout:   iprogLayout,
1683  		BlendDesc: BlendDesc{
1684  			Enable:    true,
1685  			SrcFactor: BlendFactorDstColor,
1686  			DstFactor: BlendFactorZero,
1687  		},
1688  		PixelFormat: TextureFormatFloat,
1689  		Topology:    TopologyTriangleStrip,
1690  	})
1691  	if err != nil {
1692  		panic(err)
1693  	}
1694  	st.ipipeline.pipeline = &blitPipeline{ipipe, vertUniforms2}
1695  	return st
1696  }
1697  
1698  func (s *fboSet) resize(ctx Device, format TextureFormat, sizes []image.Point) {
1699  	for i := len(s.fbos); i < len(sizes); i++ {
1700  		s.fbos = append(s.fbos, FBO{})
1701  	}
1702  	for i, sz := range sizes {
1703  		f := &s.fbos[i]
1704  		resize := sz.X > f.size.X || sz.Y > f.size.Y
1705  		if f.size.X > 0 && f.size.Y > 0 {
1706  			waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y)
1707  			resize = resize || waste > 1.2
1708  		}
1709  		if resize {
1710  			if f.tex != nil {
1711  				f.tex.Release()
1712  			}
1713  			sz = sz.Mul(105).Div(100)
1714  			max := ctx.Caps().MaxTextureSize
1715  			if sz.Y > max {
1716  				sz.Y = max
1717  			}
1718  			if sz.X > max {
1719  				sz.X = max
1720  			}
1721  			tex, err := ctx.NewTexture(format, sz.X, sz.Y, FilterNearest, FilterNearest,
1722  				BufferBindingTexture|BufferBindingFramebuffer)
1723  			if err != nil {
1724  				panic(err)
1725  			}
1726  			f.size = sz
1727  			f.tex = tex
1728  		}
1729  	}
1730  	s.delete(ctx, len(sizes))
1731  }
1732  
1733  func (s *fboSet) delete(ctx Device, idx int) {
1734  	for i := idx; i < len(s.fbos); i++ {
1735  		f := s.fbos[i]
1736  		f.tex.Release()
1737  	}
1738  	s.fbos = s.fbos[:idx]
1739  }
1740  
1741  func (s *stenciler) release() {
1742  	s.fbos.delete(s.ctx, 0)
1743  	s.intersections.delete(s.ctx, 0)
1744  	s.pipeline.pipeline.Release()
1745  	s.ipipeline.pipeline.Release()
1746  	s.indexBuf.Release()
1747  }
1748  
1749  func (p *pather) release() {
1750  	p.stenciler.release()
1751  	p.coverer.release()
1752  }
1753  
1754  func (c *coverer) release() {
1755  	for _, p := range c.pipelines {
1756  		for _, p := range p {
1757  			p.Release()
1758  		}
1759  	}
1760  }
1761  
1762  func buildPath(ctx Device, p []byte) pathData {
1763  	buf, err := ctx.NewImmutableBuffer(BufferBindingVertices, p)
1764  	if err != nil {
1765  		panic(err)
1766  	}
1767  	return pathData{ncurves: len(p) / vertStride, data: buf}
1768  }
1769  
1770  func (p pathData) release() {
1771  	p.data.Release()
1772  }
1773  
1774  func (p *pather) begin(sizes []image.Point) {
1775  	p.stenciler.begin(sizes)
1776  }
1777  
1778  func (p *pather) stencilPath(bounds image.Rectangle, offset Point, uv image.Point, data pathData) {
1779  	p.stenciler.stencilPath(bounds, offset, uv, data)
1780  }
1781  
1782  func (s *stenciler) beginIntersect(sizes []image.Point) {
1783  	s.intersections.resize(s.ctx, TextureFormatFloat, sizes)
1784  }
1785  
1786  func (s *stenciler) cover(idx int) FBO {
1787  	return s.fbos.fbos[idx]
1788  }
1789  
1790  func (s *stenciler) begin(sizes []image.Point) {
1791  	s.fbos.resize(s.ctx, TextureFormatFloat, sizes)
1792  }
1793  
1794  func (s *stenciler) stencilPath(bounds image.Rectangle, offset Point, uv image.Point, data pathData) {
1795  	s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
1796  	texSize := Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
1797  	scale := Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
1798  	orig := Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
1799  	s.pipeline.uniforms.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y}
1800  	s.pipeline.uniforms.pathOffset = [2]float32{offset.X, offset.Y}
1801  	s.pipeline.pipeline.UploadUniforms(s.ctx)
1802  	start := 0
1803  	nquads := data.ncurves / 4
1804  	for start < nquads {
1805  		batch := nquads - start
1806  		if batch > pathBatchSize {
1807  			batch = pathBatchSize
1808  		}
1809  		off := vertStride * start * 4
1810  		s.ctx.BindVertexBuffer(data.data, off)
1811  		s.ctx.DrawElements(0, batch*6)
1812  		start += batch
1813  	}
1814  }
1815  
1816  func (p *pather) cover(mat materialType, isFBO bool, col RGBA, col1, col2 RGBA, scale, off Point, uvTrans Affine2D, coverScale, coverOff Point) {
1817  	p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
1818  }
1819  
1820  func (c *coverer) cover(mat materialType, isFBO bool, col RGBA, col1, col2 RGBA, scale, off Point, uvTrans Affine2D, coverScale, coverOff Point) {
1821  	var uniforms *coverUniforms
1822  	switch mat {
1823  	case materialColor:
1824  		c.colUniforms.color = col
1825  		uniforms = &c.colUniforms.coverUniforms
1826  	case materialLinearGradient:
1827  		c.linearGradientUniforms.color1 = col1
1828  		c.linearGradientUniforms.color2 = col2
1829  		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
1830  		c.linearGradientUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
1831  		c.linearGradientUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
1832  		uniforms = &c.linearGradientUniforms.coverUniforms
1833  	case materialTexture:
1834  		t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
1835  		c.texUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
1836  		c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
1837  		uniforms = &c.texUniforms.coverUniforms
1838  	}
1839  	uniforms.fbo = 0
1840  	if isFBO {
1841  		uniforms.fbo = 1
1842  	}
1843  	uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
1844  	uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
1845  	fboIdx := 0
1846  	if isFBO {
1847  		fboIdx = 1
1848  	}
1849  	c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
1850  	c.ctx.DrawArrays(0, 4)
1851  }
1852  
1853  // vertex is 8 x float32 = 32 bytes = vertStride. Keep in sync.
1854  
1855  // --- Clip encoding (from clip.go) ---
1856  
1857  func encodeQuadTo(data []byte, meta uint32, from, ctrl, to Point) {
1858  	bo := binary.LittleEndian()
1859  	data = data[:vertStride*4]
1860  	bo.PutUint32(data[4:8], meta)
1861  	bo.PutUint32(data[8:12], math.Float32bits(from.X))
1862  	bo.PutUint32(data[12:16], math.Float32bits(from.Y))
1863  	bo.PutUint32(data[16:20], math.Float32bits(ctrl.X))
1864  	bo.PutUint32(data[20:24], math.Float32bits(ctrl.Y))
1865  	bo.PutUint32(data[24:28], math.Float32bits(to.X))
1866  	bo.PutUint32(data[28:32], math.Float32bits(to.Y))
1867  	copy(data[vertStride*1:vertStride*2], data[vertStride*0:vertStride*1])
1868  	copy(data[vertStride*2:vertStride*3], data[vertStride*0:vertStride*1])
1869  	copy(data[vertStride*3:vertStride*4], data[vertStride*0:vertStride*1])
1870  	bo.PutUint32(data[vertStride*0:vertStride*0+4], math.Float32bits(nwCorner))
1871  	bo.PutUint32(data[vertStride*1:vertStride*1+4], math.Float32bits(neCorner))
1872  	bo.PutUint32(data[vertStride*2:vertStride*2+4], math.Float32bits(swCorner))
1873  	bo.PutUint32(data[vertStride*3:vertStride*3+4], math.Float32bits(seCorner))
1874  }
1875  
1876  const (
1877  	nwCorner = float32(1*0.5 + 0*0.25)
1878  	neCorner = float32(1*0.5 + 1*0.25)
1879  	swCorner = float32(0*0.5 + 0*0.25)
1880  	seCorner = float32(0*0.5 + 1*0.25)
1881  )
1882  
1883  type quadSplitter struct {
1884  	bounds  Rectangle
1885  	contour uint32
1886  	d       *drawOps
1887  	scratch []QuadSegment
1888  }
1889  
1890  func (qs *quadSplitter) encodeQuadTo(from, ctrl, to Point) {
1891  	data := qs.d.writeVertCache(vertStride * 4)
1892  	encodeQuadTo(data, qs.contour, from, ctrl, to)
1893  }
1894  
1895  func (qs *quadSplitter) splitAndEncode(quad QuadSegment) {
1896  	cbnd := Rectangle{Min: quad.From, Max: quad.To}.Canon()
1897  	from, ctrl, to := quad.From, quad.Ctrl, quad.To
1898  	v0 := ctrl.Sub(from)
1899  	v1 := to.Sub(ctrl)
1900  	d := v0.X - v1.X
1901  	if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
1902  		t := v0.X / d
1903  		ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t))
1904  		ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
1905  		mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
1906  		qs.encodeQuadTo(from, ctrl0, mid)
1907  		qs.encodeQuadTo(mid, ctrl1, to)
1908  		if mid.X > cbnd.Max.X {
1909  			cbnd.Max.X = mid.X
1910  		}
1911  		if mid.X < cbnd.Min.X {
1912  			cbnd.Min.X = mid.X
1913  		}
1914  	} else {
1915  		qs.encodeQuadTo(from, ctrl, to)
1916  	}
1917  	d = v0.Y - v1.Y
1918  	if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
1919  		t := v0.Y / d
1920  		y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
1921  		if y > cbnd.Max.Y {
1922  			cbnd.Max.Y = y
1923  		}
1924  		if y < cbnd.Min.Y {
1925  			cbnd.Min.Y = y
1926  		}
1927  	}
1928  	qs.bounds = unionRect(qs.bounds, cbnd)
1929  }
1930  
1931  func unionRect(r, s Rectangle) Rectangle {
1932  	if r.Min.X > s.Min.X {
1933  		r.Min.X = s.Min.X
1934  	}
1935  	if r.Min.Y > s.Min.Y {
1936  		r.Min.Y = s.Min.Y
1937  	}
1938  	if r.Max.X < s.Max.X {
1939  		r.Max.X = s.Max.X
1940  	}
1941  	if r.Max.Y < s.Max.Y {
1942  		r.Max.Y = s.Max.Y
1943  	}
1944  	return r
1945  }
1946  
1947  // --- Caches (from caches.go) ---
1948  
1949  type textureCacheKey struct {
1950  	filter byte
1951  	handle any
1952  }
1953  
1954  type textureCache struct {
1955  	res map[textureCacheKey]resourceCacheValue
1956  }
1957  
1958  type resourceCacheValue struct {
1959  	used     bool
1960  	resource gpuResource
1961  }
1962  
1963  type opCache struct {
1964  	index    map[opKey]int
1965  	freelist []int
1966  	cache    []opCacheValue
1967  }
1968  
1969  type opCacheValue struct {
1970  	data   pathData
1971  	bounds Rectangle
1972  	key    opKey
1973  	keep   bool
1974  }
1975  
1976  func newTextureCache() *textureCache {
1977  	return &textureCache{res: map[textureCacheKey]resourceCacheValue{}}
1978  }
1979  
1980  func (r *textureCache) get(key textureCacheKey) (gpuResource, bool) {
1981  	v, exists := r.res[key]
1982  	if !exists {
1983  		return nil, false
1984  	}
1985  	if !v.used {
1986  		v.used = true
1987  		r.res[key] = v
1988  	}
1989  	return v.resource, exists
1990  }
1991  
1992  func (r *textureCache) put(key textureCacheKey, val gpuResource) {
1993  	v, exists := r.res[key]
1994  	if exists && v.used {
1995  		panic(errorf("key exists, %v", key))
1996  	}
1997  	v.used = true
1998  	v.resource = val
1999  	r.res[key] = v
2000  }
2001  
2002  func (r *textureCache) frame() {
2003  	for k, v := range r.res {
2004  		if v.used {
2005  			v.used = false
2006  			r.res[k] = v
2007  		} else {
2008  			delete(r.res, k)
2009  			v.resource.release()
2010  		}
2011  	}
2012  }
2013  
2014  func (r *textureCache) release() {
2015  	for _, v := range r.res {
2016  		v.resource.release()
2017  	}
2018  	r.res = nil
2019  }
2020  
2021  func newOpCache() *opCache {
2022  	return &opCache{
2023  		index:    map[opKey]int{},
2024  		freelist: []int{},
2025  		cache:    []opCacheValue{},
2026  	}
2027  }
2028  
2029  func (r *opCache) get(key opKey) (o opCacheValue, exist bool) {
2030  	v := r.index[key]
2031  	if v == 0 {
2032  		return
2033  	}
2034  	r.cache[v-1].keep = true
2035  	return r.cache[v-1], true
2036  }
2037  
2038  func (r *opCache) put(key opKey, val opCacheValue) {
2039  	v := r.index[key]
2040  	val.keep = true
2041  	val.key = key
2042  	if v == 0 {
2043  		i := len(r.cache)
2044  		if len(r.freelist) > 0 {
2045  			i = r.freelist[len(r.freelist)-1]
2046  			r.freelist = r.freelist[:len(r.freelist)-1]
2047  			r.cache[i] = val
2048  		} else {
2049  			r.cache = append(r.cache, val)
2050  		}
2051  		r.index[key] = i + 1
2052  	} else {
2053  		r.cache[v-1] = val
2054  	}
2055  }
2056  
2057  func (r *opCache) frame() {
2058  	r.freelist = r.freelist[:0]
2059  	for i, v := range r.cache {
2060  		r.cache[i].keep = false
2061  		if v.keep {
2062  			continue
2063  		}
2064  		if v.data.data != nil {
2065  			v.data.release()
2066  			r.cache[i].data.data = nil
2067  		}
2068  		delete(r.index, v.key)
2069  		r.freelist = append(r.freelist, i)
2070  	}
2071  }
2072  
2073  func (r *opCache) release() {
2074  	for i := range r.cache {
2075  		r.cache[i].keep = false
2076  	}
2077  	r.frame()
2078  	r.index = nil
2079  	r.freelist = nil
2080  	r.cache = nil
2081  }
2082  
2083  // --- Packer (from pack.go) ---
2084  
2085  type packer struct {
2086  	maxDims image.Point
2087  	spaces  []image.Rectangle
2088  	sizes   []image.Point
2089  	pos     image.Point
2090  }
2091  
2092  type placement struct {
2093  	Idx int
2094  	Pos image.Point
2095  }
2096  
2097  func (p *packer) add(s image.Point) (placement, bool) {
2098  	if place, ok := p.tryAdd(s); ok {
2099  		return place, true
2100  	}
2101  	p.newPage()
2102  	return p.tryAdd(s)
2103  }
2104  
2105  func (p *packer) clear() {
2106  	p.sizes = p.sizes[:0]
2107  	p.spaces = p.spaces[:0]
2108  }
2109  
2110  func (p *packer) newPage() {
2111  	p.pos = image.Point{}
2112  	p.sizes = append(p.sizes, image.Point{})
2113  	p.spaces = p.spaces[:0]
2114  	p.spaces = append(p.spaces, image.Rectangle{
2115  		Max: image.Point{X: 1e6, Y: 1e6},
2116  	})
2117  }
2118  
2119  func (p *packer) tryAdd(s image.Point) (placement, bool) {
2120  	if len(p.spaces) == 0 || len(p.sizes) == 0 {
2121  		return placement{}, false
2122  	}
2123  	var (
2124  		bestIdx  *image.Rectangle
2125  		bestSize = p.maxDims
2126  		lastSize = p.sizes[len(p.sizes)-1]
2127  	)
2128  	for i := range p.spaces {
2129  		space := &p.spaces[i]
2130  		rightSpace := space.Dx() - s.X
2131  		bottomSpace := space.Dy() - s.Y
2132  		if rightSpace < 0 || bottomSpace < 0 {
2133  			continue
2134  		}
2135  		size := lastSize
2136  		if x := space.Min.X + s.X; x > size.X {
2137  			if x > p.maxDims.X {
2138  				continue
2139  			}
2140  			size.X = x
2141  		}
2142  		if y := space.Min.Y + s.Y; y > size.Y {
2143  			if y > p.maxDims.Y {
2144  				continue
2145  			}
2146  			size.Y = y
2147  		}
2148  		if size.X*size.Y < bestSize.X*bestSize.Y {
2149  			bestIdx = space
2150  			bestSize = size
2151  		}
2152  	}
2153  	if bestIdx == nil {
2154  		return placement{}, false
2155  	}
2156  	bestSpace := *bestIdx
2157  	*bestIdx = p.spaces[len(p.spaces)-1]
2158  	p.spaces = p.spaces[:len(p.spaces)-1]
2159  	pos := bestSpace.Min
2160  	if rem := bestSpace.Dy() - s.Y; rem > 0 {
2161  		p.spaces = append(p.spaces, image.Rectangle{
2162  			Min: image.Point{X: pos.X, Y: pos.Y + s.Y},
2163  			Max: image.Point{X: bestSpace.Max.X, Y: bestSpace.Max.Y},
2164  		})
2165  	}
2166  	if rem := bestSpace.Dx() - s.X; rem > 0 {
2167  		p.spaces = append(p.spaces, image.Rectangle{
2168  			Min: image.Point{X: pos.X + s.X, Y: pos.Y},
2169  			Max: image.Point{X: bestSpace.Max.X, Y: pos.Y + s.Y},
2170  		})
2171  	}
2172  	idx := len(p.sizes) - 1
2173  	p.sizes[idx] = bestSize
2174  	return placement{Idx: idx, Pos: pos}, true
2175  }
2176  
2177  // errorf creates a formatted error without importing fmt (it's already imported).
2178  func errorf(format string, args ...any) error {
2179  	return fmt.Errorf(format, args...)
2180  }
2181