app: be more careful about android redraw events

EGL is a poor source of window dimensions. On some fraction of
rotations, it reported the old dimensions. Switch to extracting the
data from ANativeWindow, which appears to be more reliable.

While here, plumb through orientation, and follow the recommendation
in the Android platform docs to block onNativeWindowRedrawNeeded
until the draw is complete.

Fixes golang/go#11741.

Change-Id: I6b29b6a1e5743c612c9af2a8cce4260c5d7e9ca6
Reviewed-on: https://go-review.googlesource.com/12381
Reviewed-by: Nigel Tao <nigeltao@golang.org>
This commit is contained in:
David Crawshaw 2015-07-17 15:59:46 -04:00
parent 09206a7c58
commit 34f7142b5a
3 changed files with 99 additions and 66 deletions

View File

@ -90,15 +90,14 @@ void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_
activity->callbacks->onDestroy = onDestroy; activity->callbacks->onDestroy = onDestroy;
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
activity->callbacks->onNativeWindowResized = onNativeWindowResized;
activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded; activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
activity->callbacks->onInputQueueCreated = onInputQueueCreated; activity->callbacks->onInputQueueCreated = onInputQueueCreated;
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed; activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
// TODO(crawshaw): Type mismatch for onContentRectChanged.
//activity->callbacks->onContentRectChanged = onContentRectChanged;
activity->callbacks->onConfigurationChanged = onConfigurationChanged; activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onLowMemory = onLowMemory; activity->callbacks->onLowMemory = onLowMemory;
// Note that onNativeWindowResized is not called on resize. Avoid it.
// https://code.google.com/p/android/issues/detail?id=180645
onCreate(activity); onCreate(activity);
} }

View File

@ -81,32 +81,8 @@ func callMain(mainPC uintptr) {
//export onCreate //export onCreate
func onCreate(activity *C.ANativeActivity) { func onCreate(activity *C.ANativeActivity) {
config := C.AConfiguration_new() config := windowConfigRead(activity)
C.AConfiguration_fromAssetManager(config, activity.assetManager) pixelsPerPt = config.pixelsPerPt
density := C.AConfiguration_getDensity(config)
C.AConfiguration_delete(config)
var dpi int
switch density {
case C.ACONFIGURATION_DENSITY_DEFAULT:
dpi = 160
case C.ACONFIGURATION_DENSITY_LOW,
C.ACONFIGURATION_DENSITY_MEDIUM,
213, // C.ACONFIGURATION_DENSITY_TV
C.ACONFIGURATION_DENSITY_HIGH,
320, // ACONFIGURATION_DENSITY_XHIGH
480, // ACONFIGURATION_DENSITY_XXHIGH
640: // ACONFIGURATION_DENSITY_XXXHIGH
dpi = int(density)
case C.ACONFIGURATION_DENSITY_NONE:
log.Print("android device reports no screen density")
dpi = 72
default:
log.Print("android device reports unknown density: %d", density)
dpi = int(density) // This is a guess.
}
pixelsPerPt = float32(dpi) / 72
} }
//export onStart //export onStart
@ -143,13 +119,15 @@ func onNativeWindowCreated(activity *C.ANativeActivity, w *C.ANativeWindow) {
windowCreated <- w windowCreated <- w
} }
//export onNativeWindowResized
func onNativeWindowResized(activity *C.ANativeActivity, window *C.ANativeWindow) {
}
//export onNativeWindowRedrawNeeded //export onNativeWindowRedrawNeeded
func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWindow) { func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWindow) {
// Called on orientation change and window resize.
// Send a request for redraw, and block this function
// until a complete draw and buffer swap is completed.
// This is required by the redraw documentation to
// avoid bad draws.
windowRedrawNeeded <- window windowRedrawNeeded <- window
<-windowRedrawDone
} }
//export onNativeWindowDestroyed //export onNativeWindowDestroyed
@ -173,8 +151,61 @@ func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) {
func onContentRectChanged(activity *C.ANativeActivity, rect *C.ARect) { func onContentRectChanged(activity *C.ANativeActivity, rect *C.ARect) {
} }
type windowConfig struct {
// TODO(crawshaw): report orientation
// ACONFIGURATION_ORIENTATION_ANY
// ACONFIGURATION_ORIENTATION_PORT
// ACONFIGURATION_ORIENTATION_LAND
// ACONFIGURATION_ORIENTATION_SQUARE
// Needs to be merged with iOS's notion of orientation first.
orientation int
pixelsPerPt float32
}
func windowConfigRead(activity *C.ANativeActivity) windowConfig {
aconfig := C.AConfiguration_new()
C.AConfiguration_fromAssetManager(aconfig, activity.assetManager)
orient := C.AConfiguration_getOrientation(aconfig)
density := C.AConfiguration_getDensity(aconfig)
C.AConfiguration_delete(aconfig)
var dpi int
switch density {
case C.ACONFIGURATION_DENSITY_DEFAULT:
dpi = 160
case C.ACONFIGURATION_DENSITY_LOW,
C.ACONFIGURATION_DENSITY_MEDIUM,
213, // C.ACONFIGURATION_DENSITY_TV
C.ACONFIGURATION_DENSITY_HIGH,
320, // ACONFIGURATION_DENSITY_XHIGH
480, // ACONFIGURATION_DENSITY_XXHIGH
640: // ACONFIGURATION_DENSITY_XXXHIGH
dpi = int(density)
case C.ACONFIGURATION_DENSITY_NONE:
log.Print("android device reports no screen density")
dpi = 72
default:
log.Printf("android device reports unknown density: %d", density)
// All we can do is guess.
if density > 0 {
dpi = int(density)
} else {
dpi = 72
}
}
return windowConfig{
orientation: int(orient),
pixelsPerPt: float32(dpi) / 72,
}
}
//export onConfigurationChanged //export onConfigurationChanged
func onConfigurationChanged(activity *C.ANativeActivity) { func onConfigurationChanged(activity *C.ANativeActivity) {
// A rotation event first triggers onConfigurationChanged, then
// calls onNativeWindowRedrawNeeded. We extract the orientation
// here and save it for the redraw event.
windowConfigChange <- windowConfigRead(activity)
} }
//export onLowMemory //export onLowMemory
@ -185,6 +216,8 @@ var (
windowDestroyed = make(chan bool) windowDestroyed = make(chan bool)
windowCreated = make(chan *C.ANativeWindow) windowCreated = make(chan *C.ANativeWindow)
windowRedrawNeeded = make(chan *C.ANativeWindow) windowRedrawNeeded = make(chan *C.ANativeWindow)
windowRedrawDone = make(chan struct{})
windowConfigChange = make(chan windowConfig)
) )
func init() { func init() {

View File

@ -7,6 +7,7 @@ package app
/* /*
#include <android/log.h> #include <android/log.h>
#include <android/native_activity.h> #include <android/native_activity.h>
#include <android/native_window.h>
#include <android/input.h> #include <android/input.h>
#include <EGL/egl.h> #include <EGL/egl.h>
#include <GLES/gl.h> #include <GLES/gl.h>
@ -23,18 +24,11 @@ const EGLint RGB_888[] = {
EGL_NONE EGL_NONE
}; };
EGLint windowWidth;
EGLint windowHeight;
EGLDisplay display; EGLDisplay display;
EGLSurface surface; EGLSurface surface;
#define LOG_ERROR(...) __android_log_print(ANDROID_LOG_ERROR, "Go", __VA_ARGS__) #define LOG_ERROR(...) __android_log_print(ANDROID_LOG_ERROR, "Go", __VA_ARGS__)
void querySurfaceWidthAndHeight() {
eglQuerySurface(display, surface, EGL_WIDTH, &windowWidth);
eglQuerySurface(display, surface, EGL_HEIGHT, &windowHeight);
}
void createEGLWindow(ANativeWindow* window) { void createEGLWindow(ANativeWindow* window) {
EGLint numConfigs, format; EGLint numConfigs, format;
EGLConfig config; EGLConfig config;
@ -74,8 +68,6 @@ void createEGLWindow(ANativeWindow* window) {
LOG_ERROR("eglMakeCurrent failed"); LOG_ERROR("eglMakeCurrent failed");
return; return;
} }
querySurfaceWidthAndHeight();
} }
#undef LOG_ERROR #undef LOG_ERROR
@ -92,46 +84,55 @@ import (
"golang.org/x/mobile/gl" "golang.org/x/mobile/gl"
) )
var firstWindowDraw = true
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) { func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) {
C.createEGLWindow(w) // Android can send a windowRedrawNeeded event any time, including
// in the middle of a paint cycle. The redraw event may have changed
// TODO: is this needed if we also have the "case <-windowRedrawNeeded:" below?? // the size of the screen, so any partial painting is now invalidated.
sendLifecycle(lifecycle.StageFocused) // We must also not return to Android (via sending on windowRedrawDone)
eventsIn <- config.Event{ // until a complete paint with the new configuration is complete.
Width: geom.Pt(float32(C.windowWidth) / pixelsPerPt), //
Height: geom.Pt(float32(C.windowHeight) / pixelsPerPt), // When a windowRedrawNeeded request comes in, we increment redrawGen
PixelsPerPt: pixelsPerPt, // (Gen is short for generation number), and do not make a paint cycle
} // visible on <-endPaint unless paintGen agrees. If possible,
if firstWindowDraw { // windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded
firstWindowDraw = false // to return.
// TODO: be more principled about when to send a paint event. var redrawGen, paintGen uint32
eventsIn <- paint.Event{}
}
for { for {
processEvents(queue) processEvents(queue)
select { select {
case <-donec: case <-donec:
return true return true
case <-windowRedrawNeeded: case cfg := <-windowConfigChange:
// Re-query the width and height. // TODO save orientation
C.querySurfaceWidthAndHeight() pixelsPerPt = cfg.pixelsPerPt
case w := <-windowRedrawNeeded:
sendLifecycle(lifecycle.StageFocused) sendLifecycle(lifecycle.StageFocused)
eventsIn <- config.Event{ eventsIn <- config.Event{
Width: geom.Pt(float32(C.windowWidth) / pixelsPerPt), Width: geom.Pt(float32(C.ANativeWindow_getWidth(w)) / pixelsPerPt),
Height: geom.Pt(float32(C.windowHeight) / pixelsPerPt), Height: geom.Pt(float32(C.ANativeWindow_getHeight(w)) / pixelsPerPt),
PixelsPerPt: pixelsPerPt, PixelsPerPt: pixelsPerPt,
} }
if paintGen == 0 {
paintGen++
C.createEGLWindow(w)
eventsIn <- paint.Event{}
}
redrawGen++
case <-windowDestroyed: case <-windowDestroyed:
sendLifecycle(lifecycle.StageAlive) sendLifecycle(lifecycle.StageAlive)
return false return false
case <-gl.WorkAvailable: case <-gl.WorkAvailable:
gl.DoWork() gl.DoWork()
case <-endPaint: case <-endPaint:
// eglSwapBuffers blocks until vsync. if paintGen == redrawGen {
C.eglSwapBuffers(C.display, C.surface) // eglSwapBuffers blocks until vsync.
C.eglSwapBuffers(C.display, C.surface)
select {
case windowRedrawDone <- struct{}{}:
default:
}
}
paintGen = redrawGen
eventsIn <- paint.Event{} eventsIn <- paint.Event{}
} }
} }