Files
gio/gpu/path.go
T
Elias Naur afaa31eca8 gpu: introduce pipeline abstraction
Modern API such as Metal and Vulkan want clients to compile expensive
state changes into pipeline objects. Change our GPU driver abstraction
to match, thereby paving the way for future drivers.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2021-08-21 08:31:46 +02:00

429 lines
11 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package gpu
// GPU accelerated path drawing using the algorithms from
// Pathfinder (https://github.com/servo/pathfinder).
import (
"encoding/binary"
"image"
"math"
"unsafe"
"gioui.org/f32"
"gioui.org/gpu/internal/driver"
"gioui.org/internal/byteslice"
"gioui.org/internal/f32color"
"gioui.org/shader"
"gioui.org/shader/gio"
)
type pather struct {
ctx driver.Device
viewport image.Point
stenciler *stenciler
coverer *coverer
}
type coverer struct {
ctx driver.Device
pipelines [3]*pipeline
texUniforms *coverTexUniforms
colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms
}
type coverTexUniforms struct {
vert struct {
coverUniforms
_ [12]byte // Padding to multiple of 16.
}
}
type coverColUniforms struct {
vert struct {
coverUniforms
_ [12]byte // Padding to multiple of 16.
}
frag struct {
colorUniforms
}
}
type coverLinearGradientUniforms struct {
vert struct {
coverUniforms
_ [12]byte // Padding to multiple of 16.
}
frag struct {
gradientUniforms
}
}
type coverUniforms struct {
transform [4]float32
uvCoverTransform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
z float32
}
type stenciler struct {
ctx driver.Device
pipeline struct {
pipeline *pipeline
uniforms *stencilUniforms
}
ipipeline struct {
pipeline *pipeline
uniforms *intersectUniforms
}
fbos fboSet
intersections fboSet
indexBuf driver.Buffer
}
type stencilUniforms struct {
vert struct {
transform [4]float32
pathOffset [2]float32
_ [8]byte // Padding to multiple of 16.
}
}
type intersectUniforms struct {
vert struct {
uvTransform [4]float32
subUVTransform [4]float32
}
}
type fboSet struct {
fbos []stencilFBO
}
type stencilFBO struct {
size image.Point
fbo driver.Framebuffer
tex driver.Texture
}
type pathData struct {
ncurves int
data driver.Buffer
}
// vertex data suitable for passing to vertex programs.
type vertex struct {
// Corner encodes the corner: +0.5 for south, +.25 for east.
Corner float32
MaxY float32
FromX, FromY float32
CtrlX, CtrlY float32
ToX, ToY float32
}
func (v vertex) encode(d []byte, maxy uint32) {
bo := binary.LittleEndian
bo.PutUint32(d[0:], math.Float32bits(v.Corner))
bo.PutUint32(d[4:], maxy)
bo.PutUint32(d[8:], math.Float32bits(v.FromX))
bo.PutUint32(d[12:], math.Float32bits(v.FromY))
bo.PutUint32(d[16:], math.Float32bits(v.CtrlX))
bo.PutUint32(d[20:], math.Float32bits(v.CtrlY))
bo.PutUint32(d[24:], math.Float32bits(v.ToX))
bo.PutUint32(d[28:], math.Float32bits(v.ToY))
}
const (
// Number of path quads per draw batch.
pathBatchSize = 10000
// Size of a vertex as sent to gpu
vertStride = 8 * 4
)
func newPather(ctx driver.Device) *pather {
return &pather{
ctx: ctx,
stenciler: newStenciler(ctx),
coverer: newCoverer(ctx),
}
}
func newCoverer(ctx driver.Device) *coverer {
c := &coverer{
ctx: ctx,
}
c.colUniforms = new(coverColUniforms)
c.texUniforms = new(coverTexUniforms)
c.linearGradientUniforms = new(coverLinearGradientUniforms)
pipelines, err := createColorPrograms(ctx, gio.Shader_cover_vert, gio.Shader_cover_frag,
[3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert, &c.texUniforms.vert},
[3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag, nil},
)
if err != nil {
panic(err)
}
c.pipelines = pipelines
return c
}
func newStenciler(ctx driver.Device) *stenciler {
// Allocate a suitably large index buffer for drawing paths.
indices := make([]uint16, pathBatchSize*6)
for i := 0; i < pathBatchSize; i++ {
i := uint16(i)
indices[i*6+0] = i*4 + 0
indices[i*6+1] = i*4 + 1
indices[i*6+2] = i*4 + 2
indices[i*6+3] = i*4 + 2
indices[i*6+4] = i*4 + 1
indices[i*6+5] = i*4 + 3
}
indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices, byteslice.Slice(indices))
if err != nil {
panic(err)
}
progLayout := []shader.InputDesc{
{Type: shader.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))},
{Type: shader.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))},
{Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))},
{Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))},
{Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))},
}
iprogLayout := []shader.InputDesc{
{Type: shader.DataTypeFloat, Size: 2, Offset: 0},
{Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2},
}
st := &stenciler{
ctx: ctx,
indexBuf: indexBuf,
}
vsh, fsh, err := newShaders(ctx, gio.Shader_stencil_vert, gio.Shader_stencil_frag)
if err != nil {
panic(err)
}
defer vsh.Release()
defer fsh.Release()
st.pipeline.uniforms = new(stencilUniforms)
vertUniforms := newUniformBuffer(ctx, &st.pipeline.uniforms.vert)
pipe, err := st.ctx.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
VertexLayout: progLayout,
BlendDesc: driver.BlendDesc{
Enable: true,
SrcFactor: driver.BlendFactorOne,
DstFactor: driver.BlendFactorOne,
},
PixelFormat: driver.TextureFormatFloat,
})
st.pipeline.pipeline = &pipeline{pipe, vertUniforms, nil}
if err != nil {
panic(err)
}
vsh, fsh, err = newShaders(ctx, gio.Shader_intersect_vert, gio.Shader_intersect_frag)
if err != nil {
panic(err)
}
defer vsh.Release()
defer fsh.Release()
st.ipipeline.uniforms = new(intersectUniforms)
vertUniforms = newUniformBuffer(ctx, &st.ipipeline.uniforms.vert)
ipipe, err := st.ctx.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
VertexLayout: iprogLayout,
BlendDesc: driver.BlendDesc{
Enable: true,
SrcFactor: driver.BlendFactorDstColor,
DstFactor: driver.BlendFactorZero,
},
PixelFormat: driver.TextureFormatFloat,
})
st.ipipeline.pipeline = &pipeline{ipipe, vertUniforms, nil}
return st
}
func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
// Add fbos.
for i := len(s.fbos); i < len(sizes); i++ {
s.fbos = append(s.fbos, stencilFBO{})
}
// Resize fbos.
for i, sz := range sizes {
f := &s.fbos[i]
// Resizing or recreating FBOs can introduce rendering stalls.
// Avoid if the space waste is not too high.
resize := sz.X > f.size.X || sz.Y > f.size.Y
waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y)
resize = resize || waste > 1.2
if resize {
if f.fbo != nil {
f.fbo.Release()
f.tex.Release()
}
tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
if err != nil {
panic(err)
}
fbo, err := ctx.NewFramebuffer(tex)
if err != nil {
panic(err)
}
f.size = sz
f.tex = tex
f.fbo = fbo
}
}
// Delete extra fbos.
s.delete(ctx, len(sizes))
}
func (s *fboSet) invalidate(ctx driver.Device) {
for _, f := range s.fbos {
f.fbo.Invalidate()
}
}
func (s *fboSet) delete(ctx driver.Device, idx int) {
for i := idx; i < len(s.fbos); i++ {
f := s.fbos[i]
f.fbo.Release()
f.tex.Release()
}
s.fbos = s.fbos[:idx]
}
func (s *stenciler) release() {
s.fbos.delete(s.ctx, 0)
s.pipeline.pipeline.Release()
s.ipipeline.pipeline.Release()
s.indexBuf.Release()
}
func (p *pather) release() {
p.stenciler.release()
p.coverer.release()
}
func (c *coverer) release() {
for _, p := range c.pipelines {
p.Release()
}
}
func buildPath(ctx driver.Device, p []byte) pathData {
buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p)
if err != nil {
panic(err)
}
return pathData{
ncurves: len(p) / vertStride,
data: buf,
}
}
func (p pathData) release() {
p.data.Release()
}
func (p *pather) begin(sizes []image.Point) {
p.stenciler.begin(sizes)
}
func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
p.stenciler.stencilPath(bounds, offset, uv, data)
}
func (s *stenciler) beginIntersect(sizes []image.Point) {
// 8 bit coverage is enough, but OpenGL ES only supports single channel
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
// no floating point support is available.
s.intersections.resize(s.ctx, sizes)
s.ctx.BindPipeline(s.ipipeline.pipeline.pipeline)
}
func (s *stenciler) invalidateFBO() {
s.intersections.invalidate(s.ctx)
s.fbos.invalidate(s.ctx)
}
func (s *stenciler) cover(idx int) stencilFBO {
return s.fbos.fbos[idx]
}
func (s *stenciler) begin(sizes []image.Point) {
s.fbos.resize(s.ctx, sizes)
s.ctx.BindPipeline(s.pipeline.pipeline.pipeline)
s.ctx.BindIndexBuffer(s.indexBuf)
}
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
// Transform UI coordinates to OpenGL coordinates.
texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
s.pipeline.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y}
s.pipeline.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
s.pipeline.pipeline.UploadUniforms(s.ctx)
// Draw in batches that fit in uint16 indices.
start := 0
nquads := data.ncurves / 4
for start < nquads {
batch := nquads - start
if max := pathBatchSize; batch > max {
batch = max
}
off := vertStride * start * 4
s.ctx.BindVertexBuffer(data.data, vertStride, off)
s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6)
start += batch
}
}
func (p *pather) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
}
func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p := c.pipelines[mat]
c.ctx.BindPipeline(p.pipeline)
var uniforms *coverUniforms
switch mat {
case materialColor:
c.colUniforms.frag.color = col
uniforms = &c.colUniforms.vert.coverUniforms
case materialLinearGradient:
c.linearGradientUniforms.frag.color1 = col1
c.linearGradientUniforms.frag.color2 = col2
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &c.linearGradientUniforms.vert.coverUniforms
case materialTexture:
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &c.texUniforms.vert.coverUniforms
}
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
p.UploadUniforms(c.ctx)
c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
}
func init() {
// Check that struct vertex has the expected size and
// that it contains no padding.
if unsafe.Sizeof(*(*vertex)(nil)) != vertStride {
panic("unexpected struct size")
}
}