mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
4e3bfd5b1b
Signed-off-by: Elias Naur <mail@eliasnaur.com>
1040 lines
26 KiB
Go
1040 lines
26 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"
|
|
"reflect"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/internal/opconst"
|
|
"gioui.org/internal/ops"
|
|
"gioui.org/internal/path"
|
|
gunsafe "gioui.org/internal/unsafe"
|
|
"gioui.org/op"
|
|
"gioui.org/op/paint"
|
|
)
|
|
|
|
type GPU struct {
|
|
pathCache *opCache
|
|
cache *resourceCache
|
|
|
|
profile string
|
|
timers *timers
|
|
frameStart time.Time
|
|
zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer
|
|
drawOps drawOps
|
|
ctx Backend
|
|
renderer *renderer
|
|
}
|
|
|
|
type renderer struct {
|
|
ctx Backend
|
|
blitter *blitter
|
|
pather *pather
|
|
packer packer
|
|
intersections packer
|
|
}
|
|
|
|
type drawOps struct {
|
|
profile bool
|
|
reader ops.Reader
|
|
cache *resourceCache
|
|
viewport image.Point
|
|
clearColor [3]float32
|
|
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
|
|
}
|
|
|
|
type drawState struct {
|
|
clip f32.Rectangle
|
|
t op.TransformOp
|
|
cpath *pathOp
|
|
rect bool
|
|
z int
|
|
|
|
matType materialType
|
|
// Current paint.ImageOp
|
|
image imageOpData
|
|
// Current paint.ColorOp, if any.
|
|
color color.RGBA
|
|
}
|
|
|
|
type pathOp struct {
|
|
off f32.Point
|
|
// clip is the union of all
|
|
// later clip rectangles.
|
|
clip image.Rectangle
|
|
pathKey ops.Key
|
|
path bool
|
|
pathVerts []byte
|
|
parent *pathOp
|
|
place placement
|
|
}
|
|
|
|
type imageOp struct {
|
|
z float32
|
|
path *pathOp
|
|
off f32.Point
|
|
clip image.Rectangle
|
|
material material
|
|
clipType clipType
|
|
place placement
|
|
}
|
|
|
|
type material struct {
|
|
material materialType
|
|
opaque bool
|
|
// For materialTypeColor.
|
|
color [4]float32
|
|
// For materialTypeTexture.
|
|
texture *texture
|
|
uvScale f32.Point
|
|
uvOffset f32.Point
|
|
}
|
|
|
|
// clipOp is the shadow of clip.Op.
|
|
type clipOp struct {
|
|
bounds f32.Rectangle
|
|
}
|
|
|
|
// imageOpData is the shadow of paint.ImageOp.
|
|
type imageOpData struct {
|
|
rect image.Rectangle
|
|
src *image.RGBA
|
|
handle interface{}
|
|
}
|
|
|
|
func (op *clipOp) decode(data []byte) {
|
|
if opconst.OpType(data[0]) != opconst.TypeClip {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
r := f32.Rectangle{
|
|
Min: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[1:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[5:])),
|
|
},
|
|
Max: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[9:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[13:])),
|
|
},
|
|
}
|
|
*op = clipOp{
|
|
bounds: r,
|
|
}
|
|
}
|
|
|
|
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{}
|
|
}
|
|
bo := binary.LittleEndian
|
|
return imageOpData{
|
|
rect: image.Rectangle{
|
|
Min: image.Point{
|
|
X: int(bo.Uint32(data[1:])),
|
|
Y: int(bo.Uint32(data[5:])),
|
|
},
|
|
Max: image.Point{
|
|
X: int(bo.Uint32(data[9:])),
|
|
Y: int(bo.Uint32(data[13:])),
|
|
},
|
|
},
|
|
src: refs[0].(*image.RGBA),
|
|
handle: handle,
|
|
}
|
|
}
|
|
|
|
func decodeColorOp(data []byte) color.RGBA {
|
|
if opconst.OpType(data[0]) != opconst.TypeColor {
|
|
panic("invalid op")
|
|
}
|
|
return color.RGBA{
|
|
R: data[1],
|
|
G: data[2],
|
|
B: data[3],
|
|
A: data[4],
|
|
}
|
|
}
|
|
|
|
func decodePaintOp(data []byte) paint.PaintOp {
|
|
bo := binary.LittleEndian
|
|
if opconst.OpType(data[0]) != opconst.TypePaint {
|
|
panic("invalid op")
|
|
}
|
|
r := f32.Rectangle{
|
|
Min: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[1:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[5:])),
|
|
},
|
|
Max: f32.Point{
|
|
X: math.Float32frombits(bo.Uint32(data[9:])),
|
|
Y: math.Float32frombits(bo.Uint32(data[13:])),
|
|
},
|
|
}
|
|
return paint.PaintOp{
|
|
Rect: r,
|
|
}
|
|
}
|
|
|
|
type clipType uint8
|
|
|
|
type resource interface {
|
|
release()
|
|
}
|
|
|
|
type texture struct {
|
|
src *image.RGBA
|
|
tex Texture
|
|
}
|
|
|
|
type blitter struct {
|
|
ctx Backend
|
|
viewport image.Point
|
|
prog [2]*program
|
|
layout InputLayout
|
|
colUniforms struct {
|
|
vert struct {
|
|
blitUniforms
|
|
_ [8]byte // Padding to a multiple of 16.
|
|
}
|
|
frag struct {
|
|
colorUniforms
|
|
}
|
|
}
|
|
texUniforms struct {
|
|
vert struct {
|
|
blitUniforms
|
|
_ [8]byte // Padding to a multiple of 16.
|
|
}
|
|
}
|
|
quadVerts Buffer
|
|
}
|
|
|
|
type uniformBuffer struct {
|
|
buf Buffer
|
|
ptr []byte
|
|
}
|
|
|
|
type program struct {
|
|
prog Program
|
|
vertUniforms *uniformBuffer
|
|
fragUniforms *uniformBuffer
|
|
}
|
|
|
|
type blitUniforms struct {
|
|
z float32
|
|
_ float32 // Padding.
|
|
scale [2]float32
|
|
offset [2]float32
|
|
uvScale [2]float32
|
|
uvOffset [2]float32
|
|
}
|
|
|
|
type colorUniforms struct {
|
|
color [4]float32
|
|
}
|
|
|
|
type materialType uint8
|
|
|
|
const (
|
|
clipTypeNone clipType = iota
|
|
clipTypePath
|
|
clipTypeIntersection
|
|
)
|
|
|
|
const (
|
|
materialColor materialType = iota
|
|
materialTexture
|
|
)
|
|
|
|
const (
|
|
attribPos = 0
|
|
attribUV = 1
|
|
)
|
|
|
|
func New(ctx Backend) (*GPU, error) {
|
|
g := &GPU{
|
|
pathCache: newOpCache(),
|
|
cache: newResourceCache(),
|
|
}
|
|
if err := g.init(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (g *GPU) init(ctx Backend) error {
|
|
g.ctx = ctx
|
|
g.renderer = newRenderer(ctx)
|
|
return nil
|
|
}
|
|
|
|
func (g *GPU) Release() {
|
|
g.renderer.release()
|
|
g.pathCache.release()
|
|
g.cache.release()
|
|
if g.timers != nil {
|
|
g.timers.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.cache, frameOps, viewport)
|
|
g.frameStart = time.Now()
|
|
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(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()
|
|
}
|
|
for _, p := range g.drawOps.pathOps {
|
|
if _, exists := g.pathCache.get(p.pathKey); !exists {
|
|
data := buildPath(g.ctx, p.pathVerts)
|
|
g.pathCache.put(p.pathKey, data)
|
|
}
|
|
p.pathVerts = nil
|
|
}
|
|
}
|
|
|
|
func (g *GPU) BeginFrame() {
|
|
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.DepthFunc(DepthFuncGreater)
|
|
g.ctx.ClearColor(g.drawOps.clearColor[0], g.drawOps.clearColor[1], g.drawOps.clearColor[2], 1.0)
|
|
g.ctx.ClearDepth(0.0)
|
|
g.ctx.Clear(BufferAttachmentColor | BufferAttachmentDepth)
|
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
|
g.renderer.drawZOps(g.drawOps.zimageOps)
|
|
g.zopsTimer.end()
|
|
g.stencilTimer.begin()
|
|
g.ctx.SetBlend(true)
|
|
g.renderer.packStencils(&g.drawOps.pathOps)
|
|
g.renderer.stencilClips(g.pathCache, g.drawOps.pathOps)
|
|
g.renderer.packIntersections(g.drawOps.imageOps)
|
|
g.renderer.intersect(g.drawOps.imageOps)
|
|
g.stencilTimer.end()
|
|
g.coverTimer.begin()
|
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
|
g.renderer.drawOps(g.drawOps.imageOps)
|
|
g.ctx.SetBlend(false)
|
|
g.renderer.pather.stenciler.invalidateFBO()
|
|
g.coverTimer.end()
|
|
}
|
|
|
|
func (g *GPU) EndFrame() {
|
|
g.cleanupTimer.begin()
|
|
g.cache.frame()
|
|
g.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)
|
|
}
|
|
}
|
|
|
|
func (g *GPU) Profile() string {
|
|
return g.profile
|
|
}
|
|
|
|
func (r *renderer) texHandle(t *texture) Texture {
|
|
if t.tex != nil {
|
|
return t.tex
|
|
}
|
|
t.tex = r.ctx.NewTexture(FilterLinear, FilterLinear)
|
|
t.tex.Upload(t.src)
|
|
return t.tex
|
|
}
|
|
|
|
func (t *texture) release() {
|
|
if t.tex != nil {
|
|
t.tex.Release()
|
|
}
|
|
}
|
|
|
|
func newRenderer(ctx Backend) *renderer {
|
|
r := &renderer{
|
|
ctx: ctx,
|
|
blitter: newBlitter(ctx),
|
|
pather: newPather(ctx),
|
|
}
|
|
r.packer.maxDim = ctx.Caps().MaxTextureSize
|
|
r.intersections.maxDim = r.packer.maxDim
|
|
return r
|
|
}
|
|
|
|
func (r *renderer) release() {
|
|
r.pather.release()
|
|
r.blitter.release()
|
|
}
|
|
|
|
func newBlitter(ctx Backend) *blitter {
|
|
quadVerts := ctx.NewImmutableBuffer(BufferTypeVertices,
|
|
gunsafe.BytesView([]float32{
|
|
-1, +1, 0, 0,
|
|
+1, +1, 1, 0,
|
|
-1, -1, 0, 1,
|
|
+1, -1, 1, 1,
|
|
}),
|
|
)
|
|
b := &blitter{
|
|
ctx: ctx,
|
|
quadVerts: quadVerts,
|
|
}
|
|
prog, layout, err := createColorPrograms(ctx, shader_blit_vert, shader_blit_frag,
|
|
[2]interface{}{&b.colUniforms.vert, &b.texUniforms.vert}, [2]interface{}{&b.colUniforms.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 Backend, vsSrc ShaderSources, fsSrc [2]ShaderSources, vertUniforms, fragUniforms [2]interface{}) ([2]*program, InputLayout, error) {
|
|
var progs [2]*program
|
|
prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture])
|
|
if err != nil {
|
|
return progs, nil, err
|
|
}
|
|
var vertBuffer *uniformBuffer
|
|
if u := vertUniforms[materialTexture]; u != nil {
|
|
vertBuffer = newUniformBuffer(b, u)
|
|
prog.SetVertexUniforms(vertBuffer.buf)
|
|
}
|
|
var fragBuffer *uniformBuffer
|
|
if u := fragUniforms[materialTexture]; u != nil {
|
|
fragBuffer = newUniformBuffer(b, u)
|
|
prog.SetFragmentUniforms(fragBuffer.buf)
|
|
}
|
|
progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer)
|
|
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)
|
|
layout, err := b.NewInputLayout(vsSrc, []InputDesc{
|
|
{Type: DataTypeFloat, Size: 2, Offset: 0},
|
|
{Type: DataTypeFloat, Size: 2, Offset: 4 * 2},
|
|
})
|
|
if err != nil {
|
|
progs[materialTexture].Release()
|
|
progs[materialColor].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)
|
|
bindFramebuffer(f.fbo)
|
|
r.ctx.Clear(BufferAttachmentColor)
|
|
}
|
|
data, _ := pathCache.get(p.pathKey)
|
|
r.pather.stencilPath(p.clip, p.off, p.place.Pos, data.(*pathData))
|
|
}
|
|
r.pather.end()
|
|
}
|
|
|
|
func (r *renderer) intersect(ops []imageOp) {
|
|
if len(r.intersections.sizes) == 0 {
|
|
return
|
|
}
|
|
fbo := -1
|
|
r.pather.stenciler.beginIntersect(r.intersections.sizes)
|
|
r.blitter.quadVerts.BindVertex(4*4, 0)
|
|
r.pather.stenciler.iprog.layout.Bind()
|
|
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]
|
|
bindFramebuffer(f.fbo)
|
|
r.ctx.Clear(BufferAttachmentColor)
|
|
}
|
|
r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
|
|
r.intersectPath(img.path, img.clip)
|
|
}
|
|
r.pather.stenciler.endIntersect()
|
|
}
|
|
|
|
func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
|
|
if p.parent != nil {
|
|
r.intersectPath(p.parent, clip)
|
|
}
|
|
if !p.path {
|
|
return
|
|
}
|
|
o := p.place.Pos.Add(clip.Min).Sub(p.clip.Min)
|
|
uv := image.Rectangle{
|
|
Min: o,
|
|
Max: o.Add(clip.Size()),
|
|
}
|
|
fbo := r.pather.stenciler.cover(p.place.Idx)
|
|
fbo.tex.Bind(0)
|
|
coverScale, coverOff := texSpaceTransform(toRectF(uv), fbo.size)
|
|
r.pather.stenciler.iprog.uniforms.vert.uvScale = [2]float32{coverScale.X, coverScale.Y}
|
|
r.pather.stenciler.iprog.uniforms.vert.uvOffset = [2]float32{coverOff.X, coverOff.Y}
|
|
r.pather.stenciler.iprog.prog.UploadUniforms()
|
|
r.ctx.DrawArrays(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 toRectF(r image.Rectangle) f32.Rectangle {
|
|
return f32.Rectangle{
|
|
Min: f32.Point{
|
|
X: float32(r.Min.X),
|
|
Y: float32(r.Min.Y),
|
|
},
|
|
Max: f32.Point{
|
|
X: float32(r.Max.X),
|
|
Y: float32(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.clearColor = [3]float32{1.0, 1.0, 1.0}
|
|
d.cache = cache
|
|
d.viewport = viewport
|
|
d.imageOps = d.imageOps[:0]
|
|
d.zimageOps = d.zimageOps[:0]
|
|
d.pathOps = d.pathOps[:0]
|
|
d.pathOpCache = d.pathOpCache[:0]
|
|
}
|
|
|
|
func (d *drawOps) collect(cache *resourceCache, root *op.Ops, viewport image.Point) {
|
|
d.reset(cache, viewport)
|
|
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.RGBA{A: 0xff},
|
|
}
|
|
d.collectOps(&d.reader, state)
|
|
}
|
|
|
|
func (d *drawOps) newPathOp() *pathOp {
|
|
d.pathOpCache = append(d.pathOpCache, pathOp{})
|
|
return &d.pathOpCache[len(d.pathOpCache)-1]
|
|
}
|
|
|
|
func (d *drawOps) collectOps(r *ops.Reader, state drawState) int {
|
|
var aux []byte
|
|
var auxKey ops.Key
|
|
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.DecodeTransformOp(encOp.Data)
|
|
state.t = state.t.Multiply(op.TransformOp(dop))
|
|
case opconst.TypeAux:
|
|
aux = encOp.Data[opconst.TypeAuxLen:]
|
|
// The first data byte stores whether the MaxY
|
|
// fields have been initialized.
|
|
maxyFilled := aux[0] == 1
|
|
aux[0] = 1
|
|
aux = aux[1:]
|
|
if !maxyFilled {
|
|
fillMaxY(aux)
|
|
}
|
|
auxKey = encOp.Key
|
|
case opconst.TypeClip:
|
|
var op clipOp
|
|
op.decode(encOp.Data)
|
|
off := state.t.Transform(f32.Point{})
|
|
state.clip = state.clip.Intersect(op.bounds.Add(off))
|
|
if state.clip.Empty() {
|
|
continue
|
|
}
|
|
npath := d.newPathOp()
|
|
*npath = pathOp{
|
|
parent: state.cpath,
|
|
off: off,
|
|
}
|
|
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)
|
|
}
|
|
aux = nil
|
|
auxKey = ops.Key{}
|
|
case opconst.TypeColor:
|
|
state.matType = materialColor
|
|
state.color = decodeColorOp(encOp.Data)
|
|
case opconst.TypeImage:
|
|
state.matType = materialTexture
|
|
state.image = decodeImageOp(encOp.Data, encOp.Refs)
|
|
case opconst.TypePaint:
|
|
op := decodePaintOp(encOp.Data)
|
|
off := state.t.Transform(f32.Point{})
|
|
clip := state.clip.Intersect(op.Rect.Add(off))
|
|
if clip.Empty() {
|
|
continue
|
|
}
|
|
bounds := boundRectF(clip)
|
|
mat := state.materialFor(d.cache, op.Rect, off, bounds)
|
|
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.zimageOps = d.zimageOps[:0]
|
|
d.imageOps = d.imageOps[:0]
|
|
state.z = 0
|
|
copy(d.clearColor[:], mat.color[:3])
|
|
continue
|
|
}
|
|
state.z++
|
|
// Assume 16-bit depth buffer.
|
|
const zdepth = 1 << 16
|
|
// Convert z to window-space, assuming depth range [0;1].
|
|
zf := float32(state.z)*2/zdepth - 1.0
|
|
img := imageOp{
|
|
z: zf,
|
|
path: state.cpath,
|
|
off: off,
|
|
clip: bounds,
|
|
material: mat,
|
|
}
|
|
if state.rect && img.material.opaque {
|
|
d.zimageOps = append(d.zimageOps, img)
|
|
} else {
|
|
d.imageOps = append(d.imageOps, img)
|
|
}
|
|
case opconst.TypePush:
|
|
state.z = d.collectOps(r, state)
|
|
case opconst.TypePop:
|
|
break loop
|
|
}
|
|
}
|
|
return state.z
|
|
}
|
|
|
|
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(cache *resourceCache, rect f32.Rectangle, off f32.Point, clip image.Rectangle) material {
|
|
var m material
|
|
switch d.matType {
|
|
case materialColor:
|
|
m.material = materialColor
|
|
m.color = gamma(d.color.RGBA())
|
|
m.opaque = m.color[3] == 1.0
|
|
case materialTexture:
|
|
m.material = materialTexture
|
|
dr := boundRectF(rect.Add(off))
|
|
sz := d.image.src.Bounds().Size()
|
|
sr := toRectF(d.image.rect)
|
|
if dx := float32(dr.Dx()); dx != 0 {
|
|
// Don't clip 1 px width sources.
|
|
if sdx := sr.Dx(); sdx > 1 {
|
|
sr.Min.X += (float32(clip.Min.X-dr.Min.X)*sdx + dx/2) / dx
|
|
sr.Max.X -= (float32(dr.Max.X-clip.Max.X)*sdx + dx/2) / dx
|
|
}
|
|
}
|
|
if dy := float32(dr.Dy()); dy != 0 {
|
|
// Don't clip 1 px height sources.
|
|
if sdy := sr.Dy(); sdy > 1 {
|
|
sr.Min.Y += (float32(clip.Min.Y-dr.Min.Y)*sdy + dy/2) / dy
|
|
sr.Max.Y -= (float32(dr.Max.Y-clip.Max.Y)*sdy + dy/2) / dy
|
|
}
|
|
}
|
|
tex, exists := cache.get(d.image.handle)
|
|
if !exists {
|
|
t := &texture{
|
|
src: d.image.src,
|
|
}
|
|
cache.put(d.image.handle, t)
|
|
tex = t
|
|
}
|
|
m.texture = tex.(*texture)
|
|
m.uvScale, m.uvOffset = texSpaceTransform(sr, sz)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *renderer) drawZOps(ops []imageOp) {
|
|
r.ctx.SetDepthTest(true)
|
|
r.blitter.quadVerts.BindVertex(4*4, 0)
|
|
r.blitter.layout.Bind()
|
|
// Render front to back.
|
|
for i := len(ops) - 1; i >= 0; i-- {
|
|
img := ops[i]
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.texHandle(m.texture).Bind(0)
|
|
}
|
|
drc := img.clip
|
|
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
|
|
r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset)
|
|
}
|
|
r.ctx.SetDepthTest(false)
|
|
}
|
|
|
|
func (r *renderer) drawOps(ops []imageOp) {
|
|
r.ctx.SetDepthTest(true)
|
|
r.ctx.DepthMask(false)
|
|
r.ctx.BlendFunc(BlendFactorOne, BlendFactorOneMinusSrcAlpha)
|
|
r.blitter.quadVerts.BindVertex(4*4, 0)
|
|
r.pather.coverer.layout.Bind()
|
|
var coverTex Texture
|
|
for _, img := range ops {
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.texHandle(m.texture).Bind(0)
|
|
}
|
|
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, scale, off, m.uvScale, m.uvOffset)
|
|
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
|
|
coverTex.Bind(1)
|
|
}
|
|
uv := image.Rectangle{
|
|
Min: img.place.Pos,
|
|
Max: img.place.Pos.Add(drc.Size()),
|
|
}
|
|
coverScale, coverOff := texSpaceTransform(toRectF(uv), fbo.size)
|
|
r.pather.cover(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset, coverScale, coverOff)
|
|
}
|
|
r.ctx.DepthMask(true)
|
|
r.ctx.SetDepthTest(false)
|
|
}
|
|
|
|
func gamma(r, g, b, a uint32) [4]float32 {
|
|
color := [4]float32{float32(r) / 0xffff, float32(g) / 0xffff, float32(b) / 0xffff, float32(a) / 0xffff}
|
|
// Assume that image.Uniform colors are in sRGB space. Linearize.
|
|
for i := 0; i <= 2; i++ {
|
|
c := color[i]
|
|
// Use the formula from EXT_sRGB.
|
|
if c <= 0.04045 {
|
|
c = c / 12.92
|
|
} else {
|
|
c = float32(math.Pow(float64((c+0.055)/1.055), 2.4))
|
|
}
|
|
color[i] = c
|
|
}
|
|
return color
|
|
}
|
|
|
|
func (b *blitter) blit(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff f32.Point) {
|
|
p := b.prog[mat]
|
|
p.prog.Bind()
|
|
var uniforms *blitUniforms
|
|
switch mat {
|
|
case materialColor:
|
|
b.colUniforms.frag.color = col
|
|
uniforms = &b.colUniforms.vert.blitUniforms
|
|
case materialTexture:
|
|
b.texUniforms.vert.uvScale = [2]float32{uvScale.X, uvScale.Y}
|
|
b.texUniforms.vert.uvOffset = [2]float32{uvOff.X, uvOff.Y}
|
|
uniforms = &b.texUniforms.vert.blitUniforms
|
|
}
|
|
uniforms.z = z
|
|
uniforms.scale = [2]float32{scale.X, scale.Y}
|
|
uniforms.offset = [2]float32{off.X, off.Y}
|
|
p.UploadUniforms()
|
|
b.ctx.DrawArrays(DrawModeTriangleStrip, 0, 4)
|
|
}
|
|
|
|
// newUniformBuffer creates a new GPU uniform buffer backed by the
|
|
// structure uniformBlock points to.
|
|
func newUniformBuffer(b Backend, 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 := b.NewBuffer(BufferTypeUniforms, len(ptr))
|
|
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 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func bindFramebuffer(fbo Framebuffer) {
|
|
fbo.Bind()
|
|
if err := fbo.IsComplete(); err != nil {
|
|
panic(fmt.Errorf("AA FBO not complete: %v", err))
|
|
}
|
|
}
|
|
|
|
// 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+path.VertStride*4 <= len(verts); i += path.VertStride * 4 {
|
|
vert := verts[i : i+path.VertStride]
|
|
// MaxY contains the integer contour index.
|
|
pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)):]))
|
|
if contour != pathContour {
|
|
contour = pathContour
|
|
break
|
|
}
|
|
fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).FromY)):]))
|
|
ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).CtrlY)):]))
|
|
toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.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 += path.VertStride {
|
|
off := int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY))
|
|
bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
|
|
}
|
|
}
|