Files
gio/gpu/compute.go
T
Elias Naur 84b586ae6c gpu: don't automatically clear screen before rendering
Gio UI may be overlaid on top of custom graphics such as in the glfw example.
That will only work if Gio doesn't clear the screen (to white).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-01-22 18:33:34 +01:00

881 lines
23 KiB
Go

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