//export onResize func onResize(w, h int) { // 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 main(f func(App)) { runtime.LockOSThread() C.createWindow() // TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen. sendLifecycle(lifecycle.StageFocused) donec := make(chan struct{}) go func() { f(app{}) close(donec) }() // TODO: can we get the actual vsync signal? ticker := time.NewTicker(time.Second / 60) defer ticker.Stop() tc := ticker.C for { select { case <-donec: return case <-gl.WorkAvailable: gl.DoWork() case <-endPaint: C.swapBuffers() tc = ticker.C case <-tc: tc = nil eventsIn <- paint.Event{} } C.processEvents() } }
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) { // Android can send a windowRedrawNeeded event any time, including // in the middle of a paint cycle. The redraw event may have changed // the size of the screen, so any partial painting is now invalidated. // We must also not return to Android (via sending on windowRedrawDone) // until a complete paint with the new configuration is complete. // // When a windowRedrawNeeded request comes in, we increment redrawGen // (Gen is short for generation number), and do not make a paint cycle // visible on <-endPaint unless paintGen agrees. If possible, // windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded // to return. var redrawGen, paintGen uint32 for { processEvents(queue) select { case <-donec: return true case cfg := <-windowConfigChange: // TODO save orientation pixelsPerPt = cfg.pixelsPerPt case w := <-windowRedrawNeeded: sendLifecycle(lifecycle.StageFocused) widthPx := int(C.ANativeWindow_getWidth(w)) heightPx := int(C.ANativeWindow_getHeight(w)) eventsIn <- config.Event{ WidthPx: widthPx, HeightPx: heightPx, WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt), HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt), PixelsPerPt: pixelsPerPt, } if paintGen == 0 { paintGen++ C.createEGLWindow(w) eventsIn <- paint.Event{} } redrawGen++ case <-windowDestroyed: sendLifecycle(lifecycle.StageAlive) return false case <-gl.WorkAvailable: gl.DoWork() case <-endPaint: if paintGen == redrawGen { // eglSwapBuffers blocks until vsync. C.eglSwapBuffers(C.display, C.surface) select { case windowRedrawDone <- struct{}{}: default: } } paintGen = redrawGen eventsIn <- paint.Event{} } } }
// loop is the primary drawing loop. // // After Cocoa has captured the initial OS thread for processing Cocoa // events in runApp, it starts loop on another goroutine. It is locked // to an OS thread for its OpenGL context. // // Two Cocoa threads deliver draw signals to loop. The primary source of // draw events is the CVDisplayLink timer, which is tied to the display // vsync. Secondary draw events come from [NSView drawRect:] when the // window is resized. func loop(ctx C.GLintptr) { runtime.LockOSThread() C.makeCurrentContext(ctx) for { select { case <-gl.WorkAvailable: gl.DoWork() case <-draw: loop1: for { select { case <-gl.WorkAvailable: gl.DoWork() case <-publish: C.CGLFlushDrawable(C.CGLGetCurrentContext()) publishResult <- PublishResult{} break loop1 } } drawDone <- struct{}{} } } }
// drawLoop is the primary drawing loop. // // After Cocoa has created an NSWindow on the initial OS thread for // processing Cocoa events in newWindow, it starts drawLoop on another // goroutine. It is locked to an OS thread for its OpenGL context. // // Two Cocoa threads deliver draw signals to drawLoop. The primary // source of draw events is the CVDisplayLink timer, which is tied to // the display vsync. Secondary draw events come from [NSView drawRect:] // when the window is resized. func (w *windowImpl) drawLoop(ctx uintptr) { runtime.LockOSThread() // TODO(crawshaw): there are several problematic issues around having // a draw loop per window, but resolving them requires some thought. // Firstly, nothing should race on gl.DoWork, so only one person can // do that at a time. Secondly, which GL ctx we use matters. A ctx // carries window-specific state (for example, the current glViewport // value), so we only want to run GL commands on the right context // between a <-w.draw and a <-w.drawDone. Thirdly, some GL functions // can be legitimately called outside of a window draw cycle, for // example, gl.CreateTexture. It doesn't matter which GL ctx we use // for that, but we have to use a valid one. So if a window gets // closed, it's important we swap the default ctx. More work needed. C.makeCurrentContext(C.uintptr_t(ctx)) // TODO(crawshaw): exit this goroutine on Release. for { select { case <-gl.WorkAvailable: gl.DoWork() case <-w.draw: w.Send(paint.Event{}) loop: for { select { case <-gl.WorkAvailable: gl.DoWork() case <-w.endPaint: C.CGLFlushDrawable(C.CGLGetCurrentContext()) break loop } } w.drawDone <- struct{}{} } } }
//export drawgl func drawgl(ctx uintptr) { if !startedgl { startedgl = true C.setContext(unsafe.Pointer(ctx)) // TODO(crawshaw): not just on process start. sendLifecycle(lifecycle.StageFocused) } eventsIn <- paint.Event{} for { select { case <-gl.WorkAvailable: gl.DoWork() case <-endDraw: return } } }
// loop is the primary drawing loop. // // After Cocoa has captured the initial OS thread for processing Cocoa // events in runApp, it starts loop on another goroutine. It is locked // to an OS thread for its OpenGL context. // // Two Cocoa threads deliver draw signals to loop. The primary source of // draw events is the CVDisplayLink timer, which is tied to the display // vsync. Secondary draw events come from [NSView drawRect:] when the // window is resized. func loop(ctx C.GLintptr) { runtime.LockOSThread() C.makeCurrentContext(ctx) for range draw { eventsIn <- paint.Event{} loop1: for { select { case <-gl.WorkAvailable: gl.DoWork() case <-endPaint: C.CGLFlushDrawable(C.CGLGetCurrentContext()) break loop1 } } drawDone <- struct{}{} } }
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) { C.createEGLWindow(w) // TODO: is this needed if we also have the "case <-windowRedrawNeeded:" below?? sendLifecycle(lifecycle.StageFocused) eventsIn <- config.Event{ 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 paint event. eventsIn <- paint.Event{} } for { processEvents(queue) select { case <-donec: return true case <-windowRedrawNeeded: // Re-query the width and height. C.querySurfaceWidthAndHeight() sendLifecycle(lifecycle.StageFocused) eventsIn <- config.Event{ Width: geom.Pt(float32(C.windowWidth) / pixelsPerPt), Height: geom.Pt(float32(C.windowHeight) / pixelsPerPt), PixelsPerPt: pixelsPerPt, } case <-windowDestroyed: sendLifecycle(lifecycle.StageAlive) return false case <-gl.WorkAvailable: gl.DoWork() case <-endDraw: // eglSwapBuffers blocks until vsync. C.eglSwapBuffers(C.display, C.surface) eventsIn <- paint.Event{} } } }
//export drawgl func drawgl(ctx uintptr) { if !startedgl { startedgl = true C.setContext(unsafe.Pointer(ctx)) // TODO(crawshaw): not just on process start. sendLifecycle(lifecycle.StageFocused) } // TODO(crawshaw): don't send a paint.Event unconditionally. Only send one // if the window actually needs redrawing. eventsIn <- paint.Event{} for { select { case <-gl.WorkAvailable: gl.DoWork() case <-publish: publishResult <- PublishResult{} return } } }
func main(f func(App)) { runtime.LockOSThread() C.createWindow() // TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen. sendLifecycle(lifecycle.StageFocused) // TODO: translate X11 expose events to shiny paint events, instead of // sending this synthetic paint event as a hack. eventsIn <- paint.Event{} donec := make(chan struct{}) go func() { f(app{}) close(donec) }() // TODO: can we get the actual vsync signal? ticker := time.NewTicker(time.Second / 60) defer ticker.Stop() var tc <-chan time.Time for { select { case <-donec: return case <-gl.WorkAvailable: gl.DoWork() case <-publish: C.swapBuffers() tc = ticker.C case <-tc: tc = nil publishResult <- PublishResult{} } C.processEvents() } }
func mainUI(vm, jniEnv, ctx uintptr) error { env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer donec := make(chan struct{}) go func() { mainUserFn(app{}) close(donec) }() var q *C.AInputQueue var pixelsPerPt float32 var orientation size.Orientation // Android can send a windowRedrawNeeded event any time, including // in the middle of a paint cycle. The redraw event may have changed // the size of the screen, so any partial painting is now invalidated. // We must also not return to Android (via sending on windowRedrawDone) // until a complete paint with the new configuration is complete. // // When a windowRedrawNeeded request comes in, we increment redrawGen // (Gen is short for generation number), and do not make a paint cycle // visible on <-endPaint unless Generation agrees. If possible, // windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded // to return. var redrawGen uint32 for { if q != nil { processEvents(env, q) } select { case <-windowCreated: case q = <-inputQueue: case <-donec: return nil case cfg := <-windowConfigChange: pixelsPerPt = cfg.pixelsPerPt orientation = cfg.orientation case w := <-windowRedrawNeeded: if C.surface == nil { if errStr := C.createEGLSurface(w); errStr != nil { return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) } } sendLifecycle(lifecycle.StageFocused) widthPx := int(C.ANativeWindow_getWidth(w)) heightPx := int(C.ANativeWindow_getHeight(w)) eventsIn <- size.Event{ WidthPx: widthPx, HeightPx: heightPx, WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt), HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt), PixelsPerPt: pixelsPerPt, Orientation: orientation, } redrawGen++ eventsIn <- paint.Event{redrawGen} case <-windowDestroyed: if C.surface != nil { if errStr := C.destroyEGLSurface(); errStr != nil { return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) } } C.surface = nil sendLifecycle(lifecycle.StageAlive) case <-gl.WorkAvailable: gl.DoWork() case p := <-endPaint: if p.Generation != redrawGen { continue } if C.surface != nil { // eglSwapBuffers blocks until vsync. if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE { log.Printf("app: failed to swap buffers (%s)", eglGetError()) } } select { case windowRedrawDone <- struct{}{}: default: } if C.surface != nil { redrawGen++ eventsIn <- paint.Event{redrawGen} } } } }
func windowDraw(w *C.ANativeWindow, queue *C.AInputQueue, donec chan struct{}) (done bool) { C.createEGLWindow(w) // 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(queue) select { case <-donec: return true case <-windowRedrawNeeded: // Re-query the width and height. C.querySurfaceWidthAndHeight() 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 } } } case <-windowDestroyed: sendLifecycle(event.LifecycleStageAlive) return false case <-gl.WorkAvailable: gl.DoWork() case <-endDraw: // eglSwapBuffers blocks until vsync. C.eglSwapBuffers(C.display, C.surface) eventsIn <- event.Draw{} } } }
func main(f func(App)) { // Preserve this OS thread for the GL context created below. runtime.LockOSThread() donec := make(chan struct{}) go func() { f(app{}) close(donec) }() var q *C.AInputQueue // Android can send a windowRedrawNeeded event any time, including // in the middle of a paint cycle. The redraw event may have changed // the size of the screen, so any partial painting is now invalidated. // We must also not return to Android (via sending on windowRedrawDone) // until a complete paint with the new configuration is complete. // // When a windowRedrawNeeded request comes in, we increment redrawGen // (Gen is short for generation number), and do not make a paint cycle // visible on <-endPaint unless Generation agrees. If possible, // windowRedrawDone is signalled, allowing onNativeWindowRedrawNeeded // to return. var redrawGen uint32 for { if q != nil { processEvents(q) } select { case <-windowCreated: case q = <-inputQueue: case <-donec: return case cfg := <-windowConfigChange: // TODO save orientation pixelsPerPt = cfg.pixelsPerPt case w := <-windowRedrawNeeded: newWindow := C.surface == nil if newWindow { if errStr := C.createEGLSurface(w); errStr != nil { log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError()) return } } sendLifecycle(lifecycle.StageFocused) widthPx := int(C.ANativeWindow_getWidth(w)) heightPx := int(C.ANativeWindow_getHeight(w)) eventsIn <- config.Event{ WidthPx: widthPx, HeightPx: heightPx, WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt), HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt), PixelsPerPt: pixelsPerPt, } redrawGen++ if newWindow { // New window, begin paint loop. eventsIn <- paint.Event{redrawGen} } case <-windowDestroyed: if C.surface != nil { if errStr := C.destroyEGLSurface(); errStr != nil { log.Printf("app: %s (%s)", C.GoString(errStr), eglGetError()) return } } C.surface = nil sendLifecycle(lifecycle.StageAlive) case <-gl.WorkAvailable: gl.DoWork() case p := <-endPaint: if p.Generation != redrawGen { continue } if C.surface != nil { // eglSwapBuffers blocks until vsync. if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE { log.Printf("app: failed to swap buffers (%s)", eglGetError()) } } select { case windowRedrawDone <- struct{}{}: default: } if C.surface != nil { redrawGen++ eventsIn <- paint.Event{redrawGen} } } } }
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. // 3. Run test code: use glimage to draw testdata. // 4. Copy GL texture back into system memory. // 5. Compare to a pre-computed image. f, err := os.Open("../../../testdata/testpattern.png") if err != nil { t.Fatal(err) } defer f.Close() src, _, err := image.Decode(f) if err != nil { t.Fatal(err) } const ( pixW = 100 pixH = 100 ptW = geom.Pt(50) ptH = geom.Pt(50) ) cfg := config.Event{ WidthPx: pixW, HeightPx: pixH, WidthPt: ptW, HeightPt: ptH, PixelsPerPt: float32(pixW) / float32(ptW), } fBuf := gl.CreateFramebuffer() gl.BindFramebuffer(gl.FRAMEBUFFER, fBuf) colorBuf := gl.CreateRenderbuffer() gl.BindRenderbuffer(gl.RENDERBUFFER, colorBuf) // https://www.khronos.org/opengles/sdk/docs/man/xhtml/glRenderbufferStorage.xml // says that the internalFormat "must be one of the following symbolic constants: // GL_RGBA4, GL_RGB565, GL_RGB5_A1, GL_DEPTH_COMPONENT16, or GL_STENCIL_INDEX8". gl.RenderbufferStorage(gl.RENDERBUFFER, gl.RGB565, pixW, pixH) gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuf) if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FRAMEBUFFER_COMPLETE { t.Fatalf("framebuffer create failed: %v", status) } allocs := testing.AllocsPerRun(100, func() { gl.ClearColor(0, 0, 1, 1) // blue }) if allocs != 0 { t.Errorf("unexpected allocations from calling gl.ClearColor: %f", allocs) } gl.Clear(gl.COLOR_BUFFER_BIT) gl.Viewport(0, 0, pixW, pixH) m := NewImage(src.Bounds().Dx(), src.Bounds().Dy()) b := m.RGBA.Bounds() draw.Draw(m.RGBA, b, src, src.Bounds().Min, draw.Src) m.Upload() b.Min.X += 10 b.Max.Y /= 2 // All-integer right-angled triangles offsetting the // box: 24-32-40, 12-16-20. ptTopLeft := geom.Point{0, 24} ptTopRight := geom.Point{32, 0} ptBottomLeft := geom.Point{12, 24 + 16} ptBottomRight := geom.Point{12 + 32, 16} 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: // // (-1, -1) ( 1, -1) // (-1, 1) ( 1, 1) // // To avoid modifying live code for tests, we flip the rows // recovered from the renderbuffer. We are not the first: // // http://lists.apple.com/archives/mac-opengl/2010/Jun/msg00080.html got := image.NewRGBA(image.Rect(0, 0, pixW, pixH)) upsideDownPix := make([]byte, len(got.Pix)) gl.ReadPixels(upsideDownPix, 0, 0, pixW, pixH, gl.RGBA, gl.UNSIGNED_BYTE) for y := 0; y < pixH; y++ { i0 := (pixH - 1 - y) * got.Stride i1 := i0 + pixW*4 copy(got.Pix[y*got.Stride:], upsideDownPix[i0:i1]) } drawCross(got, 0, 0) 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" f, err = os.Open(wantPath) if err != nil { t.Fatal(err) } defer f.Close() wantSrc, _, err := image.Decode(f) if err != nil { t.Fatal(err) } want, ok := wantSrc.(*image.RGBA) if !ok { b := wantSrc.Bounds() want = image.NewRGBA(b) draw.Draw(want, b, wantSrc, b.Min, draw.Src) } if !imageEq(got, want) { // Write out the image we got. f, err = ioutil.TempFile("", "testpattern-window-got") if err != nil { t.Fatal(err) } f.Close() gotPath := f.Name() + ".png" f, err = os.Create(gotPath) if err != nil { t.Fatal(err) } if err := png.Encode(f, got); err != nil { t.Fatal(err) } if err := f.Close(); err != nil { t.Fatal(err) } t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath) } }