app: use one thread for both GL and other UI C code.

This change will break Darwin. I have only built and tested this on
desktop linux and Android linux. A follow-up CL will fix Darwin.

Currently, OpenGL gets its own thread, and UI C code (e.g. the Android
event loop, or the X11 event loop) gets its own thread. This relies on
multiple system-provided UI-related C libraries working nicely together,
even when running on different threads. Keeping all the C code on the
one thread seems more sound.

As side-effects:
  - In package app/debug, DrawFPS now takes an explicit Config.
  - In package app, some callbacks now take an explicit Config.
  - In package exp/sprite, Render now takes an explicit Config.
  - In package event, there are new events (Config, Draw, Lifecycle),
    and an event filter mechanism to replace multiple app Callbacks.
  - In package geom, the deprecated Width, Height and PixelsPerPt global
    variables were removed in favor of an event.Config that is
    explicitly passed around (and does not require mutex-locking).
    Converting a geom.Pt to pixels now requires passing a pixelsPerPt.
  - In package gl, the Do, Start and Stop functions are removed, as well
    as the need to call Start in its own goroutine. There is no longer a
    separate GL thread. Instead, package app explicitly performs any GL
    work (gl.DoWork) when some is available (gl.WorkAvailable).
  - In package gl/glutil, Image.Draw now takes an explicit Config.

Callbacks are no longer executed on 'the UI thread'.

Changing the app programming model from callbacks to events (since a
channel of events works with select) will be a follow-up change.

Change-Id: Id9865cd9ee1c45a98c613e9021a63c17226a64b1
Reviewed-on: https://go-review.googlesource.com/11351
Reviewed-by: David Crawshaw <crawshaw@golang.org>
This commit is contained in:
Nigel Tao
2015-06-23 16:41:48 +10:00
parent 0a8a8b8cb6
commit 42f0d17876
26 changed files with 3068 additions and 2450 deletions

View File

@ -63,12 +63,10 @@ import (
"io"
"log"
"os"
"runtime"
"time"
"unsafe"
"golang.org/x/mobile/app/internal/callfn"
"golang.org/x/mobile/geom"
)
//export callMain
@ -93,7 +91,6 @@ func callMain(mainPC uintptr) {
time.Local = time.FixedZone(tz, tzOffset)
go callfn.CallFn(mainPC)
<-mainCalled
log.Print("app.Run called")
}
@ -126,7 +123,7 @@ func onCreate(activity *C.ANativeActivity) {
dpi = int(density) // This is a guess.
}
geom.PixelsPerPt = float32(dpi) / 72
pixelsPerPt = float32(dpi) / 72
}
//export onStart
@ -201,12 +198,23 @@ func onConfigurationChanged(activity *C.ANativeActivity) {
func onLowMemory(activity *C.ANativeActivity) {
}
func (Config) JavaVM() unsafe.Pointer {
return unsafe.Pointer(C.current_vm)
// Context holds global OS-specific context.
//
// Its extra methods are deliberately difficult to access because they must be
// used with care. Their use implies the use of cgo, which probably requires
// you understand the initialization process in the app package. Also care must
// be taken to write both Android, iOS, and desktop-testing versions to
// maintain portability.
type Context struct{}
// AndroidContext returns a jobject for the app android.context.Context.
func (Context) AndroidContext() unsafe.Pointer {
return unsafe.Pointer(C.current_ctx)
}
func (Config) AndroidContext() unsafe.Pointer {
return unsafe.Pointer(C.current_ctx)
// JavaVM returns a JNI *JavaVM.
func (Context) JavaVM() unsafe.Pointer {
return unsafe.Pointer(C.current_vm)
}
var (
@ -266,26 +274,41 @@ func (a *asset) Close() error {
return nil
}
// TODO(crawshaw): fix up this comment??
// notifyInitDone informs Java that the program is initialized.
// A NativeActivity will not create a window until this is called.
func run(callbacks []Callbacks) {
// We want to keep the event loop on a consistent OS thread.
runtime.LockOSThread()
func main(f func(App) error) error {
ctag := C.CString("Go")
cstr := C.CString("app.Run")
C.__android_log_write(C.ANDROID_LOG_INFO, ctag, cstr)
C.free(unsafe.Pointer(ctag))
C.free(unsafe.Pointer(cstr))
close(mainCalled)
donec := make(chan error, 1)
go func() {
donec <- f(app{})
}()
if C.current_native_activity == nil {
stateStart(callbacks)
// TODO: stateStop under some conditions.
// TODO: Even though c-shared mode doesn't require main to be called
// now, gobind relies on the main being called. In main, app.Run is
// called and the start callback initializes Java-Go communication.
//
// The problem is if the main exits (because app.Run returns), go
// runtime exits and kills the app.
//
// Many things have changed in cgo recently. If we can manage to split
// gobind app, native Go app initialization logic, we may able to
// consider gobind app not to use main of the go package.
//
// TODO: do we need to do what used to be stateStart or stateStop?
select {}
} else {
for w := range windowCreated {
windowDraw(callbacks, w, queue)
if done, err := windowDraw(w, queue, donec); done {
return err
}
}
}
panic("unreachable")
}

View File

@ -8,25 +8,188 @@ package app
import (
"io"
"log"
"runtime"
"golang.org/x/mobile/event"
"golang.org/x/mobile/geom"
)
var callbacks []Callbacks
// Main is called by the main.main function to run the mobile application.
//
// It calls f on the App, in a separate goroutine, as some OS-specific
// libraries require being on 'the main thread'.
func Main(f func(App) error) {
runtime.LockOSThread()
if err := main(f); err != nil {
log.Fatal(err)
}
}
// Run starts the app.
// App is how a GUI mobile application interacts with the OS.
type App interface {
// Events returns the events channel. It carries events from the system to
// the app. The type of such events include:
// - event.Config
// - event.Draw
// - event.Lifecycle
// - event.Touch
// from the golang.org/x/mobile/events package. Other packages may define
// other event types that are carried on this channel.
Events() <-chan interface{}
// Send sends an event on the events channel. It does not block.
Send(event interface{})
// EndDraw flushes any pending OpenGL commands or buffers to the screen.
EndDraw()
}
var (
lifecycleStage = event.LifecycleStageDead
pixelsPerPt = float32(1)
eventsOut = make(chan interface{})
eventsIn = pump(eventsOut)
endDraw = make(chan struct{}, 1)
)
func sendLifecycle(to event.LifecycleStage) {
if lifecycleStage == to {
return
}
eventsIn <- event.Lifecycle{
From: lifecycleStage,
To: to,
}
lifecycleStage = to
}
type app struct{}
func (app) Events() <-chan interface{} {
return eventsOut
}
func (app) Send(event interface{}) {
eventsIn <- event
}
func (app) EndDraw() {
select {
case endDraw <- struct{}{}:
default:
}
}
type stopPumping struct{}
// pump returns a channel src such that sending on src will eventually send on
// dst, in order, but that src will always be ready to send/receive soon, even
// if dst currently isn't. It is effectively an infinitely buffered channel.
//
// It must be called directly from the main function and will
// block until the app exits.
// In particular, goroutine A sending on src will not deadlock even if goroutine
// B that's responsible for receiving on dst is currently blocked trying to
// send to A on a separate channel.
//
// TODO(crawshaw): Remove cb parameter.
// Send a stopPumping on the src channel to close the dst channel after all queued
// events are sent on dst. After that, other goroutines can still send to src,
// so that such sends won't block forever, but such events will be ignored.
func pump(dst chan interface{}) (src chan interface{}) {
src = make(chan interface{})
go func() {
// initialSize is the initial size of the circular buffer. It must be a
// power of 2.
const initialSize = 16
i, j, buf, mask := 0, 0, make([]interface{}, initialSize), initialSize-1
maybeSrc := src
for {
maybeDst := dst
if i == j {
maybeDst = nil
}
if maybeDst == nil && maybeSrc == nil {
break
}
select {
case maybeDst <- buf[i&mask]:
buf[i&mask] = nil
i++
case e := <-maybeSrc:
if _, ok := e.(stopPumping); ok {
maybeSrc = nil
continue
}
// Allocate a bigger buffer if necessary.
if i+len(buf) == j {
b := make([]interface{}, 2*len(buf))
n := copy(b, buf[j&mask:])
copy(b[n:], buf[:j&mask])
i, j = 0, len(buf)
buf, mask = b, len(b)-1
}
buf[j&mask] = e
j++
}
}
close(dst)
// Block forever.
for range src {
}
}()
return src
}
// Run starts the mobile application.
//
// It must be called directly from the main function and will block until the
// application exits.
//
// Deprecated: call Main directly instead.
func Run(cb Callbacks) {
callbacks = append(callbacks, cb)
run(callbacks)
Main(func(a App) error {
var c event.Config
for e := range a.Events() {
switch e := event.Filter(e).(type) {
case event.Lifecycle:
switch e.Crosses(event.LifecycleStageVisible) {
case event.ChangeOn:
if cb.Start != nil {
cb.Start()
}
case event.ChangeOff:
if cb.Stop != nil {
cb.Stop()
}
}
case event.Config:
if cb.Config != nil {
cb.Config(e, c)
}
c = e
case event.Draw:
if cb.Draw != nil {
cb.Draw(c)
}
a.EndDraw()
case event.Touch:
if cb.Touch != nil {
cb.Touch(e, c)
}
}
}
return nil
})
}
// Callbacks is the set of functions called by the app.
//
// Deprecated: call Main directly instead.
type Callbacks struct {
// Start is called when the app enters the foreground.
// The app will start receiving Draw and Touch calls.
@ -65,19 +228,13 @@ type Callbacks struct {
//
// Drawing is done into a framebuffer, which is then swapped onto the
// screen when Draw returns. It is called 60 times a second.
Draw func()
Draw func(event.Config)
// Touch is called by the app when a touch event occurs.
Touch func(event.Touch)
Touch func(event.Touch, event.Config)
// Config is called by the app when configuration has changed.
Config func(new, old Config)
}
// Register registers a set of callbacks.
// Must be called before Run.
func Register(cb Callbacks) {
callbacks = append(callbacks, cb)
Config func(new, old event.Config)
}
// Open opens a named asset.
@ -101,42 +258,3 @@ type ReadSeekCloser interface {
io.ReadSeeker
io.Closer
}
// GetConfig returns the current application state.
// It will block until Run has been called.
func GetConfig() Config {
select {
case <-mainCalled:
default:
panic("app.GetConfig is not available before app.Run is called")
}
configCurMu.Lock()
defer configCurMu.Unlock()
return configCur
}
// Config is global application-specific configuration.
//
// The Config variable also holds operating system specific state.
// Android apps have the extra methods:
//
// // JavaVM returns a JNI *JavaVM.
// JavaVM() unsafe.Pointer
//
// // AndroidContext returns a jobject for the app android.context.Context.
// AndroidContext() unsafe.Pointer
//
// These extra methods are deliberately difficult to access because they
// must be used with care. Their use implies the use of cgo, which probably
// requires you understand the initialization process in the app package.
// Also care must be taken to write both Android, iOS, and desktop-testing
// versions to maintain portability.
type Config struct {
// Width is the width of the device screen.
Width geom.Pt
// Height is the height of the device screen.
Height geom.Pt
// TODO: Orientation
}

View File

@ -15,6 +15,7 @@ import (
"time"
"code.google.com/p/freetype-go/freetype"
"golang.org/x/mobile/event"
"golang.org/x/mobile/exp/font"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl/glutil"
@ -25,13 +26,16 @@ var lastDraw = time.Now()
var monofont = freetype.NewContext()
var fps struct {
sync.Once
*glutil.Image
mu sync.Mutex
c event.Config
m *glutil.Image
}
// 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.
// TODO(nigeltao): no need to re-load the font on every config change (e.g.
// phone rotation between portrait and landscape).
func fpsInit() {
b := font.Monospace()
f, err := freetype.ParseFont(b)
@ -42,35 +46,41 @@ func fpsInit() {
monofont.SetSrc(image.Black)
monofont.SetHinting(freetype.FullHinting)
toPx := func(x geom.Pt) int { return int(math.Ceil(float64(geom.Pt(x).Px()))) }
fps.Image = glutil.NewImage(toPx(50), toPx(12))
monofont.SetDst(fps.Image.RGBA)
monofont.SetClip(fps.Bounds())
monofont.SetDPI(72 * float64(geom.PixelsPerPt))
toPx := func(x geom.Pt) int { return int(math.Ceil(float64(geom.Pt(x).Px(fps.c.PixelsPerPt)))) }
fps.m = glutil.NewImage(toPx(50), toPx(12))
monofont.SetDst(fps.m.RGBA)
monofont.SetClip(fps.m.Bounds())
monofont.SetDPI(72 * float64(fps.c.PixelsPerPt))
monofont.SetFontSize(12)
}
// DrawFPS draws the per second framerate in the bottom-left of the screen.
func DrawFPS() {
fps.Do(fpsInit)
func DrawFPS(c event.Config) {
fps.mu.Lock()
if fps.c != c || fps.m == nil {
fps.c = c
fpsInit()
}
fps.mu.Unlock()
now := time.Now()
diff := now.Sub(lastDraw)
str := fmt.Sprintf("%.0f FPS", float32(time.Second)/float32(diff))
draw.Draw(fps.Image.RGBA, fps.Image.Rect, image.White, image.Point{}, draw.Src)
draw.Draw(fps.m.RGBA, fps.m.Rect, image.White, image.Point{}, draw.Src)
ftpt12 := freetype.Pt(0, int(12*geom.PixelsPerPt))
ftpt12 := freetype.Pt(0, int(12*c.PixelsPerPt))
if _, err := monofont.DrawString(str, ftpt12); err != nil {
log.Printf("DrawFPS: %v", err)
return
}
fps.Upload()
fps.Draw(
geom.Point{0, geom.Height - 12},
geom.Point{50, geom.Height - 12},
geom.Point{0, geom.Height},
fps.Bounds(),
fps.m.Upload()
fps.m.Draw(
c,
geom.Point{0, c.Height - 12},
geom.Point{50, c.Height - 12},
geom.Point{0, c.Height},
fps.m.Bounds(),
)
lastDraw = now

View File

@ -90,79 +90,113 @@ import (
"golang.org/x/mobile/gl"
)
func windowDraw(callbacks []Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) {
go gl.Start(func() {
C.createEGLWindow(w)
})
var firstWindowDraw = true
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan error) (done bool, err error) {
C.createEGLWindow(w)
// TODO: is the library or the app responsible for clearing the buffers?
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.Do(func() { C.eglSwapBuffers(C.display, C.surface) })
if errv := gl.GetError(); errv != gl.NO_ERROR {
log.Printf("GL initialization error: %s", errv)
// The first thing that the example apps' draw functions do is their own
// gl.ClearColor + gl.Clear calls, so this one here seems redundant.
{
c := make(chan struct{})
go func() {
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if errv := gl.GetError(); errv != gl.NO_ERROR {
log.Printf("GL initialization error: %s", errv)
}
close(c)
}()
loop0:
for {
select {
case <-gl.WorkAvailable:
gl.DoWork()
case <-c:
break loop0
}
}
C.eglSwapBuffers(C.display, C.surface)
}
configAlt.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
configSwap(callbacks)
// Wait until geometry and GL is initialized before cb.Start.
stateStart(callbacks)
// TODO: is this needed if we also have the "case <-windowRedrawNeeded:" below??
sendLifecycle(event.LifecycleStageFocused)
eventsIn <- event.Config{
Width: geom.Pt(float32(C.windowWidth) / pixelsPerPt),
Height: geom.Pt(float32(C.windowHeight) / pixelsPerPt),
PixelsPerPt: pixelsPerPt,
}
if firstWindowDraw {
firstWindowDraw = false
// TODO: be more principled about when to send a draw event.
eventsIn <- event.Draw{}
}
for {
processEvents(callbacks, queue)
processEvents(queue)
select {
case err := <-donec:
return true, err
case <-windowRedrawNeeded:
// Re-query the width and height.
C.querySurfaceWidthAndHeight()
configAlt.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt)
configAlt.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt)
gl.Viewport(0, 0, int(C.windowWidth), int(C.windowHeight))
configSwap(callbacks)
case <-windowDestroyed:
stateStop(callbacks)
gl.Stop()
return
default:
for _, cb := range callbacks {
if cb.Draw != nil {
cb.Draw()
sendLifecycle(event.LifecycleStageFocused)
eventsIn <- event.Config{
Width: geom.Pt(float32(C.windowWidth) / pixelsPerPt),
Height: geom.Pt(float32(C.windowHeight) / pixelsPerPt),
PixelsPerPt: pixelsPerPt,
}
// This gl.Viewport call has to be in a separate goroutine because any gl
// call can block until gl.DoWork is called, but this goroutine is the one
// responsible for calling gl.DoWork.
// TODO: again, should x/mobile/app be responsible for calling GL code, or
// should package gl instead call event.RegisterFilter?
{
c := make(chan struct{})
go func() {
gl.Viewport(0, 0, int(C.windowWidth), int(C.windowHeight))
close(c)
}()
loop1:
for {
select {
case <-gl.WorkAvailable:
gl.DoWork()
case <-c:
break loop1
}
}
}
gl.Do(func() { C.eglSwapBuffers(C.display, C.surface) })
case <-windowDestroyed:
sendLifecycle(event.LifecycleStageAlive)
return false, nil
case <-gl.WorkAvailable:
gl.DoWork()
case <-endDraw:
// eglSwapBuffers blocks until vsync.
C.eglSwapBuffers(C.display, C.surface)
eventsIn <- event.Draw{}
}
}
}
func processEvents(callbacks []Callbacks, queue *C.AInputQueue) {
func processEvents(queue *C.AInputQueue) {
var event *C.AInputEvent
for C.AInputQueue_getEvent(queue, &event) >= 0 {
if C.AInputQueue_preDispatchEvent(queue, event) != 0 {
continue
}
processEvent(callbacks, event)
processEvent(event)
C.AInputQueue_finishEvent(queue, event, 0)
}
}
func processEvent(callbacks []Callbacks, e *C.AInputEvent) {
func processEvent(e *C.AInputEvent) {
switch C.AInputEvent_getType(e) {
case C.AINPUT_EVENT_TYPE_KEY:
log.Printf("TODO input event: key")
case C.AINPUT_EVENT_TYPE_MOTION:
// TODO: calculate hasTouch once in run
hasTouch := false
for _, cb := range callbacks {
if cb.Touch != nil {
hasTouch = true
}
}
if !hasTouch {
return
}
// At most one of the events in this batch is an up or down event; get its index and type.
upDownIndex := C.size_t(C.AMotionEvent_getAction(e)&C.AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> C.AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT
upDownTyp := event.TouchMove
@ -178,19 +212,14 @@ func processEvent(callbacks []Callbacks, e *C.AInputEvent) {
if i == upDownIndex {
typ = upDownTyp
}
t := event.Touch{
eventsIn <- event.Touch{
ID: event.TouchSequenceID(C.AMotionEvent_getPointerId(e, i)),
Type: typ,
Loc: geom.Point{
X: geom.Pt(float32(C.AMotionEvent_getX(e, i)) / geom.PixelsPerPt),
Y: geom.Pt(float32(C.AMotionEvent_getY(e, i)) / geom.PixelsPerPt),
X: geom.Pt(float32(C.AMotionEvent_getX(e, i)) / pixelsPerPt),
Y: geom.Pt(float32(C.AMotionEvent_getY(e, i)) / pixelsPerPt),
},
}
for _, cb := range callbacks {
if cb.Touch != nil {
cb.Touch(t)
}
}
}
default:
log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e))

View File

@ -1,106 +0,0 @@
// Copyright 2015 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 app
/*
There are three notions of state at work in this package.
The first is Unix process state. Because mobile devices can be compiled
as -buildmode=c-shared and -buildmode=c-archive, there is code in this
package executed by global constructor which runs before (and even is
reponsible for triggering) the Go main function call. This is tracked
by the mainCalled channel.
The second is runState. An app may "Start" and "Stop" multiple times
over the life of the unix process. This involes the creation and
destruction of OpenGL windows and calling user Callbacks. Some user
functions must block in the stop state.
The third is Config, user-visible app configuration. It is only
available after the app has started.
*/
import (
"sync"
"golang.org/x/mobile/geom"
)
// mainCalled is closed after the Go main and app.Run functions have
// been called. This happens before an app enters the start state and
// may happen before a window is created (on android).
var mainCalled = make(chan struct{})
var (
configCurMu sync.Mutex // guards configCur pointer, not contents
configCur Config
configAlt Config // used to stage new state
)
func init() {
// Configuration is not available while the app is stopped,
// so we begin the program with configCurMu locked. It will
// be locked whenever !running.
configCurMu.Lock()
}
var (
running = false
startFuncs []func()
stopFuncs []func()
)
func stateStart(callbacks []Callbacks) {
if running {
return
}
running = true
configCurMu.Unlock() // GetConfig is now available
for _, cb := range callbacks {
if cb.Start != nil {
cb.Start()
}
}
}
func stateStop(callbacks []Callbacks) {
if !running {
return
}
running = false
configCurMu.Lock() // GetConfig is no longer available
for _, cb := range callbacks {
if cb.Stop != nil {
cb.Stop()
}
}
}
// configSwap is called to replace configCur with configAlt and if
// necessary inform the running the app. Calls to configSwap must be
// made after updating configAlt.
func configSwap(callbacks []Callbacks) {
if !running {
// configCurMu is already locked, and no-one else
// is around to look at configCur, so we modify it
// directly.
configCur = configAlt
geom.Width, geom.Height = configCur.Width, configCur.Height // TODO: remove
return
}
configCurMu.Lock()
old := configCur
configCur = configAlt
configCurMu.Unlock()
geom.Width, geom.Height = configCur.Width, configCur.Height // TODO: remove
for _, cb := range callbacks {
if cb.Config != nil {
cb.Config(configCur, old)
}
}
}

View File

@ -4,6 +4,11 @@
package app
/*
To view the log output run:
adb logcat GoLog:I *:S
*/
// Android redirects stdout and stderr to /dev/null.
// As these are common debugging utilities in Go,
// we redirect them to logcat.

View File

@ -166,7 +166,7 @@ processEvents(void) {
void
swapBuffers(void) {
if(eglSwapBuffers(e_dpy, e_surf) == EGL_FALSE) {
if (eglSwapBuffers(e_dpy, e_surf) == EGL_FALSE) {
fprintf(stderr, "eglSwapBuffer failed\n");
exit(1);
}

View File

@ -24,8 +24,6 @@ void swapBuffers(void);
*/
import "C"
import (
"runtime"
"sync"
"time"
"golang.org/x/mobile/event"
@ -33,116 +31,79 @@ import (
"golang.org/x/mobile/gl"
)
type windowEventType byte
func main(f func(App) error) error {
C.createWindow()
const (
start windowEventType = iota
stop
resize
)
// TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen.
sendLifecycle(event.LifecycleStageFocused)
type windowEvent struct {
eventType windowEventType
arg1, arg2 int
}
donec := make(chan error, 1)
go func() {
donec <- f(app{})
}()
var windowEvents struct {
sync.Mutex
events []windowEvent
touches []event.Touch
}
// TODO: can we get the actual vsync signal?
ticker := time.NewTicker(time.Second / 60)
defer ticker.Stop()
tc := ticker.C
func run(cbs []Callbacks) {
runtime.LockOSThread()
callbacks = cbs
go gl.Start(func() {
C.createWindow()
sendEvent(windowEvent{start, 0, 0})
})
for range time.Tick(time.Second / 60) {
windowEvents.Lock()
events := windowEvents.events
touches := windowEvents.touches
windowEvents.events = nil
windowEvents.touches = nil
windowEvents.Unlock()
for _, ev := range events {
switch ev.eventType {
case start:
close(mainCalled)
stateStart(callbacks)
case stop:
stateStop(callbacks)
return
case resize:
w := ev.arg1
h := ev.arg2
// TODO(nigeltao): don't assume 72 DPI. DisplayWidth / DisplayWidthMM
// is probably the best place to start looking.
if geom.PixelsPerPt == 0 {
geom.PixelsPerPt = 1
}
configAlt.Width = geom.Pt(w)
configAlt.Height = geom.Pt(h)
configSwap(callbacks)
}
}
if !running {
// Drop touch events before app started.
continue
}
for _, cb := range callbacks {
if cb.Touch != nil {
for _, e := range touches {
cb.Touch(e)
}
}
}
for _, cb := range callbacks {
if cb.Draw != nil {
cb.Draw()
}
}
gl.Do(func() {
for {
select {
case err := <-donec:
return err
case <-gl.WorkAvailable:
gl.DoWork()
case <-endDraw:
C.swapBuffers()
C.processEvents()
})
tc = ticker.C
case <-tc:
tc = nil
eventsIn <- event.Draw{}
}
C.processEvents()
}
}
func sendEvent(ev windowEvent) {
windowEvents.Lock()
windowEvents.events = append(windowEvents.events, ev)
windowEvents.Unlock()
}
//export onResize
func onResize(w, h int) {
gl.Viewport(0, 0, w, h)
sendEvent(windowEvent{resize, w, h})
// TODO(nigeltao): don't assume 72 DPI. DisplayWidth and DisplayWidthMM
// is probably the best place to start looking.
pixelsPerPt = 1
eventsIn <- event.Config{
Width: geom.Pt(w),
Height: geom.Pt(h),
PixelsPerPt: pixelsPerPt,
}
// This gl.Viewport call has to be in a separate goroutine because any gl
// call can block until gl.DoWork is called, but this goroutine is the one
// responsible for calling gl.DoWork.
// TODO: does this (GL-using) code belong here in the x/mobile/app
// package?? See similar TODOs in the Android x/mobile/app implementation.
c := make(chan struct{})
go func() {
gl.Viewport(0, 0, w, h)
close(c)
}()
for {
select {
case <-gl.WorkAvailable:
gl.DoWork()
case <-c:
return
}
}
}
func sendTouch(ty event.TouchType, x, y float32) {
windowEvents.Lock()
windowEvents.touches = append(windowEvents.touches, event.Touch{
ID: 0,
eventsIn <- event.Touch{
ID: 0, // TODO: button??
Type: ty,
Loc: geom.Point{
X: geom.Pt(x / geom.PixelsPerPt),
Y: geom.Pt(y / geom.PixelsPerPt),
X: geom.Pt(x / pixelsPerPt),
Y: geom.Pt(y / pixelsPerPt),
},
})
windowEvents.Unlock()
}
}
//export onTouchStart
@ -154,7 +115,14 @@ func onTouchMove(x, y float32) { sendTouch(event.TouchMove, x, y) }
//export onTouchEnd
func onTouchEnd(x, y float32) { sendTouch(event.TouchEnd, x, y) }
var stopped bool
//export onStop
func onStop() {
sendEvent(windowEvent{stop, 0, 0})
if stopped {
return
}
stopped = true
sendLifecycle(event.LifecycleStageDead)
eventsIn <- stopPumping{}
}

225
event/event.go Normal file
View File

@ -0,0 +1,225 @@
// 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 event defines mobile app events, such as user input events.
//
// An event is represented by the empty interface type interface{}. Any value
// can be an event. This package defines a number of commonly used events used
// by the golang.org/x/mobile/app package:
// - Config
// - Draw
// - Lifecycle
// - Touch
// Other packages may define their own events, and post them onto an app's
// event channel.
//
// Other packages can also register event filters, e.g. to manage resources in
// response to lifecycle events. Such packages should call:
// event.RegisterFilter(etc)
// in an init function inside that package.
//
// The program code that consumes an app's events is expected to call
// event.Filter on every event they receive, and then switch on its type:
// for e := range a.Events() {
// switch e := event.Filter(e).(type) {
// etc
// }
// }
package event // import "golang.org/x/mobile/event"
// The best source on android input events is the NDK: include/android/input.h
//
// iOS event handling guide:
// https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS
// TODO: keyboard events.
import (
"fmt"
"golang.org/x/mobile/geom"
)
var filters []func(interface{}) interface{}
// Filter calls each registered filter function in sequence.
func Filter(event interface{}) interface{} {
for _, f := range filters {
event = f(event)
}
return event
}
// RegisterFilter registers a filter function to be called by Filter. The
// function can return a different event, or return nil to consume the event,
// but the function can also return its argument unchanged, where its purpose
// is to trigger a side effect rather than modify the event.
//
// RegisterFilter should only be called from init functions.
func RegisterFilter(f func(interface{}) interface{}) {
filters = append(filters, f)
}
// Change is a change in state, such as key or mouse button being up (off) or
// down (on). Some events with a Change-typed field can have no change in
// state, such as a key repeat or a mouse or touch drag.
type Change uint32
func (c Change) String() string {
switch c {
case ChangeOn:
return "on"
case ChangeOff:
return "off"
}
return "none"
}
const (
ChangeNone Change = 0
ChangeOn Change = 1
ChangeOff Change = 2
)
// Config holds the dimensions and physical resolution of the app's window.
type Config struct {
// Width and Height are the window's dimensions.
Width, Height geom.Pt
// PixelsPerPt is the window's physical resolution. It is the number of
// pixels in a single geom.Pt, from the golang.org/x/mobile/geom package.
//
// There are a wide variety of pixel densities in existing phones and
// tablets, so apps should be written to expect various non-integer
// PixelsPerPt values. In general, work in geom.Pt.
PixelsPerPt float32
}
// Draw indicates that the app is ready to draw the next frame of the GUI. A
// frame is completed by calling the App's EndDraw method.
type Draw struct{}
// Lifecycle is a lifecycle change from an old stage to a new stage.
type Lifecycle struct {
From, To LifecycleStage
}
// Crosses returns whether the transition from From to To crosses the stage s:
// - It returns ChangeOn if it does, and the Lifecycle change is positive.
// - It returns ChangeOff if it does, and the Lifecycle change is negative.
// - Otherwise, it returns ChangeNone.
// See the documentation for LifecycleStage for more discussion of positive and
// negative changes, and crosses.
func (l Lifecycle) Crosses(s LifecycleStage) Change {
switch {
case l.From < s && l.To >= s:
return ChangeOn
case l.From >= s && l.To < s:
return ChangeOff
}
return ChangeNone
}
// LifecycleStage is a stage in the app's lifecycle. The values are ordered, so
// that a lifecycle change from stage From to stage To implicitly crosses every
// stage in the range (min, max], exclusive on the low end and inclusive on the
// high end, where min is the minimum of From and To, and max is the maximum.
//
// The documentation for individual stages talk about positive and negative
// crosses. A positive Lifecycle change is one where its From stage is less
// than its To stage. Similarly, a negative Lifecycle change is one where From
// is greater than To. Thus, a positive Lifecycle change crosses every stage in
// the range (From, To] in increasing order, and a negative Lifecycle change
// crosses every stage in the range (To, From] in decreasing order.
type LifecycleStage uint32
// TODO: how does iOS map to these stages? What do cross-platform mobile
// abstractions do?
const (
// LifecycleStageDead is the zero stage. No Lifecycle change crosses this
// stage, but:
// - A positive change from this stage is the very first lifecycle change.
// - A negative change to this stage is the very last lifecycle change.
LifecycleStageDead LifecycleStage = iota
// LifecycleStageAlive means that the app is alive.
// - A positive cross means that the app has been created.
// - A negative cross means that the app is being destroyed.
// Each cross, either from or to LifecycleStageDead, will occur only once.
// On Android, these correspond to onCreate and onDestroy.
LifecycleStageAlive
// LifecycleStageVisible means that the app window is visible.
// - A positive cross means that the app window has become visible.
// - A negative cross means that the app window has become invisible.
// On Android, these correspond to onStart and onStop.
// On Desktop, an app window can become invisible if e.g. it is minimized,
// unmapped, or not on a visible workspace.
LifecycleStageVisible
// LifecycleStageFocused means that the app window has the focus.
// - A positive cross means that the app window has gained the focus.
// - A negative cross means that the app window has lost the focus.
// On Android, these correspond to onResume and onFreeze.
LifecycleStageFocused
)
// Touch is a user touch event.
//
// The same ID is shared by all events in a sequence. A sequence starts with a
// single TouchStart, is followed by zero or more TouchMoves, and ends with a
// single TouchEnd. An ID distinguishes concurrent sequences but is
// subsequently reused.
//
// On Android, Touch is an AInputEvent with AINPUT_EVENT_TYPE_MOTION.
// On iOS, Touch is the UIEvent delivered to a UIView.
type Touch struct {
ID TouchSequenceID
Type TouchType
Loc geom.Point
}
func (t Touch) String() string {
var ty string
switch t.Type {
case TouchStart:
ty = "start"
case TouchMove:
ty = "move "
case TouchEnd:
ty = "end "
}
return fmt.Sprintf("Touch{ %s, %s }", ty, t.Loc)
}
// TouchSequenceID identifies a sequence of Touch events.
type TouchSequenceID int64
// TODO: change TouchType to Change.
// TouchType describes the type of a touch event.
type TouchType byte
const (
// TouchStart is a user first touching the device.
//
// On Android, this is a AMOTION_EVENT_ACTION_DOWN.
// On iOS, this is a call to touchesBegan.
TouchStart TouchType = iota
// TouchMove is a user dragging across the device.
//
// A TouchMove is delivered between a TouchStart and TouchEnd.
//
// On Android, this is a AMOTION_EVENT_ACTION_MOVE.
// On iOS, this is a call to touchesMoved.
TouchMove
// TouchEnd is a user no longer touching the device.
//
// On Android, this is a AMOTION_EVENT_ACTION_UP.
// On iOS, this is a call to touchesEnded.
TouchEnd
)

View File

@ -1,75 +0,0 @@
// 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 event defines user input events.
package event // import "golang.org/x/mobile/event"
/*
The best source on android input events is the NDK: include/android/input.h
iOS event handling guide:
https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS
*/
import (
"fmt"
"golang.org/x/mobile/geom"
)
// Touch is a user touch event.
//
// The same ID is shared by all events in a sequence. A sequence starts with a
// single TouchStart, is followed by zero or more TouchMoves, and ends with a
// single TouchEnd. An ID distinguishes concurrent sequences but is
// subsequently reused.
//
// On Android, this is an AInputEvent with AINPUT_EVENT_TYPE_MOTION.
// On iOS, it is the UIEvent delivered to a UIView.
type Touch struct {
ID TouchSequenceID
Type TouchType
Loc geom.Point
}
func (t Touch) String() string {
var ty string
switch t.Type {
case TouchStart:
ty = "start"
case TouchMove:
ty = "move "
case TouchEnd:
ty = "end "
}
return fmt.Sprintf("Touch{ %s, %s }", ty, t.Loc)
}
// TouchSequenceID identifies a sequence of Touch events.
type TouchSequenceID int64
// TouchType describes the type of a touch event.
type TouchType byte
const (
// TouchStart is a user first touching the device.
//
// On Android, this is a AMOTION_EVENT_ACTION_DOWN.
// On iOS, this is a call to touchesBegan.
TouchStart TouchType = iota
// TouchMove is a user dragging across the device.
//
// A TouchMove is delivered between a TouchStart and TouchEnd.
//
// On Android, this is a AMOTION_EVENT_ACTION_MOVE.
// On iOS, this is a call to touchesMoved.
TouchMove
// TouchEnd is a user no longer touching the device.
//
// On Android, this is a AMOTION_EVENT_ACTION_UP.
// On iOS, this is a call to touchesEnded.
TouchEnd
)

View File

@ -39,12 +39,12 @@ import (
"golang.org/x/mobile/app"
"golang.org/x/mobile/app/debug"
"golang.org/x/mobile/event"
"golang.org/x/mobile/exp/audio"
"golang.org/x/mobile/exp/sprite"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/exp/sprite/glsprite"
"golang.org/x/mobile/f32"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl"
)
@ -54,8 +54,7 @@ const (
)
var (
startClock = time.Now()
lastClock = clock.Time(-1)
startTime = time.Now()
eng = glsprite.Engine()
scene *sprite.Node
@ -86,24 +85,15 @@ func stop() {
player.Close()
}
func draw() {
func draw(c event.Config) {
if scene == nil {
loadScene()
loadScene(c)
}
now := clock.Time(time.Since(startClock) * 60 / time.Second)
if now == lastClock {
// TODO: figure out how to limit draw callbacks to 60Hz instead of
// burning the CPU as fast as possible.
// TODO: (relatedly??) sync to vblank?
return
}
lastClock = now
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
eng.Render(scene, now)
debug.DrawFPS()
now := clock.Time(time.Since(startTime) * 60 / time.Second)
eng.Render(scene, now, c)
debug.DrawFPS(c)
}
func newNode() *sprite.Node {
@ -113,7 +103,7 @@ func newNode() *sprite.Node {
return n
}
func loadScene() {
func loadScene(c event.Config) {
gopher := loadGopher()
scene = &sprite.Node{}
eng.Register(scene)
@ -137,11 +127,11 @@ func loadScene() {
dy = 1
boing()
}
if x+width > float32(geom.Width) {
if x+width > float32(c.Width) {
dx = -1
boing()
}
if y+height > float32(geom.Height) {
if y+height > float32(c.Height) {
dy = -1
boing()
}

View File

@ -52,10 +52,11 @@ var (
func main() {
app.Run(app.Callbacks{
Start: start,
Stop: stop,
Draw: draw,
Touch: touch,
Start: start,
Stop: stop,
Draw: draw,
Touch: touch,
Config: config,
})
}
@ -74,9 +75,9 @@ func start() {
position = gl.GetAttribLocation(program, "position")
color = gl.GetUniformLocation(program, "color")
offset = gl.GetUniformLocation(program, "offset")
touchLoc = geom.Point{geom.Width / 2, geom.Height / 2}
// TODO(crawshaw): the debug package needs to put GL state init here
// Can this be an event.Register call now??
}
func stop() {
@ -84,11 +85,15 @@ func stop() {
gl.DeleteBuffer(buf)
}
func touch(t event.Touch) {
func config(new, old event.Config) {
touchLoc = geom.Point{new.Width / 2, new.Height / 2}
}
func touch(t event.Touch, c event.Config) {
touchLoc = t.Loc
}
func draw() {
func draw(c event.Config) {
gl.ClearColor(1, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
@ -100,7 +105,7 @@ func draw() {
}
gl.Uniform4f(color, 0, green, 0, 1)
gl.Uniform2f(offset, float32(touchLoc.X/geom.Width), float32(touchLoc.Y/geom.Height))
gl.Uniform2f(offset, float32(touchLoc.X/c.Width), float32(touchLoc.Y/c.Height))
gl.BindBuffer(gl.ARRAY_BUFFER, buf)
gl.EnableVertexAttribArray(position)
@ -108,7 +113,7 @@ func draw() {
gl.DrawArrays(gl.TRIANGLES, 0, vertexCount)
gl.DisableVertexAttribArray(position)
debug.DrawFPS()
debug.DrawFPS(c)
}
var triangleData = f32.Bytes(binary.LittleEndian,

View File

@ -45,41 +45,26 @@ import (
)
var (
start = time.Now()
lastClock = clock.Time(-1)
eng = glsprite.Engine()
scene *sprite.Node
startTime = time.Now()
eng = glsprite.Engine()
scene *sprite.Node
)
func main() {
app.Run(app.Callbacks{
Draw: draw,
Touch: touch,
Draw: draw,
})
}
func draw() {
func draw(c event.Config) {
if scene == nil {
loadScene()
}
now := clock.Time(time.Since(start) * 60 / time.Second)
if now == lastClock {
// TODO: figure out how to limit draw callbacks to 60Hz instead of
// burning the CPU as fast as possible.
// TODO: (relatedly??) sync to vblank?
return
}
lastClock = now
gl.ClearColor(1, 1, 1, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
eng.Render(scene, now)
debug.DrawFPS()
}
func touch(t event.Touch) {
now := clock.Time(time.Since(startTime) * 60 / time.Second)
eng.Render(scene, now, c)
debug.DrawFPS(c)
}
func newNode() *sprite.Node {

View File

@ -193,9 +193,12 @@ ALboolean call_alIsBuffer(LPALISBUFFER fn, ALuint b) {
}
*/
import "C"
import "unsafe"
import (
"log"
"unsafe"
import "golang.org/x/mobile/app"
"golang.org/x/mobile/app"
)
var (
alHandle unsafe.Pointer
@ -243,8 +246,8 @@ var (
)
func initAL() {
cfg := app.GetConfig()
alHandle = C.al_init(cfg.JavaVM(), cfg.AndroidContext())
ctx := app.Context{}
alHandle = C.al_init(ctx.JavaVM(), ctx.AndroidContext())
alEnableFunc = C.LPALENABLE(fn("alEnable"))
alDisableFunc = C.LPALDISABLE(fn("alDisable"))
alIsEnabledFunc = C.LPALISENABLED(fn("alIsEnabled"))
@ -286,6 +289,8 @@ func initAL() {
alcCreateContextFunc = C.LPALCCREATECONTEXT(fn("alcCreateContext"))
alcMakeContextCurrentFunc = C.LPALCMAKECONTEXTCURRENT(fn("alcMakeContextCurrent"))
alcDestroyContextFunc = C.LPALCDESTROYCONTEXT(fn("alcDestroyContext"))
log.Printf("alcOpenDeviceFunc=%v", alcOpenDeviceFunc)
}
func fn(fname string) unsafe.Pointer {

View File

@ -12,6 +12,7 @@ import (
"image"
"image/draw"
"golang.org/x/mobile/event"
"golang.org/x/mobile/exp/sprite"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/f32"
@ -90,15 +91,15 @@ func (e *engine) SetTransform(n *sprite.Node, m f32.Affine) {
e.nodes[n.EngineFields.Index].relTransform = m
}
func (e *engine) Render(scene *sprite.Node, t clock.Time) {
func (e *engine) Render(scene *sprite.Node, t clock.Time, cfg event.Config) {
e.absTransforms = append(e.absTransforms[:0], f32.Affine{
{1, 0, 0},
{0, 1, 0},
})
e.render(scene, t)
e.render(scene, t, cfg)
}
func (e *engine) render(n *sprite.Node, t clock.Time) {
func (e *engine) render(n *sprite.Node, t clock.Time, cfg event.Config) {
if n.EngineFields.Index == 0 {
panic("glsprite: sprite.Node not registered")
}
@ -115,6 +116,7 @@ func (e *engine) render(n *sprite.Node, t clock.Time) {
if x := n.EngineFields.SubTex; x.T != nil {
x.T.(*texture).glImage.Draw(
cfg,
geom.Point{
geom.Pt(m[0][2]),
geom.Pt(m[1][2]),
@ -132,7 +134,7 @@ func (e *engine) render(n *sprite.Node, t clock.Time) {
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
e.render(c, t)
e.render(c, t, cfg)
}
// Pop absTransforms.

View File

@ -14,6 +14,7 @@ import (
"os"
"testing"
"golang.org/x/mobile/event"
"golang.org/x/mobile/f32"
"golang.org/x/mobile/geom"
)
@ -37,9 +38,11 @@ func TestAffine(t *testing.T) {
ptW = geom.Pt(50)
ptH = geom.Pt(50)
)
geom.PixelsPerPt = float32(pixW) / float32(ptW)
geom.Width = ptW
geom.Height = ptH
cfg := event.Config{
Width: ptW,
Height: ptH,
PixelsPerPt: float32(pixW) / float32(ptW),
}
got := image.NewRGBA(image.Rect(0, 0, pixW, pixH))
blue := image.NewUniform(color.RGBA{B: 0xff, A: 0xff})
@ -51,7 +54,7 @@ func TestAffine(t *testing.T) {
var a f32.Affine
a.Identity()
a.Scale(&a, geom.PixelsPerPt, geom.PixelsPerPt)
a.Scale(&a, cfg.PixelsPerPt, cfg.PixelsPerPt)
a.Translate(&a, 0, 24)
a.Rotate(&a, float32(math.Asin(12./20)))
// See commentary in the render method defined in portable.go.
@ -64,8 +67,8 @@ func TestAffine(t *testing.T) {
ptBottomRight := geom.Point{12 + 32, 16}
drawCross(got, 0, 0)
drawCross(got, int(ptTopLeft.X.Px()), int(ptTopLeft.Y.Px()))
drawCross(got, int(ptBottomRight.X.Px()), int(ptBottomRight.Y.Px()))
drawCross(got, int(ptTopLeft.X.Px(cfg.PixelsPerPt)), int(ptTopLeft.Y.Px(cfg.PixelsPerPt)))
drawCross(got, int(ptBottomRight.X.Px(cfg.PixelsPerPt)), int(ptBottomRight.Y.Px(cfg.PixelsPerPt)))
drawCross(got, pixW-1, pixH-1)
const wantPath = "../../../testdata/testpattern-window.png"

View File

@ -13,10 +13,10 @@ import (
"image"
"image/draw"
"golang.org/x/mobile/event"
"golang.org/x/mobile/exp/sprite"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/f32"
"golang.org/x/mobile/geom"
)
// Engine builds a sprite Engine that renders onto dst.
@ -92,13 +92,13 @@ func (e *engine) SetTransform(n *sprite.Node, m f32.Affine) {
e.nodes[n.EngineFields.Index].relTransform = m
}
func (e *engine) Render(scene *sprite.Node, t clock.Time) {
func (e *engine) Render(scene *sprite.Node, t clock.Time, cfg event.Config) {
// Affine transforms are done in geom.Pt. When finally drawing
// the geom.Pt onto an image.Image we need to convert to system
// pixels. We scale by geom.PixelsPerPt to do this.
// pixels. We scale by cfg.PixelsPerPt to do this.
e.absTransforms = append(e.absTransforms[:0], f32.Affine{
{geom.PixelsPerPt, 0, 0},
{0, geom.PixelsPerPt, 0},
{cfg.PixelsPerPt, 0, 0},
{0, cfg.PixelsPerPt, 0},
})
e.render(scene, t)
}
@ -128,7 +128,7 @@ func (e *engine) render(n *sprite.Node, t clock.Time) {
// should have the dimensions (1pt, 1pt). To do this we divide
// by the pixel width and height, reducing the texture to
// (1px, 1px) of the destination image. Multiplying by
// geom.PixelsPerPt, done in Render above, makes it (1pt, 1pt).
// cfg.PixelsPerPt, done in Render above, makes it (1pt, 1pt).
dx, dy := x.R.Dx(), x.R.Dy()
if dx > 0 && dy > 0 {
m.Scale(&m, 1/float32(dx), 1/float32(dy))

View File

@ -20,7 +20,7 @@
// quantize time.Now() to a clock.Time
// process UI events
// modify the scene's nodes and animations (Arranger values)
// e.Render(scene, t)
// e.Render(scene, t, c)
// }
package sprite // import "golang.org/x/mobile/exp/sprite"
@ -28,6 +28,7 @@ import (
"image"
"image/draw"
"golang.org/x/mobile/event"
"golang.org/x/mobile/exp/sprite/clock"
"golang.org/x/mobile/f32"
)
@ -57,7 +58,9 @@ type Engine interface {
SetSubTex(n *Node, x SubTex)
SetTransform(n *Node, m f32.Affine) // sets transform relative to parent.
Render(scene *Node, t clock.Time)
// Render renders the scene arranged at the given time, for the given
// window configuration (dimensions and resolution).
Render(scene *Node, t clock.Time, c event.Config)
}
// A Node is a renderable element and forms a tree of Nodes.

View File

@ -79,7 +79,7 @@ import "fmt"
type Pt float32
// Px converts the length to current device pixels.
func (p Pt) Px() float32 { return float32(p) * PixelsPerPt }
func (p Pt) Px(pixelsPerPt float32) float32 { return float32(p) * pixelsPerPt }
// String returns a string representation of p like "3.2pt".
func (p Pt) String() string { return fmt.Sprintf("%.2fpt", p) }
@ -100,18 +100,3 @@ type Rectangle struct {
// String returns a string representation of r like "(3,4)-(6,5)".
func (r Rectangle) String() string { return r.Min.String() + "-" + r.Max.String() }
// PixelsPerPt is the number of pixels in a single Pt on the current device.
//
// There are a wide variety of pixel densities in existing phones and
// tablets, so apps should be written to expect various non-integer
// PixelsPerPt values. In general, work in Pt.
//
// Not valid until app initialization has completed.
var PixelsPerPt float32
// Width is deprecated. Use app.GetConfig().Width.
var Width Pt
// Height is deprecated. Use app.GetConfig().Height.
var Height Pt

2089
gl/gl.go

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ import (
"runtime"
"sync"
"golang.org/x/mobile/app"
"golang.org/x/mobile/event"
"golang.org/x/mobile/f32"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl"
@ -31,9 +31,16 @@ var glimage struct {
}
func init() {
app.Register(app.Callbacks{
Start: start,
Stop: stop,
event.RegisterFilter(func(e interface{}) interface{} {
if e, ok := e.(event.Lifecycle); ok {
switch e.Crosses(event.LifecycleStageVisible) {
case event.ChangeOn:
start()
case event.ChangeOff:
stop()
}
}
return e
})
}
@ -216,7 +223,7 @@ func (img *Image) Delete() {
// 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(topLeft, topRight, bottomLeft geom.Point, srcBounds image.Rectangle) {
func (img *Image) Draw(c event.Config, topLeft, topRight, bottomLeft geom.Point, srcBounds image.Rectangle) {
// TODO(crawshaw): Adjust viewport for the top bar on android?
gl.UseProgram(glimage.program)
tex := texmap.get(*img.key)
@ -256,12 +263,12 @@ func (img *Image) Draw(topLeft, topRight, bottomLeft geom.Point, srcBounds image
// First of all, convert from geom space to framebuffer space. For
// later convenience, we divide everything by 2 here: px2 is half of
// the P.X co-ordinate (in framebuffer space).
px2 := -0.5 + float32(topLeft.X/geom.Width)
py2 := +0.5 - float32(topLeft.Y/geom.Height)
qx2 := -0.5 + float32(topRight.X/geom.Width)
qy2 := +0.5 - float32(topRight.Y/geom.Height)
sx2 := -0.5 + float32(bottomLeft.X/geom.Width)
sy2 := +0.5 - float32(bottomLeft.Y/geom.Height)
px2 := -0.5 + float32(topLeft.X/c.Width)
py2 := +0.5 - float32(topLeft.Y/c.Height)
qx2 := -0.5 + float32(topRight.X/c.Width)
qy2 := +0.5 - float32(topRight.Y/c.Height)
sx2 := -0.5 + float32(bottomLeft.X/c.Width)
sy2 := +0.5 - float32(bottomLeft.Y/c.Height)
// Next, solve for the affine transformation matrix
// [ a00 a01 a02 ]
// a = [ a10 a11 a12 ]

View File

@ -15,13 +15,33 @@ import (
"image/png"
"io/ioutil"
"os"
"runtime"
"testing"
"golang.org/x/mobile/event"
"golang.org/x/mobile/geom"
"golang.org/x/mobile/gl"
)
func TestImage(t *testing.T) {
done := make(chan struct{})
defer close(done)
go func() {
runtime.LockOSThread()
ctx := createContext()
for {
select {
case <-gl.WorkAvailable:
gl.DoWork()
case <-done:
ctx.destroy()
return
}
}
}()
start()
defer stop()
// GL testing strategy:
// 1. Create an offscreen framebuffer object.
// 2. Configure framebuffer to render to a GL texture.
@ -39,26 +59,17 @@ func TestImage(t *testing.T) {
t.Fatal(err)
}
var ctxGL *contextGL
go gl.Start(func() {
ctxGL = createContext()
})
start()
defer func() {
stop()
gl.Stop()
ctxGL.destroy()
}()
const (
pixW = 100
pixH = 100
ptW = geom.Pt(50)
ptH = geom.Pt(50)
)
geom.PixelsPerPt = float32(pixW) / float32(ptW)
geom.Width = ptW
geom.Height = ptH
cfg := event.Config{
Width: ptW,
Height: ptH,
PixelsPerPt: float32(pixW) / float32(ptW),
}
fBuf := gl.CreateFramebuffer()
gl.BindFramebuffer(gl.FRAMEBUFFER, fBuf)
@ -96,7 +107,7 @@ func TestImage(t *testing.T) {
ptTopRight := geom.Point{32, 0}
ptBottomLeft := geom.Point{12, 24 + 16}
ptBottomRight := geom.Point{12 + 32, 16}
m.Draw(ptTopLeft, ptTopRight, ptBottomLeft, b)
m.Draw(cfg, ptTopLeft, ptTopRight, ptBottomLeft, b)
// For unknown reasons, a windowless OpenGL context renders upside-
// down. That is, a quad covering the initial viewport spans:
@ -118,8 +129,8 @@ func TestImage(t *testing.T) {
}
drawCross(got, 0, 0)
drawCross(got, int(ptTopLeft.X.Px()), int(ptTopLeft.Y.Px()))
drawCross(got, int(ptBottomRight.X.Px()), int(ptBottomRight.Y.Px()))
drawCross(got, int(ptTopLeft.X.Px(cfg.PixelsPerPt)), int(ptTopLeft.Y.Px(cfg.PixelsPerPt)))
drawCross(got, int(ptBottomRight.X.Px(cfg.PixelsPerPt)), int(ptBottomRight.Y.Px(cfg.PixelsPerPt)))
drawCross(got, pixW-1, pixH-1)
const wantPath = "../../testdata/testpattern-window.png"

View File

@ -11,9 +11,6 @@ void processFn(struct fnargs* args) {
case glfnUNDEFINED:
abort(); // bad glfn
break;
case glfnStop:
abort(); // should never make it into C
break;
case glfnActiveTexture:
glActiveTexture((GLenum)args->a0);
break;

View File

@ -29,7 +29,6 @@ void process(int count) {
}
*/
import "C"
import "runtime"
// work is a queue of calls to execute.
var work = make(chan call, 10)
@ -37,46 +36,47 @@ var work = make(chan call, 10)
// retvalue is sent a return value when blocking calls complete.
// It is safe to use a global unbuffered channel here as calls
// cannot currently be made concurrently.
//
// TODO: the comment above about concurrent calls isn't actually true: package
// app calls package gl, but it has to do so in a separate goroutine, which
// means that its gl calls (which may be blocking) can race with other gl calls
// in the main program. We should make it safe to issue blocking gl calls
// concurrently, or get the gl calls out of package app, or both.
var retvalue = make(chan C.uintptr_t)
type call struct {
blocking bool
fn func()
args C.struct_fnargs
blocking bool
}
// Do calls fn on the OS thread with the GL context.
func Do(fn func()) {
work <- call{
fn: fn,
blocking: true,
func enqueue(c call) C.uintptr_t {
work <- c
select {
case workAvailable <- struct{}{}:
default:
}
<-retvalue
if c.blocking {
return <-retvalue
}
return 0
}
// Stop stops the current GL processing.
func Stop() {
var call call
call.blocking = true
call.args.fn = C.glfnStop
work <- call
<-retvalue
}
var (
workAvailable = make(chan struct{}, 1)
// WorkAvailable communicates when DoWork should be called.
//
// This is an internal implementation detail and should only be used by the
// golang.org/x/mobile/app package.
WorkAvailable <-chan struct{} = workAvailable
)
// Start executes GL functions on a fixed OS thread, starting with initCtx.
// It blocks until Stop is called. Typical use:
// DoWork performs any pending OpenGL calls.
//
// go gl.Start(func() {
// // establish a GL context, using for example, EGL.
// })
//
// // long running GL calls from any goroutine
//
// gl.Stop()
func Start(initCtx func()) {
runtime.LockOSThread()
initCtx()
// This is an internal implementation detail and should only be used by the
// golang.org/x/mobile/app package.
func DoWork() {
queue := make([]call, 0, len(work))
for {
// Wait until at least one piece of work is ready.
@ -84,6 +84,8 @@ func Start(initCtx func()) {
select {
case w := <-work:
queue = append(queue, w)
default:
return
}
blocking := queue[len(queue)-1].blocking
enqueue:
@ -98,27 +100,16 @@ func Start(initCtx func()) {
}
// Process the queued GL functions.
fn := queue[len(queue)-1].fn
stop := queue[len(queue)-1].args.fn == C.glfnStop
if fn != nil || stop {
queue = queue[:len(queue)-1]
}
for i := range queue {
C.cargs[i] = queue[i].args
for i, q := range queue {
C.cargs[i] = q.args
}
C.process(C.int(len(queue)))
if fn != nil {
fn()
}
// Cleanup and signal.
queue = queue[:0]
if blocking {
retvalue <- C.ret
}
if stop {
return
}
}
}

View File

@ -17,7 +17,6 @@
typedef enum {
glfnUNDEFINED,
glfnStop,
glfnActiveTexture,
glfnAttachShader,