From 022508a5a3109c81d638a22f46a265ec4c5e08a4 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Mon, 22 Sep 2014 11:18:43 -0400 Subject: [PATCH] go.mobile/app/debug: basic GL-based debugging tools LGTM=nigeltao R=nigeltao, bryanturley, crawshaw CC=adg, davidday, golang-codereviews https://golang.org/cl/136550043 --- app/debug/fps.go | 78 +++++++++++++++++ app/debug/glimage.go | 184 ++++++++++++++++++++++++++++++++++++++++ app/debug/glimage_es.go | 27 ++++++ 3 files changed, 289 insertions(+) create mode 100644 app/debug/fps.go create mode 100644 app/debug/glimage.go create mode 100644 app/debug/glimage_es.go diff --git a/app/debug/fps.go b/app/debug/fps.go new file mode 100644 index 0000000..3849c8f --- /dev/null +++ b/app/debug/fps.go @@ -0,0 +1,78 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package debug provides GL-based debugging tools for apps. +package debug + +import ( + "fmt" + "image" + "image/draw" + "io/ioutil" + "log" + "runtime" + "sync" + "time" + + "code.google.com/p/freetype-go/freetype" + "code.google.com/p/go.mobile/geom" +) + +var lastDraw = time.Now() + +var monofont = freetype.NewContext() + +var fps struct { + sync.Once + *rgba +} + +// TODO(crawshaw): It looks like we need a gl.RegisterInit feature. +// TODO(crawshaw): The gldebug mode needs to complain loudly when GL functions +// are called before init, because often they fail silently. +func fpsInit() { + font := "/system/fonts/DroidSansMono.ttf" + if runtime.GOOS == "darwin" { + font = "/Library/Fonts/Andale Mono.ttf" + } + b, err := ioutil.ReadFile(font) + if err != nil { + panic(err) + } + f, err := freetype.ParseFont(b) + if err != nil { + panic(err) + } + monofont.SetFont(f) + monofont.SetSrc(image.Black) + monofont.SetHinting(freetype.FullHinting) + + fps.rgba = newRGBA(geom.Point{50, 12}) + monofont.SetDst(fps.Image) + monofont.SetClip(fps.Image.Bounds()) + monofont.SetDPI(72 * float64(geom.Scale)) + monofont.SetFontSize(12) + log.Printf("fps.Image bounds: %s", fps.Image.Bounds()) +} + +// DrawFPS draws the per second framerate in the bottom-left of the screen. +func DrawFPS() { + fps.Do(fpsInit) + + now := time.Now() + diff := now.Sub(lastDraw) + str := fmt.Sprintf("%.0f FPS", float32(time.Second)/float32(diff)) + draw.Draw(fps.Image, fps.Image.Rect, image.White, image.Point{}, draw.Src) + + ftpt12 := freetype.Pt(0, int(12*geom.Scale)) + if _, err := monofont.DrawString(str, ftpt12); err != nil { + log.Printf("DrawFPS: %v", err) + return + } + + fps.Upload() + fps.Draw(geom.Point{0, geom.Height - 12}) + + lastDraw = now +} diff --git a/app/debug/glimage.go b/app/debug/glimage.go new file mode 100644 index 0000000..414ee25 --- /dev/null +++ b/app/debug/glimage.go @@ -0,0 +1,184 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO(crawshaw): A GL texture backed by an *image.RGBA is a common +// concept we will want to export. Find a generally useful interface. +// Think about the tricky things: mipmaps, a varying Rect, just backed +// by texture, not 2D drawer, etc. Maybe skip all of these issues and +// just solve the smaller problem: an unscaled sprite. + +package debug + +import ( + "bytes" + "encoding/binary" + "image" + "log" + "runtime" + "sync" + + "code.google.com/p/go.mobile/f32" + "code.google.com/p/go.mobile/geom" + "code.google.com/p/go.mobile/gl" + "code.google.com/p/go.mobile/gl/glutil" +) + +var glimage struct { + sync.Once + square gl.Buffer + squareUV gl.Buffer + program gl.Program + pos gl.Attrib + mvp gl.Uniform + inUV gl.Attrib + textureSample gl.Uniform +} + +func glInit() { + var err error + glimage.program, err = glutil.CreateProgram(vertexShader, fragmentShader) + if err != nil { + panic(err) + } + + glimage.square = gl.GenBuffer() + glimage.squareUV = gl.GenBuffer() + + gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square) + gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareCoords) + gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV) + gl.BufferData(gl.ARRAY_BUFFER, gl.STATIC_DRAW, squareUVCoords) + + glimage.pos = gl.GetAttribLocation(glimage.program, "pos") + glimage.mvp = gl.GetUniformLocation(glimage.program, "mvp") + glimage.inUV = gl.GetAttribLocation(glimage.program, "inUV") + glimage.textureSample = gl.GetUniformLocation(glimage.program, "textureSample") +} + +type rgba struct { + Image *image.RGBA + Texture gl.Texture + sizeX, sizeY float32 +} + +func roundToPower2(x int) int { + x2 := 1 + for x2 < x { + x2 *= 2 + } + return x2 +} + +func newRGBA(size geom.Point) *rgba { + dx := roundToPower2(int(size.X.Px())) + dy := roundToPower2(int(size.Y.Px())) + + imgRGBA := image.NewRGBA(image.Rect(0, 0, dx, dy)) + + glimage.Do(glInit) + + w, h := imgRGBA.Rect.Dx(), imgRGBA.Rect.Dy() + + img := &rgba{ + Image: imgRGBA, // TODO: embed? + Texture: gl.GenTexture(), + } + runtime.SetFinalizer(img, func(img *rgba) { + gl.DeleteTexture(img.Texture) + }) + gl.BindTexture(gl.TEXTURE_2D, img.Texture) + gl.TexImage2D(gl.TEXTURE_2D, 0, w, h, 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) + + img.sizeX = float32(size.X / geom.Width) + img.sizeY = float32(size.Y / geom.Height) + + return img +} + +func (img *rgba) Upload() { + gl.BindTexture(gl.TEXTURE_2D, img.Texture) + w, h := img.Image.Rect.Dx(), img.Image.Rect.Dy() + gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, img.Image.Pix) +} + +// TODO(crawshaw): Clip/Scale options. Introduce geom.Rectangle? +// TODO(crawshaw): Adjust viewport for the top bar on android? +func (img *rgba) Draw(at geom.Point) { + gl.UseProgram(glimage.program) + + // Screen plane + // (-1, 1) ( 1, 1) + // (-1,-1) ( 1,-1) + // Unscaled image corners + // (1,0) (1,1) + // (0,0) (0,1) + // Orig size.X / geom.Width is a [0,1] fraction of screen. + // The at position is referenced from the top of the image. + var m f32.Mat4 + m.Identity() + m.Translate(m, &f32.Vec3{ + -1 + float32(2*at.X/geom.Width), + +1 - 2*img.sizeY - float32(2*at.Y/geom.Height), + 0, + }) + m.Scale(&m, &f32.Vec3{ + 2 * img.sizeX, + 2 * img.sizeY, + 1, + }) + glimage.mvp.WriteMat4(&m) + + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, img.Texture) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + gl.Uniform1i(glimage.textureSample, 0) + + gl.BindBuffer(gl.ARRAY_BUFFER, glimage.square) + gl.EnableVertexAttribArray(glimage.pos) + gl.VertexAttribPointer(glimage.pos, 2, gl.FLOAT, false, 0, 0) + + gl.BindBuffer(gl.ARRAY_BUFFER, glimage.squareUV) + gl.EnableVertexAttribArray(glimage.inUV) + gl.VertexAttribPointer(glimage.inUV, 2, gl.FLOAT, false, 0, 0) + + gl.DrawArrays(gl.TRIANGLES, 0, 6) + + gl.DisableVertexAttribArray(glimage.pos) + gl.DisableVertexAttribArray(glimage.inUV) +} + +// Vertices of two triangles. +var squareCoords = toBytes([]float32{ + 0, 0, + 1, 0, + 1, 1, + + 0, 0, + 1, 1, + 0, 1, +}) + +var squareUVCoords = toBytes([]float32{ + 0, 1, + 1, 1, + 1, 0, + + 0, 1, + 1, 0, + 0, 0, +}) + +func toBytes(v []float32) []byte { + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.LittleEndian, v); err != nil { + log.Fatal(err) + } + return buf.Bytes() +} diff --git a/app/debug/glimage_es.go b/app/debug/glimage_es.go new file mode 100644 index 0000000..23c032e --- /dev/null +++ b/app/debug/glimage_es.go @@ -0,0 +1,27 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build android + +package debug + +const vertexShader = ` +uniform mat4 mvp; +attribute vec4 pos; +attribute vec2 inUV; +varying vec2 UV; +void main() { + gl_Position = mvp * pos; + UV = inUV; +} +` + +const fragmentShader = ` +precision mediump float; +varying vec2 UV; +uniform sampler2D textureSample; +void main(){ + gl_FragColor = texture2D(textureSample, UV); +} +`