mirror of
https://github.com/go-i2p/gomobile-java.git
synced 2025-07-12 10:55:28 -04:00
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:
@ -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")
|
||||
}
|
||||
|
230
app/app.go
230
app/app.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
106
app/state.go
106
app/state.go
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
162
app/x11.go
162
app/x11.go
@ -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
225
event/event.go
Normal 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
|
||||
)
|
@ -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
|
||||
)
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
17
geom/geom.go
17
geom/geom.go
@ -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
|
||||
|
2079
gl/gldebug.go
2079
gl/gldebug.go
File diff suppressed because it is too large
Load Diff
@ -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 ]
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
77
gl/work.go
77
gl/work.go
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user