exp/gl/glutil: remove global texture cache
It is now the user's job to track the lifetime of a glutil.Image
relative to a (currently implicit, soon to be explicit) GL context.
This is an attempt to move glutil.Image closer to the model for
buffers and textures in shiny. Long-term, I would like to adopt that
model, and this is a step in that direction. It also makes the
introduction of *gl.Context possible, so this is a pre-req for
cl/13431.
Change-Id: I8e6855211b3e67c97d5831c5c4e443e857c83d50
Reviewed-on: https://go-review.googlesource.com/14795
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/example/audio/main.go b/example/audio/main.go
index 3c4a84d..6d95568 100644
--- a/example/audio/main.go
+++ b/example/audio/main.go
@@ -44,9 +44,9 @@
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"
- "golang.org/x/mobile/exp/app/debug"
"golang.org/x/mobile/exp/audio"
"golang.org/x/mobile/exp/f32"
+ "golang.org/x/mobile/exp/gl/glutil"
"golang.org/x/mobile/exp/sprite"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/exp/sprite/glsprite"
@@ -61,8 +61,9 @@
var (
startTime = time.Now()
- eng = glsprite.Engine()
- scene *sprite.Node
+ images *glutil.Images
+ eng sprite.Engine
+ scene *sprite.Node
player *audio.Player
@@ -98,6 +99,10 @@
}
func onStart() {
+ images = glutil.NewImages()
+ eng = glsprite.Engine(images)
+ loadScene()
+
rc, err := asset.Open("boing.wav")
if err != nil {
log.Fatal(err)
@@ -109,18 +114,16 @@
}
func onStop() {
+ eng.Release()
+ images.Release()
player.Close()
}
func onPaint() {
- if scene == nil {
- loadScene()
- }
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
now := clock.Time(time.Since(startTime) * 60 / time.Second)
eng.Render(scene, now, sz)
- debug.DrawFPS(sz)
}
func newNode() *sprite.Node {
diff --git a/example/basic/main.go b/example/basic/main.go
index abec164..af93ce1 100644
--- a/example/basic/main.go
+++ b/example/basic/main.go
@@ -44,6 +44,8 @@
)
var (
+ images *glutil.Images
+ fps *debug.FPS
program gl.Program
position gl.Attrib
offset gl.Uniform
@@ -109,13 +111,15 @@
color = gl.GetUniformLocation(program, "color")
offset = gl.GetUniformLocation(program, "offset")
- // TODO(crawshaw): the debug package needs to put GL state init here
- // Can this be an app.RegisterFilter call now??
+ images = glutil.NewImages()
+ fps = debug.NewFPS(images)
}
func onStop() {
gl.DeleteProgram(program)
gl.DeleteBuffer(buf)
+ fps.Release()
+ images.Release()
}
func onPaint(sz size.Event) {
@@ -138,7 +142,7 @@
gl.DrawArrays(gl.TRIANGLES, 0, vertexCount)
gl.DisableVertexAttribArray(position)
- debug.DrawFPS(sz)
+ fps.Draw(sz)
}
var triangleData = f32.Bytes(binary.LittleEndian,
diff --git a/example/sprite/main.go b/example/sprite/main.go
index a18d870..586704b 100644
--- a/example/sprite/main.go
+++ b/example/sprite/main.go
@@ -43,6 +43,7 @@
"golang.org/x/mobile/event/size"
"golang.org/x/mobile/exp/app/debug"
"golang.org/x/mobile/exp/f32"
+ "golang.org/x/mobile/exp/gl/glutil"
"golang.org/x/mobile/exp/sprite"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/exp/sprite/glsprite"
@@ -51,8 +52,10 @@
var (
startTime = time.Now()
- eng = glsprite.Engine()
+ images *glutil.Images
+ eng sprite.Engine
scene *sprite.Node
+ fps *debug.FPS
)
func main() {
@@ -83,13 +86,16 @@
func onPaint(sz size.Event) {
if scene == nil {
+ images = glutil.NewImages()
+ fps = debug.NewFPS(images)
+ eng = glsprite.Engine(images)
loadScene()
}
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
now := clock.Time(time.Since(startTime) * 60 / time.Second)
eng.Render(scene, now, sz)
- debug.DrawFPS(sz)
+ fps.Draw(sz)
}
func newNode() *sprite.Node {
diff --git a/exp/app/debug/fps.go b/exp/app/debug/fps.go
index d90d60f..16eab6e 100644
--- a/exp/app/debug/fps.go
+++ b/exp/app/debug/fps.go
@@ -11,7 +11,6 @@
"image"
"image/color"
"image/draw"
- "sync"
"time"
"golang.org/x/mobile/event/size"
@@ -19,24 +18,37 @@
"golang.org/x/mobile/geom"
)
-var lastDraw = time.Now()
-
-var fps struct {
- mu sync.Mutex
- sz size.Event
- m *glutil.Image
+// FPS draws a count of the frames rendered per second.
+type FPS struct {
+ sz size.Event
+ images *glutil.Images
+ m *glutil.Image
+ lastDraw time.Time
+ // TODO: store *gl.Context
}
-// DrawFPS draws the per second framerate in the bottom-left of the screen.
-func DrawFPS(sz size.Event) {
+// NewFPS creates an FPS tied to the current GL context.
+func NewFPS(images *glutil.Images) *FPS {
+ return &FPS{
+ lastDraw: time.Now(),
+ images: images,
+ }
+}
+
+// Draw draws the per second framerate in the bottom-left of the screen.
+func (p *FPS) Draw(sz size.Event) {
const imgW, imgH = 7*(fontWidth+1) + 1, fontHeight + 2
- fps.mu.Lock()
- if fps.sz != sz || fps.m == nil {
- fps.sz = sz
- fps.m = glutil.NewImage(imgW, imgH)
+ if sz.WidthPx == 0 && sz.HeightPx == 0 {
+ return
}
- fps.mu.Unlock()
+ if p.sz != sz {
+ p.sz = sz
+ if p.m != nil {
+ p.m.Release()
+ }
+ p.m = p.images.NewImage(imgW, imgH)
+ }
display := [7]byte{
4: 'F',
@@ -45,13 +57,13 @@
}
now := time.Now()
f := 0
- if dur := now.Sub(lastDraw); dur > 0 {
+ if dur := now.Sub(p.lastDraw); dur > 0 {
f = int(time.Second / dur)
}
display[2] = '0' + byte((f/1e0)%10)
display[1] = '0' + byte((f/1e1)%10)
display[0] = '0' + byte((f/1e2)%10)
- draw.Draw(fps.m.RGBA, fps.m.RGBA.Bounds(), image.White, image.Point{}, draw.Src)
+ draw.Draw(p.m.RGBA, p.m.RGBA.Bounds(), image.White, image.Point{}, draw.Src)
for i, c := range display {
glyph := glyphs[c]
if len(glyph) != fontWidth*fontHeight {
@@ -62,21 +74,29 @@
if glyph[fontWidth*y+x] == ' ' {
continue
}
- fps.m.RGBA.SetRGBA((fontWidth+1)*i+x+1, y+1, color.RGBA{A: 0xff})
+ p.m.RGBA.SetRGBA((fontWidth+1)*i+x+1, y+1, color.RGBA{A: 0xff})
}
}
}
- fps.m.Upload()
- fps.m.Draw(
+ p.m.Upload()
+ p.m.Draw(
sz,
geom.Point{0, sz.HeightPt - imgH},
geom.Point{imgW, sz.HeightPt - imgH},
geom.Point{0, sz.HeightPt},
- fps.m.RGBA.Bounds(),
+ p.m.RGBA.Bounds(),
)
- lastDraw = now
+ p.lastDraw = now
+}
+
+func (f *FPS) Release() {
+ if f.m != nil {
+ f.m.Release()
+ f.m = nil
+ f.images = nil
+ }
}
const (
diff --git a/exp/gl/glutil/glimage.go b/exp/gl/glutil/glimage.go
index 6b73c4b..0405adc 100644
--- a/exp/gl/glutil/glimage.go
+++ b/exp/gl/glutil/glimage.go
@@ -8,20 +8,18 @@
import (
"encoding/binary"
- "fmt"
"image"
"runtime"
"sync"
- "golang.org/x/mobile/app"
- "golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/size"
"golang.org/x/mobile/exp/f32"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl"
)
-var glimage struct {
+// Images maintains the shared state used by a set of *Image objects.
+type Images struct {
quadXY gl.Buffer
quadUV gl.Buffer
program gl.Program
@@ -30,142 +28,59 @@
uvp gl.Uniform
inUV gl.Attrib
textureSample gl.Uniform
+
+ // TODO(crawshaw): store *gl.Context
+
+ mu sync.Mutex
+ activeImages int
}
-func init() {
- app.RegisterFilter(func(e interface{}) interface{} {
- if e, ok := e.(lifecycle.Event); ok {
- switch e.Crosses(lifecycle.StageVisible) {
- case lifecycle.CrossOn:
- start()
- case lifecycle.CrossOff:
- stop()
- }
- }
- return e
- })
-}
-
-func start() {
- var err error
- glimage.program, err = CreateProgram(vertexShader, fragmentShader)
+// NewImages creates an *Images.
+// TODO(crawshaw): take *gl.Context parameter
+func NewImages() *Images {
+ program, err := CreateProgram(vertexShader, fragmentShader)
if err != nil {
panic(err)
}
- glimage.quadXY = gl.CreateBuffer()
- glimage.quadUV = gl.CreateBuffer()
+ p := &Images{
+ quadXY: gl.CreateBuffer(),
+ quadUV: gl.CreateBuffer(),
+ program: program,
+ pos: gl.GetAttribLocation(program, "pos"),
+ mvp: gl.GetUniformLocation(program, "mvp"),
+ uvp: gl.GetUniformLocation(program, "uvp"),
+ inUV: gl.GetAttribLocation(program, "inUV"),
+ textureSample: gl.GetUniformLocation(program, "textureSample"),
+ }
- gl.BindBuffer(gl.ARRAY_BUFFER, glimage.quadXY)
+ gl.BindBuffer(gl.ARRAY_BUFFER, p.quadXY)
gl.BufferData(gl.ARRAY_BUFFER, quadXYCoords, gl.STATIC_DRAW)
- gl.BindBuffer(gl.ARRAY_BUFFER, glimage.quadUV)
+ gl.BindBuffer(gl.ARRAY_BUFFER, p.quadUV)
gl.BufferData(gl.ARRAY_BUFFER, quadUVCoords, gl.STATIC_DRAW)
- glimage.pos = gl.GetAttribLocation(glimage.program, "pos")
- glimage.mvp = gl.GetUniformLocation(glimage.program, "mvp")
- glimage.uvp = gl.GetUniformLocation(glimage.program, "uvp")
- glimage.inUV = gl.GetAttribLocation(glimage.program, "inUV")
- glimage.textureSample = gl.GetUniformLocation(glimage.program, "textureSample")
-
- texmap.Lock()
- defer texmap.Unlock()
- for key, tex := range texmap.texs {
- texmap.init(key)
- tex.needsUpload = true
- }
+ return p
}
-func stop() {
- gl.DeleteProgram(glimage.program)
- gl.DeleteBuffer(glimage.quadXY)
- gl.DeleteBuffer(glimage.quadUV)
-
- texmap.Lock()
- for _, t := range texmap.texs {
- if t.gltex.Value != 0 {
- gl.DeleteTexture(t.gltex)
- }
- t.gltex = gl.Texture{}
- }
- texmap.Unlock()
-}
-
-type texture struct {
- gltex gl.Texture
- width int
- height int
- needsUpload bool
-}
-
-var texmap = &texmapCache{
- texs: make(map[texmapKey]*texture),
- next: 1, // avoid using 0 to aid debugging
-}
-
-type texmapKey int
-
-type texmapCache struct {
- sync.Mutex
- texs map[texmapKey]*texture
- next texmapKey
-
- // TODO(crawshaw): This is a workaround for having nowhere better to clean up deleted textures.
- // Better: app.UI(func() { gl.DeleteTexture(t) } in texmap.delete
- // Best: Redesign the gl package to do away with this painful notion of a UI thread.
- toDelete []gl.Texture
-}
-
-func (tm *texmapCache) create(dx, dy int) *texmapKey {
- tm.Lock()
- defer tm.Unlock()
- key := tm.next
- tm.next++
- tm.texs[key] = &texture{
- width: dx,
- height: dy,
- }
- tm.init(key)
- return &key
-}
-
-// init creates an underlying GL texture for a key.
-// Must be called with a valid GL context.
-// Must hold tm.Mutex before calling.
-func (tm *texmapCache) init(key texmapKey) {
- tex := tm.texs[key]
- if tex.gltex.Value != 0 {
- panic(fmt.Sprintf("attempting to init key (%v) with valid texture", key))
- }
- tex.gltex = gl.CreateTexture()
-
- gl.BindTexture(gl.TEXTURE_2D, tex.gltex)
- gl.TexImage2D(gl.TEXTURE_2D, 0, tex.width, tex.height, gl.RGBA, gl.UNSIGNED_BYTE, nil)
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
- gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
-
- for _, t := range tm.toDelete {
- gl.DeleteTexture(t)
- }
- tm.toDelete = nil
-}
-
-func (tm *texmapCache) delete(key texmapKey) {
- tm.Lock()
- defer tm.Unlock()
- tex := tm.texs[key]
- delete(tm.texs, key)
- if tex == nil {
+// Release releases any held OpenGL resources.
+// All *Image objects must be released first, or this function panics.
+func (p *Images) Release() {
+ if p.program == (gl.Program{}) {
return
}
- tm.toDelete = append(tm.toDelete, tex.gltex)
-}
-func (tm *texmapCache) get(key texmapKey) *texture {
- tm.Lock()
- defer tm.Unlock()
- return tm.texs[key]
+ p.mu.Lock()
+ rem := p.activeImages
+ p.mu.Unlock()
+ if rem > 0 {
+ panic("glutil.Images.Release called, but active *Image objects remain")
+ }
+
+ gl.DeleteProgram(p.program)
+ gl.DeleteBuffer(p.quadXY)
+ gl.DeleteBuffer(p.quadUV)
+
+ p.program = gl.Program{}
}
// Image bridges between an *image.RGBA and an OpenGL texture.
@@ -177,13 +92,17 @@
// The typical use of an Image is as a texture atlas.
type Image struct {
RGBA *image.RGBA
- key *texmapKey
+
+ gltex gl.Texture
+ width int
+ height int
+ images *Images
}
// NewImage creates an Image of the given size.
//
// Both a host-memory *image.RGBA and a GL texture are created.
-func NewImage(w, h int) *Image {
+func (p *Images) NewImage(w, h int) *Image {
dx := roundToPower2(w)
dy := roundToPower2(h)
@@ -193,12 +112,26 @@
m := image.NewRGBA(image.Rect(0, 0, dx, dy))
img := &Image{
- RGBA: m.SubImage(image.Rect(0, 0, w, h)).(*image.RGBA),
- key: texmap.create(dx, dy),
+ RGBA: m.SubImage(image.Rect(0, 0, w, h)).(*image.RGBA),
+ images: p,
+ width: dx,
+ height: dy,
}
- runtime.SetFinalizer(img.key, func(key *texmapKey) {
- texmap.delete(*key)
- })
+
+ p.mu.Lock()
+ p.activeImages++
+ p.mu.Unlock()
+
+ img.gltex = gl.CreateTexture()
+
+ gl.BindTexture(gl.TEXTURE_2D, img.gltex)
+ gl.TexImage2D(gl.TEXTURE_2D, 0, img.width, img.height, gl.RGBA, gl.UNSIGNED_BYTE, nil)
+ gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
+ gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
+ gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+
+ runtime.SetFinalizer(img, (*Image).Release)
return img
}
@@ -212,28 +145,32 @@
// Upload copies the host image data to the GL device.
func (img *Image) Upload() {
- tex := texmap.get(*img.key)
- gl.BindTexture(gl.TEXTURE_2D, tex.gltex)
- gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, tex.width, tex.height, gl.RGBA, gl.UNSIGNED_BYTE, img.RGBA.Pix)
+ gl.BindTexture(gl.TEXTURE_2D, img.gltex)
+ gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, img.width, img.height, gl.RGBA, gl.UNSIGNED_BYTE, img.RGBA.Pix)
}
-// Delete invalidates the Image and removes any underlying data structures.
+// Release invalidates the Image and removes any underlying data structures.
// The Image cannot be used after being deleted.
-func (img *Image) Delete() {
- texmap.delete(*img.key)
+func (img *Image) Release() {
+ if img.gltex == (gl.Texture{}) {
+ return
+ }
+
+ gl.DeleteTexture(img.gltex)
+ img.gltex = gl.Texture{}
+
+ img.images.mu.Lock()
+ img.images.activeImages--
+ img.images.mu.Unlock()
}
// Draw draws the srcBounds part of the image onto a parallelogram, defined by
// three of its corners, in the current GL framebuffer.
func (img *Image) Draw(sz size.Event, topLeft, topRight, bottomLeft geom.Point, srcBounds image.Rectangle) {
+ glimage := img.images
+
// TODO(crawshaw): Adjust viewport for the top bar on android?
gl.UseProgram(glimage.program)
- tex := texmap.get(*img.key)
- if tex.needsUpload {
- img.Upload()
- tex.needsUpload = false
- }
-
{
// We are drawing a parallelogram PQRS, defined by three of its
// corners, onto the entire GL framebuffer ABCD. The two quads may
@@ -309,8 +246,8 @@
//
// and the PQRS quad is always axis-aligned. First of all, convert
// from pixel space to texture space.
- w := float32(tex.width)
- h := float32(tex.height)
+ w := float32(img.width)
+ h := float32(img.height)
px := float32(srcBounds.Min.X-img.RGBA.Rect.Min.X) / w
py := float32(srcBounds.Min.Y-img.RGBA.Rect.Min.Y) / h
qx := float32(srcBounds.Max.X-img.RGBA.Rect.Min.X) / w
@@ -336,7 +273,7 @@
}
gl.ActiveTexture(gl.TEXTURE0)
- gl.BindTexture(gl.TEXTURE_2D, tex.gltex)
+ gl.BindTexture(gl.TEXTURE_2D, img.gltex)
gl.Uniform1i(glimage.textureSample, 0)
gl.BindBuffer(gl.ARRAY_BUFFER, glimage.quadXY)
diff --git a/exp/sprite/glsprite/glsprite.go b/exp/sprite/glsprite/glsprite.go
index b335996..f5b6a77 100644
--- a/exp/sprite/glsprite/glsprite.go
+++ b/exp/sprite/glsprite/glsprite.go
@@ -28,6 +28,7 @@
}
type texture struct {
+ e *engine
glImage *glutil.Image
b image.Rectangle
}
@@ -43,18 +44,23 @@
t.glImage.Upload()
}
-func (t *texture) Unload() {
- panic("TODO")
+func (t *texture) Release() {
+ t.glImage.Release()
+ delete(t.e.textures, t)
}
-func Engine() sprite.Engine {
+// Engine creates an OpenGL-based sprite.Engine.
+func Engine(images *glutil.Images) sprite.Engine {
return &engine{
- nodes: []*node{nil},
+ nodes: []*node{nil},
+ images: images,
+ textures: make(map[*texture]struct{}),
}
}
type engine struct {
- glImages map[sprite.Texture]*glutil.Image
+ images *glutil.Images
+ textures map[*texture]struct{}
nodes []*node
absTransforms []f32.Affine
@@ -77,7 +83,12 @@
func (e *engine) LoadTexture(src image.Image) (sprite.Texture, error) {
b := src.Bounds()
- t := &texture{glutil.NewImage(b.Dx(), b.Dy()), b}
+ t := &texture{
+ e: e,
+ glImage: e.images.NewImage(b.Dx(), b.Dy()),
+ b: b,
+ }
+ e.textures[t] = struct{}{}
t.Upload(b, src)
// TODO: set "glImage.Pix = nil"?? We don't need the CPU-side image any more.
return t, nil
@@ -142,3 +153,9 @@
// Pop absTransforms.
e.absTransforms = e.absTransforms[:len(e.absTransforms)-1]
}
+
+func (e *engine) Release() {
+ for img := range e.textures {
+ img.Release()
+ }
+}
diff --git a/exp/sprite/portable/portable.go b/exp/sprite/portable/portable.go
index 6e1961e..7a7aecc 100644
--- a/exp/sprite/portable/portable.go
+++ b/exp/sprite/portable/portable.go
@@ -51,7 +51,7 @@
draw.Draw(t.m, r, src, src.Bounds().Min, draw.Src)
}
-func (t *texture) Unload() { panic("TODO") }
+func (t *texture) Release() {}
type engine struct {
dst *image.RGBA
@@ -149,6 +149,8 @@
e.absTransforms = e.absTransforms[:len(e.absTransforms)-1]
}
+func (e *engine) Release() {}
+
// affine draws each pixel of dst using bilinear interpolation of the
// affine-transformed position in src. This is equivalent to:
//
diff --git a/exp/sprite/sprite.go b/exp/sprite/sprite.go
index b93b25f..df13fb5 100644
--- a/exp/sprite/sprite.go
+++ b/exp/sprite/sprite.go
@@ -41,7 +41,7 @@
Bounds() (w, h int)
Download(r image.Rectangle, dst draw.Image)
Upload(r image.Rectangle, src image.Image)
- Unload()
+ Release()
}
type SubTex struct {
@@ -61,6 +61,8 @@
// Render renders the scene arranged at the given time, for the given
// window configuration (dimensions and resolution).
Render(scene *Node, t clock.Time, sz size.Event)
+
+ Release()
}
// A Node is a renderable element and forms a tree of Nodes.