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.