| // 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 |
| } |
| |
| 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 |
| } |