// SPDX-License-Identifier: Unlicense OR MIT package gpu import ( "encoding/binary" "errors" "fmt" "image" "image/color" "math" "time" "unsafe" "gioui.org/f32" "gioui.org/gpu/backend" "gioui.org/internal/f32color" gunsafe "gioui.org/internal/unsafe" "gioui.org/layout" "gioui.org/op" ) type compute struct { ctx backend.Device enc encoder drawOps drawOps cache *resourceCache maxTextureDim int defFBO backend.Framebuffer programs struct { elements backend.Program tileAlloc backend.Program pathCoarse backend.Program backdrop backend.Program binning backend.Program coarse backend.Program kernel4 backend.Program } buffers struct { config backend.Buffer scene sizedBuffer state sizedBuffer memory sizedBuffer } output struct { size image.Point // image is the output texture. Note that it is RGBA format, // but contains data in sRGB. See blitOutput for more detail. image backend.Texture blitProg backend.Program } atlas struct { packer packer // positions maps imageOpData.handles to positions inside tex. positions map[interface{}]image.Point tex backend.Texture } timers struct { profile string t *timers elements *timer tileAlloc *timer pathCoarse *timer backdropBinning *timer coarse *timer kernel4 *timer } // The following fields hold scratch space to avoid garbage. zeroSlice []byte memHeader *memoryHeader conf *config } type encoder struct { scene []byte npath int npathseg int } type encodeState struct { trans f32.Affine2D clip f32.Rectangle } type sizedBuffer struct { size int buffer backend.Buffer } // config matches Config in setup.h type config struct { n_elements uint32 // paths n_pathseg uint32 width_in_tiles uint32 height_in_tiles uint32 tile_alloc memAlloc bin_alloc memAlloc ptcl_alloc memAlloc pathseg_alloc memAlloc anno_alloc memAlloc } // memAlloc matches Alloc in mem.h type memAlloc struct { offset uint32 //size uint32 } // memoryHeader matches the header of Memory in mem.h. type memoryHeader struct { mem_offset uint32 mem_error uint32 } // GPU structure sizes and constants. const ( tileWidthPx = 32 tileHeightPx = 32 ptclInitialAlloc = 1024 kernel4OutputUnit = 2 kernel4AtlasUnit = 3 pathSize = 12 binSize = 8 pathsegSize = 48 annoSize = 52 stateSize = 56 stateStride = 4 + 2*stateSize sceneElemSize = 36 ) // GPU commands from scene.h const ( elemNop = iota elemStrokeLine elemFillLine elemStrokeQuad elemFillQuad elemStrokeCubic elemFillCubic elemStroke elemFill elemLineWidth elemTransform elemBeginClip elemEndClip elemFillTexture ) // mem.h constants. const ( memNoError = 0 // NO_ERROR memMallocFailed = 1 // ERR_MALLOC_FAILED ) func newCompute(ctx backend.Device) (*compute, error) { maxDim := ctx.Caps().MaxTextureSize // Large atlas textures cause artifacts due to precision loss in // shaders. if cap := 8192; maxDim > cap { maxDim = cap } g := &compute{ ctx: ctx, defFBO: ctx.CurrentFramebuffer(), cache: newResourceCache(), maxTextureDim: maxDim, conf: new(config), memHeader: new(memoryHeader), } blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag) if err != nil { g.Release() return nil, err } g.output.blitProg = blitProg g.drawOps.pathCache = newOpCache() g.drawOps.retainPathData = true buf, err := ctx.NewBuffer(backend.BufferBindingShaderStorage, int(unsafe.Sizeof(config{}))) if err != nil { g.Release() return nil, err } g.buffers.config = buf shaders := []struct { prog *backend.Program src backend.ShaderSources }{ {&g.programs.elements, shader_elements_comp}, {&g.programs.tileAlloc, shader_tile_alloc_comp}, {&g.programs.pathCoarse, shader_path_coarse_comp}, {&g.programs.backdrop, shader_backdrop_comp}, {&g.programs.binning, shader_binning_comp}, {&g.programs.coarse, shader_coarse_comp}, {&g.programs.kernel4, shader_kernel4_comp}, } for _, shader := range shaders { p, err := ctx.NewComputeProgram(shader.src) if err != nil { g.Release() return nil, err } *shader.prog = p } return g, nil } func (g *compute) Collect(viewport image.Point, ops *op.Ops) { g.drawOps.reset(g.cache, viewport) g.drawOps.collect(g.ctx, g.cache, ops, viewport) for _, img := range g.drawOps.allImageOps { expandPathOp(img.path, img.clip) } if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(backend.FeatureTimers) { t := &g.timers t.t = newTimers(g.ctx) t.elements = g.timers.t.newTimer() t.tileAlloc = g.timers.t.newTimer() t.pathCoarse = g.timers.t.newTimer() t.backdropBinning = g.timers.t.newTimer() t.coarse = g.timers.t.newTimer() t.kernel4 = g.timers.t.newTimer() } } func (g *compute) Clear(col color.NRGBA) { g.drawOps.clear = true g.drawOps.clearColor = f32color.LinearFromSRGB(col) } func (g *compute) Frame() error { viewport := g.drawOps.viewport tileDims := image.Point{ X: (viewport.X + tileWidthPx - 1) / tileWidthPx, Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx, } g.ctx.BeginFrame() defer g.ctx.EndFrame() if err := g.uploadImages(g.drawOps.allImageOps); err != nil { return err } g.encode(viewport) if err := g.render(tileDims); err != nil { return err } g.blitOutput(viewport) g.cache.frame() g.drawOps.pathCache.frame() t := &g.timers if g.drawOps.profile && t.t.ready() { et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed ft := et + tat + pct + bbt + ct + k4t q := 100 * time.Microsecond ft = ft.Round(q) et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q) ct, k4t = ct.Round(q), k4t.Round(q) t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s", ft, et, tat, pct, bbt, ct, k4t) } return nil } func (g *compute) Profile() string { return g.timers.profile } // blitOutput copies the compute render output to the output FBO. We need to // copy because compute shaders can only write to textures, not FBOs. Compute // shader can only write to RGBA textures, but since we actually render in sRGB // format we can't use glBlitFramebuffer, because it does sRGB conversion. func (g *compute) blitOutput(viewport image.Point) { g.ctx.BindFramebuffer(g.defFBO) g.ctx.Viewport(0, 0, viewport.X, viewport.Y) g.ctx.BindTexture(0, g.output.image) g.ctx.BindProgram(g.output.blitProg) g.ctx.DrawArrays(backend.DrawModeTriangleStrip, 0, 4) } func (g *compute) encode(viewport image.Point) { g.enc.reset() // Flip Y-axis. flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0, float32(viewport.Y))) g.enc.transform(flipY) if g.drawOps.clear { g.drawOps.clear = false g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)}, false) g.enc.fill(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB())) } g.encodeOps(flipY, viewport, g.drawOps.allImageOps) } func (g *compute) uploadImages(ops []imageOp) error { // padding is the number of pixels added to the right and below // images, to avoid atlas filtering artifacts. const padding = 1 a := &g.atlas var uploads map[interface{}]*image.RGBA resize := false reclaimed := false restart: for { for _, op := range ops { switch m := op.material; m.material { case materialTexture: if _, exists := a.positions[m.data.handle]; exists { continue } size := m.data.src.Bounds().Size() size.X += padding size.Y += padding place, fits := a.packer.tryAdd(size) if !fits { maxDim := a.packer.maxDim a.positions = nil uploads = nil a.packer = packer{ maxDim: maxDim, } if !reclaimed { // Some images may no longer be in use, try again // after clearing existing maps. reclaimed = true } else { a.packer.maxDim += 256 resize = true if a.packer.maxDim > g.maxTextureDim { return errors.New("compute: no space left in atlas texture") } } a.packer.newPage() continue restart } if a.positions == nil { g.atlas.positions = make(map[interface{}]image.Point) } a.positions[m.data.handle] = place.Pos if uploads == nil { uploads = make(map[interface{}]*image.RGBA) } uploads[m.data.handle] = m.data.src } } break } if len(uploads) == 0 { return nil } if resize { if a.tex != nil { a.tex.Release() a.tex = nil } sz := a.packer.maxDim handle, err := g.ctx.NewTexture(backend.TextureFormatSRGB, sz, sz, backend.FilterLinear, backend.FilterLinear, backend.BufferBindingTexture) if err != nil { return fmt.Errorf("compute: failed to create atlas texture: %v", err) } a.tex = handle } for h, img := range uploads { pos, ok := a.positions[h] if !ok { panic("compute: internal error: image not placed") } size := img.Bounds().Size() backend.UploadImage(a.tex, pos, img) rightPadding := image.Pt(padding, size.Y) a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding, g.zeros(rightPadding.X*rightPadding.Y*4)) bottomPadding := image.Pt(size.X, padding) a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding, g.zeros(bottomPadding.X*bottomPadding.Y*4)) } return nil } func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point, ops []imageOp) { for _, op := range ops { bounds := layout.FRect(op.clip) // clip is the union of all drawing affected by the clipping // operation. TODO: tigthen. clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y)) nclips := g.encodeClipStack(clip, bounds, op.path) m := op.material switch m.material { case materialTexture: img := m.data pos, ok := g.atlas.positions[img.handle] if !ok { panic("compute: internal error: image not placed") } bounds := image.Rectangle{ Min: pos, Max: pos.Add(img.src.Bounds().Size()), } maxDim := g.atlas.packer.maxDim atlasSize := f32.Pt(float32(maxDim), float32(maxDim)) uvBounds := f32.Rectangle{ Min: f32.Point{ X: float32(bounds.Min.X) / atlasSize.X, Y: float32(bounds.Min.Y) / atlasSize.Y, }, Max: f32.Point{ X: float32(bounds.Max.X) / atlasSize.X, Y: float32(bounds.Max.Y) / atlasSize.Y, }, } fpos := layout.FPt(pos) texScale := f32.Pt(1.0/atlasSize.X, 1.0/atlasSize.Y) mat := f32.Affine2D{}. Mul(trans.Invert()). Mul(f32.Affine2D{}.Scale(f32.Pt(0, 0), texScale)). Mul(f32.Affine2D{}.Offset(fpos)). Mul(trans.Mul(m.trans).Invert()) g.enc.transform(mat) g.enc.fillTexture(uvBounds) g.enc.transform(mat.Invert()) case materialColor: g.enc.fill(f32color.NRGBAToRGBA(op.material.color.SRGB())) case materialLinearGradient: // TODO: implement. g.enc.fill(f32color.NRGBAToRGBA(op.material.color1.SRGB())) default: panic("not implemented") } // Pop the clip stack. for i := 0; i < nclips; i++ { g.enc.endClip(clip) } } } // encodeClips encodes a stack of clip paths and return the stack depth. func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp) int { nclips := 0 if p != nil && p.parent != nil { nclips += g.encodeClipStack(clip, bounds, p.parent) g.enc.beginClip(clip) nclips += 1 } if p != nil && p.path { pathData, _ := g.drawOps.pathCache.get(p.pathKey) g.enc.transform(f32.Affine2D{}.Offset(p.off)) g.enc.append(pathData.computePath) g.enc.transform(f32.Affine2D{}.Offset(p.off.Mul(-1))) } else { g.enc.rect(bounds, false) } return nclips } // encodePath takes a Path encoded with quadSplitter and encode it for elements.comp. // This is certainly wasteful, but minimizes implementation differences to the old // renderer. func encodePath(p []byte) encoder { var enc encoder for len(p) > 0 { // p contains quadratic curves encoded in vertex structs. vertex := p[:vertStride] // We only need some of the values. This code undoes vertex.encode. from := f32.Pt( math.Float32frombits(bo.Uint32(vertex[8:])), math.Float32frombits(bo.Uint32(vertex[12:])), ) ctrl := f32.Pt( math.Float32frombits(bo.Uint32(vertex[16:])), math.Float32frombits(bo.Uint32(vertex[20:])), ) to := f32.Pt( math.Float32frombits(bo.Uint32(vertex[24:])), math.Float32frombits(bo.Uint32(vertex[28:])), ) enc.quad(from, ctrl, to, false) // The vertex is duplicated 4 times, one for each corner of quads drawn // by the old renderer. p = p[vertStride*4:] } return enc } func (g *compute) render(tileDims image.Point) error { const ( // wgSize is the largest and most common workgroup size. wgSize = 128 // PARTITION_SIZE from elements.comp partitionSize = 32 * 4 ) widthInBins := (tileDims.X + 15) / 16 heightInBins := (tileDims.Y + 7) / 8 if widthInBins*heightInBins > wgSize { return fmt.Errorf("gpu: output too large (%dx%d)", tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx) } // Pad scene with zeroes to avoid reading garbage in elements.comp. scenePadding := partitionSize*sceneElemSize - len(g.enc.scene)%(partitionSize*sceneElemSize) g.enc.scene = append(g.enc.scene, make([]byte, scenePadding)...) realloced := false if s := len(g.enc.scene); s > g.buffers.scene.size { realloced = true paddedCap := s * 11 / 10 if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil { return err } } g.buffers.scene.buffer.Upload(g.enc.scene) w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx if g.output.size.X < w || g.output.size.Y < h { if err := g.resizeOutput(image.Pt(w, h)); err != nil { return err } } g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image, backend.AccessWrite, backend.TextureFormatRGBA8) if g.atlas.tex != nil { g.ctx.BindTexture(kernel4AtlasUnit, g.atlas.tex) } // alloc is the number of allocated bytes for static buffers. var alloc uint32 round := func(v, quantum int) int { return (v + quantum - 1) &^ (quantum - 1) } malloc := func(size int) memAlloc { size = round(size, 4) offset := alloc alloc += uint32(size) return memAlloc{offset /*, uint32(size)*/} } *g.conf = config{ n_elements: uint32(g.enc.npath), n_pathseg: uint32(g.enc.npathseg), width_in_tiles: uint32(tileDims.X), height_in_tiles: uint32(tileDims.Y), tile_alloc: malloc(g.enc.npath * pathSize), bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize), ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc), pathseg_alloc: malloc(g.enc.npathseg * pathsegSize), anno_alloc: malloc(g.enc.npath * annoSize), } numPartitions := (g.enc.numElements() + 127) / 128 // clearSize is the atomic partition counter plus flag and 2 states per partition. clearSize := 4 + numPartitions*stateStride if clearSize > g.buffers.state.size { realloced = true paddedCap := clearSize * 11 / 10 if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil { return err } } g.buffers.config.Upload(gunsafe.StructView(g.conf)) minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc) if minSize > g.buffers.memory.size { realloced = true // Add space for dynamic GPU allocations. const sizeBump = 4 * 1024 * 1024 minSize += sizeBump if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil { return err } } for { *g.memHeader = memoryHeader{ mem_offset: alloc, } g.buffers.memory.buffer.Upload(gunsafe.StructView(g.memHeader)) g.buffers.state.buffer.Upload(g.zeros(clearSize)) if realloced { realloced = false g.bindBuffers() } t := &g.timers g.ctx.MemoryBarrier() t.elements.begin() g.ctx.BindProgram(g.programs.elements) g.ctx.DispatchCompute(numPartitions, 1, 1) g.ctx.MemoryBarrier() t.elements.end() t.tileAlloc.begin() g.ctx.BindProgram(g.programs.tileAlloc) g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) g.ctx.MemoryBarrier() t.tileAlloc.end() t.pathCoarse.begin() g.ctx.BindProgram(g.programs.pathCoarse) g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1) g.ctx.MemoryBarrier() t.pathCoarse.end() t.backdropBinning.begin() g.ctx.BindProgram(g.programs.backdrop) g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) // No barrier needed between backdrop and binning. g.ctx.BindProgram(g.programs.binning) g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) g.ctx.MemoryBarrier() t.backdropBinning.end() t.coarse.begin() g.ctx.BindProgram(g.programs.coarse) g.ctx.DispatchCompute(widthInBins, heightInBins, 1) g.ctx.MemoryBarrier() t.coarse.end() t.kernel4.begin() g.ctx.BindProgram(g.programs.kernel4) g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1) g.ctx.MemoryBarrier() t.kernel4.end() if err := g.buffers.memory.buffer.Download(gunsafe.StructView(g.memHeader)); err != nil { return err } switch errCode := g.memHeader.mem_error; errCode { case memNoError: return nil case memMallocFailed: // Resize memory and try again. realloced = true sz := g.buffers.memory.size * 15 / 10 if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil { return err } continue default: return fmt.Errorf("compute: shader program failed with error %d", errCode) } } } // zeros returns a byte slice with size bytes of zeros. func (g *compute) zeros(size int) []byte { if cap(g.zeroSlice) < size { g.zeroSlice = append(g.zeroSlice, make([]byte, size)...) } return g.zeroSlice[:size] } func (g *compute) resizeOutput(size image.Point) error { if g.output.image != nil { g.output.image.Release() g.output.image = nil } img, err := g.ctx.NewTexture(backend.TextureFormatRGBA8, size.X, size.Y, backend.FilterNearest, backend.FilterNearest, backend.BufferBindingShaderStorage|backend.BufferBindingFramebuffer) if err != nil { return err } g.output.image = img g.output.size = size return nil } func (g *compute) Release() { g.drawOps.pathCache.release() g.cache.release() progs := []backend.Program{ g.programs.elements, g.programs.tileAlloc, g.programs.pathCoarse, g.programs.backdrop, g.programs.binning, g.programs.coarse, g.programs.kernel4, } if p := g.output.blitProg; p != nil { p.Release() } for _, p := range progs { if p != nil { p.Release() } } g.buffers.scene.release() g.buffers.state.release() g.buffers.memory.release() if b := g.buffers.config; b != nil { b.Release() } if g.output.image != nil { g.output.image.Release() } if g.atlas.tex != nil { g.atlas.tex.Release() } if g.timers.t != nil { g.timers.t.release() } *g = compute{} } func (g *compute) bindBuffers() { bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer, g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer) bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer, g.buffers.config) bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer, g.buffers.config) bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer, g.buffers.config) bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer, g.buffers.config) bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer, g.buffers.config) bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer, g.buffers.config) } func (b *sizedBuffer) release() { if b.buffer == nil { return } b.buffer.Release() *b = sizedBuffer{} } func (b *sizedBuffer) ensureCapacity(ctx backend.Device, size int) error { if b.size >= size { return nil } if b.buffer != nil { b.release() } buf, err := ctx.NewBuffer(backend.BufferBindingShaderStorage, size) if err != nil { return err } b.buffer = buf b.size = size return nil } func bindStorageBuffers(prog backend.Program, buffers ...backend.Buffer) { for i, buf := range buffers { prog.SetStorageBuffer(i, buf) } } var bo = binary.LittleEndian func (e *encoder) reset() { e.scene = e.scene[:0] e.npath = 0 e.npathseg = 0 } func (e *encoder) numElements() int { return len(e.scene) / sceneElemSize } func (e *encoder) append(e2 encoder) { e.scene = append(e.scene, e2.scene...) e.npath += e2.npath e.npathseg += e2.npathseg } func (e *encoder) transform(m f32.Affine2D) { sx, hx, ox, hy, sy, oy := m.Elems() cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd[0:4], elemTransform) bo.PutUint32(cmd[4:8], math.Float32bits(sx)) bo.PutUint32(cmd[8:12], math.Float32bits(hy)) bo.PutUint32(cmd[12:16], math.Float32bits(hx)) bo.PutUint32(cmd[16:20], math.Float32bits(sy)) bo.PutUint32(cmd[20:24], math.Float32bits(ox)) bo.PutUint32(cmd[24:28], math.Float32bits(oy)) e.cmd(cmd) } func (e *encoder) lineWidth(width float32) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemLineWidth) bo.PutUint32(cmd[4:8], math.Float32bits(width)) e.cmd(cmd) } func (e *encoder) stroke(col color.RGBA) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemStroke) c := uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A) bo.PutUint32(cmd[4:8], c) e.cmd(cmd) e.npath++ } func (e *encoder) beginClip(bbox f32.Rectangle) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemBeginClip) bo.PutUint32(cmd[4:8], math.Float32bits(bbox.Min.X)) bo.PutUint32(cmd[8:12], math.Float32bits(bbox.Min.Y)) bo.PutUint32(cmd[12:16], math.Float32bits(bbox.Max.X)) bo.PutUint32(cmd[16:20], math.Float32bits(bbox.Max.Y)) e.cmd(cmd) e.npath++ } func (e *encoder) endClip(bbox f32.Rectangle) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemEndClip) bo.PutUint32(cmd[4:8], math.Float32bits(bbox.Min.X)) bo.PutUint32(cmd[8:12], math.Float32bits(bbox.Min.Y)) bo.PutUint32(cmd[12:16], math.Float32bits(bbox.Max.X)) bo.PutUint32(cmd[16:20], math.Float32bits(bbox.Max.Y)) e.cmd(cmd) e.npath++ } func (e *encoder) rect(r f32.Rectangle, stroke bool) { // Rectangle corners, clock-wise. c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X, r.Min.Y) e.line(c0, c1, stroke) e.line(c1, c2, stroke) e.line(c2, c3, stroke) e.line(c3, c0, stroke) } func (e *encoder) fill(col color.RGBA) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemFill) c := uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A) bo.PutUint32(cmd[4:8], c) e.cmd(cmd) e.npath++ } func (e *encoder) fillTexture(uvBounds f32.Rectangle) { cmd := make([]byte, sceneElemSize) bo.PutUint32(cmd, elemFillTexture) umin := uint16(uvBounds.Min.X*math.MaxUint16 + .5) vmin := uint16(uvBounds.Min.Y*math.MaxUint16 + .5) umax := uint16(uvBounds.Max.X*math.MaxUint16 + .5) vmax := uint16(uvBounds.Max.Y*math.MaxUint16 + .5) bo.PutUint32(cmd[4:8], uint32(umin)|uint32(vmin)<<16) bo.PutUint32(cmd[8:12], uint32(umax)|uint32(vmax)<<16) e.cmd(cmd) e.npath++ } func (e *encoder) line(start, end f32.Point, stroke bool) { cmd := make([]byte, sceneElemSize) if stroke { bo.PutUint32(cmd, elemStrokeLine) } else { bo.PutUint32(cmd, elemFillLine) } bo.PutUint32(cmd[4:8], math.Float32bits(start.X)) bo.PutUint32(cmd[8:12], math.Float32bits(start.Y)) bo.PutUint32(cmd[12:16], math.Float32bits(end.X)) bo.PutUint32(cmd[16:20], math.Float32bits(end.Y)) e.cmd(cmd) e.npathseg++ } func (e *encoder) quad(start, ctrl, end f32.Point, stroke bool) { cmd := make([]byte, sceneElemSize) if stroke { bo.PutUint32(cmd, elemStrokeQuad) } else { bo.PutUint32(cmd, elemFillQuad) } bo.PutUint32(cmd[4:8], math.Float32bits(start.X)) bo.PutUint32(cmd[8:12], math.Float32bits(start.Y)) bo.PutUint32(cmd[12:16], math.Float32bits(ctrl.X)) bo.PutUint32(cmd[16:20], math.Float32bits(ctrl.Y)) bo.PutUint32(cmd[20:24], math.Float32bits(end.X)) bo.PutUint32(cmd[24:28], math.Float32bits(end.Y)) e.cmd(cmd) e.npathseg++ } func (e *encoder) cmd(cmd []byte) { e.scene = append(e.scene, cmd...) }