mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
22cd88df9f
The "ui" is redundant and stutters. Signed-off-by: Elias Naur <mail@eliasnaur.com>
1056 lines
26 KiB
Go
1056 lines
26 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package gpu
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"gioui.org/ui"
|
|
"gioui.org/app/internal/gl"
|
|
"gioui.org/f32"
|
|
"gioui.org/internal/opconst"
|
|
"gioui.org/internal/ops"
|
|
"gioui.org/paint"
|
|
"golang.org/x/image/draw"
|
|
)
|
|
|
|
type GPU struct {
|
|
drawing bool
|
|
summary string
|
|
err error
|
|
|
|
pathCache *opCache
|
|
cache *resourceCache
|
|
|
|
frames chan frame
|
|
results chan frameResult
|
|
refresh chan struct{}
|
|
refreshErr chan error
|
|
ack chan struct{}
|
|
stop chan struct{}
|
|
stopped chan struct{}
|
|
ops drawOps
|
|
}
|
|
|
|
type frame struct {
|
|
collectStats bool
|
|
viewport image.Point
|
|
ops drawOps
|
|
}
|
|
|
|
type frameResult struct {
|
|
summary string
|
|
err error
|
|
}
|
|
|
|
type renderer struct {
|
|
ctx *context
|
|
blitter *blitter
|
|
pather *pather
|
|
packer packer
|
|
intersections packer
|
|
}
|
|
|
|
type drawOps struct {
|
|
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 ui.TransformOp
|
|
cpath *pathOp
|
|
rect bool
|
|
z int
|
|
|
|
// Current ImageOp image and rect, if any.
|
|
img image.Image
|
|
imgRect image.Rectangle
|
|
// Current 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 draw.ClipOp.
|
|
type clipOp struct {
|
|
bounds f32.Rectangle
|
|
}
|
|
|
|
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{}) paint.ImageOp {
|
|
bo := binary.LittleEndian
|
|
if opconst.OpType(data[0]) != opconst.TypeImage {
|
|
panic("invalid op")
|
|
}
|
|
sr := 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:]))),
|
|
},
|
|
}
|
|
return paint.ImageOp{
|
|
Src: refs[0].(image.Image),
|
|
Rect: sr,
|
|
}
|
|
}
|
|
|
|
func decodeColorOp(data []byte) paint.ColorOp {
|
|
if opconst.OpType(data[0]) != opconst.TypeColor {
|
|
panic("invalid op")
|
|
}
|
|
return paint.ColorOp{
|
|
Color: 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(ctx *context)
|
|
}
|
|
|
|
type texture struct {
|
|
src image.Image
|
|
id gl.Texture
|
|
}
|
|
|
|
type blitter struct {
|
|
ctx *context
|
|
viewport image.Point
|
|
prog [2]gl.Program
|
|
vars [2]struct {
|
|
z gl.Uniform
|
|
uScale, uOffset gl.Uniform
|
|
uUVScale, uUVOffset gl.Uniform
|
|
uColor gl.Uniform
|
|
}
|
|
quadVerts gl.Buffer
|
|
}
|
|
|
|
type materialType uint8
|
|
|
|
const (
|
|
clipTypeNone clipType = iota
|
|
clipTypePath
|
|
clipTypeIntersection
|
|
)
|
|
|
|
const (
|
|
materialTexture materialType = iota
|
|
materialColor
|
|
)
|
|
|
|
var (
|
|
blitAttribs = []string{"pos", "uv"}
|
|
attribPos gl.Attrib = 0
|
|
attribUV gl.Attrib = 1
|
|
)
|
|
|
|
func NewGPU(ctx gl.Context) (*GPU, error) {
|
|
g := &GPU{
|
|
frames: make(chan frame),
|
|
results: make(chan frameResult),
|
|
refresh: make(chan struct{}),
|
|
refreshErr: make(chan error),
|
|
ack: make(chan struct{}),
|
|
stop: make(chan struct{}),
|
|
stopped: make(chan struct{}),
|
|
pathCache: newOpCache(),
|
|
cache: newResourceCache(),
|
|
}
|
|
if err := g.renderLoop(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (g *GPU) renderLoop(glctx gl.Context) error {
|
|
// GL Operations must happen on a single OS thread, so
|
|
// pass initialization result through a channel.
|
|
initErr := make(chan error)
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
// Don't UnlockOSThread to avoid reuse by the Go runtime.
|
|
defer close(g.stopped)
|
|
defer glctx.Release()
|
|
|
|
if err := glctx.MakeCurrent(); err != nil {
|
|
initErr <- err
|
|
return
|
|
}
|
|
ctx, err := newContext(glctx)
|
|
if err != nil {
|
|
initErr <- err
|
|
return
|
|
}
|
|
defer g.cache.release(ctx)
|
|
defer g.pathCache.release(ctx)
|
|
r := newRenderer(ctx)
|
|
defer r.release()
|
|
var timers *timers
|
|
var zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer
|
|
initErr <- nil
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-g.refresh:
|
|
g.refreshErr <- glctx.MakeCurrent()
|
|
case frame := <-g.frames:
|
|
glctx.Lock()
|
|
if frame.collectStats && timers == nil && ctx.caps.EXT_disjoint_timer_query {
|
|
timers = newTimers(ctx)
|
|
zopsTimer = timers.newTimer()
|
|
stencilTimer = timers.newTimer()
|
|
coverTimer = timers.newTimer()
|
|
cleanupTimer = timers.newTimer()
|
|
defer timers.release()
|
|
}
|
|
ops := frame.ops
|
|
// Upload path data to GPU before ack'ing the frame data for re-use.
|
|
for _, p := range ops.pathOps {
|
|
if _, exists := g.pathCache.get(p.pathKey); !exists {
|
|
data := buildPath(r.ctx, p.pathVerts)
|
|
g.pathCache.put(p.pathKey, data)
|
|
}
|
|
p.pathVerts = nil
|
|
}
|
|
g.ack <- struct{}{}
|
|
r.blitter.viewport = frame.viewport
|
|
r.pather.viewport = frame.viewport
|
|
for _, img := range ops.imageOps {
|
|
expandPathOp(img.path, img.clip)
|
|
}
|
|
if frame.collectStats {
|
|
zopsTimer.begin()
|
|
}
|
|
ctx.DepthFunc(gl.GREATER)
|
|
ctx.ClearColor(ops.clearColor[0], ops.clearColor[1], ops.clearColor[2], 1.0)
|
|
ctx.ClearDepthf(0.0)
|
|
ctx.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
|
ctx.Viewport(0, 0, frame.viewport.X, frame.viewport.Y)
|
|
r.drawZOps(ops.zimageOps)
|
|
zopsTimer.end()
|
|
stencilTimer.begin()
|
|
ctx.Enable(gl.BLEND)
|
|
r.packStencils(&ops.pathOps)
|
|
r.stencilClips(g.pathCache, ops.pathOps)
|
|
r.packIntersections(ops.imageOps)
|
|
r.intersect(ops.imageOps)
|
|
stencilTimer.end()
|
|
coverTimer.begin()
|
|
ctx.Viewport(0, 0, frame.viewport.X, frame.viewport.Y)
|
|
r.drawOps(ops.imageOps)
|
|
ctx.Disable(gl.BLEND)
|
|
r.pather.stenciler.invalidateFBO()
|
|
coverTimer.end()
|
|
err := glctx.Present()
|
|
cleanupTimer.begin()
|
|
g.cache.frame(ctx)
|
|
g.pathCache.frame(ctx)
|
|
cleanupTimer.end()
|
|
var res frameResult
|
|
if frame.collectStats && timers.ready() {
|
|
zt, st, covt, cleant := zopsTimer.Elapsed, stencilTimer.Elapsed, coverTimer.Elapsed, cleanupTimer.Elapsed
|
|
ft := zt + st + covt + cleant
|
|
q := 100 * time.Microsecond
|
|
zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q)
|
|
ft = ft.Round(q)
|
|
res.summary = fmt.Sprintf("f:%7s zt:%7s st:%7s cov:%7s", ft, zt, st, covt)
|
|
}
|
|
res.err = err
|
|
glctx.Unlock()
|
|
g.results <- res
|
|
case <-g.stop:
|
|
break loop
|
|
}
|
|
}
|
|
}()
|
|
return <-initErr
|
|
}
|
|
|
|
func (g *GPU) Release() {
|
|
// Flush error.
|
|
g.Flush()
|
|
close(g.stop)
|
|
<-g.stopped
|
|
g.stop = nil
|
|
}
|
|
|
|
func (g *GPU) Flush() error {
|
|
if g.drawing {
|
|
st := <-g.results
|
|
g.setErr(st.err)
|
|
if st.summary != "" {
|
|
g.summary = st.summary
|
|
}
|
|
g.drawing = false
|
|
}
|
|
return g.err
|
|
}
|
|
|
|
func (g *GPU) Timings() string {
|
|
return g.summary
|
|
}
|
|
|
|
func (g *GPU) Refresh() {
|
|
if g.err != nil {
|
|
return
|
|
}
|
|
// Make sure any pending frame is complete.
|
|
g.Flush()
|
|
g.refresh <- struct{}{}
|
|
g.setErr(<-g.refreshErr)
|
|
}
|
|
|
|
func (g *GPU) Draw(profile bool, viewport image.Point, root *ui.Ops) {
|
|
if g.err != nil {
|
|
return
|
|
}
|
|
g.Flush()
|
|
g.ops.reset(g.cache, viewport)
|
|
g.ops.collect(g.cache, root, viewport)
|
|
g.frames <- frame{profile, viewport, g.ops}
|
|
<-g.ack
|
|
g.drawing = true
|
|
}
|
|
|
|
func (g *GPU) setErr(err error) {
|
|
if g.err == nil {
|
|
g.err = err
|
|
}
|
|
}
|
|
|
|
func (r *renderer) texHandle(t *texture) gl.Texture {
|
|
if t.id != (gl.Texture{}) {
|
|
return t.id
|
|
}
|
|
t.id = createTexture(r.ctx)
|
|
r.ctx.BindTexture(gl.TEXTURE_2D, t.id)
|
|
r.uploadTexture(t.src)
|
|
return t.id
|
|
}
|
|
|
|
func (t *texture) release(ctx *context) {
|
|
if t.id != (gl.Texture{}) {
|
|
ctx.DeleteTexture(t.id)
|
|
}
|
|
}
|
|
|
|
func newRenderer(ctx *context) *renderer {
|
|
r := &renderer{
|
|
ctx: ctx,
|
|
blitter: newBlitter(ctx),
|
|
pather: newPather(ctx),
|
|
}
|
|
r.packer.maxDim = ctx.GetInteger(gl.MAX_TEXTURE_SIZE)
|
|
r.intersections.maxDim = r.packer.maxDim
|
|
return r
|
|
}
|
|
|
|
func (r *renderer) release() {
|
|
r.pather.release()
|
|
r.blitter.release()
|
|
}
|
|
|
|
func newBlitter(ctx *context) *blitter {
|
|
prog, err := createColorPrograms(ctx, blitVSrc, blitFSrc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
quadVerts := ctx.CreateBuffer()
|
|
ctx.BindBuffer(gl.ARRAY_BUFFER, quadVerts)
|
|
ctx.BufferData(gl.ARRAY_BUFFER,
|
|
gl.BytesView([]float32{
|
|
-1, +1, 0, 0,
|
|
+1, +1, 1, 0,
|
|
-1, -1, 0, 1,
|
|
+1, -1, 1, 1,
|
|
}),
|
|
gl.STATIC_DRAW)
|
|
b := &blitter{
|
|
ctx: ctx,
|
|
prog: prog,
|
|
quadVerts: quadVerts,
|
|
}
|
|
for i, prog := range prog {
|
|
ctx.UseProgram(prog)
|
|
switch materialType(i) {
|
|
case materialTexture:
|
|
uTex := gl.GetUniformLocation(ctx.Functions, prog, "tex")
|
|
ctx.Uniform1i(uTex, 0)
|
|
b.vars[i].uUVScale = gl.GetUniformLocation(ctx.Functions, prog, "uvScale")
|
|
b.vars[i].uUVOffset = gl.GetUniformLocation(ctx.Functions, prog, "uvOffset")
|
|
case materialColor:
|
|
b.vars[i].uColor = gl.GetUniformLocation(ctx.Functions, prog, "color")
|
|
}
|
|
b.vars[i].z = gl.GetUniformLocation(ctx.Functions, prog, "z")
|
|
b.vars[i].uScale = gl.GetUniformLocation(ctx.Functions, prog, "scale")
|
|
b.vars[i].uOffset = gl.GetUniformLocation(ctx.Functions, prog, "offset")
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *blitter) release() {
|
|
b.ctx.DeleteBuffer(b.quadVerts)
|
|
for _, p := range b.prog {
|
|
b.ctx.DeleteProgram(p)
|
|
}
|
|
}
|
|
|
|
func createColorPrograms(ctx *context, vsSrc, fsSrc string) ([2]gl.Program, error) {
|
|
var prog [2]gl.Program
|
|
frep := strings.NewReplacer(
|
|
"HEADER", `
|
|
uniform sampler2D tex;
|
|
`,
|
|
"GET_COLOR", `texture2D(tex, vUV)`,
|
|
)
|
|
fsSrcTex := frep.Replace(fsSrc)
|
|
var err error
|
|
prog[materialTexture], err = gl.CreateProgram(ctx.Functions, vsSrc, fsSrcTex, blitAttribs)
|
|
if err != nil {
|
|
return prog, err
|
|
}
|
|
frep = strings.NewReplacer(
|
|
"HEADER", `
|
|
uniform vec4 color;
|
|
`,
|
|
"GET_COLOR", `color`,
|
|
)
|
|
fsSrcCol := frep.Replace(fsSrc)
|
|
prog[materialColor], err = gl.CreateProgram(ctx.Functions, vsSrc, fsSrcCol, blitAttribs)
|
|
if err != nil {
|
|
ctx.DeleteProgram(prog[materialTexture])
|
|
return prog, err
|
|
}
|
|
return prog, 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(r.ctx, f.fbo)
|
|
r.ctx.Clear(gl.COLOR_BUFFER_BIT)
|
|
}
|
|
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.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts)
|
|
r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0)
|
|
r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2)
|
|
r.ctx.EnableVertexAttribArray(attribPos)
|
|
r.ctx.EnableVertexAttribArray(attribUV)
|
|
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(r.ctx, f.fbo)
|
|
r.ctx.Clear(gl.COLOR_BUFFER_BIT)
|
|
}
|
|
r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy())
|
|
r.intersectPath(img.path, img.clip)
|
|
}
|
|
r.ctx.DisableVertexAttribArray(attribPos)
|
|
r.ctx.DisableVertexAttribArray(attribUV)
|
|
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)
|
|
r.ctx.BindTexture(gl.TEXTURE_2D, fbo.tex)
|
|
coverScale, coverOff := texSpaceTransform(uv, fbo.size)
|
|
r.ctx.Uniform2f(r.pather.stenciler.uIntersectUVScale, coverScale.X, coverScale.Y)
|
|
r.ctx.Uniform2f(r.pather.stenciler.uIntersectUVOffset, coverOff.X, coverOff.Y)
|
|
r.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
}
|
|
|
|
func (r *renderer) packIntersections(ops []imageOp) {
|
|
r.intersections.clear()
|
|
for i, img := range ops {
|
|
var npaths int
|
|
var onePath *pathOp
|
|
for p := img.path; p != nil; p = p.parent {
|
|
if p.path {
|
|
onePath = p
|
|
npaths++
|
|
}
|
|
}
|
|
switch npaths {
|
|
case 0:
|
|
case 1:
|
|
place := onePath.place
|
|
place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
|
|
ops[i].place = place
|
|
ops[i].clipType = clipTypePath
|
|
default:
|
|
sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
|
|
place, ok := r.intersections.add(sz)
|
|
if !ok {
|
|
panic("internal error: if the intersection fit, the intersection should fit as well")
|
|
}
|
|
ops[i].clipType = clipTypeIntersection
|
|
ops[i].place = place
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *renderer) packStencils(pops *[]*pathOp) {
|
|
r.packer.clear()
|
|
ops := *pops
|
|
// Allocate atlas space for cover textures.
|
|
var i int
|
|
for i < len(ops) {
|
|
p := ops[i]
|
|
if p.clip.Empty() {
|
|
ops[i] = ops[len(ops)-1]
|
|
ops = ops[:len(ops)-1]
|
|
continue
|
|
}
|
|
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
|
|
place, ok := r.packer.add(sz)
|
|
if !ok {
|
|
// The clip area is at most the entire screen. Hopefully no
|
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
|
panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", p.clip, r.packer.maxDim, r.packer.maxDim))
|
|
}
|
|
p.place = place
|
|
i++
|
|
}
|
|
*pops = ops
|
|
}
|
|
|
|
// intersects intersects clip and b where b is offset by off.
|
|
// ceilRect returns a bounding image.Rectangle for a f32.Rectangle.
|
|
func boundRectF(r f32.Rectangle) image.Rectangle {
|
|
return image.Rectangle{
|
|
Min: image.Point{
|
|
X: int(floor(r.Min.X)),
|
|
Y: int(floor(r.Min.Y)),
|
|
},
|
|
Max: image.Point{
|
|
X: int(ceil(r.Max.X)),
|
|
Y: int(ceil(r.Max.Y)),
|
|
},
|
|
}
|
|
}
|
|
|
|
func ceil(v float32) int {
|
|
return int(math.Ceil(float64(v)))
|
|
}
|
|
|
|
func floor(v float32) int {
|
|
return int(math.Floor(float64(v)))
|
|
}
|
|
|
|
func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
|
|
d.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 *ui.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.TypeTransform:
|
|
op := ops.DecodeTransformOp(encOp.Data)
|
|
state.t = state.t.Multiply(ui.TransformOp(op))
|
|
case opconst.TypeAux:
|
|
aux = encOp.Data[opconst.TypeAuxLen:]
|
|
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:
|
|
op := decodeColorOp(encOp.Data)
|
|
state.img = nil
|
|
state.color = op.Color
|
|
case opconst.TypeImage:
|
|
op := decodeImageOp(encOp.Data, encOp.Refs)
|
|
state.img = op.Src
|
|
state.imgRect = op.Rect
|
|
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
|
|
if d.img == nil {
|
|
m.material = materialColor
|
|
m.color = gamma(d.color.RGBA())
|
|
m.opaque = m.color[3] == 1.0
|
|
} else if uniform, ok := d.img.(*image.Uniform); ok {
|
|
m.material = materialColor
|
|
m.color = gamma(uniform.RGBA())
|
|
m.opaque = m.color[3] == 1.0
|
|
} else {
|
|
m.material = materialTexture
|
|
dr := boundRectF(rect.Add(off))
|
|
sr := d.imgRect
|
|
if dx := dr.Dx(); dx != 0 {
|
|
// Don't clip 1 px width sources.
|
|
if sdx := sr.Dx(); sdx > 1 {
|
|
sr.Min.X += ((clip.Min.X-dr.Min.X)*sdx + dx/2) / dx
|
|
sr.Max.X -= ((dr.Max.X-clip.Max.X)*sdx + dx/2) / dx
|
|
}
|
|
}
|
|
if dy := dr.Dy(); dy != 0 {
|
|
// Don't clip 1 px height sources.
|
|
if sdy := sr.Dy(); sdy > 1 {
|
|
sr.Min.Y += ((clip.Min.Y-dr.Min.Y)*sdy + dy/2) / dy
|
|
sr.Max.Y -= ((dr.Max.Y-clip.Max.Y)*sdy + dy/2) / dy
|
|
}
|
|
}
|
|
tex, exists := cache.get(d.img)
|
|
if !exists {
|
|
t := &texture{
|
|
src: d.img,
|
|
}
|
|
cache.put(d.img, t)
|
|
tex = t
|
|
}
|
|
m.texture = tex.(*texture)
|
|
m.uvScale, m.uvOffset = texSpaceTransform(sr, d.img.Bounds().Size())
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *renderer) drawZOps(ops []imageOp) {
|
|
r.ctx.Enable(gl.DEPTH_TEST)
|
|
r.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts)
|
|
r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0)
|
|
r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2)
|
|
r.ctx.EnableVertexAttribArray(attribPos)
|
|
r.ctx.EnableVertexAttribArray(attribUV)
|
|
// Render front to back.
|
|
for i := len(ops) - 1; i >= 0; i-- {
|
|
img := ops[i]
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.BindTexture(gl.TEXTURE_2D, r.texHandle(m.texture))
|
|
}
|
|
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.DisableVertexAttribArray(attribPos)
|
|
r.ctx.DisableVertexAttribArray(attribUV)
|
|
r.ctx.Disable(gl.DEPTH_TEST)
|
|
}
|
|
|
|
func (r *renderer) drawOps(ops []imageOp) {
|
|
r.ctx.Enable(gl.DEPTH_TEST)
|
|
r.ctx.DepthMask(false)
|
|
r.ctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
|
r.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts)
|
|
r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0)
|
|
r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2)
|
|
r.ctx.EnableVertexAttribArray(attribPos)
|
|
r.ctx.EnableVertexAttribArray(attribUV)
|
|
var coverTex gl.Texture
|
|
for _, img := range ops {
|
|
m := img.material
|
|
switch m.material {
|
|
case materialTexture:
|
|
r.ctx.BindTexture(gl.TEXTURE_2D, r.texHandle(m.texture))
|
|
}
|
|
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
|
|
r.ctx.ActiveTexture(gl.TEXTURE1)
|
|
r.ctx.BindTexture(gl.TEXTURE_2D, coverTex)
|
|
r.ctx.ActiveTexture(gl.TEXTURE0)
|
|
}
|
|
uv := image.Rectangle{
|
|
Min: img.place.Pos,
|
|
Max: img.place.Pos.Add(drc.Size()),
|
|
}
|
|
coverScale, coverOff := texSpaceTransform(uv, fbo.size)
|
|
r.pather.cover(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset, coverScale, coverOff)
|
|
}
|
|
r.ctx.DisableVertexAttribArray(attribPos)
|
|
r.ctx.DisableVertexAttribArray(attribUV)
|
|
r.ctx.DepthMask(true)
|
|
r.ctx.Disable(gl.DEPTH_TEST)
|
|
}
|
|
|
|
func (r *renderer) uploadTexture(img image.Image) {
|
|
var pixels []byte
|
|
b := img.Bounds()
|
|
w, h := b.Dx(), b.Dy()
|
|
switch img := img.(type) {
|
|
case *image.RGBA:
|
|
if img.Stride == w*4 {
|
|
start := (b.Min.X + b.Min.Y*w) * 4
|
|
end := (b.Max.X + (b.Max.Y-1)*w) * 4
|
|
pixels = img.Pix[start:end]
|
|
} else {
|
|
pixels = copyImage(img, b).Pix
|
|
}
|
|
default:
|
|
pixels = copyImage(img, b).Pix
|
|
}
|
|
tt := r.ctx.caps.srgbaTriple
|
|
r.ctx.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, w, h, tt.format, tt.typ, pixels)
|
|
|
|
}
|
|
|
|
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, c := range color {
|
|
// 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) {
|
|
b.ctx.UseProgram(b.prog[mat])
|
|
switch mat {
|
|
case materialColor:
|
|
b.ctx.Uniform4f(b.vars[mat].uColor, col[0], col[1], col[2], col[3])
|
|
case materialTexture:
|
|
b.ctx.Uniform2f(b.vars[mat].uUVScale, uvScale.X, uvScale.Y)
|
|
b.ctx.Uniform2f(b.vars[mat].uUVOffset, uvOff.X, uvOff.Y)
|
|
}
|
|
b.ctx.Uniform1f(b.vars[mat].z, z)
|
|
b.ctx.Uniform2f(b.vars[mat].uScale, scale.X, scale.Y)
|
|
b.ctx.Uniform2f(b.vars[mat].uOffset, off.X, off.Y)
|
|
b.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
}
|
|
|
|
// texSpaceTransform return the scale and offset that transforms the given subimage
|
|
// into quad texture coordinates.
|
|
func texSpaceTransform(r image.Rectangle, bounds image.Point) (f32.Point, f32.Point) {
|
|
size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)}
|
|
scale := f32.Point{X: float32(r.Dx()) / size.X, Y: float32(r.Dy()) / size.Y}
|
|
offset := f32.Point{X: float32(r.Min.X) / size.X, Y: float32(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(ctx *context, fbo gl.Framebuffer) {
|
|
ctx.BindFramebuffer(gl.FRAMEBUFFER, fbo)
|
|
if st := ctx.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
|
|
panic(fmt.Errorf("AA FBO not complete; status = 0x%x, err = %d", st, ctx.GetError()))
|
|
}
|
|
}
|
|
|
|
func createTexture(ctx *context) gl.Texture {
|
|
tex := ctx.CreateTexture()
|
|
ctx.BindTexture(gl.TEXTURE_2D, tex)
|
|
ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
|
ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
|
|
ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
|
return tex
|
|
}
|
|
|
|
func copyImage(img image.Image, r image.Rectangle) *image.RGBA {
|
|
tmp := image.NewRGBA(r)
|
|
draw.Draw(tmp, r, img, r.Min, draw.Src)
|
|
return tmp
|
|
}
|
|
|
|
const blitVSrc = `
|
|
#version 100
|
|
|
|
precision highp float;
|
|
|
|
uniform float z;
|
|
uniform vec2 scale;
|
|
uniform vec2 offset;
|
|
|
|
attribute vec2 pos;
|
|
|
|
attribute vec2 uv;
|
|
uniform vec2 uvScale;
|
|
uniform vec2 uvOffset;
|
|
|
|
varying vec2 vUV;
|
|
|
|
void main() {
|
|
vec2 p = pos;
|
|
p *= scale;
|
|
p += offset;
|
|
gl_Position = vec4(p, z, 1);
|
|
vUV = uv*uvScale + uvOffset;
|
|
}
|
|
`
|
|
|
|
const blitFSrc = `
|
|
#version 100
|
|
|
|
precision mediump float;
|
|
|
|
varying vec2 vUV;
|
|
|
|
HEADER
|
|
|
|
void main() {
|
|
gl_FragColor = GET_COLOR;
|
|
}
|
|
`
|