// 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/gpu/internal/driver" "gioui.org/internal/byteslice" "gioui.org/internal/f32" "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 [2][3]*pipeline texUniforms *coverTexUniforms colUniforms *coverColUniforms linearGradientUniforms *coverLinearGradientUniforms } type coverTexUniforms struct { coverUniforms _ [12]byte // Padding to multiple of 16. } type coverColUniforms struct { coverUniforms _ [128 - unsafe.Sizeof(coverUniforms{}) - unsafe.Sizeof(colorUniforms{})]byte // Padding to 128 bytes. colorUniforms } type coverLinearGradientUniforms struct { coverUniforms _ [128 - unsafe.Sizeof(coverUniforms{}) - unsafe.Sizeof(gradientUniforms{})]byte // Padding to 128. gradientUniforms } type coverUniforms struct { transform [4]float32 uvCoverTransform [4]float32 uvTransformR1 [4]float32 uvTransformR2 [4]float32 fbo 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 { 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 []FBO } type FBO struct { size image.Point 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 } // encode needs to stay in-sync with the code in clip.go encodeQuadTo. func (v vertex) encode(d []byte, maxy uint32) { d = d[0:32] bo := binary.LittleEndian bo.PutUint32(d[0:4], math.Float32bits(v.Corner)) bo.PutUint32(d[4:8], maxy) bo.PutUint32(d[8:12], math.Float32bits(v.FromX)) bo.PutUint32(d[12:16], math.Float32bits(v.FromY)) bo.PutUint32(d[16:20], math.Float32bits(v.CtrlX)) bo.PutUint32(d[20:24], math.Float32bits(v.CtrlY)) bo.PutUint32(d[24:28], math.Float32bits(v.ToX)) bo.PutUint32(d[28:32], 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, c.linearGradientUniforms, c.texUniforms}, ) 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 := driver.VertexLayout{ Inputs: []driver.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))}, }, Stride: vertStride, } iprogLayout := driver.VertexLayout{ Inputs: []driver.InputDesc{ {Type: shader.DataTypeFloat, Size: 2, Offset: 0}, {Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2}, }, Stride: 4 * 4, } 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) 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, Topology: driver.TopologyTriangles, }) st.pipeline.pipeline = &pipeline{pipe, vertUniforms} 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, Topology: driver.TopologyTriangleStrip, }) st.ipipeline.pipeline = &pipeline{ipipe, vertUniforms} if err != nil { panic(err) } return st } func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) { // Add fbos. for i := len(s.fbos); i < len(sizes); i++ { s.fbos = append(s.fbos, FBO{}) } // 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.tex != nil { f.tex.Release() } // Add 5% extra space in each dimension to minimize resizing. sz = sz.Mul(105).Div(100) max := ctx.Caps().MaxTextureSize if sz.Y > max { sz.Y = max } if sz.X > max { sz.X = max } tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, driver.BufferBindingTexture|driver.BufferBindingFramebuffer) if err != nil { panic(err) } f.size = sz f.tex = tex } } // Delete extra fbos. s.delete(ctx, len(sizes)) } func (s *fboSet) delete(ctx driver.Device, idx int) { for i := idx; i < len(s.fbos); i++ { f := s.fbos[i] f.tex.Release() } s.fbos = s.fbos[:idx] } func (s *stenciler) release() { s.fbos.delete(s.ctx, 0) s.intersections.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 { for _, p := range p { 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, driver.TextureFormatFloat, sizes) } func (s *stenciler) cover(idx int) FBO { return s.fbos.fbos[idx] } func (s *stenciler) begin(sizes []image.Point) { s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes) } 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.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y} s.pipeline.uniforms.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, off) s.ctx.DrawElements(0, batch*6) start += batch } } func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff) } func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { var uniforms *coverUniforms switch mat { case materialColor: c.colUniforms.color = col uniforms = &c.colUniforms.coverUniforms case materialLinearGradient: c.linearGradientUniforms.color1 = col1 c.linearGradientUniforms.color2 = col2 t1, t2, t3, t4, t5, t6 := uvTrans.Elems() c.linearGradientUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} c.linearGradientUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &c.linearGradientUniforms.coverUniforms case materialTexture: t1, t2, t3, t4, t5, t6 := uvTrans.Elems() c.texUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} uniforms = &c.texUniforms.coverUniforms } uniforms.fbo = 0 if isFBO { uniforms.fbo = 1 } uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} fboIdx := 0 if isFBO { fboIdx = 1 } c.pipelines[fboIdx][mat].UploadUniforms(c.ctx) c.ctx.DrawArrays(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") } }