mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
936c266b03
The op.Save and Load methods exist to support the need for
transformation, clip, pointer area state to behave as stacks. For
example, layout needs to apply an offset to its children but not
subsequent operations.
Before this change, op.Save and Load were used to save and restore the
state:
ops := new(op.Ops)
// Save state.
state := op.Save(ops)
// Apply offset.
op.Offset(...).Add(ops)
// Draw with offset applied.
draw(ops)
// Restore state.
state.Load()
A drawback with the op.Save mechanism is that there is no direct
connection between the state change and the saving and loading of state.
This causes confusion as to when a Save/Load is needed and who is
responsible for performing them, which leads to subtle bugs and over-use
of Save/Loads.
This change gets rid of the general state stack and replaces it with
per-state stacks. There is now a stack for transformation, clip, pointer
areas, and they can only be restored by the code pushing state to them.
The example above now becomes:
ops := new(op.Ops)
// Push offset to the transformation stack.
stack := op.Offset(...).Push(ops)
// Draw with offset applied.
draw(ops)
// Restore state.
stack.Pop()
For convenience, transformation also be Add'ed if the stack operation is
not required.
Simple state such as the current material no longer has a way to be
restored; it is assumed the client of a PaintOp adds their desired
material operation before it.
API change: replace op.Save/Load with explicit Push/Pop scopes for
op.TransformOps, pointer.AreaOps, clip.Ops.
To ease porting, this change retains a version of op.Save/Load that
saves and restores the transformation and clip stacks. It also retains
an Add method for clip.Op.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
1472 lines
38 KiB
Go
1472 lines
38 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"
|
|
"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"
|
|
"gioui.org/shader"
|
|
"gioui.org/shader/gio"
|
|
|
|
// Register backends.
|
|
_ "gioui.org/gpu/internal/d3d11"
|
|
_ "gioui.org/gpu/internal/metal"
|
|
_ "gioui.org/gpu/internal/opengl"
|
|
_ "gioui.org/gpu/internal/vulkan"
|
|
)
|
|
|
|
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)
|
|
// Frame draws the graphics operations from op into a viewport of target.
|
|
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
|
|
// Profile returns the last available profiling information. Profiling
|
|
// information is requested when Frame sees an io/profile.Op, 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
|
|
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 []f32.Affine2D
|
|
transStack []f32.Affine2D
|
|
cache *resourceCache
|
|
vertCache []byte
|
|
viewport image.Point
|
|
clear bool
|
|
clearColor f32color.RGBA
|
|
imageOps []imageOp
|
|
pathOps []*pathOp
|
|
pathOpCache []pathOp
|
|
qs quadSplitter
|
|
pathCache *opCache
|
|
}
|
|
|
|
type drawState struct {
|
|
t f32.Affine2D
|
|
cpath *pathOp
|
|
|
|
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
|
|
// rect tracks whether the clip stack can be represented by a
|
|
// pixel-aligned rectangle.
|
|
rect bool
|
|
// push is set to true for clip operations that corresponds to
|
|
// a push operation.
|
|
push bool
|
|
// clip is the union of all
|
|
// later clip rectangles.
|
|
clip image.Rectangle
|
|
bounds f32.Rectangle
|
|
// intersect is the intersection of bounds and all
|
|
// previous clip bounds.
|
|
intersect f32.Rectangle
|
|
pathKey opKey
|
|
path bool
|
|
pathVerts []byte
|
|
parent *pathOp
|
|
place placement
|
|
}
|
|
|
|
type imageOp struct {
|
|
path *pathOp
|
|
clip image.Rectangle
|
|
material material
|
|
clipType clipType
|
|
place placement
|
|
}
|
|
|
|
func decodeStrokeOp(data []byte) clip.StrokeStyle {
|
|
_ = data[4]
|
|
if opconst.OpType(data[0]) != opconst.TypeStroke {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return clip.StrokeStyle{
|
|
Width: math.Float32frombits(bo.Uint32(data[1:])),
|
|
}
|
|
}
|
|
|
|
type quadsOp struct {
|
|
key opKey
|
|
aux []byte
|
|
}
|
|
|
|
type opKey struct {
|
|
sx, hx, sy, hy float32
|
|
ops.Key
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// clipOp is the shadow of clip.Op.
|
|
type clipOp struct {
|
|
// TODO: Use image.Rectangle?
|
|
bounds f32.Rectangle
|
|
outline bool
|
|
push 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,
|
|
push: data[18] == 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
|
|
pipelines [3]*pipeline
|
|
colUniforms *blitColUniforms
|
|
texUniforms *blitTexUniforms
|
|
linearGradientUniforms *blitLinearGradientUniforms
|
|
quadVerts driver.Buffer
|
|
}
|
|
|
|
type blitColUniforms struct {
|
|
blitUniforms
|
|
_ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(colorUniforms{})]byte // Padding to 128 bytes.
|
|
colorUniforms
|
|
}
|
|
|
|
type blitTexUniforms struct {
|
|
blitUniforms
|
|
}
|
|
|
|
type blitLinearGradientUniforms struct {
|
|
blitUniforms
|
|
_ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(gradientUniforms{})]byte // Padding to 128 bytes.
|
|
gradientUniforms
|
|
}
|
|
|
|
type uniformBuffer struct {
|
|
buf driver.Buffer
|
|
ptr []byte
|
|
}
|
|
|
|
type pipeline struct {
|
|
pipeline driver.Pipeline
|
|
uniforms *uniformBuffer
|
|
}
|
|
|
|
type blitUniforms struct {
|
|
transform [4]float32
|
|
uvTransformR1 [4]float32
|
|
uvTransformR2 [4]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
|
|
}
|
|
d.BeginFrame(nil, false, image.Point{})
|
|
defer d.EndFrame()
|
|
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
|
|
feats := d.Caps().Features
|
|
switch {
|
|
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
|
|
return newGPU(d)
|
|
}
|
|
return newCompute(d)
|
|
}
|
|
|
|
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) Frame(frameOps *op.Ops, target RenderTarget, viewport image.Point) error {
|
|
g.collect(viewport, frameOps)
|
|
return g.frame(target)
|
|
}
|
|
|
|
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(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.stencilTimer = g.timers.newTimer()
|
|
g.coverTimer = g.timers.newTimer()
|
|
g.cleanupTimer = g.timers.newTimer()
|
|
}
|
|
}
|
|
|
|
func (g *gpu) frame(target RenderTarget) error {
|
|
viewport := g.renderer.blitter.viewport
|
|
defFBO := g.ctx.BeginFrame(target, g.drawOps.clear, viewport)
|
|
defer g.ctx.EndFrame()
|
|
g.drawOps.buildPaths(g.ctx)
|
|
for _, img := range g.drawOps.imageOps {
|
|
expandPathOp(img.path, img.clip)
|
|
}
|
|
g.stencilTimer.begin()
|
|
g.renderer.packStencils(&g.drawOps.pathOps)
|
|
g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
|
|
g.renderer.packIntersections(g.drawOps.imageOps)
|
|
g.renderer.prepareIntersections(g.drawOps.imageOps)
|
|
g.renderer.intersect(g.drawOps.imageOps)
|
|
g.stencilTimer.end()
|
|
g.coverTimer.begin()
|
|
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
|
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
|
d := driver.LoadDesc{
|
|
ClearColor: g.drawOps.clearColor,
|
|
}
|
|
if g.drawOps.clear {
|
|
g.drawOps.clear = false
|
|
d.Action = driver.LoadActionClear
|
|
}
|
|
g.ctx.BeginRenderPass(defFBO, d)
|
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
|
g.renderer.drawOps(g.cache, g.drawOps.imageOps)
|
|
g.coverTimer.end()
|
|
g.ctx.EndRenderPass()
|
|
g.cleanupTimer.begin()
|
|
g.cache.frame()
|
|
g.drawOps.pathCache.frame()
|
|
g.cleanupTimer.end()
|
|
if g.drawOps.profile && g.timers.ready() {
|
|
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
|
|
ft := st + covt + cleant
|
|
q := 100 * time.Microsecond
|
|
st, covt = 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 st:%7s cov:%7s", frameDur, ft, 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.TextureFormatSRGBA, 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.maxDims = image.Pt(maxDim, maxDim)
|
|
r.intersections.maxDims = image.Pt(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)
|
|
pipelines, err := createColorPrograms(ctx, gio.Shader_blit_vert, gio.Shader_blit_frag,
|
|
[3]interface{}{b.colUniforms, b.linearGradientUniforms, b.texUniforms},
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b.pipelines = pipelines
|
|
return b
|
|
}
|
|
|
|
func (b *blitter) release() {
|
|
b.quadVerts.Release()
|
|
for _, p := range b.pipelines {
|
|
p.Release()
|
|
}
|
|
}
|
|
|
|
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) {
|
|
var pipelines [3]*pipeline
|
|
blend := driver.BlendDesc{
|
|
Enable: true,
|
|
SrcFactor: driver.BlendFactorOne,
|
|
DstFactor: driver.BlendFactorOneMinusSrcAlpha,
|
|
}
|
|
layout := driver.VertexLayout{
|
|
Inputs: []driver.InputDesc{
|
|
{Type: shader.DataTypeFloat, Size: 2, Offset: 0},
|
|
{Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2},
|
|
},
|
|
Stride: 4 * 4,
|
|
}
|
|
vsh, err := b.NewVertexShader(vsSrc)
|
|
if err != nil {
|
|
return pipelines, err
|
|
}
|
|
defer vsh.Release()
|
|
{
|
|
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
|
|
if err != nil {
|
|
return pipelines, err
|
|
}
|
|
defer fsh.Release()
|
|
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
|
VertexShader: vsh,
|
|
FragmentShader: fsh,
|
|
BlendDesc: blend,
|
|
VertexLayout: layout,
|
|
PixelFormat: driver.TextureFormatOutput,
|
|
Topology: driver.TopologyTriangleStrip,
|
|
})
|
|
if err != nil {
|
|
return pipelines, err
|
|
}
|
|
var vertBuffer *uniformBuffer
|
|
if u := uniforms[materialTexture]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
}
|
|
pipelines[materialTexture] = &pipeline{pipe, vertBuffer}
|
|
}
|
|
{
|
|
var vertBuffer *uniformBuffer
|
|
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
|
|
if err != nil {
|
|
pipelines[materialTexture].Release()
|
|
return pipelines, err
|
|
}
|
|
defer fsh.Release()
|
|
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
|
VertexShader: vsh,
|
|
FragmentShader: fsh,
|
|
BlendDesc: blend,
|
|
VertexLayout: layout,
|
|
PixelFormat: driver.TextureFormatOutput,
|
|
Topology: driver.TopologyTriangleStrip,
|
|
})
|
|
if err != nil {
|
|
pipelines[materialTexture].Release()
|
|
return pipelines, err
|
|
}
|
|
if u := uniforms[materialColor]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
}
|
|
pipelines[materialColor] = &pipeline{pipe, vertBuffer}
|
|
}
|
|
{
|
|
var vertBuffer *uniformBuffer
|
|
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
|
|
if err != nil {
|
|
pipelines[materialTexture].Release()
|
|
pipelines[materialColor].Release()
|
|
return pipelines, err
|
|
}
|
|
defer fsh.Release()
|
|
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
|
VertexShader: vsh,
|
|
FragmentShader: fsh,
|
|
BlendDesc: blend,
|
|
VertexLayout: layout,
|
|
PixelFormat: driver.TextureFormatOutput,
|
|
Topology: driver.TopologyTriangleStrip,
|
|
})
|
|
if err != nil {
|
|
pipelines[materialTexture].Release()
|
|
pipelines[materialColor].Release()
|
|
return pipelines, err
|
|
}
|
|
if u := uniforms[materialLinearGradient]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
}
|
|
pipelines[materialLinearGradient] = &pipeline{pipe, vertBuffer}
|
|
}
|
|
if err != nil {
|
|
for _, p := range pipelines {
|
|
p.Release()
|
|
}
|
|
return pipelines, err
|
|
}
|
|
return pipelines, 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 {
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
}
|
|
fbo = p.place.Idx
|
|
f := r.pather.stenciler.cover(fbo)
|
|
r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
|
|
r.ctx.BindPipeline(r.pather.stenciler.pipeline.pipeline.pipeline)
|
|
r.ctx.BindIndexBuffer(r.pather.stenciler.indexBuf)
|
|
}
|
|
v, _ := pathCache.get(p.pathKey)
|
|
r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
|
|
}
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
}
|
|
}
|
|
|
|
func (r *renderer) prepareIntersections(ops []imageOp) {
|
|
for _, img := range ops {
|
|
if img.clipType != clipTypeIntersection {
|
|
continue
|
|
}
|
|
fbo := r.pather.stenciler.cover(img.path.place.Idx)
|
|
r.ctx.PrepareTexture(fbo.tex)
|
|
}
|
|
}
|
|
|
|
func (r *renderer) intersect(ops []imageOp) {
|
|
if len(r.intersections.sizes) == 0 {
|
|
return
|
|
}
|
|
fbo := -1
|
|
r.pather.stenciler.beginIntersect(r.intersections.sizes)
|
|
for _, img := range ops {
|
|
if img.clipType != clipTypeIntersection {
|
|
continue
|
|
}
|
|
if fbo != img.place.Idx {
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
}
|
|
fbo = img.place.Idx
|
|
f := r.pather.stenciler.intersections.fbos[fbo]
|
|
d := driver.LoadDesc{Action: driver.LoadActionClear}
|
|
d.ClearColor.R = 1.0
|
|
r.ctx.BeginRenderPass(f.tex, d)
|
|
r.ctx.BindPipeline(r.pather.stenciler.ipipeline.pipeline.pipeline)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
|
}
|
|
r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
|
|
r.intersectPath(img.path, img.clip)
|
|
}
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
}
|
|
}
|
|
|
|
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.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
|
r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y}
|
|
r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx)
|
|
r.ctx.DrawArrays(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 %v", p.clip, r.packer.maxDims))
|
|
}
|
|
p.place = place
|
|
i++
|
|
}
|
|
*pops = ops
|
|
}
|
|
|
|
// boundRectF 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.pathOps = d.pathOps[:0]
|
|
d.pathOpCache = d.pathOpCache[:0]
|
|
d.vertCache = d.vertCache[:0]
|
|
d.transStack = d.transStack[:0]
|
|
}
|
|
|
|
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
|
|
viewf := f32.Rectangle{
|
|
Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
|
|
}
|
|
d.reader.Reset(root)
|
|
d.collectOps(&d.reader, viewf)
|
|
}
|
|
|
|
func (d *drawOps) buildPaths(ctx driver.Device) {
|
|
for _, p := range d.pathOps {
|
|
if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
|
|
data := buildPath(ctx, p.pathVerts)
|
|
d.pathCache.put(p.pathKey, opCacheValue{
|
|
data: data,
|
|
bounds: p.bounds,
|
|
})
|
|
}
|
|
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 opKey, bounds f32.Rectangle, off f32.Point, push bool) {
|
|
npath := d.newPathOp()
|
|
*npath = pathOp{
|
|
parent: state.cpath,
|
|
bounds: bounds,
|
|
off: off,
|
|
intersect: bounds.Add(off),
|
|
rect: true,
|
|
push: push,
|
|
}
|
|
if npath.parent != nil {
|
|
npath.rect = npath.parent.rect
|
|
npath.intersect = npath.parent.intersect.Intersect(npath.intersect)
|
|
}
|
|
if len(aux) > 0 {
|
|
npath.rect = false
|
|
npath.pathKey = auxKey
|
|
npath.path = true
|
|
npath.pathVerts = aux
|
|
d.pathOps = append(d.pathOps, npath)
|
|
}
|
|
state.cpath = npath
|
|
}
|
|
|
|
// split a transform into two parts, one which is pure 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 f32.Affine2D) {
|
|
if extra := id - len(d.states) + 1; extra > 0 {
|
|
d.states = append(d.states, make([]f32.Affine2D, extra)...)
|
|
}
|
|
d.states[id] = state
|
|
}
|
|
|
|
func (k opKey) SetTransform(t f32.Affine2D) opKey {
|
|
sx, hx, _, hy, sy, _ := t.Elems()
|
|
k.sx = sx
|
|
k.hx = hx
|
|
k.hy = hy
|
|
k.sy = sy
|
|
return k
|
|
}
|
|
|
|
func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
|
|
var (
|
|
quads quadsOp
|
|
str clip.StrokeStyle
|
|
state drawState
|
|
)
|
|
reset := func() {
|
|
state = drawState{
|
|
color: color.NRGBA{A: 0xff},
|
|
}
|
|
}
|
|
reset()
|
|
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, push := ops.DecodeTransform(encOp.Data)
|
|
if push {
|
|
d.transStack = append(d.transStack, state.t)
|
|
}
|
|
state.t = state.t.Mul(dop)
|
|
case opconst.TypePopTransform:
|
|
n := len(d.transStack)
|
|
state.t = d.transStack[n-1]
|
|
d.transStack = d.transStack[:n-1]
|
|
|
|
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 = opKey{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,
|
|
)
|
|
op.bounds = bounds
|
|
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 = opKey{Key: encOp.Key}
|
|
}
|
|
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, op.push)
|
|
quads = quadsOp{}
|
|
str = clip.StrokeStyle{}
|
|
case opconst.TypePopClip:
|
|
for {
|
|
push := state.cpath.push
|
|
state.cpath = state.cpath.parent
|
|
if push {
|
|
break
|
|
}
|
|
}
|
|
|
|
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.
|
|
t, 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, t)
|
|
cl := viewport.Intersect(bnd.Add(off))
|
|
if state.cpath != nil {
|
|
cl = state.cpath.intersect.Intersect(cl)
|
|
}
|
|
if cl.Empty() {
|
|
continue
|
|
}
|
|
|
|
if clipData != nil {
|
|
// The paint operation is sheared or rotated, add a clip path representing
|
|
// this transformed rectangle.
|
|
k := opKey{Key: encOp.Key}
|
|
k.SetTransform(t) // TODO: This call has no effect.
|
|
d.addClipPath(&state, clipData, k, bnd, off, false)
|
|
}
|
|
|
|
bounds := boundRectF(cl)
|
|
mat := state.materialFor(bnd, off, partialTrans, bounds)
|
|
|
|
rect := state.cpath == nil || state.cpath.rect
|
|
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && 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.imageOps = d.imageOps[:0]
|
|
d.clearColor = mat.color.Opaque()
|
|
d.clear = true
|
|
continue
|
|
}
|
|
img := imageOp{
|
|
path: state.cpath,
|
|
clip: bounds,
|
|
material: mat,
|
|
}
|
|
|
|
d.imageOps = append(d.imageOps, img)
|
|
if clipData != nil {
|
|
// we added a clip path that should not remain
|
|
state.cpath = state.cpath.parent
|
|
}
|
|
case opconst.TypeSave:
|
|
id := ops.DecodeSave(encOp.Data)
|
|
d.save(id, state.t)
|
|
case opconst.TypeLoad:
|
|
reset()
|
|
id := ops.DecodeLoad(encOp.Data)
|
|
state.t = d.states[id]
|
|
}
|
|
}
|
|
}
|
|
|
|
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) 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.data = d.image
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
|
for _, img := range ops {
|
|
m := img.material
|
|
if m.material == materialTexture {
|
|
r.texHandle(cache, m.data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|
for _, img := range ops {
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.PrepareTexture(r.texHandle(cache, m.data))
|
|
}
|
|
|
|
var fbo stencilFBO
|
|
switch img.clipType {
|
|
case clipTypeNone:
|
|
continue
|
|
case clipTypePath:
|
|
fbo = r.pather.stenciler.cover(img.place.Idx)
|
|
case clipTypeIntersection:
|
|
fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
|
|
}
|
|
r.ctx.PrepareTexture(fbo.tex)
|
|
}
|
|
}
|
|
|
|
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
|
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:
|
|
p := r.blitter.pipelines[m.material]
|
|
r.ctx.BindPipeline(p.pipeline)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
|
r.blitter.blit(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)
|
|
p := r.pather.coverer.pipelines[m.material]
|
|
r.ctx.BindPipeline(p.pipeline)
|
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
|
r.pather.cover(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
|
}
|
|
}
|
|
|
|
func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
|
|
p := b.pipelines[mat]
|
|
b.ctx.BindPipeline(p.pipeline)
|
|
var uniforms *blitUniforms
|
|
switch mat {
|
|
case materialColor:
|
|
b.colUniforms.color = col
|
|
uniforms = &b.colUniforms.blitUniforms
|
|
case materialTexture:
|
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
|
b.texUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
b.texUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
uniforms = &b.texUniforms.blitUniforms
|
|
case materialLinearGradient:
|
|
b.linearGradientUniforms.color1 = col1
|
|
b.linearGradientUniforms.color2 = col2
|
|
|
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
|
b.linearGradientUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
b.linearGradientUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
uniforms = &b.linearGradientUniforms.blitUniforms
|
|
}
|
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
|
p.UploadUniforms(b.ctx)
|
|
b.ctx.DrawArrays(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 (p *pipeline) UploadUniforms(ctx driver.Device) {
|
|
if p.uniforms != nil {
|
|
p.uniforms.Upload()
|
|
ctx.BindUniforms(p.uniforms.buf)
|
|
}
|
|
}
|
|
|
|
func (p *pipeline) Release() {
|
|
p.pipeline.Release()
|
|
if p.uniforms != nil {
|
|
p.uniforms.Release()
|
|
}
|
|
*p = pipeline{}
|
|
}
|
|
|
|
// 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 GPU driver device coordinates.
|
|
func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) {
|
|
// First, transform UI coordinates to device 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 = y*vy - 1
|
|
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, str clip.StrokeStyle) (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 str.Width > 0:
|
|
// Stroke path.
|
|
ss := stroke.StrokeStyle{
|
|
Width: str.Width,
|
|
Miter: str.Miter,
|
|
Cap: stroke.StrokeCap(str.Cap),
|
|
Join: stroke.StrokeJoin(str.Join),
|
|
}
|
|
quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData)
|
|
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 stroke.SplitCubic(scene.DecodeCubic(cmd)) {
|
|
q = q.Transform(tr)
|
|
qs.splitAndEncode(q)
|
|
}
|
|
default:
|
|
panic("unsupported scene command")
|
|
}
|
|
pathData = pathData[scene.CommandSize+4:]
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
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)
|
|
|
|
// 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
|
|
}
|