1package wayland
2
3import (
4 "context"
5 "slices"
6 "time"
7
8 "golang.org/x/sys/unix"
9
10 "github.com/bnema/neferwl/internal/ports"
11
12 "github.com/bnema/go-wayland-bindings/server/wayland"
13)
14
15// Frame callbacks (wl_surface.frame) tell a client when to draw its next
16// frame. They are paced per output: on its page flip when it flips, else
17// at its refresh rate, so a 165 Hz output lets clients draw 165 frames a
18// second and a V-Sync game follows the screen. Cursors and unplaced
19// surfaces are paced at 60 Hz; invisible toplevel trees at 1 Hz. The
20// pacer sleeps while no callback waits.
21
22// defaultFramePeriod paces surfaces without an output (60 Hz).
23const defaultFramePeriod = time.Second / 60
24const suspendedFramePeriod = time.Second
25const suspendedFrameQueue = "\x00suspended" // never a wl_output name
26
27// maxPacePasses bounds the passes one tick repeats while a pass queues
28// more work; the rest goes back to the pacer goroutine.
29const maxPacePasses = 3
30
31// pace sleeps until a flip, a wake or the deadline armed by the last tick,
32// then runs a tick on the display goroutine. The timer is only armed from
33// ticks, which all run there, so a commit that ticks inline (paceNow) never
34// races the pacer for it.
35func (s *Server) pace(ctx context.Context) {
36 timer := s.paceTimer // created by Run before the pacer starts
37 defer timer.Stop()
38 presented := s.channels.Presented
39 if s.flipped == nil {
40 s.flipped = make(map[string]bool)
41 }
42 // One method value for the whole loop: no closure per wake.
43 tick := s.paceFromPacer
44 for {
45 clear(s.flipped)
46 flipped := s.flipped
47 s.frameReports = s.frameReports[:0]
48 reports := s.frameReports
49 select {
50 case <-ctx.Done():
51 return
52 case <-s.display.Stopped():
53 return
54 case p := <-presented:
55 flipped[p.Output] = flipped[p.Output] || paces(p)
56 reports = append(reports, p)
57 case <-s.frameReady:
58 case <-timer.C:
59 }
60 // Coalesce flips queued while the display was busy: one burst of
61 // callbacks per output, not one per stale flip.
62 drain:
63 for {
64 select {
65 case p := <-presented:
66 flipped[p.Output] = flipped[p.Output] || paces(p)
67 reports = append(reports, p)
68 default:
69 break drain
70 }
71 }
72 s.frameReports = reports
73 if !s.display.Do(tick) {
74 return
75 }
76 }
77}
78
79// paceFromPacer runs the pacer's collected flips (flipped, frameReports)
80// on the display goroutine.
81func (s *Server) paceFromPacer() { s.paceTick(s.flipped, s.frameReports) }
82
83// paceNow runs a tick from code already on the display goroutine (a client
84// commit): the pacer goroutine would only call Do to get back here. Without
85// a running pacer (no timer) it falls back to waking it.
86func (s *Server) paceNow() {
87 if s.paceTimer == nil {
88 s.signalPacer()
89 return
90 }
91 s.paceTick(nil, nil)
92}
93
94// paceTick answers the flips in reports, fires the due frame callbacks,
95// releases buffers and applies the ready FIFO commits, then arms the timer
96// for the next deadline (stopped when nothing waits). Display goroutine only.
97func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresented) {
98 if s.ctx != nil && s.ctx.Err() != nil {
99 return
100 }
101 wait, idle := defaultFramePeriod, false
102 for pass := 0; ; pass++ {
103 s.paceDirty = false
104 s.paceDepth++
105 wait, idle = s.pacePass(flipped, reports)
106 s.paceDepth--
107 if !s.paceDirty {
108 break
109 }
110 // The pass queued work behind its own checks (applied commits
111 // queue callbacks, released buffers): look again, as a flip-less
112 // tick, instead of waking the pacer for it.
113 flipped, reports = nil, nil
114 if pass+1 == maxPacePasses {
115 s.paceDirty = false
116 s.signalPacer()
117 break
118 }
119 }
120 if s.paceTimer == nil {
121 return
122 }
123 if idle {
124 // Nothing waits: sleep until a commit queues a callback
125 // (wakePacer) or a flip.
126 s.paceTimer.Stop()
127 return
128 }
129 s.paceTimer.Reset(wait)
130}
131
132// pacePass is one pass of paceTick; it returns the time to the next
133// deadline and whether nothing waits.
134func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresented) (wait time.Duration, idle bool) {
135 // Each flip answers its own presentation feedbacks, in order: flips
136 // are never merged here.
137 for _, p := range reports {
138 if p.Flip != nil {
139 s.flips[p.Output] = *p.Flip
140 s.presentFlip(p.Output, p.Flip)
141 }
142 }
143 s.dropFeedbacks(time.Now())
144 var due []string
145 due, wait, idle = s.dueFrames(time.Now(), flipped)
146 if len(s.feedbacks) > 0 {
147 // Unanswered feedbacks time out: keep ticking.
148 wait, idle = min(wait, feedbackTimeout), false
149 }
150 for _, name := range due {
151 s.sendFrames(name, flipped[name])
152 }
153 if s.releaseHeld(time.Now(), reports) && idle {
154 // Held buffers wait for a flip or heldTimeout.
155 wait, idle = heldTimeout, false
156 }
157 // Fifo barriers and queued commits wait for a refresh.
158 if fw, waiting := s.tickFifo(time.Now(), flipped); waiting {
159 wait, idle = min(wait, fw), false
160 }
161 return wait, idle
162}
163
164// toplevelRoot resolves subsurfaces and popups to their parent toplevel.
165func toplevelRoot(surf *surface) *surface {
166 surf = surf.root()
167 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
168 surf = surf.xdg.window.popup.parent.xdg.surface.root()
169 }
170 return surf
171}
172
173// invisible reports that no display shows the toplevel tree: the physical
174// truth, used for scanout and presentation feedback. A tree rendered only
175// for a capture session is invisible.
176func (s *Server) invisible(surf *surface) bool {
177 root := toplevelRoot(surf)
178 return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible
179}
180
181// suspended reports that nothing draws the toplevel tree, not even a capture
182// session: it is throttled and marked suspended. A captured tree keeps the
183// frame callbacks of its output so the capture keeps moving.
184func (s *Server) suspended(surf *surface) bool {
185 root := toplevelRoot(surf)
186 if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast {
187 return false
188 }
189 return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured
190}
191
192// frameOutput is the pacing class for a surface; "" is the 60 Hz
193// outputless class, not the invisible toplevel class.
194func (s *Server) frameOutput(surf *surface) string {
195 surf = toplevelRoot(surf)
196 switch {
197 case surf.xdg != nil && surf.xdg.window != nil:
198 if s.suspended(surf) {
199 return suspendedFrameQueue
200 }
201 return surf.xdg.window.last.Output
202 case surf.layer != nil && surf.layer.output != nil:
203 return surf.layer.output.name()
204 case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed:
205 return surf.lock.output.name()
206 }
207 return ""
208}
209
210// queueFrames adds a committed surface's callbacks to its output's queue
211// ("" when it has none) and wakes the pacer.
212func (s *Server) queueFrames(surf *surface, callbacks []*wayland.Callback) {
213 name := s.frameOutput(surf)
214 s.awaiting[name] = append(s.awaiting[name], callbacks...)
215 if s.frameOwners == nil {
216 s.frameOwners = make(map[*wayland.Callback]*surface)
217 }
218 for _, cb := range callbacks {
219 s.frameOwners[cb] = surf
220 }
221 s.wakePacer()
222}
223
224// relocateCallbacks reclassifies callbacks committed before a visibility change.
225func (s *Server) relocateCallbacks(root *surface) {
226 moved := make(map[string][]*wayland.Callback)
227 for name, callbacks := range s.awaiting {
228 kept := callbacks[:0]
229 for _, cb := range callbacks {
230 owner := s.frameOwners[cb]
231 if owner == nil || toplevelRoot(owner) != root || s.frameOutput(owner) == name {
232 kept = append(kept, cb)
233 continue
234 }
235 dest := s.frameOutput(owner)
236 moved[dest] = append(moved[dest], cb)
237 }
238 if len(kept) == 0 {
239 delete(s.awaiting, name)
240 } else {
241 s.awaiting[name] = kept
242 }
243 }
244 for name, callbacks := range moved {
245 s.awaiting[name] = append(s.awaiting[name], callbacks...)
246 if name != suspendedFrameQueue {
247 delete(s.frameDue, name) // resume without waiting for the old phase
248 }
249 }
250 s.wakePacer()
251}
252
253// dueFrames returns the queues to fire now: flipped outputs and those past
254// their deadline. wait is the time to the next deadline; idle is set when
255// no callback waits after firing.
256func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []string, wait time.Duration, idle bool) {
257 if s.framePeriods == nil {
258 s.framePeriods = make(map[string]time.Duration, len(s.outputs)+1)
259 }
260 periods := s.framePeriods
261 clear(periods)
262 for _, o := range s.outputs {
263 periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
264 }
265 // Callbacks of surfaces whose output is gone go with the outputless ones.
266 for name, callbacks := range s.awaiting {
267 if _, ok := periods[name]; !ok && name != "" && name != suspendedFrameQueue {
268 delete(s.awaiting, name)
269 s.awaiting[""] = append(s.awaiting[""], callbacks...)
270 }
271 }
272 periods[""] = defaultFramePeriod
273 periods[suspendedFrameQueue] = suspendedFramePeriod
274 for name := range s.frameDue {
275 if _, ok := periods[name]; !ok {
276 delete(s.frameDue, name)
277 }
278 }
279 for name, p := range periods {
280 due, ok := s.frameDue[name]
281 switch {
282 case flipped[name] && name != "":
283 // A flipping output is paced by its flips; its deadline only
284 // catches a missed one.
285 fire = append(fire, name)
286 s.frameDue[name] = now.Add(p + p/2)
287 case !ok || !now.Before(due):
288 if len(s.awaiting[name]) == 0 {
289 // Idle: the next callback goes out as soon as it comes.
290 delete(s.frameDue, name)
291 continue
292 }
293 fire = append(fire, name)
294 // Keep the phase; after a stall, restart from now.
295 next := due.Add(p)
296 if !ok || !next.After(now) {
297 next = now.Add(p)
298 }
299 s.frameDue[name] = next
300 }
301 }
302 waiting := false
303 wait = suspendedFramePeriod
304 for name, callbacks := range s.awaiting {
305 if len(callbacks) == 0 || slices.Contains(fire, name) {
306 continue
307 }
308 waiting = true
309 if due, ok := s.frameDue[name]; ok {
310 wait = min(wait, due.Sub(now))
311 } else {
312 wait = 0
313 }
314 }
315 return fire, max(wait, time.Millisecond), !waiting
316}
317
318// paces reports whether a report paces frame callbacks: a flip of a real
319// display. Software flips (headless) answer presentation feedback, but
320// callbacks keep the output's refresh period.
321func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock }
322
323// sendFrames fires an output's callbacks; flipped: a flip of it fired them.
324func (s *Server) sendFrames(name string, flipped bool) {
325 callbacks := s.awaiting[name]
326 delete(s.awaiting, name)
327 if len(callbacks) == 0 {
328 return
329 }
330 // done carries the flip's time when the output flips (the frame the
331 // client drew for is on screen), else now.
332 ms := uint32(time.Since(s.started).Milliseconds())
333 if f, ok := s.flips[name]; ok && flipped && f.When > 0 {
334 ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds())
335 }
336 for _, cb := range callbacks {
337 delete(s.frameOwners, cb)
338 if !cb.Resource.Alive() {
339 continue
340 }
341 cb.SendDone(ms)
342 cb.Destroy()
343 s.frames++
344 }
345}
346
347// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
348func framePeriod(refreshMilli int) time.Duration {
349 if refreshMilli <= 0 {
350 return defaultFramePeriod
351 }
352 return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
353}
354
355// monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps.
356func monotonic(t time.Time) time.Duration {
357 var ts unix.Timespec
358 if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil {
359 return 0
360 }
361 return time.Duration(ts.Nano()) - time.Since(t)
362}