summaryrefslogtreecommitdiffstats
path: root/shiny/driver/gldriver/window.go
diff options
context:
space:
mode:
Diffstat (limited to 'shiny/driver/gldriver/window.go')
-rw-r--r--shiny/driver/gldriver/window.go389
1 files changed, 389 insertions, 0 deletions
diff --git a/shiny/driver/gldriver/window.go b/shiny/driver/gldriver/window.go
new file mode 100644
index 0000000..82baf2c
--- /dev/null
+++ b/shiny/driver/gldriver/window.go
@@ -0,0 +1,389 @@
+// 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 gldriver
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ "sync"
+
+ "golang.org/x/exp/shiny/driver/internal/drawer"
+ "golang.org/x/exp/shiny/driver/internal/event"
+ "golang.org/x/exp/shiny/driver/internal/lifecycler"
+ "golang.org/x/exp/shiny/screen"
+ "golang.org/x/image/math/f64"
+ "golang.org/x/mobile/event/lifecycle"
+ "golang.org/x/mobile/event/size"
+ "golang.org/x/mobile/gl"
+)
+
+type windowImpl struct {
+ s *screenImpl
+
+ // id is an OS-specific data structure for the window.
+ // - Cocoa: ScreenGLView*
+ // - X11: Window
+ // - Windows: win32.HWND
+ id uintptr
+
+ // ctx is a C data structure for the GL context.
+ // - Cocoa: uintptr holding a NSOpenGLContext*.
+ // - X11: uintptr holding an EGLSurface.
+ // - Windows: ctxWin32
+ ctx interface{}
+
+ lifecycler lifecycler.State
+ // TODO: Delete the field below (and the useLifecycler constant), and use
+ // the field above for cocoa and win32.
+ lifecycleStage lifecycle.Stage // current stage
+
+ event.Deque
+ publish chan struct{}
+ publishDone chan screen.PublishResult
+ drawDone chan struct{}
+
+ // glctxMu is a mutex that enforces the atomicity of methods like
+ // Texture.Upload or Window.Draw that are conceptually one operation
+ // but are implemented by multiple OpenGL calls. OpenGL is a stateful
+ // API, so interleaving OpenGL calls from separate higher-level
+ // operations causes inconsistencies.
+ glctxMu sync.Mutex
+ glctx gl.Context
+ worker gl.Worker
+ // backBufferBound is whether the default Framebuffer, with ID 0, also
+ // known as the back buffer or the window's Framebuffer, is bound and its
+ // viewport is known to equal the window size. It can become false when we
+ // bind to a texture's Framebuffer or when the window size changes.
+ backBufferBound bool
+
+ // szMu protects only sz. If you need to hold both glctxMu and szMu, the
+ // lock ordering is to lock glctxMu first (and unlock it last).
+ szMu sync.Mutex
+ sz size.Event
+}
+
+// NextEvent implements the screen.EventDeque interface.
+func (w *windowImpl) NextEvent() interface{} {
+ e := w.Deque.NextEvent()
+ if handleSizeEventsAtChannelReceive {
+ if sz, ok := e.(size.Event); ok {
+ w.glctxMu.Lock()
+ w.backBufferBound = false
+ w.szMu.Lock()
+ w.sz = sz
+ w.szMu.Unlock()
+ w.glctxMu.Unlock()
+ }
+ }
+ return e
+}
+
+func (w *windowImpl) Release() {
+ // There are two ways a window can be closed: the Operating System or
+ // Desktop Environment can initiate (e.g. in response to a user clicking a
+ // red button), or the Go app can programatically close the window (by
+ // calling Window.Release).
+ //
+ // When the OS closes a window:
+ // - Cocoa: Obj-C's windowWillClose calls Go's windowClosing.
+ // - X11: the X11 server sends a WM_DELETE_WINDOW message.
+ // - Windows: TODO: implement and document this.
+ //
+ // This should send a lifecycle event (To: StageDead) to the Go app's event
+ // loop, which should respond by calling Window.Release (this method).
+ // Window.Release is where system resources are actually cleaned up.
+ //
+ // When Window.Release is called, the closeWindow call below:
+ // - Cocoa: calls Obj-C's performClose, which emulates the red button
+ // being clicked. (TODO: document how this actually cleans up
+ // resources??)
+ // - X11: calls C's XDestroyWindow.
+ // - Windows: TODO: implement and document this.
+ //
+ // On Cocoa, if these two approaches race, experiments suggest that the
+ // race is won by performClose (which is called serially on the main
+ // thread). Even if that isn't true, the windowWillClose handler is
+ // idempotent.
+
+ theScreen.mu.Lock()
+ delete(theScreen.windows, w.id)
+ theScreen.mu.Unlock()
+
+ closeWindow(w.id)
+}
+
+func (w *windowImpl) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle) {
+ originalSRMin := sr.Min
+ sr = sr.Intersect(src.Bounds())
+ if sr.Empty() {
+ return
+ }
+ dp = dp.Add(sr.Min.Sub(originalSRMin))
+ // TODO: keep a texture around for this purpose?
+ t, err := w.s.NewTexture(sr.Size())
+ if err != nil {
+ panic(err)
+ }
+ t.Upload(image.Point{}, src, sr)
+ w.Draw(f64.Aff3{
+ 1, 0, float64(dp.X),
+ 0, 1, float64(dp.Y),
+ }, t, t.Bounds(), draw.Src, nil)
+ t.Release()
+}
+
+func useOp(glctx gl.Context, op draw.Op) {
+ if op == draw.Over {
+ glctx.Enable(gl.BLEND)
+ glctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
+ } else {
+ glctx.Disable(gl.BLEND)
+ }
+}
+
+func (w *windowImpl) bindBackBuffer() {
+ w.szMu.Lock()
+ sz := w.sz
+ w.szMu.Unlock()
+
+ w.backBufferBound = true
+ w.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0})
+ w.glctx.Viewport(0, 0, sz.WidthPx, sz.HeightPx)
+}
+
+func (w *windowImpl) fill(mvp f64.Aff3, src color.Color, op draw.Op) {
+ w.glctxMu.Lock()
+ defer w.glctxMu.Unlock()
+
+ if !w.backBufferBound {
+ w.bindBackBuffer()
+ }
+
+ doFill(w.s, w.glctx, mvp, src, op)
+}
+
+func doFill(s *screenImpl, glctx gl.Context, mvp f64.Aff3, src color.Color, op draw.Op) {
+ useOp(glctx, op)
+ if !glctx.IsProgram(s.fill.program) {
+ p, err := compileProgram(glctx, fillVertexSrc, fillFragmentSrc)
+ if err != nil {
+ // TODO: initialize this somewhere else we can better handle the error.
+ panic(err.Error())
+ }
+ s.fill.program = p
+ s.fill.pos = glctx.GetAttribLocation(p, "pos")
+ s.fill.mvp = glctx.GetUniformLocation(p, "mvp")
+ s.fill.color = glctx.GetUniformLocation(p, "color")
+ s.fill.quad = glctx.CreateBuffer()
+
+ glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad)
+ glctx.BufferData(gl.ARRAY_BUFFER, quadCoords, gl.STATIC_DRAW)
+ }
+ glctx.UseProgram(s.fill.program)
+
+ writeAff3(glctx, s.fill.mvp, mvp)
+
+ r, g, b, a := src.RGBA()
+ glctx.Uniform4f(
+ s.fill.color,
+ float32(r)/65535,
+ float32(g)/65535,
+ float32(b)/65535,
+ float32(a)/65535,
+ )
+
+ glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad)
+ glctx.EnableVertexAttribArray(s.fill.pos)
+ glctx.VertexAttribPointer(s.fill.pos, 2, gl.FLOAT, false, 0, 0)
+
+ glctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+
+ glctx.DisableVertexAttribArray(s.fill.pos)
+}
+
+func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+ minX := float64(dr.Min.X)
+ minY := float64(dr.Min.Y)
+ maxX := float64(dr.Max.X)
+ maxY := float64(dr.Max.Y)
+ w.fill(w.mvp(
+ minX, minY,
+ maxX, minY,
+ minX, maxY,
+ ), src, op)
+}
+
+func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ minX := float64(sr.Min.X)
+ minY := float64(sr.Min.Y)
+ maxX := float64(sr.Max.X)
+ maxY := float64(sr.Max.Y)
+ w.fill(w.mvp(
+ src2dst[0]*minX+src2dst[1]*minY+src2dst[2],
+ src2dst[3]*minX+src2dst[4]*minY+src2dst[5],
+ src2dst[0]*maxX+src2dst[1]*minY+src2dst[2],
+ src2dst[3]*maxX+src2dst[4]*minY+src2dst[5],
+ src2dst[0]*minX+src2dst[1]*maxY+src2dst[2],
+ src2dst[3]*minX+src2dst[4]*maxY+src2dst[5],
+ ), src, op)
+}
+
+func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ t := src.(*textureImpl)
+ sr = sr.Intersect(t.Bounds())
+ if sr.Empty() {
+ return
+ }
+
+ w.glctxMu.Lock()
+ defer w.glctxMu.Unlock()
+
+ if !w.backBufferBound {
+ w.bindBackBuffer()
+ }
+
+ useOp(w.glctx, op)
+ w.glctx.UseProgram(w.s.texture.program)
+
+ // Start with src-space left, top, right and bottom.
+ srcL := float64(sr.Min.X)
+ srcT := float64(sr.Min.Y)
+ srcR := float64(sr.Max.X)
+ srcB := float64(sr.Max.Y)
+ // Transform to dst-space via the src2dst matrix, then to a MVP matrix.
+ writeAff3(w.glctx, w.s.texture.mvp, w.mvp(
+ src2dst[0]*srcL+src2dst[1]*srcT+src2dst[2],
+ src2dst[3]*srcL+src2dst[4]*srcT+src2dst[5],
+ src2dst[0]*srcR+src2dst[1]*srcT+src2dst[2],
+ src2dst[3]*srcR+src2dst[4]*srcT+src2dst[5],
+ src2dst[0]*srcL+src2dst[1]*srcB+src2dst[2],
+ src2dst[3]*srcL+src2dst[4]*srcB+src2dst[5],
+ ))
+
+ // OpenGL's fragment shaders' UV coordinates run from (0,0)-(1,1),
+ // unlike vertex shaders' XY coordinates running from (-1,+1)-(+1,-1).
+ //
+ // We are drawing a rectangle PQRS, defined by two of its
+ // corners, onto the entire texture. The two quads may actually
+ // be equal, but in the general case, PQRS can be smaller.
+ //
+ // (0,0) +---------------+ (1,0)
+ // | P +-----+ Q |
+ // | | | |
+ // | S +-----+ R |
+ // (0,1) +---------------+ (1,1)
+ //
+ // The PQRS quad is always axis-aligned. First of all, convert
+ // from pixel space to texture space.
+ tw := float64(t.size.X)
+ th := float64(t.size.Y)
+ px := float64(sr.Min.X-0) / tw
+ py := float64(sr.Min.Y-0) / th
+ qx := float64(sr.Max.X-0) / tw
+ sy := float64(sr.Max.Y-0) / th
+ // Due to axis alignment, qy = py and sx = px.
+ //
+ // The simultaneous equations are:
+ // 0 + 0 + a02 = px
+ // 0 + 0 + a12 = py
+ // a00 + 0 + a02 = qx
+ // a10 + 0 + a12 = qy = py
+ // 0 + a01 + a02 = sx = px
+ // 0 + a11 + a12 = sy
+ writeAff3(w.glctx, w.s.texture.uvp, f64.Aff3{
+ qx - px, 0, px,
+ 0, sy - py, py,
+ })
+
+ w.glctx.ActiveTexture(gl.TEXTURE0)
+ w.glctx.BindTexture(gl.TEXTURE_2D, t.id)
+ w.glctx.Uniform1i(w.s.texture.sample, 0)
+
+ w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad)
+ w.glctx.EnableVertexAttribArray(w.s.texture.pos)
+ w.glctx.VertexAttribPointer(w.s.texture.pos, 2, gl.FLOAT, false, 0, 0)
+
+ w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad)
+ w.glctx.EnableVertexAttribArray(w.s.texture.inUV)
+ w.glctx.VertexAttribPointer(w.s.texture.inUV, 2, gl.FLOAT, false, 0, 0)
+
+ w.glctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+
+ w.glctx.DisableVertexAttribArray(w.s.texture.pos)
+ w.glctx.DisableVertexAttribArray(w.s.texture.inUV)
+}
+
+func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ drawer.Copy(w, dp, src, sr, op, opts)
+}
+
+func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ drawer.Scale(w, dr, src, sr, op, opts)
+}
+
+func (w *windowImpl) mvp(tlx, tly, trx, try, blx, bly float64) f64.Aff3 {
+ w.szMu.Lock()
+ sz := w.sz
+ w.szMu.Unlock()
+
+ return calcMVP(sz.WidthPx, sz.HeightPx, tlx, tly, trx, try, blx, bly)
+}
+
+// calcMVP returns the Model View Projection matrix that maps the quadCoords
+// unit square, (0, 0) to (1, 1), to a quad QV, such that QV in vertex shader
+// space corresponds to the quad QP in pixel space, where QP is defined by
+// three of its four corners - the arguments to this function. The three
+// corners are nominally the top-left, top-right and bottom-left, but there is
+// no constraint that e.g. tlx < trx.
+//
+// In pixel space, the window ranges from (0, 0) to (widthPx, heightPx). The
+// Y-axis points downwards.
+//
+// In vertex shader space, the window ranges from (-1, +1) to (+1, -1), which
+// is a 2-unit by 2-unit square. The Y-axis points upwards.
+func calcMVP(widthPx, heightPx int, tlx, tly, trx, try, blx, bly float64) f64.Aff3 {
+ // Convert from pixel coords to vertex shader coords.
+ invHalfWidth := +2 / float64(widthPx)
+ invHalfHeight := -2 / float64(heightPx)
+ tlx = tlx*invHalfWidth - 1
+ tly = tly*invHalfHeight + 1
+ trx = trx*invHalfWidth - 1
+ try = try*invHalfHeight + 1
+ blx = blx*invHalfWidth - 1
+ bly = bly*invHalfHeight + 1
+
+ // The resultant affine matrix:
+ // - maps (0, 0) to (tlx, tly).
+ // - maps (1, 0) to (trx, try).
+ // - maps (0, 1) to (blx, bly).
+ return f64.Aff3{
+ trx - tlx, blx - tlx, tlx,
+ try - tly, bly - tly, tly,
+ }
+}
+
+func (w *windowImpl) Publish() screen.PublishResult {
+ // gl.Flush is a lightweight (on modern GL drivers) blocking call
+ // that ensures all GL functions pending in the gl package have
+ // been passed onto the GL driver before the app package attempts
+ // to swap the screen buffer.
+ //
+ // This enforces that the final receive (for this paint cycle) on
+ // gl.WorkAvailable happens before the send on publish.
+ w.glctxMu.Lock()
+ w.glctx.Flush()
+ w.glctxMu.Unlock()
+
+ w.publish <- struct{}{}
+ res := <-w.publishDone
+
+ select {
+ case w.drawDone <- struct{}{}:
+ default:
+ }
+
+ return res
+}