mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 08:25:34 +00:00
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:
-157
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user