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:
Elias Naur
2020-12-30 18:09:15 +01:00
parent 0218546161
commit d23514fd58
6 changed files with 1006 additions and 2 deletions
+807
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+22
View File
@@ -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);
}
+22
View File
@@ -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;
}
}
-1
View File
@@ -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);