Files
gio-patched/ui/app/internal/gpu/gpu.go
T
Elias Naur 0061c73a89 ui: split OpImage into OpImage and OpDraw
In preparation for an OpColor (and future OpGradient and similar).

Label and Editor no longer take an explicit source image. They
draw with the current image.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-06-01 12:44:49 +02:00

1011 lines
24 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import (
"fmt"
"image"
"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 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
// Current OpImage image and rect, if any.
img image.Image
imgRect image.Rectangle
}
type drawState struct {
clip f32.Rectangle
t ui.Transform
cpath *pathOp
rect bool
z int
}
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.Data(), root.Refs())
d.collectOps(&d.reader, clip, ui.Transform{}, nil, true, 0)
}
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, clip f32.Rectangle, t ui.Transform, cpath *pathOp, rect bool, z int) 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)
t = t.Mul(op.Transform)
case ops.TypeClip:
var op gdraw.OpClip
op.Decode(data, r.Refs)
if op.Path == nil {
clip = f32.Rectangle{}
continue
}
data := op.Path.Data().(*path.Path)
off := t.Transform(f32.Point{})
clip = clip.Intersect(data.Bounds.Add(off))
if clip.Empty() {
continue
}
npath := d.newPathOp()
*npath = pathOp{
parent: cpath,
off: off,
}
cpath = npath
if len(data.Vertices) > 0 {
rect = false
cpath.path = data
d.pathOps = append(d.pathOps, cpath)
}
case ops.TypeImage:
var op gdraw.OpImage
op.Decode(data, r.Refs)
d.img = op.Img
d.imgRect = op.Rect
case ops.TypeDraw:
var op gdraw.OpDraw
op.Decode(data, r.Refs)
off := t.Transform(f32.Point{})
clip := clip.Intersect(op.Rect.Add(off))
if clip.Empty() {
continue
}
bounds := boundRectF(clip)
mat := d.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]
z = 0
copy(d.clearColor[:], mat.color[:3])
continue
}
z++
// Assume 16-bit depth buffer.
const zdepth = 1 << 16
// Convert z to window-space, assuming depth range [0;1].
zf := float32(z)*2/zdepth - 1.0
img := imageOp{
z: zf,
path: cpath,
off: off,
clip: bounds,
material: mat,
}
if rect && img.material.opaque {
d.zimageOps = append(d.zimageOps, img)
} else {
d.imageOps = append(d.imageOps, img)
}
case ops.TypePush:
z = d.collectOps(r, clip, t, cpath, rect, z)
case ops.TypePop:
break loop
}
}
return 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 *drawOps) materialFor(cache *resourceCache, rect f32.Rectangle, off f32.Point, clip image.Rectangle) material {
var m material
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;
}
`