// SPDX-License-Identifier: Unlicense OR MIT // GPU rendering pipeline. Ported from gioui.org/gpu. // Merges gpu.go, path.go, clip.go, caches.go, pack.go. // Timer support dropped. Compute shader path dropped (stencil-and-cover only). package gio import ( "encoding/binary" "fmt" "image" "image/color" "math" "slices" "unsafe" ) // GPU is the interface for rendering Gio operations. type GPU interface { Release() Clear(color color.NRGBA) Frame(frame *OpList, target RenderTarget, viewport image.Point) error } type gpu struct { cache *textureCache drawOps drawOps ctx Device renderer *renderer tess tessGenCache generation uint32 // Async tess channels (non-nil when spawn worker is running). // In sync mode (WASM / moxie_sync_tess build tag), both are nil. tessReqCh chan TessRequest tessResCh chan TessResult } type renderer struct { ctx Device blitter *blitter pather *pather packer packer intersections packer layers packer layerFBOs fboSet } type drawOps struct { reader Reader states []Affine2D transStack []Affine2D layers []opacityLayer opacityStack []int vertCache []byte viewport image.Point clear bool clearColor RGBA imageOps []imageOp pathOps []*pathOp pathOpCache []pathOp tessCache *tessGenCache // set by gpu each frame; nil disables caching generation uint32 // current frame generation (for tessCache lastGen) qs quadSplitter pathCache *opCache } type opacityLayer struct { opacity float32 parent int depth int opStart int opEnd int clip image.Rectangle place placement } type drawState struct { t Affine2D cpath *pathOp matType materialType image imageOpData color color.NRGBA stop1 Point stop2 Point color1 color.NRGBA color2 color.NRGBA } type pathOp struct { off Point rect bool clip image.Rectangle bounds Rectangle intersect Rectangle pathKey opKey pathHash uint64 // geometry hash for tessGenCache; 0 = not set path bool pathVerts []byte parent *pathOp place placement } type imageOp struct { path *pathOp clip image.Rectangle material material clipType clipType place placement 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 Key } type material struct { material materialType opaque bool color RGBA color1 RGBA color2 RGBA opacity float32 data imageOpData tex Texture uvTrans Affine2D } const ( filterLinear = 0 filterNearest = 1 ) type imageOpData struct { src *image.RGBA handle any filter byte } type linearGradientOpData struct { stop1 Point color1 color.NRGBA stop2 Point color2 color.NRGBA } func decodeImageOp(data []byte, refs []any) 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[:TypeColorLen] return color.NRGBA{ R: data[1], G: data[2], B: data[3], A: data[4], } } func decodeLinearGradientOp(data []byte) linearGradientOpData { data = data[:TypeLinearGradientLen] bo := binary.LittleEndian() return linearGradientOpData{ stop1: Point{ X: math.Float32frombits(bo.Uint32(data[1:])), Y: math.Float32frombits(bo.Uint32(data[5:])), }, stop2: 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 gpuResource interface { release() } type gpuTexture struct { src *image.RGBA tex Texture } type blitter struct { ctx Device viewport image.Point pipelines [2][3]*blitPipeline colUniforms *blitColUniforms texUniforms *blitTexUniforms linearGradientUniforms *blitLinearGradientUniforms quadVerts Buffer } type blitColUniforms struct { blitUniforms _ [48]byte // 128 - 64 (blitUniforms) - 16 (colorUniforms) colorUniforms } type blitTexUniforms struct { blitUniforms } type blitLinearGradientUniforms struct { blitUniforms _ [32]byte // 128 - 64 (blitUniforms) - 32 (gradientUniforms) gradientUniforms } type gpuUniformBuffer struct { buf Buffer ptr []byte } type blitPipeline struct { pipeline Pipeline uniforms *gpuUniformBuffer } type blitUniforms struct { transform [4]float32 uvTransformR1 [4]float32 uvTransformR2 [4]float32 opacity float32 fbo float32 _ [2]float32 } type colorUniforms struct { color RGBA } type gradientUniforms struct { color1 RGBA color2 RGBA } type clipType uint8 const ( clipTypeNone clipType = iota clipTypePath clipTypeIntersection ) type materialType uint8 const ( materialColor materialType = iota materialLinearGradient materialTexture ) // NewGPU creates a GPU for the given API. func NewGPU(api API) (GPU, error) { d, err := NewDevice(api) if err != nil { return nil, err } return NewGPUWithDevice(d) } // NewGPUWithDevice creates a GPU with a pre-existing device. func NewGPUWithDevice(d Device) (GPU, error) { d.BeginFrame(nil, false, image.Point{}) defer d.EndFrame() feats := d.Caps().Features switch { case feats.Has(FeatureFloatRenderTargets) && feats.Has(FeatureSRGB): return newGPU(d) } return nil, errorf("gio: no available GPU driver") } func newGPU(ctx Device) (*gpu, error) { g := &gpu{ cache: newTextureCache(), tess: newTessGenCache(), } g.drawOps.pathCache = newOpCache() // Pre-size slices that grow during frame collection to avoid per-frame // allocations with the leaking GC. Size for typical UI frame (16 ops). g.drawOps.imageOps = []imageOp{:0:16} g.drawOps.pathOpCache = []pathOp{:0:16} g.drawOps.transStack = []Affine2D{:0:8} g.drawOps.states = []Affine2D{:0:8} g.drawOps.layers = []opacityLayer{:0:4} g.drawOps.opacityStack = []int{:0:4} g.ctx = ctx g.renderer = newRenderer(ctx) return g, nil } func (g *gpu) Clear(col color.NRGBA) { g.drawOps.clear = true g.drawOps.clearColor = LinearFromSRGB(col) } func (g *gpu) Release() { g.renderer.release() g.drawOps.pathCache.release() g.cache.release() g.ctx.Release() // tessGenCache has no GPU resources; just drop the map. g.tess = tessGenCache{} } func (g *gpu) Frame(frameOps *OpList, target RenderTarget, viewport image.Point) error { g.collect(viewport, frameOps) return g.frame(target) } func (g *gpu) collect(viewport image.Point, frameOps *OpList) { g.renderer.blitter.viewport = viewport g.renderer.pather.viewport = viewport g.generation++ g.drawOps.tessCache = &g.tess g.drawOps.generation = g.generation g.drawOps.reset(viewport) g.drawOps.collect(frameOps, viewport) } 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.buildPathsTess() for _, img := range g.drawOps.imageOps { expandPathOp(img.path, img.clip) } 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.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 := LoadDesc{ ClearColor: g.drawOps.clearColor, } if g.drawOps.clear { g.drawOps.clear = false d.Action = 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.ctx.EndRenderPass() g.cache.frame() g.drawOps.pathCache.frame() return nil } func (r *renderer) texHandle(cache *textureCache, data imageOpData) Texture { key := textureCacheKey{filter: data.filter, handle: data.handle} var tex *gpuTexture t, exists := cache.get(key) if !exists { t = &gpuTexture{src: data.src} cache.put(key, t) } tex = t.(*gpuTexture) if tex.tex != nil { return tex.tex } var minFilter, magFilter TextureFilter switch data.filter { case filterLinear: minFilter, magFilter = FilterLinearMipmapLinear, FilterLinear case filterNearest: minFilter, magFilter = FilterNearest, FilterNearest } handle, err := r.ctx.NewTexture(TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), minFilter, magFilter, BufferBindingTexture, ) if err != nil { panic(err) } UploadImage(handle, image.Pt(0, 0), data.src) tex.tex = handle return tex.tex } func (t *gpuTexture) release() { if t.tex != nil { t.tex.Release() } } func newRenderer(ctx Device) *renderer { r := &renderer{ ctx: ctx, blitter: newBlitter(ctx), pather: newPather(ctx), } maxDim := ctx.Caps().MaxTextureSize 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 newGPUUniformBuffer(b Device, ptr unsafe.Pointer, size int) *gpuUniformBuffer { ubuf, err := b.NewBuffer(BufferBindingUniforms, size) if err != nil { panic(err) } data := unsafe.Slice((*byte)(ptr), size) return &gpuUniformBuffer{buf: ubuf, ptr: data} } func (u *gpuUniformBuffer) Upload() { u.buf.Upload(u.ptr) } func (u *gpuUniformBuffer) Release() { u.buf.Release() u.buf = nil } func (p *blitPipeline) UploadUniforms(ctx Device) { if p.uniforms != nil { p.uniforms.Upload() ctx.BindUniforms(p.uniforms.buf) } } func (p *blitPipeline) Release() { p.pipeline.Release() if p.uniforms != nil { p.uniforms.Release() } *p = blitPipeline{} } func newBlitter(ctx Device) *blitter { quadVerts, err := ctx.NewImmutableBuffer(BufferBindingVertices, Float32sToBytes([]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 = &blitColUniforms{} b.texUniforms = &blitTexUniforms{} b.linearGradientUniforms = &blitLinearGradientUniforms{} pipelines, err := createColorPrograms(ctx, ShaderBlitVert(), ShaderBlitFrag(), [3]unsafe.Pointer{ unsafe.Pointer(b.colUniforms), unsafe.Pointer(b.linearGradientUniforms), unsafe.Pointer(b.texUniforms), }, [3]int{ int(unsafe.Sizeof(*b.colUniforms)), int(unsafe.Sizeof(*b.linearGradientUniforms)), int(unsafe.Sizeof(*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 Device, vsSrc ShaderSources, fsSrc [3]ShaderSources, uniformPtrs [3]unsafe.Pointer, uniformSizes [3]int) (pipelines [2][3]*blitPipeline, err error) { defer func() { if err != nil { for _, p := range pipelines { for _, p := range p { if p != nil { p.Release() } } } } }() blend := BlendDesc{ Enable: true, SrcFactor: BlendFactorOne, DstFactor: BlendFactorOneMinusSrcAlpha, } layout := VertexLayout{ Inputs: []InputDesc{ {Type: ShaderDataTypeFloat, Size: 2, Offset: 0}, {Type: ShaderDataTypeFloat, 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 []TextureFormat{TextureFormatOutput, TextureFormatSRGBA} { for mat := 0; mat < 3; mat++ { fsh, err := b.NewFragmentShader(fsSrc[mat]) if err != nil { return pipelines, err } defer fsh.Release() pipe, err := b.NewPipeline(PipelineDesc{ VertexShader: vsh, FragmentShader: fsh, BlendDesc: blend, VertexLayout: layout, PixelFormat: format, Topology: TopologyTriangleStrip, }) if err != nil { return pipelines, err } var vertBuffer *gpuUniformBuffer if uniformPtrs[mat] != nil { vertBuffer = newGPUUniformBuffer(b, uniformPtrs[mat], uniformSizes[mat]) } pipelines[i][mat] = &blitPipeline{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, LoadDesc{Action: 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 := LoadDesc{Action: 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(FRect(uv), fbo.size) subScale, subOff := texSpaceTransform(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: intersection should fit") } ops[i].clipType = clipTypeIntersection ops[i].place = place } } } func (r *renderer) packStencils(pops *[]*pathOp) { r.packer.clear() ops := *pops 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 { panic(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 { 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 = slices.Delete(layers, i, i+1) } } r.layers.clear() depth := 0 for i := range layers { l := &layers[i] if l.depth != depth { r.layers.newPage() } place, ok := r.layers.add(l.clip.Size()) if !ok { panic(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, 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, LoadDesc{Action: 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 := FRect(v) uvScale, uvOffset := texSpaceTransform(sr, f.size) uvTrans := AffineId().Scale(Point{}, uvScale).Offset(uvOffset) 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] d.states = d.states[:0] // must reset; TypeLoad uses index from current frame's TypeSave } func (d *drawOps) collect(root *OpList, viewport image.Point) { viewf := Rectangle{ Max: Point{X: float32(viewport.X), Y: float32(viewport.Y)}, } var ops *Ops if root != nil { ops = &root.Internal } d.reader.ResetReader(ops) d.collectOps(&d.reader, viewf) } func (d *drawOps) buildPaths(ctx 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 } } // buildPathsTess uploads GPU vertex buffers for all path ops that need them. // Tessellation itself already happened in collectOps (via buildVerts), with // results cached in tessGenCache. This function just handles the GPU upload // step and drains any async tessellation results from the worker channel. func (g *gpu) buildPathsTess() { // Drain async results if worker is running. if g.tessResCh != nil { drainLoop: for { select { case res := <-g.tessResCh: entry := &tessEntry{ key: res.Key, verts: []byte(res.Verts), bounds: Rectangle{ Min: Point{X: res.BoundsMinX, Y: res.BoundsMinY}, Max: Point{X: res.BoundsMaxX, Y: res.BoundsMaxY}, }, lastGen: res.Gen, } g.tess.put(entry) default: break drainLoop } } } d := &g.drawOps for _, p := range d.pathOps { // If opCache already has a GPU buffer, no upload needed. if v, exists := d.pathCache.get(p.pathKey); exists && v.data.data != nil { p.pathVerts = nil continue } // Upload tessellated vertex bytes to GPU. if len(p.pathVerts) > 0 { data := buildPath(g.ctx, p.pathVerts) d.pathCache.put(p.pathKey, opCacheValue{data: data, bounds: p.bounds}) } p.pathVerts = nil } // Evict tessGenCache entries older than tessEvictAge frames. if g.generation >= tessEvictAge { g.tess.evict(g.generation - tessEvictAge) } } 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 Rectangle, off 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 Affine2D) { for extra := id - len(d.states) + 1; extra > 0; extra-- { d.states = append(d.states, AffineId()) } d.states[id] = state } func (k opKey) SetTransform(t Affine2D) opKey { sx, hx, _, hy, sy, _ := t.Elems() k.sx = sx k.hx = hx k.hy = hy k.sy = sy return k } // toAffine reconstructs an Affine2D from the transform components stored in // opKey. The translation (ox, oy) is not stored in opKey (it was split off as // a separate integer offset), so this returns a transform with zero offset. func (k opKey) toAffine() Affine2D { return NewAffine2D(k.sx, k.hx, 0, k.hy, k.sy, 0) } func (d *drawOps) collectOps(r *Reader, viewport Rectangle) { var quads quadsOp state := drawState{t: AffineId()} reset := func() { state = drawState{ t: AffineId(), color: color.NRGBA{A: 0xff}, } } reset() loop: for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch OpType(encOp.Data[0]) { case TypeTransform: dop, push := DecodeTransform(encOp.Data) if push { d.transStack = append(d.transStack, state.t) } state.t = state.t.Mul(dop) case TypePopTransform: n := len(d.transStack) state.t = d.transStack[n-1] d.transStack = d.transStack[:n-1] case TypePushOpacity: opacity := 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 TypePopOpacity: n := len(d.opacityStack) idx := d.opacityStack[n-1] d.layers[idx].opEnd = len(d.imageOps) d.opacityStack = d.opacityStack[:n-1] case TypeStroke: quads.key.strokeWidth = decodeStrokeOp(encOp.Data) case TypePath: encOp, ok = r.Decode() if !ok { break loop } quads.aux = encOp.Data[TypeAuxLen:] quads.key.Key = encOp.Key case TypeClip: var op DecodedClipOp op.Decode(encOp.Data) quads.key.outline = op.Outline bounds := FRect(op.Bounds) trans, off := transformOffset(state.t) if len(quads.aux) > 0 { quads.key = quads.key.SetTransform(trans) if v, ok := d.pathCache.get(quads.key); ok { bounds = v.bounds } else { var pathData []byte // Check tessGenCache before tessellating. var tessHash uint64 if d.tessCache != nil { tessHash = hashPathData(quads.aux, trans, quads.key.outline, quads.key.strokeWidth) pk := PathKey{Hash: tessHash, Outline: quads.key.outline, Stroke: quads.key.strokeWidth > 0, Width: quads.key.strokeWidth} if entry, ok := d.tessCache.get(pk, d.generation); ok { pathData = entry.verts bounds = entry.bounds } } if pathData == nil { pathData, bounds = d.buildVerts(quads.aux, trans, quads.key.outline, quads.key.strokeWidth) // Store in tessGenCache (copy: pathData points into vertCache). if d.tessCache != nil && tessHash != 0 { pk := PathKey{Hash: tessHash, Outline: quads.key.outline, Stroke: quads.key.strokeWidth > 0, Width: quads.key.strokeWidth} copyVerts := []byte{:len(pathData)} copy(copyVerts, pathData) d.tessCache.put(&tessEntry{key: pk, verts: copyVerts, bounds: bounds, lastGen: d.generation}) } } quads.aux = pathData d.pathCache.put(quads.key, opCacheValue{bounds: bounds}) } } else { quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans) quads.key = opKey{Key: encOp.Key} quads.key = quads.key.SetTransform(trans) } d.addClipPath(&state, quads.aux, quads.key, bounds, off) quads = quadsOp{} case TypePopClip: state.cpath = state.cpath.parent case TypeColor: state.matType = materialColor state.color = decodeColorOp(encOp.Data) case 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 TypeImage: state.matType = materialTexture state.image = decodeImageOp(encOp.Data, encOp.Refs) case TypePaint: t, off := transformOffset(state.t) inf := float32(1e6) dst := Rect(-inf, -inf, inf, inf) if state.matType == materialTexture { sz := state.image.src.Rect.Size() dst = Rectangle{Max: 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 { k := opKey{Key: encOp.Key} k = k.SetTransform(t) 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 { 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 { state.cpath = state.cpath.parent } case TypeSave: id := DecodeSave(encOp.Data) d.save(id, state.t) case TypeLoad: reset() id := 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 Rectangle, off Point, partTrans Affine2D, clip image.Rectangle) material { m := material{ opacity: 1., uvTrans: AffineId(), } switch d.matType { case materialColor: m.material = materialColor m.color = LinearFromSRGB(d.color) m.opaque = m.color.A == 1.0 case materialLinearGradient: m.material = materialLinearGradient m.color1 = LinearFromSRGB(d.color1) m.color2 = 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 := Rectangle{Max: 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(AffineId().Scale(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 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(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 RGBA, col1, col2 RGBA, scale, off Point, opacity float32, uvTrans 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) } func texSpaceTransform(r Rectangle, bounds image.Point) (Point, Point) { size := Point{X: float32(bounds.X), Y: float32(bounds.Y)} scale := Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y} offset := Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y} return scale, offset } func gradientSpaceTransform(clip image.Rectangle, off Point, stop1, stop2 Point) 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))) zp := Point{} return AffineId(). Scale(zp, FPt(clip.Size())). Offset(zp.Sub(off).Add(FPt(clip.Min))). Offset(zp.Sub(stop1)). Rotate(zp, a). Scale(zp, Pt(1/l, 1/l)) } func clipSpaceTransform(r image.Rectangle, viewport image.Point) (Point, Point) { 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 scale := Point{X: w * .5, Y: h * .5} offset := Point{X: x + w*.5, Y: y + h*.5} return scale, offset } 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] 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, []byte{:n}...) return d.vertCache[len(d.vertCache)-n:] } func (d *drawOps) buildVerts(pathData []byte, tr Affine2D, outline bool, strWidth float32) (verts []byte, bounds Rectangle) { inf := float32(math.Inf(+1)) d.qs.bounds = Rectangle{ Min: Point{X: inf, Y: inf}, Max: Point{X: -inf, Y: -inf}, } d.qs.d = d startLength := len(d.vertCache) switch { case strWidth > 0: ss := StrokeStyle{Width: strWidth} quads := 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 } func decodeToOutlineQuads(qs *quadSplitter, tr Affine2D, pathData []byte) { for len(pathData) >= sceneElemSize+4 { qs.contour = binary.LittleEndian().Uint32(pathData) cmd := DecodeCommand(pathData[4:]) switch cmd.Op() { case OpLine: var q QuadSegment q.From, q.To = DecodeLine(cmd) q.Ctrl = q.From.Add(q.To).Mul(.5) q = q.Transform(tr) qs.splitAndEncode(q) case OpGap: var q QuadSegment q.From, q.To = DecodeGap(cmd) q.Ctrl = q.From.Add(q.To).Mul(.5) q = q.Transform(tr) qs.splitAndEncode(q) case OpQuad: var q QuadSegment q.From, q.Ctrl, q.To = DecodeQuad(cmd) q = q.Transform(tr) qs.splitAndEncode(q) case OpCubic: from, ctrl0, ctrl1, to := DecodeCubic(cmd) qs.scratch = 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[sceneElemSize+4:] } } func (d *drawOps) boundsForTransformedRect(r Rectangle, tr Affine2D) (aux []byte, bnd Rectangle, ptr Affine2D) { ptr = AffineId() if tr == AffineId() { bnd = r return } corners := [4]Point{ tr.Transform(r.Min), tr.Transform(Pt(r.Max.X, r.Min.Y)), tr.Transform(r.Max), tr.Transform(Pt(r.Min.X, r.Max.Y)), } bnd.Min = Pt(math.MaxFloat32, math.MaxFloat32) bnd.Max = 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 } } l := len(d.vertCache) d.vertCache = append(d.vertCache, []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) var P1, P2, P3 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 = NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert() return aux, bnd, ptr } func transformOffset(t Affine2D) (Affine2D, Point) { sx, hx, ox, hy, sy, oy := t.Elems() iox, fox := math.Modf(float64(ox)) ioy, foy := math.Modf(float64(oy)) ft := NewAffine2D(sx, hx, float32(fox), hy, sy, float32(foy)) ip := Pt(float32(iox), float32(ioy)) return ft, ip } func newShaders(ctx Device, vsrc, fsrc ShaderSources) (VertexShader, FragmentShader, error) { vert, err := ctx.NewVertexShader(vsrc) if err != nil { return nil, nil, err } frag, err := ctx.NewFragmentShader(fsrc) if err != nil { vert.Release() return nil, nil, err } return vert, frag, nil } // --- Path rendering (from path.go) --- type pather struct { ctx Device viewport image.Point stenciler *stenciler coverer *coverer } type coverer struct { ctx Device pipelines [2][3]*blitPipeline texUniforms *coverTexUniforms colUniforms *coverColUniforms linearGradientUniforms *coverLinearGradientUniforms } type coverTexUniforms struct { coverUniforms _ [12]byte } type coverColUniforms struct { coverUniforms _ [44]byte // 128 - 68 (coverUniforms) - 16 (colorUniforms) colorUniforms } type coverLinearGradientUniforms struct { coverUniforms _ [28]byte // 128 - 68 (coverUniforms) - 32 (gradientUniforms) gradientUniforms } type coverUniforms struct { transform [4]float32 uvCoverTransform [4]float32 uvTransformR1 [4]float32 uvTransformR2 [4]float32 fbo float32 } type stenciler struct { ctx Device pipeline struct { pipeline *blitPipeline uniforms *stencilUniforms } ipipeline struct { pipeline *blitPipeline uniforms *intersectUniforms } fbos fboSet intersections fboSet indexBuf Buffer } type stencilUniforms struct { transform [4]float32 pathOffset [2]float32 _ [8]byte } type intersectUniforms struct { vert struct { uvTransform [4]float32 subUVTransform [4]float32 } } type fboSet struct { fbos []FBO } type FBO struct { size image.Point tex Texture } type pathData struct { ncurves int data Buffer } type vertex struct { Corner float32 MaxY float32 FromX, FromY float32 CtrlX, CtrlY float32 ToX, ToY float32 } func (v vertex) encode(d []byte, maxy uint32) { d = d[0:32] bo := binary.LittleEndian() bo.PutUint32(d[0:4], math.Float32bits(v.Corner)) bo.PutUint32(d[4:8], maxy) bo.PutUint32(d[8:12], math.Float32bits(v.FromX)) bo.PutUint32(d[12:16], math.Float32bits(v.FromY)) bo.PutUint32(d[16:20], math.Float32bits(v.CtrlX)) bo.PutUint32(d[20:24], math.Float32bits(v.CtrlY)) bo.PutUint32(d[24:28], math.Float32bits(v.ToX)) bo.PutUint32(d[28:32], math.Float32bits(v.ToY)) } const ( // 10000 costs 120KB WASM heap (60000 uint16 = 120000 bytes); 1000 costs 12KB. pathBatchSize = 1000 vertStride = 8 * 4 ) func newPather(ctx Device) *pather { return &pather{ ctx: ctx, stenciler: newStenciler(ctx), coverer: newCoverer(ctx), } } func newCoverer(ctx Device) *coverer { c := &coverer{ctx: ctx} c.colUniforms = &coverColUniforms{} c.texUniforms = &coverTexUniforms{} c.linearGradientUniforms = &coverLinearGradientUniforms{} pipelines, err := createColorPrograms(ctx, ShaderCoverVert(), ShaderCoverFrag(), [3]unsafe.Pointer{ unsafe.Pointer(c.colUniforms), unsafe.Pointer(c.linearGradientUniforms), unsafe.Pointer(c.texUniforms), }, [3]int{ int(unsafe.Sizeof(*c.colUniforms)), int(unsafe.Sizeof(*c.linearGradientUniforms)), int(unsafe.Sizeof(*c.texUniforms)), }, ) if err != nil { panic(err) } c.pipelines = pipelines return c } func newStenciler(ctx Device) *stenciler { indices := []uint16{:pathBatchSize * 6} for i := int32(0); i < pathBatchSize; i++ { idx := uint16(i) indices[idx*6+0] = idx*4 + 0 indices[idx*6+1] = idx*4 + 1 indices[idx*6+2] = idx*4 + 2 indices[idx*6+3] = idx*4 + 2 indices[idx*6+4] = idx*4 + 1 indices[idx*6+5] = idx*4 + 3 } indexBuf, err := ctx.NewImmutableBuffer(BufferBindingIndices, Uint16sToBytes(indices)) if err != nil { panic(err) } progLayout := VertexLayout{ Inputs: []InputDesc{ {Type: ShaderDataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*vertex)(nil).Corner))}, {Type: ShaderDataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*vertex)(nil).MaxY))}, {Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).FromX))}, {Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).CtrlX))}, {Type: ShaderDataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*vertex)(nil).ToX))}, }, Stride: vertStride, } iprogLayout := VertexLayout{ Inputs: []InputDesc{ {Type: ShaderDataTypeFloat, Size: 2, Offset: 0}, {Type: ShaderDataTypeFloat, Size: 2, Offset: 4 * 2}, }, Stride: 4 * 4, } st := &stenciler{ctx: ctx, indexBuf: indexBuf} vsh, fsh, err := newShaders(ctx, ShaderStencilVert(), ShaderStencilFrag()) if err != nil { panic(err) } defer vsh.Release() defer fsh.Release() st.pipeline.uniforms = &stencilUniforms{} vertUniforms := newGPUUniformBuffer(ctx, unsafe.Pointer(st.pipeline.uniforms), int(unsafe.Sizeof(*st.pipeline.uniforms))) pipe, err := st.ctx.NewPipeline(PipelineDesc{ VertexShader: vsh, FragmentShader: fsh, VertexLayout: progLayout, BlendDesc: BlendDesc{ Enable: true, SrcFactor: BlendFactorOne, DstFactor: BlendFactorOne, }, PixelFormat: TextureFormatFloat, Topology: TopologyTriangles, }) if err != nil { panic(err) } st.pipeline.pipeline = &blitPipeline{pipe, vertUniforms} vsh2, fsh2, err := newShaders(ctx, ShaderIntersectVert(), ShaderIntersectFrag()) if err != nil { panic(err) } defer vsh2.Release() defer fsh2.Release() st.ipipeline.uniforms = &intersectUniforms{} vertUniforms2 := newGPUUniformBuffer(ctx, unsafe.Pointer(&st.ipipeline.uniforms.vert), int(unsafe.Sizeof(st.ipipeline.uniforms.vert))) ipipe, err := st.ctx.NewPipeline(PipelineDesc{ VertexShader: vsh2, FragmentShader: fsh2, VertexLayout: iprogLayout, BlendDesc: BlendDesc{ Enable: true, SrcFactor: BlendFactorDstColor, DstFactor: BlendFactorZero, }, PixelFormat: TextureFormatFloat, Topology: TopologyTriangleStrip, }) if err != nil { panic(err) } st.ipipeline.pipeline = &blitPipeline{ipipe, vertUniforms2} return st } func (s *fboSet) resize(ctx Device, format TextureFormat, sizes []image.Point) { for i := len(s.fbos); i < len(sizes); i++ { s.fbos = append(s.fbos, FBO{}) } for i, sz := range sizes { f := &s.fbos[i] resize := sz.X > f.size.X || sz.Y > f.size.Y if f.size.X > 0 && f.size.Y > 0 { waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) resize = resize || waste > 1.2 } if resize { if f.tex != nil { f.tex.Release() } sz = sz.Mul(105).Div(100) max := ctx.Caps().MaxTextureSize if sz.Y > max { sz.Y = max } if sz.X > max { sz.X = max } tex, err := ctx.NewTexture(format, sz.X, sz.Y, FilterNearest, FilterNearest, BufferBindingTexture|BufferBindingFramebuffer) if err != nil { panic(err) } f.size = sz f.tex = tex } } s.delete(ctx, len(sizes)) } func (s *fboSet) delete(ctx Device, idx int) { for i := idx; i < len(s.fbos); i++ { f := s.fbos[i] f.tex.Release() } s.fbos = s.fbos[:idx] } func (s *stenciler) release() { s.fbos.delete(s.ctx, 0) s.intersections.delete(s.ctx, 0) s.pipeline.pipeline.Release() s.ipipeline.pipeline.Release() s.indexBuf.Release() } func (p *pather) release() { p.stenciler.release() p.coverer.release() } func (c *coverer) release() { for _, p := range c.pipelines { for _, p := range p { p.Release() } } } func buildPath(ctx Device, p []byte) pathData { buf, err := ctx.NewImmutableBuffer(BufferBindingVertices, p) if err != nil { panic(err) } return pathData{ncurves: len(p) / vertStride, data: buf} } func (p pathData) release() { p.data.Release() } func (p *pather) begin(sizes []image.Point) { p.stenciler.begin(sizes) } func (p *pather) stencilPath(bounds image.Rectangle, offset Point, uv image.Point, data pathData) { p.stenciler.stencilPath(bounds, offset, uv, data) } func (s *stenciler) beginIntersect(sizes []image.Point) { s.intersections.resize(s.ctx, TextureFormatFloat, sizes) } func (s *stenciler) cover(idx int) FBO { return s.fbos.fbos[idx] } func (s *stenciler) begin(sizes []image.Point) { s.fbos.resize(s.ctx, TextureFormatFloat, sizes) } func (s *stenciler) stencilPath(bounds image.Rectangle, offset Point, uv image.Point, data pathData) { s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) texSize := Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} scale := Point{X: 2 / texSize.X, Y: 2 / texSize.Y} orig := Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} s.pipeline.uniforms.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y} s.pipeline.uniforms.pathOffset = [2]float32{offset.X, offset.Y} s.pipeline.pipeline.UploadUniforms(s.ctx) start := 0 nquads := data.ncurves / 4 for start < nquads { batch := nquads - start if batch > pathBatchSize { batch = pathBatchSize } off := vertStride * start * 4 s.ctx.BindVertexBuffer(data.data, off) s.ctx.DrawElements(0, batch*6) start += batch } } func (p *pather) cover(mat materialType, isFBO bool, col RGBA, col1, col2 RGBA, scale, off Point, uvTrans Affine2D, coverScale, coverOff Point) { p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff) } func (c *coverer) cover(mat materialType, isFBO bool, col RGBA, col1, col2 RGBA, scale, off Point, uvTrans Affine2D, coverScale, coverOff Point) { var uniforms *coverUniforms switch mat { case materialColor: c.colUniforms.color = col uniforms = &c.colUniforms.coverUniforms case materialLinearGradient: c.linearGradientUniforms.color1 = col1 c.linearGradientUniforms.color2 = col2 t1, t2, t3, t4, t5, t6 := uvTrans.Elems() c.linearGradientUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} c.linearGradientUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &c.linearGradientUniforms.coverUniforms case materialTexture: t1, t2, t3, t4, t5, t6 := uvTrans.Elems() c.texUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &c.texUniforms.coverUniforms } uniforms.fbo = 0 if isFBO { uniforms.fbo = 1 } uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} fboIdx := 0 if isFBO { fboIdx = 1 } c.pipelines[fboIdx][mat].UploadUniforms(c.ctx) c.ctx.DrawArrays(0, 4) } // vertex is 8 x float32 = 32 bytes = vertStride. Keep in sync. // --- Clip encoding (from clip.go) --- func encodeQuadTo(data []byte, meta uint32, from, ctrl, to Point) { bo := binary.LittleEndian() data = data[:vertStride*4] bo.PutUint32(data[4:8], meta) bo.PutUint32(data[8:12], math.Float32bits(from.X)) bo.PutUint32(data[12:16], math.Float32bits(from.Y)) bo.PutUint32(data[16:20], math.Float32bits(ctrl.X)) bo.PutUint32(data[20:24], math.Float32bits(ctrl.Y)) bo.PutUint32(data[24:28], math.Float32bits(to.X)) bo.PutUint32(data[28:32], math.Float32bits(to.Y)) copy(data[vertStride*1:vertStride*2], data[vertStride*0:vertStride*1]) copy(data[vertStride*2:vertStride*3], data[vertStride*0:vertStride*1]) copy(data[vertStride*3:vertStride*4], data[vertStride*0:vertStride*1]) bo.PutUint32(data[vertStride*0:vertStride*0+4], math.Float32bits(nwCorner)) bo.PutUint32(data[vertStride*1:vertStride*1+4], math.Float32bits(neCorner)) bo.PutUint32(data[vertStride*2:vertStride*2+4], math.Float32bits(swCorner)) bo.PutUint32(data[vertStride*3:vertStride*3+4], math.Float32bits(seCorner)) } const ( nwCorner = float32(1*0.5 + 0*0.25) neCorner = float32(1*0.5 + 1*0.25) swCorner = float32(0*0.5 + 0*0.25) seCorner = float32(0*0.5 + 1*0.25) ) type quadSplitter struct { bounds Rectangle contour uint32 d *drawOps scratch []QuadSegment } func (qs *quadSplitter) encodeQuadTo(from, ctrl, to Point) { data := qs.d.writeVertCache(vertStride * 4) encodeQuadTo(data, qs.contour, from, ctrl, to) } func (qs *quadSplitter) splitAndEncode(quad QuadSegment) { cbnd := Rectangle{Min: quad.From, Max: quad.To}.Canon() from, ctrl, to := quad.From, quad.Ctrl, quad.To v0 := ctrl.Sub(from) v1 := to.Sub(ctrl) d := v0.X - v1.X if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { t := v0.X / d ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) qs.encodeQuadTo(from, ctrl0, mid) qs.encodeQuadTo(mid, ctrl1, to) if mid.X > cbnd.Max.X { cbnd.Max.X = mid.X } if mid.X < cbnd.Min.X { cbnd.Min.X = mid.X } } else { qs.encodeQuadTo(from, ctrl, to) } d = v0.Y - v1.Y if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { t := v0.Y / d y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y if y > cbnd.Max.Y { cbnd.Max.Y = y } if y < cbnd.Min.Y { cbnd.Min.Y = y } } qs.bounds = unionRect(qs.bounds, cbnd) } func unionRect(r, s Rectangle) Rectangle { if r.Min.X > s.Min.X { r.Min.X = s.Min.X } if r.Min.Y > s.Min.Y { r.Min.Y = s.Min.Y } if r.Max.X < s.Max.X { r.Max.X = s.Max.X } if r.Max.Y < s.Max.Y { r.Max.Y = s.Max.Y } return r } // --- Caches (from caches.go) --- type textureCacheKey struct { filter byte handle any } type textureCache struct { res map[textureCacheKey]resourceCacheValue } type resourceCacheValue struct { used bool resource gpuResource } type opCache struct { index map[opKey]int freelist []int cache []opCacheValue } type opCacheValue struct { data pathData bounds Rectangle key opKey keep bool } func newTextureCache() *textureCache { return &textureCache{res: map[textureCacheKey]resourceCacheValue{}} } func (r *textureCache) get(key textureCacheKey) (gpuResource, bool) { v, exists := r.res[key] if !exists { return nil, false } if !v.used { v.used = true r.res[key] = v } return v.resource, exists } func (r *textureCache) put(key textureCacheKey, val gpuResource) { v, exists := r.res[key] if exists && v.used { panic(errorf("key exists, %v", key)) } v.used = true v.resource = val r.res[key] = v } func (r *textureCache) frame() { for k, v := range r.res { if v.used { v.used = false r.res[k] = v } else { delete(r.res, k) v.resource.release() } } } func (r *textureCache) release() { for _, v := range r.res { v.resource.release() } r.res = nil } func newOpCache() *opCache { return &opCache{ index: map[opKey]int{}, freelist: []int{}, cache: []opCacheValue{}, } } func (r *opCache) get(key opKey) (o opCacheValue, exist bool) { v := r.index[key] if v == 0 { return } r.cache[v-1].keep = true return r.cache[v-1], true } func (r *opCache) put(key opKey, val opCacheValue) { v := r.index[key] val.keep = true val.key = key if v == 0 { i := len(r.cache) if len(r.freelist) > 0 { i = r.freelist[len(r.freelist)-1] r.freelist = r.freelist[:len(r.freelist)-1] r.cache[i] = val } else { r.cache = append(r.cache, val) } r.index[key] = i + 1 } else { r.cache[v-1] = val } } func (r *opCache) frame() { r.freelist = r.freelist[:0] for i, v := range r.cache { r.cache[i].keep = false if v.keep { continue } if v.data.data != nil { v.data.release() r.cache[i].data.data = nil } delete(r.index, v.key) r.freelist = append(r.freelist, i) } } func (r *opCache) release() { for i := range r.cache { r.cache[i].keep = false } r.frame() r.index = nil r.freelist = nil r.cache = nil } // --- Packer (from pack.go) --- type packer struct { maxDims image.Point spaces []image.Rectangle sizes []image.Point pos image.Point } type placement struct { Idx int Pos image.Point } func (p *packer) add(s image.Point) (placement, bool) { if place, ok := p.tryAdd(s); ok { return place, true } p.newPage() return p.tryAdd(s) } func (p *packer) clear() { p.sizes = p.sizes[:0] p.spaces = p.spaces[:0] } func (p *packer) newPage() { p.pos = image.Point{} p.sizes = append(p.sizes, image.Point{}) p.spaces = p.spaces[:0] p.spaces = append(p.spaces, image.Rectangle{ Max: image.Point{X: 1e6, Y: 1e6}, }) } func (p *packer) tryAdd(s image.Point) (placement, bool) { if len(p.spaces) == 0 || len(p.sizes) == 0 { return placement{}, false } var ( bestIdx *image.Rectangle bestSize = p.maxDims lastSize = p.sizes[len(p.sizes)-1] ) for i := range p.spaces { space := &p.spaces[i] rightSpace := space.Dx() - s.X bottomSpace := space.Dy() - s.Y if rightSpace < 0 || bottomSpace < 0 { continue } size := lastSize if x := space.Min.X + s.X; x > size.X { if x > p.maxDims.X { continue } size.X = x } if y := space.Min.Y + s.Y; y > size.Y { if y > p.maxDims.Y { continue } size.Y = y } if size.X*size.Y < bestSize.X*bestSize.Y { bestIdx = space bestSize = size } } if bestIdx == nil { return placement{}, false } bestSpace := *bestIdx *bestIdx = p.spaces[len(p.spaces)-1] p.spaces = p.spaces[:len(p.spaces)-1] pos := bestSpace.Min if rem := bestSpace.Dy() - s.Y; rem > 0 { p.spaces = append(p.spaces, image.Rectangle{ Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, Max: image.Point{X: bestSpace.Max.X, Y: bestSpace.Max.Y}, }) } if rem := bestSpace.Dx() - s.X; rem > 0 { p.spaces = append(p.spaces, image.Rectangle{ Min: image.Point{X: pos.X + s.X, Y: pos.Y}, Max: image.Point{X: bestSpace.Max.X, Y: pos.Y + s.Y}, }) } idx := len(p.sizes) - 1 p.sizes[idx] = bestSize return placement{Idx: idx, Pos: pos}, true } // errorf creates a formatted error without importing fmt (it's already imported). func errorf(format string, args ...any) error { return fmt.Errorf(format, args...) }