mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
fe2a164d30
The only remaining use of the cache is mapping handles to textures. Using a concrete type for the key avoids the allocation caused by convT. If we need more caches again in the future we can copy the type, or make it generic. Instead of updating the benchmark, we removed it outright. It suffered from several flaws: - The amount of work for each iteration of b.N wasn't constant, because the same cache was reused, growing ever larger in size. - It only tested the cost of insertions. The comment "half are the same and half updated" wasn't true, as calling 'put' with the same key twice would've resulted in a panic. - It didn't simulate any particular workload or cache size, making the benchmark useless for comparing different cache implementations. The cost of insertions isn't particularly interesting. Signed-off-by: Dominik Honnef <dominik@honnef.co>
1580 lines
41 KiB
Go
1580 lines
41 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/gpu/internal/driver"
|
|
"gioui.org/internal/byteslice"
|
|
"gioui.org/internal/f32"
|
|
"gioui.org/internal/f32color"
|
|
"gioui.org/internal/ops"
|
|
"gioui.org/internal/scene"
|
|
"gioui.org/internal/stroke"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"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 *textureCache
|
|
|
|
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
|
|
layers packer
|
|
layerFBOs fboSet
|
|
}
|
|
|
|
type drawOps struct {
|
|
profile bool
|
|
reader ops.Reader
|
|
states []f32.Affine2D
|
|
transStack []f32.Affine2D
|
|
layers []opacityLayer
|
|
opacityStack []int
|
|
vertCache []byte
|
|
viewport image.Point
|
|
clear bool
|
|
clearColor f32color.RGBA
|
|
imageOps []imageOp
|
|
pathOps []*pathOp
|
|
pathOpCache []pathOp
|
|
qs quadSplitter
|
|
pathCache *opCache
|
|
}
|
|
|
|
type opacityLayer struct {
|
|
opacity float32
|
|
parent int
|
|
// depth of the opacity stack. Layers of equal depth are
|
|
// independent and may be packed into one atlas.
|
|
depth int
|
|
// opStart and opEnd denote the range of drawOps.imageOps
|
|
// that belong to the layer.
|
|
opStart, opEnd int
|
|
// clip of the layer operations.
|
|
clip image.Rectangle
|
|
place placement
|
|
}
|
|
|
|
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
|
|
// 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 is either a placement in the path fbos or intersection fbos,
|
|
// depending on clipType.
|
|
place placement
|
|
// layerOps is the number of operations this
|
|
// operation replaces.
|
|
layerOps int
|
|
}
|
|
|
|
func decodeStrokeOp(data []byte) float32 {
|
|
_ = data[4]
|
|
bo := binary.LittleEndian
|
|
return math.Float32frombits(bo.Uint32(data[1:]))
|
|
}
|
|
|
|
type quadsOp struct {
|
|
key opKey
|
|
aux []byte
|
|
}
|
|
|
|
type opKey struct {
|
|
outline bool
|
|
strokeWidth float32
|
|
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
|
|
opacity float32
|
|
// For materialTypeTexture.
|
|
data imageOpData
|
|
tex driver.Texture
|
|
uvTrans f32.Affine2D
|
|
}
|
|
|
|
const (
|
|
filterLinear = 0
|
|
filterNearest = 1
|
|
)
|
|
|
|
// imageOpData is the shadow of paint.ImageOp.
|
|
type imageOpData struct {
|
|
src *image.RGBA
|
|
handle interface{}
|
|
filter byte
|
|
}
|
|
|
|
type linearGradientOpData struct {
|
|
stop1 f32.Point
|
|
color1 color.NRGBA
|
|
stop2 f32.Point
|
|
color2 color.NRGBA
|
|
}
|
|
|
|
func decodeImageOp(data []byte, refs []interface{}) imageOpData {
|
|
handle := refs[1]
|
|
if handle == nil {
|
|
return imageOpData{}
|
|
}
|
|
return imageOpData{
|
|
src: refs[0].(*image.RGBA),
|
|
handle: handle,
|
|
filter: data[1],
|
|
}
|
|
}
|
|
|
|
func decodeColorOp(data []byte) color.NRGBA {
|
|
data = data[:ops.TypeColorLen]
|
|
return color.NRGBA{
|
|
R: data[1],
|
|
G: data[2],
|
|
B: data[3],
|
|
A: data[4],
|
|
}
|
|
}
|
|
|
|
func decodeLinearGradientOp(data []byte) linearGradientOpData {
|
|
data = data[:ops.TypeLinearGradientLen]
|
|
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 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
|
|
opacity float32
|
|
fbo float32
|
|
_ [2]float32
|
|
}
|
|
|
|
type colorUniforms struct {
|
|
color f32color.RGBA
|
|
}
|
|
|
|
type gradientUniforms struct {
|
|
color1 f32color.RGBA
|
|
color2 f32color.RGBA
|
|
}
|
|
|
|
type clipType uint8
|
|
|
|
const (
|
|
clipTypeNone clipType = iota
|
|
clipTypePath
|
|
clipTypeIntersection
|
|
)
|
|
|
|
type materialType uint8
|
|
|
|
const (
|
|
materialColor materialType = iota
|
|
materialLinearGradient
|
|
materialTexture
|
|
)
|
|
|
|
// New creates a GPU for the given API.
|
|
func New(api API) (GPU, error) {
|
|
d, err := driver.NewDevice(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewWithDevice(d)
|
|
}
|
|
|
|
// NewWithDevice creates a GPU with a pre-existing device.
|
|
//
|
|
// Note: for internal use only.
|
|
func NewWithDevice(d driver.Device) (GPU, error) {
|
|
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: newTextureCache(),
|
|
}
|
|
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(viewport)
|
|
g.drawOps.collect(frameOps, viewport)
|
|
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
|
g.frameStart = time.Now()
|
|
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.drawOps.imageOps)
|
|
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
|
|
g.renderer.drawLayers(g.drawOps.layers, 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(false, image.Point{}, g.renderer.blitter.viewport, 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 *textureCache, data imageOpData) driver.Texture {
|
|
key := textureCacheKey{
|
|
filter: data.filter,
|
|
handle: data.handle,
|
|
}
|
|
|
|
var tex *texture
|
|
t, exists := cache.get(key)
|
|
if !exists {
|
|
t = &texture{
|
|
src: data.src,
|
|
}
|
|
cache.put(key, t)
|
|
}
|
|
tex = t.(*texture)
|
|
if tex.tex != nil {
|
|
return tex.tex
|
|
}
|
|
|
|
var minFilter, magFilter driver.TextureFilter
|
|
switch data.filter {
|
|
case filterLinear:
|
|
minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
|
|
case filterNearest:
|
|
minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
|
|
}
|
|
|
|
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
|
|
data.src.Bounds().Dx(), data.src.Bounds().Dy(),
|
|
minFilter, magFilter,
|
|
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
|
|
}
|
|
d := image.Pt(maxDim, maxDim)
|
|
|
|
r.packer.maxDims = d
|
|
r.intersections.maxDims = d
|
|
r.layers.maxDims = d
|
|
return r
|
|
}
|
|
|
|
func (r *renderer) release() {
|
|
r.pather.release()
|
|
r.blitter.release()
|
|
r.layerFBOs.delete(r.ctx, 0)
|
|
}
|
|
|
|
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(f32.FRect(uv), fbo.size)
|
|
subScale, subOff := texSpaceTransform(f32.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
|
|
}
|
|
place, ok := r.packer.add(p.clip.Size())
|
|
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
|
|
}
|
|
|
|
func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
|
|
// Make every layer bounds contain nested layers; cull empty layers.
|
|
for i := len(layers) - 1; i >= 0; i-- {
|
|
l := layers[i]
|
|
if l.parent != -1 {
|
|
b := layers[l.parent].clip
|
|
layers[l.parent].clip = b.Union(l.clip)
|
|
}
|
|
if l.clip.Empty() {
|
|
layers = append(layers[:i], layers[i+1:]...)
|
|
}
|
|
}
|
|
// Pack layers.
|
|
r.layers.clear()
|
|
depth := 0
|
|
for i := range layers {
|
|
l := &layers[i]
|
|
// Only layers of the same depth may be packed together.
|
|
if l.depth != depth {
|
|
r.layers.newPage()
|
|
}
|
|
place, ok := r.layers.add(l.clip.Size())
|
|
if !ok {
|
|
// The layer area is at most the entire screen. Hopefully no
|
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
|
panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
|
|
}
|
|
l.place = place
|
|
}
|
|
return layers
|
|
}
|
|
|
|
func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
|
|
if len(r.layers.sizes) == 0 {
|
|
return
|
|
}
|
|
fbo := -1
|
|
r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes)
|
|
for i := len(layers) - 1; i >= 0; i-- {
|
|
l := layers[i]
|
|
if fbo != l.place.Idx {
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
|
}
|
|
fbo = l.place.Idx
|
|
f := r.layerFBOs.fbos[fbo]
|
|
r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
|
|
}
|
|
v := image.Rectangle{
|
|
Min: l.place.Pos,
|
|
Max: l.place.Pos.Add(l.clip.Size()),
|
|
}
|
|
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
|
|
f := r.layerFBOs.fbos[fbo]
|
|
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
|
|
sr := f32.FRect(v)
|
|
uvScale, uvOffset := texSpaceTransform(sr, f.size)
|
|
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
|
|
// Replace layer ops with one textured op.
|
|
ops[l.opStart] = imageOp{
|
|
clip: l.clip,
|
|
material: material{
|
|
material: materialTexture,
|
|
tex: f.tex,
|
|
uvTrans: uvTrans,
|
|
opacity: l.opacity,
|
|
},
|
|
layerOps: l.opEnd - l.opStart - 1,
|
|
}
|
|
}
|
|
if fbo != -1 {
|
|
r.ctx.EndRenderPass()
|
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
|
}
|
|
}
|
|
|
|
func (d *drawOps) reset(viewport image.Point) {
|
|
d.profile = false
|
|
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]
|
|
d.layers = d.layers[:0]
|
|
d.opacityStack = d.opacityStack[: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)},
|
|
}
|
|
var ops *ops.Ops
|
|
if root != nil {
|
|
ops = &root.Internal
|
|
}
|
|
d.reader.Reset(ops)
|
|
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,
|
|
}
|
|
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
|
|
}
|
|
|
|
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
|
|
state drawState
|
|
)
|
|
reset := func() {
|
|
state = drawState{
|
|
color: color.NRGBA{A: 0xff},
|
|
}
|
|
}
|
|
reset()
|
|
loop:
|
|
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
|
switch ops.OpType(encOp.Data[0]) {
|
|
case ops.TypeProfile:
|
|
d.profile = true
|
|
case ops.TypeTransform:
|
|
dop, push := ops.DecodeTransform(encOp.Data)
|
|
if push {
|
|
d.transStack = append(d.transStack, state.t)
|
|
}
|
|
state.t = state.t.Mul(dop)
|
|
case ops.TypePopTransform:
|
|
n := len(d.transStack)
|
|
state.t = d.transStack[n-1]
|
|
d.transStack = d.transStack[:n-1]
|
|
|
|
case ops.TypePushOpacity:
|
|
opacity := ops.DecodeOpacity(encOp.Data)
|
|
parent := -1
|
|
depth := len(d.opacityStack)
|
|
if depth > 0 {
|
|
parent = d.opacityStack[depth-1]
|
|
}
|
|
lidx := len(d.layers)
|
|
d.layers = append(d.layers, opacityLayer{
|
|
opacity: opacity,
|
|
parent: parent,
|
|
depth: depth,
|
|
opStart: len(d.imageOps),
|
|
})
|
|
d.opacityStack = append(d.opacityStack, lidx)
|
|
case ops.TypePopOpacity:
|
|
n := len(d.opacityStack)
|
|
idx := d.opacityStack[n-1]
|
|
d.layers[idx].opEnd = len(d.imageOps)
|
|
d.opacityStack = d.opacityStack[:n-1]
|
|
|
|
case ops.TypeStroke:
|
|
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
|
|
|
|
case ops.TypePath:
|
|
encOp, ok = r.Decode()
|
|
if !ok {
|
|
break loop
|
|
}
|
|
quads.aux = encOp.Data[ops.TypeAuxLen:]
|
|
quads.key.Key = encOp.Key
|
|
|
|
case ops.TypeClip:
|
|
var op ops.ClipOp
|
|
op.Decode(encOp.Data)
|
|
quads.key.outline = op.Outline
|
|
bounds := f32.FRect(op.Bounds)
|
|
trans, off := state.t.Split()
|
|
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?
|
|
bounds = v.bounds
|
|
} else {
|
|
var pathData []byte
|
|
pathData, bounds = d.buildVerts(
|
|
quads.aux, trans, quads.key.outline, quads.key.strokeWidth,
|
|
)
|
|
quads.aux = pathData
|
|
// add it to the cache, without GPU data, so the transform can be
|
|
// reused.
|
|
d.pathCache.put(quads.key, opCacheValue{bounds: bounds})
|
|
}
|
|
} else {
|
|
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
|
|
quads.key = opKey{Key: encOp.Key}
|
|
}
|
|
d.addClipPath(&state, quads.aux, quads.key, bounds, off, true)
|
|
quads = quadsOp{}
|
|
case ops.TypePopClip:
|
|
state.cpath = state.cpath.parent
|
|
|
|
case ops.TypeColor:
|
|
state.matType = materialColor
|
|
state.color = decodeColorOp(encOp.Data)
|
|
case ops.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 ops.TypeImage:
|
|
state.matType = materialTexture
|
|
state.image = decodeImageOp(encOp.Data, encOp.Refs)
|
|
case ops.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 := state.t.Split()
|
|
// 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 {
|
|
sz := state.image.src.Rect.Size()
|
|
dst = f32.Rectangle{Max: layout.FPt(sz)}
|
|
}
|
|
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 := cl.Round()
|
|
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) && len(d.opacityStack) == 0 {
|
|
// 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,
|
|
}
|
|
if n := len(d.opacityStack); n > 0 {
|
|
idx := d.opacityStack[n-1]
|
|
lb := d.layers[idx].clip
|
|
if lb.Empty() {
|
|
d.layers[idx].clip = img.clip
|
|
} else {
|
|
d.layers[idx].clip = lb.Union(img.clip)
|
|
}
|
|
}
|
|
|
|
d.imageOps = append(d.imageOps, img)
|
|
if clipData != nil {
|
|
// we added a clip path that should not remain
|
|
state.cpath = state.cpath.parent
|
|
}
|
|
case ops.TypeSave:
|
|
id := ops.DecodeSave(encOp.Data)
|
|
d.save(id, state.t)
|
|
case ops.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 {
|
|
m := material{
|
|
opacity: 1.,
|
|
}
|
|
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 := rect.Add(off).Round()
|
|
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 *textureCache, ops []imageOp) {
|
|
for i := range ops {
|
|
img := &ops[i]
|
|
m := img.material
|
|
if m.material == materialTexture {
|
|
img.material.tex = r.texHandle(cache, m.data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *renderer) prepareDrawOps(ops []imageOp) {
|
|
for _, img := range ops {
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.PrepareTexture(m.tex)
|
|
}
|
|
|
|
var fbo FBO
|
|
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(isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
|
|
var coverTex driver.Texture
|
|
for i := 0; i < len(ops); i++ {
|
|
img := ops[i]
|
|
i += img.layerOps
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.BindTexture(0, m.tex)
|
|
}
|
|
drc := img.clip.Add(opOff)
|
|
|
|
scale, off := clipSpaceTransform(drc, viewport)
|
|
var fbo FBO
|
|
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, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, 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(f32.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, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
|
}
|
|
}
|
|
|
|
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, 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()
|
|
uniforms = &b.texUniforms.blitUniforms
|
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
case materialLinearGradient:
|
|
b.linearGradientUniforms.color1 = col1
|
|
b.linearGradientUniforms.color2 = col2
|
|
|
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
|
uniforms = &b.linearGradientUniforms.blitUniforms
|
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
}
|
|
uniforms.fbo = 0
|
|
if fbo {
|
|
uniforms.fbo = 1
|
|
}
|
|
uniforms.opacity = opacity
|
|
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 := unsafe.Slice((*byte)(unsafe.Pointer(ref.Pointer())), 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, strWidth float32) (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 strWidth > 0:
|
|
// Stroke path.
|
|
ss := stroke.StrokeStyle{
|
|
Width: strWidth,
|
|
}
|
|
quads := stroke.StrokePathCommands(ss, 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.OpGap:
|
|
var q stroke.QuadSegment
|
|
q.From, q.To = scene.DecodeGap(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:
|
|
from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd)
|
|
qs.scratch = stroke.SplitCubic(from, ctrl0, ctrl1, to, qs.scratch[:0])
|
|
for _, q := range qs.scratch {
|
|
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
|
|
}
|