mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 23:55:39 +00:00
5966aab77e
To prepare support for cached OpBlock to refer to other Ops lists. The exposure of OpsReader is alleviated by the removal of the Refs and Data accessors for Ops. Signed-off-by: Elias Naur <mail@eliasnaur.com>
1027 lines
25 KiB
Go
1027 lines
25 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package gpu
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"gioui.org/ui"
|
|
"gioui.org/ui/app/internal/gl"
|
|
gdraw "gioui.org/ui/draw"
|
|
"gioui.org/ui/f32"
|
|
"gioui.org/ui/internal/ops"
|
|
"gioui.org/ui/internal/path"
|
|
"golang.org/x/image/draw"
|
|
)
|
|
|
|
type GPU struct {
|
|
drawing bool
|
|
summary string
|
|
err error
|
|
|
|
cache *resourceCache
|
|
|
|
frames chan frame
|
|
results chan frameResult
|
|
refresh chan struct{}
|
|
refreshErr chan error
|
|
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 ui.OpsReader
|
|
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.Transform
|
|
cpath *pathOp
|
|
rect bool
|
|
z int
|
|
|
|
// Current OpImage image and rect, if any.
|
|
img image.Image
|
|
imgRect image.Rectangle
|
|
// Current OpColor, if any.
|
|
color color.NRGBA
|
|
}
|
|
|
|
type pathOp struct {
|
|
off f32.Point
|
|
// clip is the union of all
|
|
// later clip rectangles.
|
|
clip image.Rectangle
|
|
path *path.Path
|
|
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
|
|
}
|
|
|
|
type clipType uint8
|
|
|
|
type resourceCache struct {
|
|
res map[interface{}]resource
|
|
newRes map[interface{}]resource
|
|
}
|
|
|
|
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),
|
|
stop: make(chan struct{}),
|
|
stopped: make(chan struct{}),
|
|
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)
|
|
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
|
|
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.cache, 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)
|
|
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, cleant = zt.Round(q), st.Round(q), covt.Round(q), cleant.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.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 newResourceCache() *resourceCache {
|
|
return &resourceCache{
|
|
res: make(map[interface{}]resource),
|
|
newRes: make(map[interface{}]resource),
|
|
}
|
|
}
|
|
|
|
func (r *resourceCache) get(key interface{}) (resource, bool) {
|
|
v, exists := r.res[key]
|
|
if exists {
|
|
r.newRes[key] = v
|
|
}
|
|
return v, exists
|
|
}
|
|
|
|
func (r *resourceCache) put(key interface{}, val resource) {
|
|
if _, exists := r.newRes[key]; exists {
|
|
panic(fmt.Errorf("key exists, %p", key))
|
|
}
|
|
r.res[key] = val
|
|
r.newRes[key] = val
|
|
}
|
|
|
|
func (r *resourceCache) frame(ctx *context) {
|
|
for k, v := range r.res {
|
|
if _, exists := r.newRes[k]; !exists {
|
|
delete(r.res, k)
|
|
v.release(ctx)
|
|
}
|
|
}
|
|
for k, v := range r.newRes {
|
|
delete(r.newRes, k)
|
|
r.res[k] = v
|
|
}
|
|
}
|
|
|
|
func (r *resourceCache) release(ctx *context) {
|
|
for _, v := range r.newRes {
|
|
v.release(ctx)
|
|
}
|
|
r.newRes = nil
|
|
r.res = nil
|
|
}
|
|
|
|
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(cache *resourceCache, 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, exists := cache.get(p.path)
|
|
if !exists {
|
|
data = buildPath(r.ctx, p.path)
|
|
cache.put(p.path, data)
|
|
}
|
|
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 == nil {
|
|
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 != nil {
|
|
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 {
|
|
switch {
|
|
case math.IsInf(float64(v), +1):
|
|
return ui.Inf
|
|
case math.IsInf(float64(v), -1):
|
|
return -ui.Inf
|
|
default:
|
|
return int(math.Ceil(float64(v)))
|
|
}
|
|
}
|
|
|
|
func floor(v float32) int {
|
|
switch {
|
|
case math.IsInf(float64(v), +1):
|
|
return ui.Inf
|
|
case math.IsInf(float64(v), -1):
|
|
return -ui.Inf
|
|
default:
|
|
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,
|
|
}
|
|
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 *ui.OpsReader, state drawState) int {
|
|
loop:
|
|
for {
|
|
data, ok := r.Decode()
|
|
if !ok {
|
|
break
|
|
}
|
|
switch ops.OpType(data[0]) {
|
|
case ops.TypeTransform:
|
|
var op ui.OpTransform
|
|
op.Decode(data)
|
|
state.t = state.t.Mul(op.Transform)
|
|
case ops.TypeClip:
|
|
var op gdraw.OpClip
|
|
op.Decode(data, r.Refs)
|
|
if op.Path == nil {
|
|
state.clip = f32.Rectangle{}
|
|
continue
|
|
}
|
|
data := op.Path.Data().(*path.Path)
|
|
off := state.t.Transform(f32.Point{})
|
|
state.clip = state.clip.Intersect(data.Bounds.Add(off))
|
|
if state.clip.Empty() {
|
|
continue
|
|
}
|
|
npath := d.newPathOp()
|
|
*npath = pathOp{
|
|
parent: state.cpath,
|
|
off: off,
|
|
}
|
|
state.cpath = npath
|
|
if len(data.Vertices) > 0 {
|
|
state.rect = false
|
|
state.cpath.path = data
|
|
d.pathOps = append(d.pathOps, state.cpath)
|
|
}
|
|
case ops.TypeColor:
|
|
var op gdraw.OpColor
|
|
op.Decode(data, r.Refs)
|
|
state.img = nil
|
|
state.color = op.Col
|
|
case ops.TypeImage:
|
|
var op gdraw.OpImage
|
|
op.Decode(data, r.Refs)
|
|
state.img = op.Img
|
|
state.imgRect = op.Rect
|
|
case ops.TypeDraw:
|
|
var op gdraw.OpDraw
|
|
op.Decode(data, r.Refs)
|
|
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 && 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 ops.TypePush:
|
|
state.z = d.collectOps(r, state)
|
|
case ops.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;
|
|
}
|
|
`
|