forked from joejulian/gio
gpu: add compute implementation
The old renderer is still the default, so the new compute renderer will only be used in the rare case the old renderer is not supported but the new is. That happens on the Samsung J2 Prime and Moto C Android phones. Or set the GIORENDERER environment variable to "forcecompute" to disable the old renderer: $ GIORENDERER=forcecompute go run ... Missing features: - Gradients are not supported yet, and render as a solid color. - Draw timers are not added, and profile.Events are not emitted. - Stroked paths may in some cases appear corrupted because their clip outlines are not continuous when generated by Gio. Sebastien is working on a fix. - The new renderer shares most CPU-side logic with the old renderer, resulting in several inefficient conversion steps between the old operations representation and the new. This is slower, but minimizes divergence in features and bugs between the two renderers. Roadmap: - The compute renderer supports features that Gio does not yet exploit: stroked paths with round caps, transformations, lines, cubic beziér curves. - More stroke styles and maybe dashed strokes natively in shaders. - Metal and Direct3D ports. The most important feature is porting the renderer to run on the CPU. A CPU renderer will both support Gio on devices with insufficient GPU support, and allow us to remove the old renderer. Two renderers is twice the maintenance but the feature set of the weakest implementation. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+807
@@ -0,0 +1,807 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"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 {
|
||||
init backend.Program
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *compute) Profile() string {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
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 + 256,
|
||||
}
|
||||
if maxDim > g.maxTextureDim {
|
||||
return errors.New("compute: no space left in atlas texture")
|
||||
}
|
||||
resize = true
|
||||
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.encodePath(p.off, pathData.cpuData)
|
||||
} 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 (g *compute) encodePath(off f32.Point, p []byte) {
|
||||
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:])),
|
||||
)
|
||||
g.enc.quad(from.Add(off), ctrl.Add(off), to.Add(off), false)
|
||||
|
||||
// The vertex is duplicated 4 times, one for each corner of quads drawn
|
||||
// by the old renderer.
|
||||
p = p[vertStride*4:]
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
g.ctx.MemoryBarrier()
|
||||
g.ctx.BindProgram(g.programs.elements)
|
||||
g.ctx.DispatchCompute(numPartitions, 1, 1)
|
||||
g.ctx.MemoryBarrier()
|
||||
g.ctx.BindProgram(g.programs.tileAlloc)
|
||||
g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
|
||||
g.ctx.MemoryBarrier()
|
||||
g.ctx.BindProgram(g.programs.pathCoarse)
|
||||
g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1)
|
||||
g.ctx.MemoryBarrier()
|
||||
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()
|
||||
g.ctx.BindProgram(g.programs.coarse)
|
||||
g.ctx.DispatchCompute(widthInBins, heightInBins, 1)
|
||||
g.ctx.MemoryBarrier()
|
||||
g.ctx.BindProgram(g.programs.kernel4)
|
||||
g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1)
|
||||
g.ctx.MemoryBarrier()
|
||||
|
||||
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.init,
|
||||
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()
|
||||
}
|
||||
*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) 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...)
|
||||
}
|
||||
+5
-1
@@ -14,6 +14,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -372,10 +373,13 @@ const (
|
||||
)
|
||||
|
||||
func New(ctx backend.Device) (GPU, error) {
|
||||
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
|
||||
feats := ctx.Caps().Features
|
||||
switch {
|
||||
case feats.Has(backend.FeatureFloatRenderTargets):
|
||||
case !forceCompute && feats.Has(backend.FeatureFloatRenderTargets):
|
||||
return newGPU(ctx)
|
||||
case feats.Has(backend.FeatureCompute):
|
||||
return newCompute(ctx)
|
||||
default:
|
||||
return nil, errors.New("gpu: no support for float render targets nor compute")
|
||||
}
|
||||
|
||||
+150
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
#version 310 es
|
||||
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(binding = 0) uniform sampler2D tex;
|
||||
|
||||
vec3 sRGBtoRGB(vec3 rgb) {
|
||||
bvec3 cutoff = greaterThanEqual(rgb, vec3(0.04045));
|
||||
vec3 below = rgb/vec3(12.92);
|
||||
vec3 above = pow((rgb + vec3(0.055))/vec3(1.055), vec3(2.4));
|
||||
return mix(below, above, cutoff);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
|
||||
vec3 rgb = sRGBtoRGB(texel.rgb);
|
||||
fragColor = vec4(rgb, texel.a);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#version 310 es
|
||||
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
precision highp float;
|
||||
|
||||
void main() {
|
||||
switch (gl_VertexIndex) {
|
||||
case 0:
|
||||
gl_Position = vec4(-1.0, +1.0, 0.0, 1.0);
|
||||
break;
|
||||
case 1:
|
||||
gl_Position = vec4(+1.0, +1.0, 0.0, 1.0);
|
||||
break;
|
||||
case 2:
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
|
||||
break;
|
||||
case 3:
|
||||
gl_Position = vec4(+1.0, -1.0, 0.0, 1.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,6 @@ __attribute__((constructor)) static void gio_loadGLFunctions() {
|
||||
_glGetUniformBlockIndex = glGetUniformBlockIndex;
|
||||
_glUniformBlockBinding = glUniformBlockBinding;
|
||||
_glGetStringi = glGetStringi;
|
||||
_glTexStorage2D = glTexStorage2D;
|
||||
#else
|
||||
// Load libGLESv3 if available.
|
||||
dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL);
|
||||
|
||||
Reference in New Issue
Block a user