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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-08-29 12:55:46 +02:00
parent fd008f39af
commit 4f198b3f87
2 changed files with 90 additions and 217 deletions
-157
View File
@@ -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
}
}
+90 -60
View File
@@ -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