shiny/driver/gldriver: add a mutex for groups of OpenGL operations.
Methods like Texture.Upload or Window.Draw 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.
Change-Id: I66ccf8844d276b82586620c5bfeadb50ba0f5bef
Reviewed-on: https://go-review.googlesource.com/14861
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/driver/gldriver/gldriver.go b/shiny/driver/gldriver/gldriver.go
index cc5134c..ae64b98 100644
--- a/shiny/driver/gldriver/gldriver.go
+++ b/shiny/driver/gldriver/gldriver.go
@@ -40,6 +40,7 @@
}
}
+// writeAff3 must only be called while holding glMu.
func writeAff3(u gl.Uniform, a f64.Aff3) {
var m [9]float32
m[0*3+0] = float32(a[0*3+0])
@@ -84,6 +85,7 @@
return b
}
+// compileProgram must only be called while holding glMu.
func compileProgram(vSrc, fSrc string) (gl.Program, error) {
program := gl.CreateProgram()
if program.Value == 0 {
@@ -115,6 +117,7 @@
return program, nil
}
+// compileShader must only be called while holding glMu.
func compileShader(shaderType gl.Enum, src string) (gl.Shader, error) {
shader := gl.CreateShader(shaderType)
if shader.Value == 0 {
diff --git a/shiny/driver/gldriver/screen.go b/shiny/driver/gldriver/screen.go
index 1b589a5..62583ac 100644
--- a/shiny/driver/gldriver/screen.go
+++ b/shiny/driver/gldriver/screen.go
@@ -13,13 +13,22 @@
"golang.org/x/mobile/gl"
)
+// glMu 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.
+//
+// glMu does not need to be held when accessing gl.WorkAvailable or gl.DoWork.
+//
+// TODO: is this affected by changing the x/mobile/gl package from an
+// (implicit) global context to a per-window context?
+var glMu sync.Mutex
+
var theScreen = &screenImpl{
windows: make(map[uintptr]*windowImpl),
}
type screenImpl struct {
- mu sync.Mutex
- windows map[uintptr]*windowImpl
texture struct {
program gl.Program
pos gl.Attrib
@@ -36,6 +45,9 @@
color gl.Uniform
quad gl.Buffer
}
+
+ mu sync.Mutex
+ windows map[uintptr]*windowImpl
}
func (s *screenImpl) NewBuffer(size image.Point) (retBuf screen.Buffer, retErr error) {
@@ -46,9 +58,10 @@
}
func (s *screenImpl) NewTexture(size image.Point) (screen.Texture, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
+ glMu.Lock()
+ defer glMu.Unlock()
+ // TODO: can we compile these programs eagerly instead of lazily?
if !gl.IsProgram(s.texture.program) {
p, err := compileProgram(textureVertexSrc, textureFragmentSrc)
if err != nil {
diff --git a/shiny/driver/gldriver/texture.go b/shiny/driver/gldriver/texture.go
index 4e7246d..3572e41 100644
--- a/shiny/driver/gldriver/texture.go
+++ b/shiny/driver/gldriver/texture.go
@@ -23,27 +23,36 @@
func (t *textureImpl) Bounds() image.Rectangle { return image.Rectangle{Max: t.size} }
func (t *textureImpl) Release() {
+ glMu.Lock()
+ defer glMu.Unlock()
+
gl.DeleteTexture(t.id)
t.id = gl.Texture{}
}
func (t *textureImpl) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle, sender screen.Sender) {
- t.upload(dp, src, sr, sender, t)
+ t.upload(dp, src, sr)
+ if sender != nil {
+ sender.Send(screen.UploadedEvent{Buffer: src, Uploader: t})
+ }
}
-func (t *textureImpl) upload(dp image.Point, src screen.Buffer, sr image.Rectangle, sender screen.Sender, u screen.Uploader) {
+func (t *textureImpl) upload(dp image.Point, src screen.Buffer, sr image.Rectangle) {
+ glMu.Lock()
+ defer glMu.Unlock()
+
// TODO: adjust if dp is outside dst bounds, or r is outside src bounds.
gl.BindTexture(gl.TEXTURE_2D, t.id)
m := src.RGBA().SubImage(sr).(*image.RGBA)
b := m.Bounds()
// TODO check m bounds smaller than t.size
gl.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, b.Dx(), b.Dy(), gl.RGBA, gl.UNSIGNED_BYTE, m.Pix)
- if sender != nil {
- sender.Send(screen.UploadedEvent{Buffer: src, Uploader: u})
- }
}
func (t *textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+ glMu.Lock()
+ defer glMu.Unlock()
+
// TODO.
}
diff --git a/shiny/driver/gldriver/window.go b/shiny/driver/gldriver/window.go
index 1b59e07..2cf537f 100644
--- a/shiny/driver/gldriver/window.go
+++ b/shiny/driver/gldriver/window.go
@@ -71,7 +71,10 @@
if err != nil {
panic(err)
}
- t.(*textureImpl).upload(dp, src, sr, sender, w)
+ t.(*textureImpl).upload(dp, src, sr)
+ if sender != nil {
+ sender.Send(screen.UploadedEvent{Buffer: src, Uploader: w})
+ }
w.Draw(f64.Aff3{
1, 0, float64(dp.X),
0, 1, float64(dp.Y),
@@ -80,6 +83,9 @@
}
func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+ glMu.Lock()
+ defer glMu.Unlock()
+
if !gl.IsProgram(w.s.fill.program) {
p, err := compileProgram(fillVertexSrc, fillFragmentSrc)
if err != nil {
@@ -126,6 +132,9 @@
}
func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+ glMu.Lock()
+ defer glMu.Unlock()
+
gl.UseProgram(w.s.texture.program)
// Start with src-space left, top, right and bottom.
@@ -242,7 +251,10 @@
//
// This enforces that the final receive (for this paint cycle) on
// gl.WorkAvailable happens before the send on publish.
+ glMu.Lock()
gl.Flush()
+ glMu.Unlock()
+
w.publish <- struct{}{}
// TODO(crawshaw): wait for an ack before returning.
return screen.PublishResult{}
diff --git a/shiny/driver/gldriver/x11.go b/shiny/driver/gldriver/x11.go
index 78ce138..2d5f1ab 100644
--- a/shiny/driver/gldriver/x11.go
+++ b/shiny/driver/gldriver/x11.go
@@ -172,7 +172,11 @@
// TODO: should this really be done on the receiving end of the w.Events()
// channel, in the same goroutine as other GL calls in the app's 'business
// logic'?
- go gl.Viewport(0, 0, int(width), int(height))
+ go func() {
+ glMu.Lock()
+ gl.Viewport(0, 0, int(width), int(height))
+ glMu.Unlock()
+ }()
sz := size.Event{
WidthPx: int(width),