From 4f198b3f87e0ea9888c74cf0092050b63719a284 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sun, 29 Aug 2021 12:55:46 +0200 Subject: [PATCH] app: render on event loop thread Before this change, the renderLoop type implemented a rendering goroutine for rendering off the event thread. However, it's not worth the complexity, so remove it and render on the event thread. Signed-off-by: Elias Naur --- app/loop.go | 157 -------------------------------------------------- app/window.go | 150 ++++++++++++++++++++++++++++------------------- 2 files changed, 90 insertions(+), 217 deletions(-) delete mode 100644 app/loop.go diff --git a/app/loop.go b/app/loop.go deleted file mode 100644 index b72a6230..00000000 --- a/app/loop.go +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "image" - "image/color" - "runtime" - - "gioui.org/gpu" - "gioui.org/op" -) - -type renderLoop struct { - summary string - drawing bool - err error - - ctx context - frames chan frame - results chan frameResult - ack chan struct{} - stop chan struct{} - stopped chan struct{} -} - -type frame struct { - viewport image.Point - ops *op.Ops -} - -type frameResult struct { - profile string - err error -} - -func newLoop(ctx context) (*renderLoop, error) { - l := &renderLoop{ - ctx: ctx, - frames: make(chan frame), - results: make(chan frameResult), - // Ack is buffered so GPU commands can be issued after - // ack'ing the frame. - ack: make(chan struct{}, 1), - stop: make(chan struct{}), - stopped: make(chan struct{}), - } - if err := l.renderLoop(ctx); err != nil { - return nil, err - } - return l, nil -} - -func (l *renderLoop) renderLoop(ctx context) error { - // GL Operations must happen on a single OS thread, so - // pass initialization result through a channel. - initErr := make(chan error) - go func() { - defer close(l.stopped) - runtime.LockOSThread() - // Don't UnlockOSThread to avoid reuse by the Go runtime. - - if err := ctx.Lock(); err != nil { - initErr <- err - return - } - g, err := gpu.New(ctx.API()) - if err != nil { - ctx.Unlock() - initErr <- err - return - } - defer func() { - if err := ctx.Lock(); err != nil { - return - } - defer ctx.Unlock() - g.Release() - }() - ctx.Unlock() - initErr <- nil - loop: - for { - select { - case frame := <-l.frames: - var res frameResult - res.err = ctx.Lock() - if res.err != nil { - l.results <- res - break - } - if runtime.GOOS == "js" { - // Use transparent black when Gio is embedded, to allow mixing of Gio and - // foreign content below. - g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) - } else { - g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) - } - res.err = g.Frame(frame.ops, ctx.RenderTarget(), frame.viewport) - // Signal that we're done with the frame ops. - l.ack <- struct{}{} - if res.err == nil { - res.err = ctx.Present() - } - res.profile = g.Profile() - ctx.Unlock() - l.results <- res - case <-l.stop: - break loop - } - } - }() - return <-initErr -} - -func (l *renderLoop) Release() { - // Flush error. - l.Flush() - close(l.stop) - <-l.stopped - l.stop = nil -} - -func (l *renderLoop) Flush() error { - if l.drawing { - st := <-l.results - l.setErr(st.err) - if st.profile != "" { - l.summary = st.profile - } - l.drawing = false - } - return l.err -} - -func (l *renderLoop) Summary() string { - return l.summary -} - -// Draw initiates a draw of a frame. It returns a channel -// than signals when the frame is no longer being accessed. -func (l *renderLoop) Draw(viewport image.Point, frameOps *op.Ops) <-chan struct{} { - if l.err != nil { - l.ack <- struct{}{} - return l.ack - } - l.Flush() - l.frames <- frame{viewport, frameOps} - l.drawing = true - return l.ack -} - -func (l *renderLoop) setErr(err error) { - if l.err == nil { - l.err = err - } -} diff --git a/app/window.go b/app/window.go index c7ae5af0..07210c6f 100644 --- a/app/window.go +++ b/app/window.go @@ -7,8 +7,10 @@ import ( "fmt" "image" "image/color" + "runtime" "time" + "gioui.org/gpu" "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/profile" @@ -25,8 +27,8 @@ type Option func(cnf *config) // Window represents an operating system window. type Window struct { - ctx context - loop *renderLoop + ctx context + gpu gpu.GPU // driverFuncs is a channel of functions to run when // the Window has a valid driver. @@ -72,7 +74,7 @@ type queue struct { // driverEvent is sent when the underlying driver changes. type driverEvent struct { - driver driver + wakeup func() } // Pre-allocate the ack event to avoid garbage. @@ -129,21 +131,12 @@ func (w *Window) update(frame *op.Ops) { <-w.frameAck } -func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error { +func (w *Window) validateAndProcess(frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error { for { - if w.loop != nil { - if err := w.loop.Flush(); err != nil { - w.destroyGPU() - if err == errDeviceLost { - continue - } - return err - } - } - if w.loop == nil && !w.nocontext { + if w.gpu == nil && !w.nocontext { var err error if w.ctx == nil { - w.driverRun(func(_ driver) { + w.driverRun(func(d driver) { w.ctx, err = d.NewContext() }) if err != nil { @@ -151,20 +144,13 @@ func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.P } sync = true } - w.loop, err = newLoop(w.ctx) - if err != nil { - w.destroyGPU() - return err - } } if sync && w.ctx != nil { - w.driverRun(func(_ driver) { - w.ctx.Refresh() + var err error + w.driverRun(func(d driver) { + err = w.ctx.Refresh() }) - } - w.processFrame(frameStart, size, frame) - if sync && w.loop != nil { - if err := w.loop.Flush(); err != nil { + if err != nil { w.destroyGPU() if err == errDeviceLost { continue @@ -172,40 +158,73 @@ func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.P return err } } + if w.gpu == nil && !w.nocontext { + if err := w.ctx.Lock(); err != nil { + w.destroyGPU() + return err + } + gpu, err := gpu.New(w.ctx.API()) + w.ctx.Unlock() + if err != nil { + w.destroyGPU() + return err + } + w.gpu = gpu + } + if w.gpu != nil { + if err := w.render(frame, size); err != nil { + w.destroyGPU() + if err == errDeviceLost { + continue + } + return err + } + } + w.processFrame(frameStart, frame) return nil } } -func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.Ops) { - var sync <-chan struct{} - if w.loop != nil { - sync = w.loop.Draw(size, frame) - } else { - s := make(chan struct{}, 1) - s <- struct{}{} - sync = s +func (w *Window) render(frame *op.Ops, viewport image.Point) error { + if err := w.ctx.Lock(); err != nil { + return err } + defer w.ctx.Unlock() + if runtime.GOOS == "js" { + // Use transparent black when Gio is embedded, to allow mixing of Gio and + // foreign content below. + w.gpu.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) + } else { + w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + } + if err := w.gpu.Frame(frame, w.ctx.RenderTarget(), viewport); err != nil { + return err + } + return w.ctx.Present() +} + +func (w *Window) processFrame(frameStart time.Time, frame *op.Ops) { w.queue.q.Frame(frame) switch w.queue.q.TextInputState() { case router.TextInputOpen: - go w.driverRun(func(d driver) { d.ShowTextInput(true) }) + w.driverRun(func(d driver) { d.ShowTextInput(true) }) case router.TextInputClose: - go w.driverRun(func(d driver) { d.ShowTextInput(false) }) + w.driverRun(func(d driver) { d.ShowTextInput(false) }) } if hint, ok := w.queue.q.TextInputHint(); ok { - go w.driverRun(func(d driver) { d.SetInputHint(hint) }) + w.driverRun(func(d driver) { d.SetInputHint(hint) }) } if txt, ok := w.queue.q.WriteClipboard(); ok { - go w.WriteClipboard(txt) + w.WriteClipboard(txt) } if w.queue.q.ReadClipboard() { - go w.ReadClipboard() + w.ReadClipboard() } - if w.queue.q.Profiling() && w.loop != nil { + if w.queue.q.Profiling() && w.gpu != nil { frameDur := time.Since(frameStart) frameDur = frameDur.Truncate(100 * time.Microsecond) q := 100 * time.Microsecond - timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.loop.Summary()) + timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.gpu.Profile()) w.queue.q.Queue(profile.Event{Timings: timings}) } if t, ok := w.queue.q.WakeupTime(); ok { @@ -219,8 +238,6 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op. default: } w.updateAnimation() - // Wait for the GPU goroutine to finish processing frame. - <-sync } // Invalidate the window such that a FrameEvent will be generated immediately. @@ -353,7 +370,11 @@ func (w *Window) setNextFrame(at time.Time) { func (c *callbacks) SetDriver(d driver) { c.d = d - c.Event(driverEvent{d}) + var wakeup func() + if d != nil { + wakeup = d.Wakeup + } + c.Event(driverEvent{wakeup}) } func (c *callbacks) Event(e event.Event) { @@ -419,9 +440,11 @@ func (w *Window) destroy(err error) { } func (w *Window) destroyGPU() { - if w.loop != nil { - w.loop.Release() - w.loop = nil + if w.gpu != nil { + w.ctx.Lock() + w.gpu.Release() + w.ctx.Unlock() + w.gpu = nil } if w.ctx != nil { w.ctx.Release() @@ -445,19 +468,26 @@ func (w *Window) waitFrame() (*op.Ops, bool) { } func (w *Window) run(cnf *config) { + // Some OpenGL drivers don't like being made current on many different + // OS threads. Force the Go runtime to map the event loop goroutine to + // only one thread. + runtime.LockOSThread() + defer close(w.out) defer close(w.dead) if err := newWindow(&w.callbacks, cnf); err != nil { w.out <- system.DestroyEvent{Err: err} return } - var driver driver + var wakeup func() for { - var wakeups chan struct{} - if driver != nil { + var ( + wakeups <-chan struct{} + timer <-chan time.Time + ) + if wakeup != nil { wakeups = w.wakeups } - var timer <-chan time.Time if w.delayedDraw != nil { timer = w.delayedDraw.C } @@ -469,16 +499,16 @@ func (w *Window) run(cnf *config) { w.setNextFrame(time.Time{}) w.updateAnimation() case <-wakeups: - driver.Wakeup() + wakeup() case e := <-w.in: switch e2 := e.(type) { case system.StageEvent: - if w.loop != nil { - if e2.Stage < system.StageRunning { - if w.loop != nil { - w.loop.Release() - w.loop = nil - } + if e2.Stage < system.StageRunning { + if w.gpu != nil { + w.ctx.Lock() + w.gpu.Release() + w.gpu = nil + w.ctx.Unlock() } } w.stage = e2.Stage @@ -499,7 +529,7 @@ func (w *Window) run(cnf *config) { e2.Queue = &w.queue w.out <- e2.FrameEvent frame, gotFrame := w.waitFrame() - err := w.validateAndProcess(driver, frameStart, e2.Size, e2.Sync, frame) + err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame) if gotFrame { // We're done with frame, let the client continue. w.frameAck <- struct{}{} @@ -514,7 +544,7 @@ func (w *Window) run(cnf *config) { w.out <- e w.waitAck() case driverEvent: - driver = e2.driver + wakeup = e2.wakeup case system.DestroyEvent: w.destroyGPU() w.out <- e2