mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 10:25:37 +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"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gioui.org/gpu"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/profile"
|
"gioui.org/io/profile"
|
||||||
@@ -25,8 +27,8 @@ type Option func(cnf *config)
|
|||||||
|
|
||||||
// Window represents an operating system window.
|
// Window represents an operating system window.
|
||||||
type Window struct {
|
type Window struct {
|
||||||
ctx context
|
ctx context
|
||||||
loop *renderLoop
|
gpu gpu.GPU
|
||||||
|
|
||||||
// driverFuncs is a channel of functions to run when
|
// driverFuncs is a channel of functions to run when
|
||||||
// the Window has a valid driver.
|
// the Window has a valid driver.
|
||||||
@@ -72,7 +74,7 @@ type queue struct {
|
|||||||
|
|
||||||
// driverEvent is sent when the underlying driver changes.
|
// driverEvent is sent when the underlying driver changes.
|
||||||
type driverEvent struct {
|
type driverEvent struct {
|
||||||
driver driver
|
wakeup func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-allocate the ack event to avoid garbage.
|
// Pre-allocate the ack event to avoid garbage.
|
||||||
@@ -129,21 +131,12 @@ func (w *Window) update(frame *op.Ops) {
|
|||||||
<-w.frameAck
|
<-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 {
|
for {
|
||||||
if w.loop != nil {
|
if w.gpu == nil && !w.nocontext {
|
||||||
if err := w.loop.Flush(); err != nil {
|
|
||||||
w.destroyGPU()
|
|
||||||
if err == errDeviceLost {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if w.loop == nil && !w.nocontext {
|
|
||||||
var err error
|
var err error
|
||||||
if w.ctx == nil {
|
if w.ctx == nil {
|
||||||
w.driverRun(func(_ driver) {
|
w.driverRun(func(d driver) {
|
||||||
w.ctx, err = d.NewContext()
|
w.ctx, err = d.NewContext()
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,20 +144,13 @@ func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.P
|
|||||||
}
|
}
|
||||||
sync = true
|
sync = true
|
||||||
}
|
}
|
||||||
w.loop, err = newLoop(w.ctx)
|
|
||||||
if err != nil {
|
|
||||||
w.destroyGPU()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if sync && w.ctx != nil {
|
if sync && w.ctx != nil {
|
||||||
w.driverRun(func(_ driver) {
|
var err error
|
||||||
w.ctx.Refresh()
|
w.driverRun(func(d driver) {
|
||||||
|
err = w.ctx.Refresh()
|
||||||
})
|
})
|
||||||
}
|
if err != nil {
|
||||||
w.processFrame(frameStart, size, frame)
|
|
||||||
if sync && w.loop != nil {
|
|
||||||
if err := w.loop.Flush(); err != nil {
|
|
||||||
w.destroyGPU()
|
w.destroyGPU()
|
||||||
if err == errDeviceLost {
|
if err == errDeviceLost {
|
||||||
continue
|
continue
|
||||||
@@ -172,40 +158,73 @@ func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.P
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.Ops) {
|
func (w *Window) render(frame *op.Ops, viewport image.Point) error {
|
||||||
var sync <-chan struct{}
|
if err := w.ctx.Lock(); err != nil {
|
||||||
if w.loop != nil {
|
return err
|
||||||
sync = w.loop.Draw(size, frame)
|
|
||||||
} else {
|
|
||||||
s := make(chan struct{}, 1)
|
|
||||||
s <- struct{}{}
|
|
||||||
sync = s
|
|
||||||
}
|
}
|
||||||
|
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)
|
w.queue.q.Frame(frame)
|
||||||
switch w.queue.q.TextInputState() {
|
switch w.queue.q.TextInputState() {
|
||||||
case router.TextInputOpen:
|
case router.TextInputOpen:
|
||||||
go w.driverRun(func(d driver) { d.ShowTextInput(true) })
|
w.driverRun(func(d driver) { d.ShowTextInput(true) })
|
||||||
case router.TextInputClose:
|
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 {
|
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 {
|
if txt, ok := w.queue.q.WriteClipboard(); ok {
|
||||||
go w.WriteClipboard(txt)
|
w.WriteClipboard(txt)
|
||||||
}
|
}
|
||||||
if w.queue.q.ReadClipboard() {
|
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 := time.Since(frameStart)
|
||||||
frameDur = frameDur.Truncate(100 * time.Microsecond)
|
frameDur = frameDur.Truncate(100 * time.Microsecond)
|
||||||
q := 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})
|
w.queue.q.Queue(profile.Event{Timings: timings})
|
||||||
}
|
}
|
||||||
if t, ok := w.queue.q.WakeupTime(); ok {
|
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:
|
default:
|
||||||
}
|
}
|
||||||
w.updateAnimation()
|
w.updateAnimation()
|
||||||
// Wait for the GPU goroutine to finish processing frame.
|
|
||||||
<-sync
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate the window such that a FrameEvent will be generated immediately.
|
// 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) {
|
func (c *callbacks) SetDriver(d driver) {
|
||||||
c.d = d
|
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) {
|
func (c *callbacks) Event(e event.Event) {
|
||||||
@@ -419,9 +440,11 @@ func (w *Window) destroy(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Window) destroyGPU() {
|
func (w *Window) destroyGPU() {
|
||||||
if w.loop != nil {
|
if w.gpu != nil {
|
||||||
w.loop.Release()
|
w.ctx.Lock()
|
||||||
w.loop = nil
|
w.gpu.Release()
|
||||||
|
w.ctx.Unlock()
|
||||||
|
w.gpu = nil
|
||||||
}
|
}
|
||||||
if w.ctx != nil {
|
if w.ctx != nil {
|
||||||
w.ctx.Release()
|
w.ctx.Release()
|
||||||
@@ -445,19 +468,26 @@ func (w *Window) waitFrame() (*op.Ops, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Window) run(cnf *config) {
|
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.out)
|
||||||
defer close(w.dead)
|
defer close(w.dead)
|
||||||
if err := newWindow(&w.callbacks, cnf); err != nil {
|
if err := newWindow(&w.callbacks, cnf); err != nil {
|
||||||
w.out <- system.DestroyEvent{Err: err}
|
w.out <- system.DestroyEvent{Err: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var driver driver
|
var wakeup func()
|
||||||
for {
|
for {
|
||||||
var wakeups chan struct{}
|
var (
|
||||||
if driver != nil {
|
wakeups <-chan struct{}
|
||||||
|
timer <-chan time.Time
|
||||||
|
)
|
||||||
|
if wakeup != nil {
|
||||||
wakeups = w.wakeups
|
wakeups = w.wakeups
|
||||||
}
|
}
|
||||||
var timer <-chan time.Time
|
|
||||||
if w.delayedDraw != nil {
|
if w.delayedDraw != nil {
|
||||||
timer = w.delayedDraw.C
|
timer = w.delayedDraw.C
|
||||||
}
|
}
|
||||||
@@ -469,16 +499,16 @@ func (w *Window) run(cnf *config) {
|
|||||||
w.setNextFrame(time.Time{})
|
w.setNextFrame(time.Time{})
|
||||||
w.updateAnimation()
|
w.updateAnimation()
|
||||||
case <-wakeups:
|
case <-wakeups:
|
||||||
driver.Wakeup()
|
wakeup()
|
||||||
case e := <-w.in:
|
case e := <-w.in:
|
||||||
switch e2 := e.(type) {
|
switch e2 := e.(type) {
|
||||||
case system.StageEvent:
|
case system.StageEvent:
|
||||||
if w.loop != nil {
|
if e2.Stage < system.StageRunning {
|
||||||
if e2.Stage < system.StageRunning {
|
if w.gpu != nil {
|
||||||
if w.loop != nil {
|
w.ctx.Lock()
|
||||||
w.loop.Release()
|
w.gpu.Release()
|
||||||
w.loop = nil
|
w.gpu = nil
|
||||||
}
|
w.ctx.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.stage = e2.Stage
|
w.stage = e2.Stage
|
||||||
@@ -499,7 +529,7 @@ func (w *Window) run(cnf *config) {
|
|||||||
e2.Queue = &w.queue
|
e2.Queue = &w.queue
|
||||||
w.out <- e2.FrameEvent
|
w.out <- e2.FrameEvent
|
||||||
frame, gotFrame := w.waitFrame()
|
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 {
|
if gotFrame {
|
||||||
// We're done with frame, let the client continue.
|
// We're done with frame, let the client continue.
|
||||||
w.frameAck <- struct{}{}
|
w.frameAck <- struct{}{}
|
||||||
@@ -514,7 +544,7 @@ func (w *Window) run(cnf *config) {
|
|||||||
w.out <- e
|
w.out <- e
|
||||||
w.waitAck()
|
w.waitAck()
|
||||||
case driverEvent:
|
case driverEvent:
|
||||||
driver = e2.driver
|
wakeup = e2.wakeup
|
||||||
case system.DestroyEvent:
|
case system.DestroyEvent:
|
||||||
w.destroyGPU()
|
w.destroyGPU()
|
||||||
w.out <- e2
|
w.out <- e2
|
||||||
|
|||||||
Reference in New Issue
Block a user