loop.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package app
4
5 import (
6 "image"
7 "image/color"
8 "runtime"
9
10 "github.com/p9c/p9/pkg/gel/gio/app/internal/wm"
11 "github.com/p9c/p9/pkg/gel/gio/gpu"
12 "github.com/p9c/p9/pkg/gel/gio/op"
13 )
14
15 type renderLoop struct {
16 summary string
17 drawing bool
18 err error
19
20 frames chan frame
21 results chan frameResult
22 refresh chan struct{}
23 refreshErr chan error
24 ack chan struct{}
25 stop chan struct{}
26 stopped chan struct{}
27 }
28
29 type frame struct {
30 viewport image.Point
31 ops *op.Ops
32 }
33
34 type frameResult struct {
35 profile string
36 err error
37 }
38
39 func newLoop(ctx wm.Context) (*renderLoop, error) {
40 l := &renderLoop{
41 frames: make(chan frame),
42 results: make(chan frameResult),
43 refresh: make(chan struct{}),
44 refreshErr: make(chan error),
45 // Ack is buffered so GPU commands can be issued after
46 // ack'ing the frame.
47 ack: make(chan struct{}, 1),
48 stop: make(chan struct{}),
49 stopped: make(chan struct{}),
50 }
51 if err := l.renderLoop(ctx); err != nil {
52 return nil, err
53 }
54 return l, nil
55 }
56
57 func (l *renderLoop) renderLoop(ctx wm.Context) error {
58 // GL Operations must happen on a single OS thread, so
59 // pass initialization result through a channel.
60 initErr := make(chan error)
61 go func() {
62 defer close(l.stopped)
63 runtime.LockOSThread()
64 // Don't UnlockOSThread to avoid reuse by the Go runtime.
65
66 if err := ctx.MakeCurrent(); err != nil {
67 initErr <- err
68 return
69 }
70 g, err := gpu.New(ctx.API())
71 if err != nil {
72 initErr <- err
73 return
74 }
75 defer g.Release()
76 initErr <- nil
77 loop:
78 for {
79 select {
80 case <-l.refresh:
81 l.refreshErr <- ctx.MakeCurrent()
82 case frame := <-l.frames:
83 ctx.Lock()
84 if runtime.GOOS == "js" {
85 // Use transparent black when Gio is embedded, to allow mixing of Gio and
86 // foreign content below.
87 g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00})
88 } else {
89 g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
90 }
91 g.Collect(frame.viewport, frame.ops)
92 // Signal that we're done with the frame ops.
93 l.ack <- struct{}{}
94 var res frameResult
95 res.err = g.Frame()
96 if res.err == nil {
97 res.err = ctx.Present()
98 }
99 res.profile = g.Profile()
100 ctx.Unlock()
101 l.results <- res
102 case <-l.stop:
103 break loop
104 }
105 }
106 }()
107 return <-initErr
108 }
109
110 func (l *renderLoop) Release() {
111 // Flush error.
112 l.Flush()
113 close(l.stop)
114 <-l.stopped
115 l.stop = nil
116 }
117
118 func (l *renderLoop) Flush() error {
119 if l.drawing {
120 st := <-l.results
121 l.setErr(st.err)
122 if st.profile != "" {
123 l.summary = st.profile
124 }
125 l.drawing = false
126 }
127 return l.err
128 }
129
130 func (l *renderLoop) Summary() string {
131 return l.summary
132 }
133
134 func (l *renderLoop) Refresh() {
135 if l.err != nil {
136 return
137 }
138 // Make sure any pending frame is complete.
139 l.Flush()
140 l.refresh <- struct{}{}
141 l.setErr(<-l.refreshErr)
142 }
143
144 // Draw initiates a draw of a frame. It returns a channel
145 // than signals when the frame is no longer being accessed.
146 func (l *renderLoop) Draw(viewport image.Point, frameOps *op.Ops) <-chan struct{} {
147 if l.err != nil {
148 l.ack <- struct{}{}
149 return l.ack
150 }
151 l.Flush()
152 l.frames <- frame{viewport, frameOps}
153 l.drawing = true
154 return l.ack
155 }
156
157 func (l *renderLoop) setErr(err error) {
158 if l.err == nil {
159 l.err = err
160 }
161 }
162