mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
8c8d1dc16f
Complex strokes are not yet supported in either of the current renderers, so they are converted to filled outlines in package gpu. We're about to move that complexity up to the op/clip package, so we're going to need the converter available from outside package gpu. This change extracts the conversion code and related types to the separate, internal package stroke. No functional changes; a follow-up moves the stroke conversion. Signed-off-by: Elias Naur <mail@eliasnaur.com>
1606 lines
44 KiB
Go
1606 lines
44 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
/*
|
|
Package gpu implements the rendering of Gio drawing operations. It
|
|
is used by package app and package app/headless and is otherwise not
|
|
useful except for integrating with external window implementations.
|
|
*/
|
|
package gpu
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/gpu/internal/driver"
|
|
"gioui.org/internal/byteslice"
|
|
"gioui.org/internal/f32color"
|
|
"gioui.org/internal/opconst"
|
|
"gioui.org/internal/ops"
|
|
"gioui.org/internal/scene"
|
|
"gioui.org/internal/stroke"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
|
|
// Register backends.
|
|
_ "gioui.org/gpu/internal/d3d11"
|
|
_ "gioui.org/gpu/internal/opengl"
|
|
)
|
|
|
|
type GPU interface {
|
|
// Release non-Go resources. The GPU is no longer valid after Release.
|
|
Release()
|
|
// Clear sets the clear color for the next Frame.
|
|
Clear(color color.NRGBA)
|
|
// Collect the graphics operations from frame, given the viewport.
|
|
Collect(viewport image.Point, frame *op.Ops)
|
|
// Frame clears the color buffer and draws the collected operations.
|
|
Frame() error
|
|
// Profile returns the last available profiling information. Profiling
|
|
// information is requested when Collect sees a ProfileOp, and the result
|
|
// is available through Profile at some later time.
|
|
Profile() string
|
|
}
|
|
|
|
type gpu struct {
|
|
cache *resourceCache
|
|
|
|
profile string
|
|
timers *timers
|
|
frameStart time.Time
|
|
zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer
|
|
drawOps drawOps
|
|
ctx driver.Device
|
|
renderer *renderer
|
|
}
|
|
|
|
type renderer struct {
|
|
ctx driver.Device
|
|
blitter *blitter
|
|
pather *pather
|
|
packer packer
|
|
intersections packer
|
|
}
|
|
|
|
type drawOps struct {
|
|
profile bool
|
|
reader ops.Reader
|
|
states []drawState
|
|
cache *resourceCache
|
|
vertCache []byte
|
|
viewport image.Point
|
|
clear bool
|
|
clearColor f32color.RGBA
|
|
// allImageOps is the combined list of imageOps and
|
|
// zimageOps, in drawing order.
|
|
allImageOps []imageOp
|
|
imageOps []imageOp
|
|
// zimageOps are the rectangle clipped opaque images
|
|
// that can use fast front-to-back rendering with z-test
|
|
// and no blending.
|
|
zimageOps []imageOp
|
|
pathOps []*pathOp
|
|
pathOpCache []pathOp
|
|
qs quadSplitter
|
|
pathCache *opCache
|
|
// hack for the compute renderer to access
|
|
// converted path data.
|
|
compute bool
|
|
}
|
|
|
|
type drawState struct {
|
|
clip f32.Rectangle
|
|
t f32.Affine2D
|
|
cpath *pathOp
|
|
rect bool
|
|
|
|
matType materialType
|
|
// Current paint.ImageOp
|
|
image imageOpData
|
|
// Current paint.ColorOp, if any.
|
|
color color.NRGBA
|
|
|
|
// Current paint.LinearGradientOp.
|
|
stop1 f32.Point
|
|
stop2 f32.Point
|
|
color1 color.NRGBA
|
|
color2 color.NRGBA
|
|
}
|
|
|
|
type pathOp struct {
|
|
off f32.Point
|
|
// clip is the union of all
|
|
// later clip rectangles.
|
|
clip image.Rectangle
|
|
bounds f32.Rectangle
|
|
pathKey ops.Key
|
|
path bool
|
|
pathVerts []byte
|
|
parent *pathOp
|
|
place placement
|
|
|
|
// For compute
|
|
trans f32.Affine2D
|
|
stroke clip.StrokeStyle
|
|
dashes stroke.DashOp
|
|
}
|
|
|
|
type imageOp struct {
|
|
z float32
|
|
path *pathOp
|
|
clip image.Rectangle
|
|
material material
|
|
clipType clipType
|
|
place placement
|
|
}
|
|
|
|
func decodeDashOp(data []byte) stroke.DashOp {
|
|
_ = data[5]
|
|
if opconst.OpType(data[0]) != opconst.TypeDash {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return stroke.DashOp{
|
|
Phase: math.Float32frombits(bo.Uint32(data[1:])),
|
|
Dashes: make([]float32, data[5]),
|
|
}
|
|
}
|
|
|
|
func decodeStrokeOp(data []byte) clip.StrokeStyle {
|
|
_ = data[10]
|
|
if opconst.OpType(data[0]) != opconst.TypeStroke {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return clip.StrokeStyle{
|
|
Width: math.Float32frombits(bo.Uint32(data[1:])),
|
|
Miter: math.Float32frombits(bo.Uint32(data[5:])),
|
|
Cap: clip.StrokeCap(data[9]),
|
|
Join: clip.StrokeJoin(data[10]),
|
|
}
|
|
}
|
|
|
|
type quadsOp struct {
|
|
key ops.Key
|
|
aux []byte
|
|
}
|
|
|
|
type material struct {
|
|
material materialType
|
|
opaque bool
|
|
// For materialTypeColor.
|
|
color f32color.RGBA
|
|
// For materialTypeLinearGradient.
|
|
color1 f32color.RGBA
|
|
color2 f32color.RGBA
|
|
// For materialTypeTexture.
|
|
data imageOpData
|
|
uvTrans f32.Affine2D
|
|
|
|
// For the compute backend.
|
|
trans f32.Affine2D
|
|
}
|
|
|
|
// clipOp is the shadow of clip.Op.
|
|
type clipOp struct {
|
|
// TODO: Use image.Rectangle?
|
|
bounds f32.Rectangle
|
|
outline bool
|
|
}
|
|
|
|
// imageOpData is the shadow of paint.ImageOp.
|
|
type imageOpData struct {
|
|
src *image.RGBA
|
|
handle interface{}
|
|
}
|
|
|
|
type linearGradientOpData struct {
|
|
stop1 f32.Point
|
|
color1 color.NRGBA
|
|
stop2 f32.Point
|
|
color2 color.NRGBA
|
|
}
|
|
|
|
func (op *clipOp) decode(data []byte) {
|
|
if opconst.OpType(data[0]) != opconst.TypeClip {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
r := image.Rectangle{
|
|
Min: image.Point{
|
|
X: int(int32(bo.Uint32(data[1:]))),
|
|
Y: int(int32(bo.Uint32(data[5:]))),
|
|
},
|
|
Max: image.Point{
|
|
X: int(int32(bo.Uint32(data[9:]))),
|
|
Y: int(int32(bo.Uint32(data[13:]))),
|
|
},
|
|
}
|
|
*op = clipOp{
|
|
bounds: layout.FRect(r),
|
|
outline: data[17] == 1,
|
|
}
|
|
}
|
|
|
|
func decodeImageOp(data []byte, refs []interface{}) imageOpData {
|
|
if opconst.OpType(data[0]) != opconst.TypeImage {
|
|
panic("invalid op")
|
|
}
|
|
handle := refs[1]
|
|
if handle == nil {
|
|
return imageOpData{}
|
|
}
|
|
return imageOpData{
|
|
src: refs[0].(*image.RGBA),
|
|
handle: handle,
|
|
}
|
|
}
|
|
|
|
func decodeColorOp(data []byte) color.NRGBA {
|
|
if opconst.OpType(data[0]) != opconst.TypeColor {
|
|
panic("invalid op")
|
|
}
|
|
return color.NRGBA{
|
|
R: data[1],
|
|
G: data[2],
|
|
B: data[3],
|
|
A: data[4],
|
|
}
|
|
}
|
|
|
|
func decodeLinearGradientOp(data []byte) linearGradientOpData {
|
|
if opconst.OpType(data[0]) != opconst.TypeLinearGradient {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return linearGradientOpData{
|
|
stop1: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[1:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[5:])),
|
|
},
|
|
stop2: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[9:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[13:])),
|
|
},
|
|
color1: color.NRGBA{
|
|
R: data[17+0],
|
|
G: data[17+1],
|
|
B: data[17+2],
|
|
A: data[17+3],
|
|
},
|
|
color2: color.NRGBA{
|
|
R: data[21+0],
|
|
G: data[21+1],
|
|
B: data[21+2],
|
|
A: data[21+3],
|
|
},
|
|
}
|
|
}
|
|
|
|
type clipType uint8
|
|
|
|
type resource interface {
|
|
release()
|
|
}
|
|
|
|
type texture struct {
|
|
src *image.RGBA
|
|
tex driver.Texture
|
|
}
|
|
|
|
type blitter struct {
|
|
ctx driver.Device
|
|
viewport image.Point
|
|
prog [3]*program
|
|
layout driver.InputLayout
|
|
colUniforms *blitColUniforms
|
|
texUniforms *blitTexUniforms
|
|
linearGradientUniforms *blitLinearGradientUniforms
|
|
quadVerts driver.Buffer
|
|
}
|
|
|
|
type blitColUniforms struct {
|
|
vert struct {
|
|
blitUniforms
|
|
_ [12]byte // Padding to a multiple of 16.
|
|
}
|
|
frag struct {
|
|
colorUniforms
|
|
}
|
|
}
|
|
|
|
type blitTexUniforms struct {
|
|
vert struct {
|
|
blitUniforms
|
|
_ [12]byte // Padding to a multiple of 16.
|
|
}
|
|
}
|
|
|
|
type blitLinearGradientUniforms struct {
|
|
vert struct {
|
|
blitUniforms
|
|
_ [12]byte // Padding to a multiple of 16.
|
|
}
|
|
frag struct {
|
|
gradientUniforms
|
|
}
|
|
}
|
|
|
|
type uniformBuffer struct {
|
|
buf driver.Buffer
|
|
ptr []byte
|
|
}
|
|
|
|
type program struct {
|
|
prog driver.Program
|
|
vertUniforms *uniformBuffer
|
|
fragUniforms *uniformBuffer
|
|
}
|
|
|
|
type blitUniforms struct {
|
|
transform [4]float32
|
|
uvTransformR1 [4]float32
|
|
uvTransformR2 [4]float32
|
|
z float32
|
|
}
|
|
|
|
type colorUniforms struct {
|
|
color f32color.RGBA
|
|
}
|
|
|
|
type gradientUniforms struct {
|
|
color1 f32color.RGBA
|
|
color2 f32color.RGBA
|
|
}
|
|
|
|
type materialType uint8
|
|
|
|
const (
|
|
clipTypeNone clipType = iota
|
|
clipTypePath
|
|
clipTypeIntersection
|
|
)
|
|
|
|
const (
|
|
materialColor materialType = iota
|
|
materialLinearGradient
|
|
materialTexture
|
|
)
|
|
|
|
func New(api API) (GPU, error) {
|
|
d, err := driver.NewDevice(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
|
|
feats := d.Caps().Features
|
|
switch {
|
|
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets):
|
|
return newGPU(d)
|
|
case feats.Has(driver.FeatureCompute):
|
|
return newCompute(d)
|
|
default:
|
|
return nil, errors.New("gpu: no support for float render targets nor compute")
|
|
}
|
|
}
|
|
|
|
func newGPU(ctx driver.Device) (*gpu, error) {
|
|
g := &gpu{
|
|
cache: newResourceCache(),
|
|
}
|
|
g.drawOps.pathCache = newOpCache()
|
|
if err := g.init(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (g *gpu) init(ctx driver.Device) error {
|
|
g.ctx = ctx
|
|
g.renderer = newRenderer(ctx)
|
|
return nil
|
|
}
|
|
|
|
func (g *gpu) Clear(col color.NRGBA) {
|
|
g.drawOps.clear = true
|
|
g.drawOps.clearColor = f32color.LinearFromSRGB(col)
|
|
}
|
|
|
|
func (g *gpu) Release() {
|
|
g.renderer.release()
|
|
g.drawOps.pathCache.release()
|
|
g.cache.release()
|
|
if g.timers != nil {
|
|
g.timers.release()
|
|
}
|
|
g.ctx.Release()
|
|
}
|
|
|
|
func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) {
|
|
g.renderer.blitter.viewport = viewport
|
|
g.renderer.pather.viewport = viewport
|
|
g.drawOps.reset(g.cache, viewport)
|
|
g.drawOps.collect(g.ctx, g.cache, frameOps, viewport)
|
|
g.frameStart = time.Now()
|
|
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
|
g.timers = newTimers(g.ctx)
|
|
g.zopsTimer = g.timers.newTimer()
|
|
g.stencilTimer = g.timers.newTimer()
|
|
g.coverTimer = g.timers.newTimer()
|
|
g.cleanupTimer = g.timers.newTimer()
|
|
}
|
|
}
|
|
|
|
func (g *gpu) Frame() error {
|
|
defFBO := g.ctx.BeginFrame()
|
|
defer g.ctx.EndFrame()
|
|
viewport := g.renderer.blitter.viewport
|
|
for _, img := range g.drawOps.imageOps {
|
|
expandPathOp(img.path, img.clip)
|
|
}
|
|
if g.drawOps.profile {
|
|
g.zopsTimer.begin()
|
|
}
|
|
g.ctx.BindFramebuffer(defFBO)
|
|
g.ctx.DepthFunc(driver.DepthFuncGreater)
|
|
// Note that Clear must be before ClearDepth if nothing else is rendered
|
|
// (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer.
|
|
if g.drawOps.clear {
|
|
g.drawOps.clear = false
|
|
g.ctx.Clear(g.drawOps.clearColor.Float32())
|
|
}
|
|
g.ctx.ClearDepth(0.0)
|
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
|
g.renderer.drawZOps(g.cache, g.drawOps.zimageOps)
|
|
g.zopsTimer.end()
|
|
g.stencilTimer.begin()
|
|
g.ctx.SetBlend(true)
|
|
g.renderer.packStencils(&g.drawOps.pathOps)
|
|
g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
|
|
g.renderer.packIntersections(g.drawOps.imageOps)
|
|
g.renderer.intersect(g.drawOps.imageOps)
|
|
g.stencilTimer.end()
|
|
g.coverTimer.begin()
|
|
g.ctx.BindFramebuffer(defFBO)
|
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
|
g.renderer.drawOps(g.cache, g.drawOps.imageOps)
|
|
g.ctx.SetBlend(false)
|
|
g.renderer.pather.stenciler.invalidateFBO()
|
|
g.coverTimer.end()
|
|
g.ctx.BindFramebuffer(defFBO)
|
|
g.cleanupTimer.begin()
|
|
g.cache.frame()
|
|
g.drawOps.pathCache.frame()
|
|
g.cleanupTimer.end()
|
|
if g.drawOps.profile && g.timers.ready() {
|
|
zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
|
|
ft := zt + st + covt + cleant
|
|
q := 100 * time.Microsecond
|
|
zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q)
|
|
frameDur := time.Since(g.frameStart).Round(q)
|
|
ft = ft.Round(q)
|
|
g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", frameDur, ft, zt, st, covt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *gpu) Profile() string {
|
|
return g.profile
|
|
}
|
|
|
|
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
|
|
var tex *texture
|
|
t, exists := cache.get(data.handle)
|
|
if !exists {
|
|
t = &texture{
|
|
src: data.src,
|
|
}
|
|
cache.put(data.handle, t)
|
|
}
|
|
tex = t.(*texture)
|
|
if tex.tex != nil {
|
|
return tex.tex
|
|
}
|
|
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear, driver.FilterLinear, driver.BufferBindingTexture)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
driver.UploadImage(handle, image.Pt(0, 0), data.src)
|
|
tex.tex = handle
|
|
return tex.tex
|
|
}
|
|
|
|
func (t *texture) release() {
|
|
if t.tex != nil {
|
|
t.tex.Release()
|
|
}
|
|
}
|
|
|
|
func newRenderer(ctx driver.Device) *renderer {
|
|
r := &renderer{
|
|
ctx: ctx,
|
|
blitter: newBlitter(ctx),
|
|
pather: newPather(ctx),
|
|
}
|
|
|
|
maxDim := ctx.Caps().MaxTextureSize
|
|
// Large atlas textures cause artifacts due to precision loss in
|
|
// shaders.
|
|
if cap := 8192; maxDim > cap {
|
|
maxDim = cap
|
|
}
|
|
|
|
r.packer.maxDim = maxDim
|
|
r.intersections.maxDim = maxDim
|
|
return r
|
|
}
|
|
|
|
func (r *renderer) release() {
|
|
r.pather.release()
|
|
r.blitter.release()
|
|
}
|
|
|
|
func newBlitter(ctx driver.Device) *blitter {
|
|
quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices,
|
|
byteslice.Slice([]float32{
|
|
-1, +1, 0, 0,
|
|
+1, +1, 1, 0,
|
|
-1, -1, 0, 1,
|
|
+1, -1, 1, 1,
|
|
}),
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b := &blitter{
|
|
ctx: ctx,
|
|
quadVerts: quadVerts,
|
|
}
|
|
b.colUniforms = new(blitColUniforms)
|
|
b.texUniforms = new(blitTexUniforms)
|
|
b.linearGradientUniforms = new(blitLinearGradientUniforms)
|
|
prog, layout, err := createColorPrograms(ctx, shader_blit_vert, shader_blit_frag,
|
|
[3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert, &b.texUniforms.vert},
|
|
[3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag, nil},
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b.prog = prog
|
|
b.layout = layout
|
|
return b
|
|
}
|
|
|
|
func (b *blitter) release() {
|
|
b.quadVerts.Release()
|
|
for _, p := range b.prog {
|
|
p.Release()
|
|
}
|
|
b.layout.Release()
|
|
}
|
|
|
|
func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources, fsSrc [3]driver.ShaderSources, vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout, error) {
|
|
var progs [3]*program
|
|
{
|
|
prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture])
|
|
if err != nil {
|
|
return progs, nil, err
|
|
}
|
|
var vertBuffer, fragBuffer *uniformBuffer
|
|
if u := vertUniforms[materialTexture]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
prog.SetVertexUniforms(vertBuffer.buf)
|
|
}
|
|
if u := fragUniforms[materialTexture]; u != nil {
|
|
fragBuffer = newUniformBuffer(b, u)
|
|
prog.SetFragmentUniforms(fragBuffer.buf)
|
|
}
|
|
progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer)
|
|
}
|
|
{
|
|
var vertBuffer, fragBuffer *uniformBuffer
|
|
prog, err := b.NewProgram(vsSrc, fsSrc[materialColor])
|
|
if err != nil {
|
|
progs[materialTexture].Release()
|
|
return progs, nil, err
|
|
}
|
|
if u := vertUniforms[materialColor]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
prog.SetVertexUniforms(vertBuffer.buf)
|
|
}
|
|
if u := fragUniforms[materialColor]; u != nil {
|
|
fragBuffer = newUniformBuffer(b, u)
|
|
prog.SetFragmentUniforms(fragBuffer.buf)
|
|
}
|
|
progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer)
|
|
}
|
|
{
|
|
var vertBuffer, fragBuffer *uniformBuffer
|
|
prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient])
|
|
if err != nil {
|
|
progs[materialTexture].Release()
|
|
progs[materialColor].Release()
|
|
return progs, nil, err
|
|
}
|
|
if u := vertUniforms[materialLinearGradient]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
prog.SetVertexUniforms(vertBuffer.buf)
|
|
}
|
|
if u := fragUniforms[materialLinearGradient]; u != nil {
|
|
fragBuffer = newUniformBuffer(b, u)
|
|
prog.SetFragmentUniforms(fragBuffer.buf)
|
|
}
|
|
progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer)
|
|
}
|
|
layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{
|
|
{Type: driver.DataTypeFloat, Size: 2, Offset: 0},
|
|
{Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
|
|
})
|
|
if err != nil {
|
|
progs[materialTexture].Release()
|
|
progs[materialColor].Release()
|
|
progs[materialLinearGradient].Release()
|
|
return progs, nil, err
|
|
}
|
|
return progs, layout, nil
|
|
}
|
|
|
|
func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
|
|
if len(r.packer.sizes) == 0 {
|
|
return
|
|
}
|
|
fbo := -1
|
|
r.pather.begin(r.packer.sizes)
|
|
for _, p := range ops {
|
|
if fbo != p.place.Idx {
|
|
fbo = p.place.Idx
|
|
f := r.pather.stenciler.cover(fbo)
|
|
r.ctx.BindFramebuffer(f.fbo)
|
|
r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
|
|
}
|
|
v, _ := pathCache.get(p.pathKey)
|
|
r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
|
|
}
|
|
}
|
|
|
|
func (r *renderer) intersect(ops []imageOp) {
|
|
if len(r.intersections.sizes) == 0 {
|
|
return
|
|
}
|
|
fbo := -1
|
|
r.pather.stenciler.beginIntersect(r.intersections.sizes)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
|
|
r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout)
|
|
for _, img := range ops {
|
|
if img.clipType != clipTypeIntersection {
|
|
continue
|
|
}
|
|
if fbo != img.place.Idx {
|
|
fbo = img.place.Idx
|
|
f := r.pather.stenciler.intersections.fbos[fbo]
|
|
r.ctx.BindFramebuffer(f.fbo)
|
|
r.ctx.Clear(1.0, 0.0, 0.0, 0.0)
|
|
}
|
|
r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
|
|
r.intersectPath(img.path, img.clip)
|
|
}
|
|
}
|
|
|
|
func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
|
|
if p.parent != nil {
|
|
r.intersectPath(p.parent, clip)
|
|
}
|
|
if !p.path {
|
|
return
|
|
}
|
|
uv := image.Rectangle{
|
|
Min: p.place.Pos,
|
|
Max: p.place.Pos.Add(p.clip.Size()),
|
|
}
|
|
o := clip.Min.Sub(p.clip.Min)
|
|
sub := image.Rectangle{
|
|
Min: o,
|
|
Max: o.Add(clip.Size()),
|
|
}
|
|
fbo := r.pather.stenciler.cover(p.place.Idx)
|
|
r.ctx.BindTexture(0, fbo.tex)
|
|
coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
|
|
subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size())
|
|
r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
|
r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y}
|
|
r.pather.stenciler.iprog.prog.UploadUniforms()
|
|
r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
|
|
}
|
|
|
|
func (r *renderer) packIntersections(ops []imageOp) {
|
|
r.intersections.clear()
|
|
for i, img := range ops {
|
|
var npaths int
|
|
var onePath *pathOp
|
|
for p := img.path; p != nil; p = p.parent {
|
|
if p.path {
|
|
onePath = p
|
|
npaths++
|
|
}
|
|
}
|
|
switch npaths {
|
|
case 0:
|
|
case 1:
|
|
place := onePath.place
|
|
place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
|
|
ops[i].place = place
|
|
ops[i].clipType = clipTypePath
|
|
default:
|
|
sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
|
|
place, ok := r.intersections.add(sz)
|
|
if !ok {
|
|
panic("internal error: if the intersection fit, the intersection should fit as well")
|
|
}
|
|
ops[i].clipType = clipTypeIntersection
|
|
ops[i].place = place
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *renderer) packStencils(pops *[]*pathOp) {
|
|
r.packer.clear()
|
|
ops := *pops
|
|
// Allocate atlas space for cover textures.
|
|
var i int
|
|
for i < len(ops) {
|
|
p := ops[i]
|
|
if p.clip.Empty() {
|
|
ops[i] = ops[len(ops)-1]
|
|
ops = ops[:len(ops)-1]
|
|
continue
|
|
}
|
|
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
|
|
place, ok := r.packer.add(sz)
|
|
if !ok {
|
|
// The clip area is at most the entire screen. Hopefully no
|
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
|
panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", p.clip, r.packer.maxDim, r.packer.maxDim))
|
|
}
|
|
p.place = place
|
|
i++
|
|
}
|
|
*pops = ops
|
|
}
|
|
|
|
// intersects intersects clip and b where b is offset by off.
|
|
// ceilRect returns a bounding image.Rectangle for a f32.Rectangle.
|
|
func boundRectF(r f32.Rectangle) image.Rectangle {
|
|
return image.Rectangle{
|
|
Min: image.Point{
|
|
X: int(floor(r.Min.X)),
|
|
Y: int(floor(r.Min.Y)),
|
|
},
|
|
Max: image.Point{
|
|
X: int(ceil(r.Max.X)),
|
|
Y: int(ceil(r.Max.Y)),
|
|
},
|
|
}
|
|
}
|
|
|
|
func ceil(v float32) int {
|
|
return int(math.Ceil(float64(v)))
|
|
}
|
|
|
|
func floor(v float32) int {
|
|
return int(math.Floor(float64(v)))
|
|
}
|
|
|
|
func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
|
|
d.profile = false
|
|
d.cache = cache
|
|
d.viewport = viewport
|
|
d.imageOps = d.imageOps[:0]
|
|
d.allImageOps = d.allImageOps[:0]
|
|
d.zimageOps = d.zimageOps[:0]
|
|
d.pathOps = d.pathOps[:0]
|
|
d.pathOpCache = d.pathOpCache[:0]
|
|
d.vertCache = d.vertCache[:0]
|
|
}
|
|
|
|
func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops, viewport image.Point) {
|
|
clip := f32.Rectangle{
|
|
Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
|
|
}
|
|
d.reader.Reset(root)
|
|
state := drawState{
|
|
clip: clip,
|
|
rect: true,
|
|
color: color.NRGBA{A: 0xff},
|
|
}
|
|
d.collectOps(&d.reader, state)
|
|
for _, p := range d.pathOps {
|
|
if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
|
|
data := buildPath(ctx, p.pathVerts)
|
|
var computePath encoder
|
|
if d.compute {
|
|
computePath = encodePath(p)
|
|
}
|
|
d.pathCache.put(p.pathKey, opCacheValue{
|
|
data: data,
|
|
bounds: p.bounds,
|
|
computePath: computePath,
|
|
})
|
|
}
|
|
p.pathVerts = nil
|
|
}
|
|
}
|
|
|
|
func (d *drawOps) newPathOp() *pathOp {
|
|
d.pathOpCache = append(d.pathOpCache, pathOp{})
|
|
return &d.pathOpCache[len(d.pathOpCache)-1]
|
|
}
|
|
|
|
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes stroke.DashOp) {
|
|
npath := d.newPathOp()
|
|
*npath = pathOp{
|
|
parent: state.cpath,
|
|
bounds: bounds,
|
|
off: off,
|
|
trans: tr,
|
|
stroke: stroke,
|
|
dashes: dashes,
|
|
}
|
|
state.cpath = npath
|
|
if len(aux) > 0 {
|
|
state.rect = false
|
|
state.cpath.pathKey = auxKey
|
|
state.cpath.path = true
|
|
state.cpath.pathVerts = aux
|
|
d.pathOps = append(d.pathOps, state.cpath)
|
|
}
|
|
}
|
|
|
|
// split a transform into two parts, one which is pur offset and the
|
|
// other representing the scaling, shearing and rotation part
|
|
func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
|
|
sx, hx, ox, hy, sy, oy := t.Elems()
|
|
offset = f32.Point{X: ox, Y: oy}
|
|
srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0)
|
|
return
|
|
}
|
|
|
|
func (d *drawOps) save(id int, state drawState) {
|
|
if extra := id - len(d.states) + 1; extra > 0 {
|
|
d.states = append(d.states, make([]drawState, extra)...)
|
|
}
|
|
d.states[id] = state
|
|
}
|
|
|
|
func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
|
|
var (
|
|
quads quadsOp
|
|
str clip.StrokeStyle
|
|
dashes stroke.DashOp
|
|
z int
|
|
)
|
|
d.save(opconst.InitialStateID, state)
|
|
loop:
|
|
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
|
switch opconst.OpType(encOp.Data[0]) {
|
|
case opconst.TypeProfile:
|
|
d.profile = true
|
|
case opconst.TypeTransform:
|
|
dop := ops.DecodeTransform(encOp.Data)
|
|
state.t = state.t.Mul(dop)
|
|
|
|
case opconst.TypeDash:
|
|
dashes = decodeDashOp(encOp.Data)
|
|
if len(dashes.Dashes) > 0 {
|
|
encOp, ok = r.Decode()
|
|
if !ok {
|
|
panic("gpu: could not decode dashes pattern")
|
|
}
|
|
data := encOp.Data[1:]
|
|
bo := binary.LittleEndian
|
|
for i := range dashes.Dashes {
|
|
dashes.Dashes[i] = math.Float32frombits(bo.Uint32(
|
|
data[i*4:],
|
|
))
|
|
}
|
|
}
|
|
|
|
case opconst.TypeStroke:
|
|
str = decodeStrokeOp(encOp.Data)
|
|
|
|
case opconst.TypePath:
|
|
encOp, ok = r.Decode()
|
|
if !ok {
|
|
break loop
|
|
}
|
|
quads.aux = encOp.Data[opconst.TypeAuxLen:]
|
|
quads.key = encOp.Key
|
|
|
|
case opconst.TypeClip:
|
|
var op clipOp
|
|
op.decode(encOp.Data)
|
|
bounds := op.bounds
|
|
trans, off := splitTransform(state.t)
|
|
if len(quads.aux) > 0 {
|
|
// There is a clipping path, build the gpu data and update the
|
|
// cache key such that it will be equal only if the transform is the
|
|
// same also. Use cached data if we have it.
|
|
quads.key = quads.key.SetTransform(trans)
|
|
if v, ok := d.pathCache.get(quads.key); ok {
|
|
// Since the GPU data exists in the cache aux will not be used.
|
|
// Why is this not used for the offset shapes?
|
|
op.bounds = v.bounds
|
|
} else {
|
|
pathData, bounds := d.buildVerts(
|
|
quads.aux, trans, op.outline, str, dashes,
|
|
)
|
|
op.bounds = bounds
|
|
if !d.compute {
|
|
quads.aux = pathData
|
|
}
|
|
// add it to the cache, without GPU data, so the transform can be
|
|
// reused.
|
|
d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
|
|
}
|
|
} else {
|
|
quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans)
|
|
quads.key = encOp.Key
|
|
quads.key.SetTransform(trans)
|
|
}
|
|
state.clip = state.clip.Intersect(op.bounds.Add(off))
|
|
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str, dashes)
|
|
quads = quadsOp{}
|
|
str = clip.StrokeStyle{}
|
|
dashes = stroke.DashOp{}
|
|
|
|
case opconst.TypeColor:
|
|
state.matType = materialColor
|
|
state.color = decodeColorOp(encOp.Data)
|
|
case opconst.TypeLinearGradient:
|
|
state.matType = materialLinearGradient
|
|
op := decodeLinearGradientOp(encOp.Data)
|
|
state.stop1 = op.stop1
|
|
state.stop2 = op.stop2
|
|
state.color1 = op.color1
|
|
state.color2 = op.color2
|
|
case opconst.TypeImage:
|
|
state.matType = materialTexture
|
|
state.image = decodeImageOp(encOp.Data, encOp.Refs)
|
|
case opconst.TypePaint:
|
|
// Transform (if needed) the painting rectangle and if so generate a clip path,
|
|
// for those cases also compute a partialTrans that maps texture coordinates between
|
|
// the new bounding rectangle and the transformed original paint rectangle.
|
|
trans, off := splitTransform(state.t)
|
|
// Fill the clip area, unless the material is a (bounded) image.
|
|
// TODO: Find a tighter bound.
|
|
inf := float32(1e6)
|
|
dst := f32.Rect(-inf, -inf, inf, inf)
|
|
if state.matType == materialTexture {
|
|
dst = layout.FRect(state.image.src.Rect)
|
|
}
|
|
clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, trans)
|
|
cl := state.clip.Intersect(bnd.Add(off))
|
|
if cl.Empty() {
|
|
continue
|
|
}
|
|
|
|
wasrect := state.rect
|
|
if clipData != nil {
|
|
// The paint operation is sheared or rotated, add a clip path representing
|
|
// this transformed rectangle.
|
|
encOp.Key.SetTransform(trans)
|
|
d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, stroke.DashOp{})
|
|
}
|
|
|
|
bounds := boundRectF(cl)
|
|
mat := state.materialFor(bnd, off, partialTrans, bounds, state.t)
|
|
|
|
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) {
|
|
// The image is a uniform opaque color and takes up the whole screen.
|
|
// Scrap images up to and including this image and set clear color.
|
|
d.allImageOps = d.allImageOps[:0]
|
|
d.zimageOps = d.zimageOps[:0]
|
|
d.imageOps = d.imageOps[:0]
|
|
z = 0
|
|
d.clearColor = mat.color.Opaque()
|
|
d.clear = true
|
|
continue
|
|
}
|
|
z++
|
|
if z != int(uint16(z)) {
|
|
// TODO(eliasnaur) gioui.org/issue/127.
|
|
panic("more than 65k paint objects not supported")
|
|
}
|
|
// Assume 16-bit depth buffer.
|
|
const zdepth = 1 << 16
|
|
// Convert z to window-space, assuming depth range [0;1].
|
|
zf := float32(z)*2/zdepth - 1.0
|
|
img := imageOp{
|
|
z: zf,
|
|
path: state.cpath,
|
|
clip: bounds,
|
|
material: mat,
|
|
}
|
|
|
|
d.allImageOps = append(d.allImageOps, img)
|
|
if state.rect && img.material.opaque {
|
|
d.zimageOps = append(d.zimageOps, img)
|
|
} else {
|
|
d.imageOps = append(d.imageOps, img)
|
|
}
|
|
if clipData != nil {
|
|
// we added a clip path that should not remain
|
|
state.cpath = state.cpath.parent
|
|
state.rect = wasrect
|
|
}
|
|
case opconst.TypeSave:
|
|
id := ops.DecodeSave(encOp.Data)
|
|
d.save(id, state)
|
|
case opconst.TypeLoad:
|
|
id, mask := ops.DecodeLoad(encOp.Data)
|
|
s := d.states[id]
|
|
if mask&opconst.TransformState != 0 {
|
|
state.t = s.t
|
|
}
|
|
if mask&^opconst.TransformState != 0 {
|
|
state = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func expandPathOp(p *pathOp, clip image.Rectangle) {
|
|
for p != nil {
|
|
pclip := p.clip
|
|
if !pclip.Empty() {
|
|
clip = clip.Union(pclip)
|
|
}
|
|
p.clip = clip
|
|
p = p.parent
|
|
}
|
|
}
|
|
|
|
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material {
|
|
var m material
|
|
switch d.matType {
|
|
case materialColor:
|
|
m.material = materialColor
|
|
m.color = f32color.LinearFromSRGB(d.color)
|
|
m.opaque = m.color.A == 1.0
|
|
case materialLinearGradient:
|
|
m.material = materialLinearGradient
|
|
|
|
m.color1 = f32color.LinearFromSRGB(d.color1)
|
|
m.color2 = f32color.LinearFromSRGB(d.color2)
|
|
m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0
|
|
|
|
m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2))
|
|
case materialTexture:
|
|
m.material = materialTexture
|
|
dr := boundRectF(rect.Add(off))
|
|
sz := d.image.src.Bounds().Size()
|
|
sr := f32.Rectangle{
|
|
Max: f32.Point{
|
|
X: float32(sz.X),
|
|
Y: float32(sz.Y),
|
|
},
|
|
}
|
|
dx := float32(dr.Dx())
|
|
sdx := sr.Dx()
|
|
sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx
|
|
sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx
|
|
dy := float32(dr.Dy())
|
|
sdy := sr.Dy()
|
|
sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
|
|
sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
|
|
uvScale, uvOffset := texSpaceTransform(sr, sz)
|
|
m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset))
|
|
m.trans = trans
|
|
m.data = d.image
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) {
|
|
r.ctx.SetDepthTest(true)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
|
|
r.ctx.BindInputLayout(r.blitter.layout)
|
|
// Render front to back.
|
|
for i := len(ops) - 1; i >= 0; i-- {
|
|
img := ops[i]
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.BindTexture(0, r.texHandle(cache, m.data))
|
|
}
|
|
drc := img.clip
|
|
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
|
|
r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans)
|
|
}
|
|
r.ctx.SetDepthTest(false)
|
|
}
|
|
|
|
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
|
r.ctx.SetDepthTest(true)
|
|
r.ctx.DepthMask(false)
|
|
r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
|
|
r.ctx.BindInputLayout(r.pather.coverer.layout)
|
|
var coverTex driver.Texture
|
|
for _, img := range ops {
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.BindTexture(0, r.texHandle(cache, m.data))
|
|
}
|
|
drc := img.clip
|
|
|
|
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
|
|
var fbo stencilFBO
|
|
switch img.clipType {
|
|
case clipTypeNone:
|
|
r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans)
|
|
continue
|
|
case clipTypePath:
|
|
fbo = r.pather.stenciler.cover(img.place.Idx)
|
|
case clipTypeIntersection:
|
|
fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
|
|
}
|
|
if coverTex != fbo.tex {
|
|
coverTex = fbo.tex
|
|
r.ctx.BindTexture(1, coverTex)
|
|
}
|
|
uv := image.Rectangle{
|
|
Min: img.place.Pos,
|
|
Max: img.place.Pos.Add(drc.Size()),
|
|
}
|
|
coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
|
|
r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
|
}
|
|
r.ctx.DepthMask(true)
|
|
r.ctx.SetDepthTest(false)
|
|
}
|
|
|
|
func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
|
|
p := b.prog[mat]
|
|
b.ctx.BindProgram(p.prog)
|
|
var uniforms *blitUniforms
|
|
switch mat {
|
|
case materialColor:
|
|
b.colUniforms.frag.color = col
|
|
uniforms = &b.colUniforms.vert.blitUniforms
|
|
case materialTexture:
|
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
|
b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
uniforms = &b.texUniforms.vert.blitUniforms
|
|
case materialLinearGradient:
|
|
b.linearGradientUniforms.frag.color1 = col1
|
|
b.linearGradientUniforms.frag.color2 = col2
|
|
|
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
|
b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
uniforms = &b.linearGradientUniforms.vert.blitUniforms
|
|
}
|
|
uniforms.z = z
|
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
|
p.UploadUniforms()
|
|
b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
|
|
}
|
|
|
|
// newUniformBuffer creates a new GPU uniform buffer backed by the
|
|
// structure uniformBlock points to.
|
|
func newUniformBuffer(b driver.Device, uniformBlock interface{}) *uniformBuffer {
|
|
ref := reflect.ValueOf(uniformBlock)
|
|
// Determine the size of the uniforms structure, *uniforms.
|
|
size := ref.Elem().Type().Size()
|
|
// Map the uniforms structure as a byte slice.
|
|
ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size]
|
|
ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &uniformBuffer{buf: ubuf, ptr: ptr}
|
|
}
|
|
|
|
func (u *uniformBuffer) Upload() {
|
|
u.buf.Upload(u.ptr)
|
|
}
|
|
|
|
func (u *uniformBuffer) Release() {
|
|
u.buf.Release()
|
|
u.buf = nil
|
|
}
|
|
|
|
func newProgram(prog driver.Program, vertUniforms, fragUniforms *uniformBuffer) *program {
|
|
if vertUniforms != nil {
|
|
prog.SetVertexUniforms(vertUniforms.buf)
|
|
}
|
|
if fragUniforms != nil {
|
|
prog.SetFragmentUniforms(fragUniforms.buf)
|
|
}
|
|
return &program{prog: prog, vertUniforms: vertUniforms, fragUniforms: fragUniforms}
|
|
}
|
|
|
|
func (p *program) UploadUniforms() {
|
|
if p.vertUniforms != nil {
|
|
p.vertUniforms.Upload()
|
|
}
|
|
if p.fragUniforms != nil {
|
|
p.fragUniforms.Upload()
|
|
}
|
|
}
|
|
|
|
func (p *program) Release() {
|
|
p.prog.Release()
|
|
p.prog = nil
|
|
if p.vertUniforms != nil {
|
|
p.vertUniforms.Release()
|
|
p.vertUniforms = nil
|
|
}
|
|
if p.fragUniforms != nil {
|
|
p.fragUniforms.Release()
|
|
p.fragUniforms = nil
|
|
}
|
|
}
|
|
|
|
// texSpaceTransform return the scale and offset that transforms the given subimage
|
|
// into quad texture coordinates.
|
|
func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, f32.Point) {
|
|
size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)}
|
|
scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y}
|
|
offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y}
|
|
return scale, offset
|
|
}
|
|
|
|
// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)].
|
|
func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f32.Point) f32.Affine2D {
|
|
d := stop2.Sub(stop1)
|
|
l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
|
|
a := float32(math.Atan2(float64(-d.Y), float64(d.X)))
|
|
|
|
// TODO: optimize
|
|
zp := f32.Point{}
|
|
return f32.Affine2D{}.
|
|
Scale(zp, layout.FPt(clip.Size())). // scale to pixel space
|
|
Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
|
|
Offset(zp.Sub(stop1)). // offset to first stop point
|
|
Rotate(zp, a). // rotate to align gradient
|
|
Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size
|
|
}
|
|
|
|
// clipSpaceTransform returns the scale and offset that transforms the given
|
|
// rectangle from a viewport into OpenGL clip space.
|
|
func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) {
|
|
// First, transform UI coordinates to OpenGL coordinates:
|
|
//
|
|
// [(-1, +1) (+1, +1)]
|
|
// [(-1, -1) (+1, -1)]
|
|
//
|
|
x, y := float32(r.Min.X), float32(r.Min.Y)
|
|
w, h := float32(r.Dx()), float32(r.Dy())
|
|
vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y)
|
|
x = x*vx - 1
|
|
y = 1 - y*vy
|
|
w *= vx
|
|
h *= vy
|
|
|
|
// Then, compute the transformation from the fullscreen quad to
|
|
// the rectangle at (x, y) and dimensions (w, h).
|
|
scale := f32.Point{X: w * .5, Y: h * .5}
|
|
offset := f32.Point{X: x + w*.5, Y: y - h*.5}
|
|
|
|
return scale, offset
|
|
}
|
|
|
|
// Fill in maximal Y coordinates of the NW and NE corners.
|
|
func fillMaxY(verts []byte) {
|
|
contour := 0
|
|
bo := binary.LittleEndian
|
|
for len(verts) > 0 {
|
|
maxy := float32(math.Inf(-1))
|
|
i := 0
|
|
for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
|
|
vert := verts[i : i+vertStride]
|
|
// MaxY contains the integer contour index.
|
|
pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):]))
|
|
if contour != pathContour {
|
|
contour = pathContour
|
|
break
|
|
}
|
|
fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):]))
|
|
ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):]))
|
|
toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):]))
|
|
if fromy > maxy {
|
|
maxy = fromy
|
|
}
|
|
if ctrly > maxy {
|
|
maxy = ctrly
|
|
}
|
|
if toy > maxy {
|
|
maxy = toy
|
|
}
|
|
}
|
|
fillContourMaxY(maxy, verts[:i])
|
|
verts = verts[i:]
|
|
}
|
|
}
|
|
|
|
func fillContourMaxY(maxy float32, verts []byte) {
|
|
bo := binary.LittleEndian
|
|
for i := 0; i < len(verts); i += vertStride {
|
|
off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY))
|
|
bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
|
|
}
|
|
}
|
|
|
|
func (d *drawOps) writeVertCache(n int) []byte {
|
|
d.vertCache = append(d.vertCache, make([]byte, n)...)
|
|
return d.vertCache[len(d.vertCache)-n:]
|
|
}
|
|
|
|
// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
|
|
func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle, dashes stroke.DashOp) (verts []byte, bounds f32.Rectangle) {
|
|
inf := float32(math.Inf(+1))
|
|
d.qs.bounds = f32.Rectangle{
|
|
Min: f32.Point{X: inf, Y: inf},
|
|
Max: f32.Point{X: -inf, Y: -inf},
|
|
}
|
|
d.qs.d = d
|
|
startLength := len(d.vertCache)
|
|
|
|
switch {
|
|
case stroke.Width > 0:
|
|
// Stroke path.
|
|
quads := decodeToStrokeQuads(pathData)
|
|
quads = quads.Stroke(stroke, dashes)
|
|
for _, quad := range quads {
|
|
d.qs.contour = quad.Contour
|
|
quad.Quad = quad.Quad.Transform(tr)
|
|
|
|
d.qs.splitAndEncode(quad.Quad)
|
|
}
|
|
|
|
case outline:
|
|
decodeToOutlineQuads(&d.qs, tr, pathData)
|
|
}
|
|
|
|
fillMaxY(d.vertCache[startLength:])
|
|
return d.vertCache[startLength:], d.qs.bounds
|
|
}
|
|
|
|
// decodeOutlineQuads decodes scene commands, splits them into quadratic béziers
|
|
// as needed and feeds them to the supplied splitter.
|
|
func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
|
|
for len(pathData) >= scene.CommandSize+4 {
|
|
qs.contour = bo.Uint32(pathData)
|
|
cmd := ops.DecodeCommand(pathData[4:])
|
|
switch cmd.Op() {
|
|
case scene.OpLine:
|
|
var q stroke.QuadSegment
|
|
q.From, q.To = scene.DecodeLine(cmd)
|
|
q.Ctrl = q.From.Add(q.To).Mul(.5)
|
|
q = q.Transform(tr)
|
|
qs.splitAndEncode(q)
|
|
case scene.OpQuad:
|
|
var q stroke.QuadSegment
|
|
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
|
|
q = q.Transform(tr)
|
|
qs.splitAndEncode(q)
|
|
case scene.OpCubic:
|
|
for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
|
|
q = q.Transform(tr)
|
|
qs.splitAndEncode(q)
|
|
}
|
|
default:
|
|
panic("unsupported scene command")
|
|
}
|
|
pathData = pathData[scene.CommandSize+4:]
|
|
}
|
|
}
|
|
|
|
// decodeToStrokeQuads is like decodeOutlineQuads, except it returns a list of stroke
|
|
// quads ready to stroke.
|
|
func decodeToStrokeQuads(pathData []byte) stroke.StrokeQuads {
|
|
quads := make(stroke.StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
|
|
for len(pathData) >= scene.CommandSize+4 {
|
|
contour := bo.Uint32(pathData)
|
|
cmd := ops.DecodeCommand(pathData[4:])
|
|
switch cmd.Op() {
|
|
case scene.OpLine:
|
|
var q stroke.QuadSegment
|
|
q.From, q.To = scene.DecodeLine(cmd)
|
|
q.Ctrl = q.From.Add(q.To).Mul(.5)
|
|
quad := stroke.StrokeQuad{
|
|
Contour: contour,
|
|
Quad: q,
|
|
}
|
|
quads = append(quads, quad)
|
|
case scene.OpQuad:
|
|
var q stroke.QuadSegment
|
|
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
|
|
quad := stroke.StrokeQuad{
|
|
Contour: contour,
|
|
Quad: q,
|
|
}
|
|
quads = append(quads, quad)
|
|
case scene.OpCubic:
|
|
for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
|
|
quad := stroke.StrokeQuad{
|
|
Contour: contour,
|
|
Quad: q,
|
|
}
|
|
quads = append(quads, quad)
|
|
}
|
|
default:
|
|
panic("unsupported scene command")
|
|
}
|
|
pathData = pathData[scene.CommandSize+4:]
|
|
}
|
|
return quads
|
|
}
|
|
|
|
// create GPU vertices for transformed r, find the bounds and establish texture transform.
|
|
func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
|
|
if isPureOffset(tr) {
|
|
// fast-path to allow blitting of pure rectangles
|
|
_, _, ox, _, _, oy := tr.Elems()
|
|
off := f32.Pt(ox, oy)
|
|
bnd.Min = r.Min.Add(off)
|
|
bnd.Max = r.Max.Add(off)
|
|
return
|
|
}
|
|
|
|
// transform all corners, find new bounds
|
|
corners := [4]f32.Point{
|
|
tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)),
|
|
tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)),
|
|
}
|
|
bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32)
|
|
bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32)
|
|
for _, c := range corners {
|
|
if c.X < bnd.Min.X {
|
|
bnd.Min.X = c.X
|
|
}
|
|
if c.Y < bnd.Min.Y {
|
|
bnd.Min.Y = c.Y
|
|
}
|
|
if c.X > bnd.Max.X {
|
|
bnd.Max.X = c.X
|
|
}
|
|
if c.Y > bnd.Max.Y {
|
|
bnd.Max.Y = c.Y
|
|
}
|
|
}
|
|
|
|
// build the GPU vertices
|
|
l := len(d.vertCache)
|
|
if !d.compute {
|
|
d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...)
|
|
aux = d.vertCache[l:]
|
|
encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1])
|
|
encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2])
|
|
encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3])
|
|
encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0])
|
|
fillMaxY(aux)
|
|
} else {
|
|
d.vertCache = append(d.vertCache, make([]byte, (scene.CommandSize+4)*4)...)
|
|
aux = d.vertCache[l:]
|
|
buf := aux
|
|
bo := binary.LittleEndian
|
|
bo.PutUint32(buf, 0) // Contour
|
|
ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y)))
|
|
buf = buf[4+scene.CommandSize:]
|
|
bo.PutUint32(buf, 0)
|
|
ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max))
|
|
buf = buf[4+scene.CommandSize:]
|
|
bo.PutUint32(buf, 0)
|
|
ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y)))
|
|
buf = buf[4+scene.CommandSize:]
|
|
bo.PutUint32(buf, 0)
|
|
ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min))
|
|
}
|
|
|
|
// establish the transform mapping from bounds rectangle to transformed corners
|
|
var P1, P2, P3 f32.Point
|
|
P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
|
|
P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
|
|
P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
|
|
P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
|
|
P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
|
|
P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
|
|
sx, sy := P2.X-P3.X, P2.Y-P3.Y
|
|
ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert()
|
|
|
|
return
|
|
}
|
|
|
|
func isPureOffset(t f32.Affine2D) bool {
|
|
a, b, _, d, e, _ := t.Elems()
|
|
return a == 1 && b == 0 && d == 0 && e == 1
|
|
}
|
|
|
|
func splitCubic(from, ctrl0, ctrl1, to f32.Point) []stroke.QuadSegment {
|
|
quads := make([]stroke.QuadSegment, 0, 10)
|
|
// Set the maximum distance proportionally to the longest side
|
|
// of the bounding rectangle.
|
|
hull := f32.Rectangle{
|
|
Min: from,
|
|
Max: ctrl0,
|
|
}.Canon().Add(ctrl1).Add(to)
|
|
l := hull.Dx()
|
|
if h := hull.Dy(); h > l {
|
|
l = h
|
|
}
|
|
approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to)
|
|
return quads
|
|
}
|
|
|
|
// approxCube approximates a cubic Bézier by a series of quadratic
|
|
// curves.
|
|
func approxCubeTo(quads *[]stroke.QuadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int {
|
|
// The idea is from
|
|
// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
|
|
// where a quadratic approximates a cubic by eliminating its t³ term
|
|
// from its polynomial expression anchored at the starting point:
|
|
//
|
|
// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
|
|
//
|
|
// The control point for the new quadratic Q1 that shares starting point, pen, with P is
|
|
//
|
|
// C1 = (3ctrl0 - pen)/2
|
|
//
|
|
// The reverse cubic anchored at the end point has the polynomial
|
|
//
|
|
// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
|
|
//
|
|
// The corresponding quadratic Q2 that shares the end point, to, with P has control
|
|
// point
|
|
//
|
|
// C2 = (3ctrl1 - to)/2
|
|
//
|
|
// The combined quadratic Bézier, Q, shares both start and end points with its cubic
|
|
// and use the midpoint between the two curves Q1 and Q2 as control point:
|
|
//
|
|
// C = (3ctrl0 - pen + 3ctrl1 - to)/4
|
|
c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
|
|
const maxSplits = 32
|
|
if splits >= maxSplits {
|
|
*quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
|
|
return splits
|
|
}
|
|
// The maximum distance between the cubic P and its approximation Q given t
|
|
// can be shown to be
|
|
//
|
|
// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
|
|
//
|
|
// To save a square root, compare d² with the squared tolerance.
|
|
v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
|
|
d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
|
|
if d2 <= maxDist*maxDist {
|
|
*quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
|
|
return splits
|
|
}
|
|
// De Casteljau split the curve and approximate the halves.
|
|
t := float32(0.5)
|
|
c0 := from.Add(ctrl0.Sub(from).Mul(t))
|
|
c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
|
|
c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
|
|
c01 := c0.Add(c1.Sub(c0).Mul(t))
|
|
c12 := c1.Add(c2.Sub(c1).Mul(t))
|
|
c0112 := c01.Add(c12.Sub(c01).Mul(t))
|
|
splits++
|
|
splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112)
|
|
splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to)
|
|
return splits
|
|
}
|