From 5fa3dbc70d2b99a1f2775aa44862a70e3e8a2563 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 29 Nov 2019 19:42:29 +0100 Subject: [PATCH] app,app/internal/gpu: split render loop from GPU The policy of rendering on a separate goroutine is separate from the actual rendering. Reflect that by introducing the RenderLoop type for driving a GPU from a separate goroutine. Signed-off-by: Elias Naur --- app/internal/gpu/context.go | 4 +- app/internal/gpu/gpu.go | 265 +++++++++++------------------------- app/loop.go | 152 +++++++++++++++++++++ app/window.go | 39 +++--- 4 files changed, 255 insertions(+), 205 deletions(-) create mode 100644 app/loop.go diff --git a/app/internal/gpu/context.go b/app/internal/gpu/context.go index 45d1d903..ee66eb17 100644 --- a/app/internal/gpu/context.go +++ b/app/internal/gpu/context.go @@ -32,9 +32,9 @@ type textureTriple struct { typ gl.Enum } -func newContext(glctx gl.Context) (*context, error) { +func newContext(glctx *gl.Functions) (*context, error) { ctx := &context{ - Functions: glctx.Functions(), + Functions: glctx, } exts := strings.Split(ctx.GetString(gl.EXTENSIONS), " ") glVer := ctx.GetString(gl.VERSION) diff --git a/app/internal/gpu/gpu.go b/app/internal/gpu/gpu.go index 61349f42..40efb5ef 100644 --- a/app/internal/gpu/gpu.go +++ b/app/internal/gpu/gpu.go @@ -8,7 +8,6 @@ import ( "image" "image/color" "math" - "runtime" "strings" "time" "unsafe" @@ -23,31 +22,15 @@ import ( ) type GPU struct { - drawing bool - summary string - err error - pathCache *opCache cache *resourceCache - frames chan frame - results chan frameResult - refresh chan struct{} - refreshErr chan error - ack chan struct{} - stop chan struct{} - stopped chan struct{} -} - -type frame struct { - collectStats bool - viewport image.Point - ops *op.Ops -} - -type frameResult struct { - summary string - err error + timers *timers + frameStart time.Time + zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer + drawOps drawOps + ctx *context + renderer *renderer } type renderer struct { @@ -239,187 +222,103 @@ var ( attribUV gl.Attrib = 1 ) -func New(ctx gl.Context) (*GPU, error) { +func New(ctx *gl.Functions) (*GPU, error) { g := &GPU{ - frames: make(chan frame), - results: make(chan frameResult), - refresh: make(chan struct{}), - refreshErr: make(chan error), - // 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{}), pathCache: newOpCache(), cache: newResourceCache(), } - if err := g.renderLoop(ctx); err != nil { + if err := g.init(ctx); err != nil { return nil, err } return g, nil } -func (g *GPU) renderLoop(glctx gl.Context) error { - // GL Operations must happen on a single OS thread, so - // pass initialization result through a channel. - initErr := make(chan error) - go func() { - runtime.LockOSThread() - // Don't UnlockOSThread to avoid reuse by the Go runtime. - defer close(g.stopped) - - if err := glctx.MakeCurrent(); err != nil { - initErr <- err - return - } - ctx, err := newContext(glctx) - if err != nil { - initErr <- err - return - } - initErr <- nil - defer glctx.Release() - defer g.cache.release(ctx) - defer g.pathCache.release(ctx) - r := newRenderer(ctx) - defer r.release() - var timers *timers - var zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer - var drawOps drawOps - loop: - for { - select { - case <-g.refresh: - g.refreshErr <- glctx.MakeCurrent() - case frame := <-g.frames: - drawOps.reset(g.cache, frame.viewport) - drawOps.collect(g.cache, frame.ops, frame.viewport) - glctx.Lock() - frameStart := time.Now() - if frame.collectStats && timers == nil && ctx.caps.EXT_disjoint_timer_query { - timers = newTimers(ctx) - zopsTimer = timers.newTimer() - stencilTimer = timers.newTimer() - coverTimer = timers.newTimer() - cleanupTimer = timers.newTimer() - defer timers.release() - } - // Upload path data to GPU before ack'ing the frame - // ops for re-use. - for _, p := range drawOps.pathOps { - if _, exists := g.pathCache.get(p.pathKey); !exists { - data := buildPath(r.ctx, p.pathVerts) - g.pathCache.put(p.pathKey, data) - } - p.pathVerts = nil - } - // Signal that we're done with the frame ops. - g.ack <- struct{}{} - r.blitter.viewport = frame.viewport - r.pather.viewport = frame.viewport - for _, img := range drawOps.imageOps { - expandPathOp(img.path, img.clip) - } - if frame.collectStats { - zopsTimer.begin() - } - ctx.DepthFunc(gl.GREATER) - ctx.ClearColor(drawOps.clearColor[0], drawOps.clearColor[1], drawOps.clearColor[2], 1.0) - ctx.ClearDepthf(0.0) - ctx.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) - ctx.Viewport(0, 0, frame.viewport.X, frame.viewport.Y) - r.drawZOps(drawOps.zimageOps) - zopsTimer.end() - stencilTimer.begin() - ctx.Enable(gl.BLEND) - r.packStencils(&drawOps.pathOps) - r.stencilClips(g.pathCache, drawOps.pathOps) - r.packIntersections(drawOps.imageOps) - r.intersect(drawOps.imageOps) - stencilTimer.end() - coverTimer.begin() - ctx.Viewport(0, 0, frame.viewport.X, frame.viewport.Y) - r.drawOps(drawOps.imageOps) - ctx.Disable(gl.BLEND) - r.pather.stenciler.invalidateFBO() - coverTimer.end() - err := glctx.Present() - cleanupTimer.begin() - g.cache.frame(ctx) - g.pathCache.frame(ctx) - cleanupTimer.end() - var res frameResult - if frame.collectStats && timers.ready() { - zt, st, covt, cleant := zopsTimer.Elapsed, stencilTimer.Elapsed, coverTimer.Elapsed, cleanupTimer.Elapsed - ft := zt + st + covt + cleant - q := 100 * time.Microsecond - zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q) - frameDur := time.Since(frameStart).Round(q) - ft = ft.Round(q) - res.summary = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", frameDur, ft, zt, st, covt) - } - res.err = err - glctx.Unlock() - g.results <- res - case <-g.stop: - break loop - } - } - }() - return <-initErr +func (g *GPU) init(glctx *gl.Functions) error { + ctx, err := newContext(glctx) + if err != nil { + return err + } + g.ctx = ctx + g.renderer = newRenderer(ctx) + return nil } func (g *GPU) Release() { - // Flush error. - g.Flush() - close(g.stop) - <-g.stopped - g.stop = nil + g.renderer.release() + g.pathCache.release(g.ctx) + g.cache.release(g.ctx) + if g.timers != nil { + g.timers.release() + } } -func (g *GPU) Flush() error { - if g.drawing { - st := <-g.results - g.setErr(st.err) - if st.summary != "" { - g.summary = st.summary +func (g *GPU) Collect(profile bool, viewport image.Point, frameOps *op.Ops) { + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.cache, frameOps, viewport) + g.frameStart = time.Now() + if profile && g.timers == nil && g.ctx.caps.EXT_disjoint_timer_query { + g.timers = newTimers(g.ctx) + g.zopsTimer = g.timers.newTimer() + g.stencilTimer = g.timers.newTimer() + g.coverTimer = g.timers.newTimer() + g.cleanupTimer = g.timers.newTimer() + } + for _, p := range g.drawOps.pathOps { + if _, exists := g.pathCache.get(p.pathKey); !exists { + data := buildPath(g.ctx, p.pathVerts) + g.pathCache.put(p.pathKey, data) } - g.drawing = false + p.pathVerts = nil } - return g.err } -func (g *GPU) Timings() string { - return g.summary +func (g *GPU) Frame(profile bool, viewport image.Point) { + g.renderer.blitter.viewport = viewport + g.renderer.pather.viewport = viewport + for _, img := range g.drawOps.imageOps { + expandPathOp(img.path, img.clip) + } + if profile { + g.zopsTimer.begin() + } + g.ctx.DepthFunc(gl.GREATER) + g.ctx.ClearColor(g.drawOps.clearColor[0], g.drawOps.clearColor[1], g.drawOps.clearColor[2], 1.0) + g.ctx.ClearDepthf(0.0) + g.ctx.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawZOps(g.drawOps.zimageOps) + g.zopsTimer.end() + g.stencilTimer.begin() + g.ctx.Enable(gl.BLEND) + g.renderer.packStencils(&g.drawOps.pathOps) + g.renderer.stencilClips(g.pathCache, g.drawOps.pathOps) + g.renderer.packIntersections(g.drawOps.imageOps) + g.renderer.intersect(g.drawOps.imageOps) + g.stencilTimer.end() + g.coverTimer.begin() + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawOps(g.drawOps.imageOps) + g.ctx.Disable(gl.BLEND) + g.renderer.pather.stenciler.invalidateFBO() + g.coverTimer.end() } -func (g *GPU) Refresh() { - if g.err != nil { - return - } - // Make sure any pending frame is complete. - g.Flush() - g.refresh <- struct{}{} - g.setErr(<-g.refreshErr) -} - -// Draw initiates a draw of a frame. It returns a channel -// than signals when the frame is no longer being accessed. -func (g *GPU) Draw(profile bool, viewport image.Point, frameOps *op.Ops) <-chan struct{} { - if g.err != nil { - g.ack <- struct{}{} - return g.ack - } - g.Flush() - g.frames <- frame{profile, viewport, frameOps} - g.drawing = true - return g.ack -} - -func (g *GPU) setErr(err error) { - if g.err == nil { - g.err = err +func (g *GPU) EndFrame(profile bool) string { + g.cleanupTimer.begin() + g.cache.frame(g.ctx) + g.pathCache.frame(g.ctx) + g.cleanupTimer.end() + var summary string + if profile && g.timers.ready() { + zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed + ft := zt + st + covt + cleant + q := 100 * time.Microsecond + zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q) + frameDur := time.Since(g.frameStart).Round(q) + ft = ft.Round(q) + summary = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", frameDur, ft, zt, st, covt) } + return summary } func (r *renderer) texHandle(t *texture) gl.Texture { diff --git a/app/loop.go b/app/loop.go new file mode 100644 index 00000000..1dc81c05 --- /dev/null +++ b/app/loop.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "image" + "runtime" + + "gioui.org/app/internal/gl" + "gioui.org/app/internal/gpu" + "gioui.org/op" +) + +type renderLoop struct { + summary string + drawing bool + err error + + frames chan frame + results chan frameResult + refresh chan struct{} + refreshErr chan error + ack chan struct{} + stop chan struct{} + stopped chan struct{} +} + +type frame struct { + collectStats bool + viewport image.Point + ops *op.Ops +} + +type frameResult struct { + summary string + err error +} + +func newLoop(ctx gl.Context) (*renderLoop, error) { + l := &renderLoop{ + frames: make(chan frame), + results: make(chan frameResult), + refresh: make(chan struct{}), + refreshErr: make(chan error), + // 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(glctx gl.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 := glctx.MakeCurrent(); err != nil { + initErr <- err + return + } + g, err := gpu.New(glctx.Functions()) + if err != nil { + initErr <- err + return + } + defer glctx.Release() + initErr <- nil + loop: + for { + select { + case <-l.refresh: + l.refreshErr <- glctx.MakeCurrent() + case frame := <-l.frames: + glctx.Lock() + g.Collect(frame.collectStats, frame.viewport, frame.ops) + // Signal that we're done with the frame ops. + l.ack <- struct{}{} + g.Frame(frame.collectStats, frame.viewport) + var res frameResult + res.err = glctx.Present() + res.summary = g.EndFrame(frame.collectStats) + glctx.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.summary != "" { + l.summary = st.summary + } + l.drawing = false + } + return l.err +} + +func (l *renderLoop) Summary() string { + return l.summary +} + +func (l *renderLoop) Refresh() { + if l.err != nil { + return + } + // Make sure any pending frame is complete. + l.Flush() + l.refresh <- struct{}{} + l.setErr(<-l.refreshErr) +} + +// 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(profile bool, viewport image.Point, frameOps *op.Ops) <-chan struct{} { + if l.err != nil { + l.ack <- struct{}{} + return l.ack + } + l.Flush() + l.frames <- frame{profile, 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 1d30dc6d..c5d173b4 100644 --- a/app/window.go +++ b/app/window.go @@ -9,7 +9,6 @@ import ( "time" "gioui.org/app/internal/gl" - "gioui.org/app/internal/gpu" "gioui.org/app/internal/input" "gioui.org/app/internal/window" "gioui.org/io/event" @@ -27,7 +26,7 @@ type Option func(opts *window.Options) // Window represents an operating system window. type Window struct { driver window.Driver - gpu *gpu.GPU + loop *renderLoop // driverFuncs is a channel of functions to run when // the Window has a valid driver. @@ -127,7 +126,7 @@ func (w *Window) update(frame *op.Ops) { } func (w *Window) draw(frameStart time.Time, size image.Point, frame *op.Ops) { - sync := w.gpu.Draw(w.queue.q.Profiling(), size, frame) + sync := w.loop.Draw(w.queue.q.Profiling(), size, frame) w.queue.q.Frame(frame) switch w.queue.q.TextInputState() { case input.TextInputOpen: @@ -139,7 +138,7 @@ func (w *Window) draw(frameStart time.Time, size image.Point, frame *op.Ops) { frameDur := time.Since(frameStart) frameDur = frameDur.Truncate(100 * time.Microsecond) q := 100 * time.Microsecond - timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.gpu.Timings()) + timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.loop.Summary()) w.queue.q.AddProfile(profile.Event{Timings: timings}) } if t, ok := w.queue.q.WakeupTime(); ok { @@ -218,9 +217,9 @@ func (w *Window) destroy(err error) { } func (w *Window) destroyGPU() { - if w.gpu != nil { - w.gpu.Release() - w.gpu = nil + if w.loop != nil { + w.loop.Release() + w.loop = nil } } @@ -252,12 +251,12 @@ func (w *Window) run(opts *window.Options) { case e := <-w.in: switch e2 := e.(type) { case system.StageEvent: - if w.gpu != nil { + if w.loop != nil { if e2.Stage < system.StageRunning { - w.gpu.Release() - w.gpu = nil + w.loop.Release() + w.loop = nil } else { - w.gpu.Refresh() + w.loop.Refresh() } } w.stage = e2.Stage @@ -277,19 +276,19 @@ func (w *Window) run(opts *window.Options) { e2.Frame = w.update w.out <- e2.FrameEvent var err error - if w.gpu != nil { + if w.loop != nil { if e2.Sync { - w.gpu.Refresh() + w.loop.Refresh() } - if err = w.gpu.Flush(); err != nil { - w.gpu.Release() - w.gpu = nil + if err = w.loop.Flush(); err != nil { + w.loop.Release() + w.loop = nil } } else { var ctx gl.Context ctx, err = w.driver.NewContext() if err == nil { - w.gpu, err = gpu.New(ctx) + w.loop, err = newLoop(ctx) if err != nil { ctx.Release() } @@ -318,9 +317,9 @@ func (w *Window) run(opts *window.Options) { w.frameAck <- struct{}{} } if e2.Sync { - if err := w.gpu.Flush(); err != nil { - w.gpu.Release() - w.gpu = nil + if err := w.loop.Flush(); err != nil { + w.loop.Release() + w.loop = nil w.destroy(err) return }