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>
This commit is contained in:
David Crawshaw 2015-09-22 17:29:00 -04:00
Родитель afa8a11c1d
Коммит 0064789433
8 изменённых файлов: 179 добавлений и 188 удалений

Просмотреть файл

@ -44,9 +44,9 @@ import (
"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 @@ const (
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 main() {
}
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 onStart() {
}
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 {

Просмотреть файл

@ -44,6 +44,8 @@ import (
)
var (
images *glutil.Images
fps *debug.FPS
program gl.Program
position gl.Attrib
offset gl.Uniform
@ -109,13 +111,15 @@ func onStart() {
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 @@ func onPaint(sz size.Event) {
gl.DrawArrays(gl.TRIANGLES, 0, vertexCount)
gl.DisableVertexAttribArray(position)
debug.DrawFPS(sz)
fps.Draw(sz)
}
var triangleData = f32.Bytes(binary.LittleEndian,

Просмотреть файл

@ -43,6 +43,7 @@ import (
"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 @@ import (
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 main() {
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 {

Просмотреть файл

@ -11,7 +11,6 @@ import (
"image"
"image/color"
"image/draw"
"sync"
"time"
"golang.org/x/mobile/event/size"
@ -19,24 +18,37 @@ import (
"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
}
if p.sz != sz {
p.sz = sz
if p.m != nil {
p.m.Release()
}
p.m = p.images.NewImage(imgW, imgH)
}
fps.mu.Unlock()
display := [7]byte{
4: 'F',
@ -45,13 +57,13 @@ func DrawFPS(sz size.Event) {
}
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 @@ func DrawFPS(sz size.Event) {
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 (

Просмотреть файл

@ -8,20 +8,18 @@ package glutil
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 @@ var glimage struct {
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 @@ func (tm *texmapCache) get(key texmapKey) *texture {
// 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 @@ func NewImage(w, h int) *Image {
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 @@ func roundToPower2(x int) int {
// 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 @@ func (img *Image) Draw(sz size.Event, topLeft, topRight, bottomLeft geom.Point,
//
// 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 @@ func (img *Image) Draw(sz size.Event, topLeft, topRight, bottomLeft geom.Point,
}
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)

Просмотреть файл

@ -28,6 +28,7 @@ type node struct {
}
type texture struct {
e *engine
glImage *glutil.Image
b image.Rectangle
}
@ -43,18 +44,23 @@ func (t *texture) Upload(r image.Rectangle, src image.Image) {
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) Unregister(n *sprite.Node) {
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 @@ func (e *engine) render(n *sprite.Node, t clock.Time, sz size.Event) {
// Pop absTransforms.
e.absTransforms = e.absTransforms[:len(e.absTransforms)-1]
}
func (e *engine) Release() {
for img := range e.textures {
img.Release()
}
}

Просмотреть файл

@ -51,7 +51,7 @@ func (t *texture) Upload(r image.Rectangle, src image.Image) {
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 @@ func (e *engine) render(n *sprite.Node, t clock.Time) {
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:
//

Просмотреть файл

@ -41,7 +41,7 @@ type Texture interface {
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 @@ type Engine interface {
// 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.