Files
gio/ui/app/internal/gpu/gpu.go
T
Elias Naur 6e26c92c75 all: remove exported Decode methods on operations
Add decode functions to the packages that need them instead. For
TransformOp that is used in multiple packages, add the decode
function to the internal ops package.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-08-07 17:21:34 +02:00

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/ui/app/internal/gl"
gdraw "gioui.org/ui/draw"
"gioui.org/ui/f32"
"gioui.org/ui/internal/opconst"
"gioui.org/ui/internal/ops"
"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{}) gdraw.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 gdraw.ImageOp{
Src: refs[0].(image.Image),
Rect: sr,
}
}
func decodeColorOp(data []byte, refs []interface{}) gdraw.ColorOp {
if opconst.OpType(data[0]) != opconst.TypeColor {
panic("invalid op")
}
return gdraw.ColorOp{
Color: color.RGBA{
R: data[1],
G: data[2],
B: data[3],
A: data[4],
},
}
}
func decodeDrawOp(data []byte, refs []interface{}) gdraw.DrawOp {
bo := binary.LittleEndian
if opconst.OpType(data[0]) != opconst.TypeDraw {
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 gdraw.DrawOp{
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 {
data, exists := g.pathCache.get(p.pathKey)
if !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, 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.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,
}
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, encOp.Refs)
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.TypeDraw:
op := decodeDrawOp(encOp.Data, encOp.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 && 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;
}
`