From 5cddc1460eb901a432e0f1c49bf4676bae8c5e7b Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Tue, 5 May 2015 06:52:19 -0700 Subject: [PATCH] app: introduce Config and start registration Config provides a way to concurrently access Width and Height. Register provides a way for packages to run code on app state change without plumbing changes all the way to the process main function. This is motivated by gl/glutil.Image which needs to rebuild its textures on start/stop and can be deeply nested. (See golang.org/cl/9707 for the followup.) Tested manually on android and darwin/amd64. Doing this kind makes it clear any CL modifying this code needs a lot of manual testing right now, so some kind of trybot support is something I'm going to prioritise. Fixes golang/go#10686 Fixes golang/go#10461 Fixes golang/go#10442 Fixes golang/go#10226 Updates golang/go#10327 Change-Id: I2882ebf3995b6ed857cda823e94fbb17c54b43a8 Reviewed-on: https://go-review.googlesource.com/9708 Reviewed-by: Hyang-Ah Hana Kim --- app/android.c | 11 ++-- app/android.go | 33 ++++-------- app/app.go | 52 ++++++++++++++++--- app/darwin_amd64.go | 36 +++++++------ app/darwin_arm.go | 7 ++- app/loop_android.go | 51 ++++++++++++------- app/state.go | 106 +++++++++++++++++++++++++++++++++++++++ app/x11.go | 37 +++++++------- audio/al/al_android.go | 2 +- bind/java/seq_android.go | 8 +-- geom/geom.go | 6 +-- 11 files changed, 252 insertions(+), 97 deletions(-) create mode 100644 app/state.go diff --git a/app/android.c b/app/android.c index 9b9b019..d2f1db2 100644 --- a/app/android.c +++ b/app/android.c @@ -148,12 +148,13 @@ static void* call_main_and_wait() { // Runtime entry point when using NativeActivity. void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) { - // Note that activity->clazz is mis-named. - current_vm = activity->vm; - current_ctx = (*activity->env)->NewGlobalRef(activity->env, activity->clazz); current_native_activity = activity; - - call_main_and_wait(); + if (current_ctx == NULL) { + // Note that activity->clazz is mis-named. + current_vm = activity->vm; + current_ctx = (*activity->env)->NewGlobalRef(activity->env, activity->clazz); + call_main_and_wait(); + } // These functions match the methods on Activity, described at // http://developer.android.com/reference/android/app/Activity.html diff --git a/app/android.go b/app/android.go index 02208c1..0f5cdc0 100644 --- a/app/android.go +++ b/app/android.go @@ -69,8 +69,6 @@ import ( "golang.org/x/mobile/geom" ) -var running = make(chan struct{}) // closed after app.Run is called - //export callMain func callMain(mainPC uintptr) { for _, name := range []string{"TMPDIR", "PATH", "LD_LIBRARY_PATH"} { @@ -79,7 +77,7 @@ func callMain(mainPC uintptr) { C.free(unsafe.Pointer(n)) } go callfn.CallFn(mainPC) - <-running + <-mainCalled log.Print("app.Run called") } @@ -187,20 +185,17 @@ func onConfigurationChanged(activity *C.ANativeActivity) { func onLowMemory(activity *C.ANativeActivity) { } -type androidState struct { -} - -func (androidState) JavaVM() unsafe.Pointer { +func (Config) JavaVM() unsafe.Pointer { return unsafe.Pointer(C.current_vm) } // ClassFinder returns a C function pointer for finding a given class using // the app class loader. (jclass) (*fn)(JNIEnv*, const char*). -func (androidState) ClassFinder() unsafe.Pointer { +func (Config) ClassFinder() unsafe.Pointer { return unsafe.Pointer(C.app_find_class) } -func (androidState) AndroidContext() unsafe.Pointer { +func (Config) AndroidContext() unsafe.Pointer { return unsafe.Pointer(C.current_ctx) } @@ -261,17 +256,9 @@ func (a *asset) Close() error { return nil } -func runStart(cb Callbacks) { - State = androidState{} - - if cb.Start != nil { - cb.Start() - } -} - // notifyInitDone informs Java that the program is initialized. // A NativeActivity will not create a window until this is called. -func run(cb Callbacks) { +func run(callbacks []Callbacks) { // We want to keep the event loop on a consistent OS thread. runtime.LockOSThread() @@ -281,12 +268,14 @@ func run(cb Callbacks) { C.free(unsafe.Pointer(ctag)) C.free(unsafe.Pointer(cstr)) + close(mainCalled) if C.current_native_activity == nil { - runStart(cb) - close(running) + stateStart(callbacks) + // TODO: stateStop under some conditions. select {} } else { - close(running) - windowDrawLoop(cb, <-windowCreated, queue) + for w := range windowCreated { + windowDraw(callbacks, w, queue) + } } } diff --git a/app/app.go b/app/app.go index 75652f7..7bd0b86 100644 --- a/app/app.go +++ b/app/app.go @@ -10,14 +10,20 @@ import ( "io" "golang.org/x/mobile/event" + "golang.org/x/mobile/geom" ) +var callbacks []Callbacks + // Run starts the app. // // It must be called directly from the main function and will // block until the app exits. +// +// TODO(crawshaw): Remove cb parameter. func Run(cb Callbacks) { - run(cb) + callbacks = append(callbacks, cb) + run(callbacks) } // Callbacks is the set of functions called by the app. @@ -38,7 +44,7 @@ type Callbacks struct { // Stop is called shortly before a program is suspended. // - // When Stop is received, the app is no longer visible and not is + // When Stop is received, the app is no longer visible and is not // receiving events. It should: // // - Save any state the user expects saved (for example text). @@ -63,6 +69,15 @@ type Callbacks struct { // Touch is called by the app when a touch event occurs. Touch func(event.Touch) + + // 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) } // Open opens a named asset. @@ -87,9 +102,22 @@ type ReadSeekCloser interface { io.Closer } -// State is global application-specific state. +// 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 State variable also holds operating system specific state. +// The Config variable also holds operating system specific state. // Android apps have the extra methods: // // // JavaVM returns a JNI *JavaVM. @@ -98,5 +126,17 @@ type ReadSeekCloser interface { // // AndroidContext returns a jobject for the app android.context.Context. // AndroidContext() unsafe.Pointer // -// State is not valid until Run has been called. -var State interface{} +// 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 +} diff --git a/app/darwin_amd64.go b/app/darwin_amd64.go index 8c8acd6..80ff04e 100644 --- a/app/darwin_amd64.go +++ b/app/darwin_amd64.go @@ -52,20 +52,23 @@ func init() { initThreadID = uint64(C.threadID()) } -func run(callbacks Callbacks) { +func run(callbacks []Callbacks) { if tid := uint64(C.threadID()); tid != initThreadID { log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID) } - cb = callbacks + close(mainCalled) C.runApp() } //export setGeom func setGeom(pixelsPerPt float32, width, height int) { - // Macs default to 72 DPI, so scales are equivalent. - geom.PixelsPerPt = pixelsPerPt - geom.Width = geom.Pt(float32(width) / pixelsPerPt) - geom.Height = geom.Pt(float32(height) / pixelsPerPt) + if geom.PixelsPerPt == 0 { + // Macs default to 72 DPI, so scales are equivalent. + geom.PixelsPerPt = pixelsPerPt + } + configAlt.Width = geom.Pt(float32(width) / geom.PixelsPerPt) + configAlt.Height = geom.Pt(float32(height) / geom.PixelsPerPt) + configSwap(callbacks) } func initGL() { @@ -74,12 +77,9 @@ func initGL() { var id C.GLuint C.glGenVertexArrays(1, &id) C.glBindVertexArray(id) - if cb.Start != nil { - cb.Start() - } + stateStart(callbacks) } -var cb Callbacks var initGLOnce sync.Once var touchEvents struct { @@ -94,7 +94,7 @@ func sendTouch(ty event.TouchType, x, y float32) { Type: ty, Loc: geom.Point{ X: geom.Pt(x / geom.PixelsPerPt), - Y: geom.Height - geom.Pt(y/geom.PixelsPerPt), + Y: GetConfig().Height - geom.Pt(y/geom.PixelsPerPt), }, }) touchEvents.Unlock() @@ -124,17 +124,21 @@ func drawgl(ctx C.GLintptr) { pending := touchEvents.pending touchEvents.pending = nil touchEvents.Unlock() - if cb.Touch != nil { - for _, e := range pending { - cb.Touch(e) + for _, cb := range callbacks { + if cb.Touch != nil { + for _, e := range pending { + cb.Touch(e) + } } } // 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) - if cb.Draw != nil { - cb.Draw() + for _, cb := range callbacks { + if cb.Draw != nil { + cb.Draw() + } } C.unlockContext(ctx) diff --git a/app/darwin_arm.go b/app/darwin_arm.go index 7ee7dce..16cf896 100644 --- a/app/darwin_arm.go +++ b/app/darwin_arm.go @@ -110,11 +110,14 @@ func setGeom(width, height int) { if geom.PixelsPerPt == 0 { geom.PixelsPerPt = float32(ppi()) / 72 } - geom.Width = geom.Pt(float32(width) / geom.PixelsPerPt) - geom.Height = geom.Pt(float32(height) / geom.PixelsPerPt) + configAlt.Width = geom.Pt(float32(width) / geom.PixelsPerPt) + configAlt.Height = geom.Pt(float32(height) / geom.PixelsPerPt) + configSwap() + } func initGL() { + stateStart() } var cb Callbacks diff --git a/app/loop_android.go b/app/loop_android.go index 3ab3945..d6dc7d5 100644 --- a/app/loop_android.go +++ b/app/loop_android.go @@ -90,7 +90,7 @@ import ( "golang.org/x/mobile/gl" ) -func windowDrawLoop(cb Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) { +func windowDraw(callbacks []Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) { C.createEGLWindow(w) // TODO: is the library or the app responsible for clearing the buffers? @@ -102,51 +102,61 @@ func windowDrawLoop(cb Callbacks, w *C.ANativeWindow, queue *C.AInputQueue) { log.Printf("GL initialization error: %s", errv) } - geom.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt) - geom.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt) + 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. - runStart(cb) + stateStart(callbacks) for { - processEvents(cb, queue) + processEvents(callbacks, queue) select { case <-windowRedrawNeeded: // Re-query the width and height. C.querySurfaceWidthAndHeight() - geom.Width = geom.Pt(float32(C.windowWidth) / geom.PixelsPerPt) - geom.Height = geom.Pt(float32(C.windowHeight) / geom.PixelsPerPt) + 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: - if cb.Stop != nil { - cb.Stop() - } + stateStop(callbacks) return default: - if cb.Draw != nil { - cb.Draw() + for _, cb := range callbacks { + if cb.Draw != nil { + cb.Draw() + } } C.eglSwapBuffers(C.display, C.surface) } } } -func processEvents(cb Callbacks, queue *C.AInputQueue) { +func processEvents(callbacks []Callbacks, queue *C.AInputQueue) { var event *C.AInputEvent for C.AInputQueue_getEvent(queue, &event) >= 0 { if C.AInputQueue_preDispatchEvent(queue, event) != 0 { continue } - processEvent(cb, event) + processEvent(callbacks, event) C.AInputQueue_finishEvent(queue, event, 0) } } -func processEvent(cb Callbacks, e *C.AInputEvent) { +func processEvent(callbacks []Callbacks, 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: - if cb.Touch == nil { + // TODO: calculate hasTouch once in run + hasTouch := false + for _, cb := range callbacks { + if cb.Touch != nil { + hasTouch = true + } + } + if !hasTouch { return } @@ -165,14 +175,19 @@ func processEvent(cb Callbacks, e *C.AInputEvent) { if i == upDownIndex { typ = upDownTyp } - cb.Touch(event.Touch{ + t := 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), }, - }) + } + for _, cb := range callbacks { + if cb.Touch != nil { + cb.Touch(t) + } + } } default: log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e)) diff --git a/app/state.go b/app/state.go new file mode 100644 index 0000000..f7d2b7c --- /dev/null +++ b/app/state.go @@ -0,0 +1,106 @@ +// 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) + } + } +} diff --git a/app/x11.go b/app/x11.go index 4b03efd..625099d 100644 --- a/app/x11.go +++ b/app/x11.go @@ -29,11 +29,11 @@ import ( "golang.org/x/mobile/geom" ) -var cb Callbacks +var callbacks []Callbacks -func run(callbacks Callbacks) { +func run(cbs []Callbacks) { runtime.LockOSThread() - cb = callbacks + callbacks = cbs C.runApp() } @@ -41,9 +41,12 @@ func run(callbacks Callbacks) { func onResize(w, h int) { // TODO(nigeltao): don't assume 72 DPI. DisplayWidth / DisplayWidthMM // is probably the best place to start looking. - geom.PixelsPerPt = 1 - geom.Width = geom.Pt(w) - geom.Height = geom.Pt(h) + if geom.PixelsPerPt == 0 { + geom.PixelsPerPt = 1 + } + configAlt.Width = geom.Pt(w) + configAlt.Height = geom.Pt(h) + configSwap(callbacks) } var touchEvents struct { @@ -79,27 +82,27 @@ func onDraw() { pending := touchEvents.pending touchEvents.pending = nil touchEvents.Unlock() - if cb.Touch != nil { - for _, e := range pending { - cb.Touch(e) + for _, cb := range callbacks { + if cb.Touch != nil { + for _, e := range pending { + cb.Touch(e) + } } } - if cb.Draw != nil { - cb.Draw() + for _, cb := range callbacks { + if cb.Draw != nil { + cb.Draw() + } } } //export onStart func onStart() { - if cb.Start != nil { - cb.Start() - } + stateInit(callbacks) } //export onStop func onStop() { - if cb.Stop != nil { - cb.Stop() - } + stateStop(callbacks) } diff --git a/audio/al/al_android.go b/audio/al/al_android.go index aa6e7a7..3da5690 100644 --- a/audio/al/al_android.go +++ b/audio/al/al_android.go @@ -243,7 +243,7 @@ var ( ) func initAL() { - state := app.State.(interface { + state := app.GetConfig().(interface { JavaVM() unsafe.Pointer AndroidContext() unsafe.Pointer }) diff --git a/bind/java/seq_android.go b/bind/java/seq_android.go index e7b94df..1d19bde 100644 --- a/bind/java/seq_android.go +++ b/bind/java/seq_android.go @@ -79,12 +79,8 @@ func init() { } func initSeq() { - vm := app.State.(interface { - JavaVM() unsafe.Pointer - }).JavaVM() - classFinder := app.State.(interface { - ClassFinder() unsafe.Pointer - }).ClassFinder() + vm := app.GetConfig().JavaVM() + classFinder := app.GetConfig().ClassFinder() C.init_seq(vm, classFinder) } diff --git a/geom/geom.go b/geom/geom.go index ee2f449..8ae3968 100644 --- a/geom/geom.go +++ b/geom/geom.go @@ -110,10 +110,8 @@ func (r Rectangle) String() string { return r.Min.String() + "-" + r.Max.String( // Not valid until app initialization has completed. var PixelsPerPt float32 -// Width is the width of the device screen. -// Not valid until app initialization has completed. +// Width is deprecated. Use app.GetConfig().Width. var Width Pt -// Height is the height of the device screen. -// Not valid until app initialization has completed. +// Height is deprecated. Use app.GetConfig().Height. var Height Pt