1package wayland
2
3import (
4 "errors"
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. Pacing runs
20// on the display loop: output reports arrive through the presentation sink
21// (presented.go) and deadlines through a timerfd; the timer sleeps while no
22// callback waits.
23
24// defaultFramePeriod paces surfaces without an output (60 Hz).
25const defaultFramePeriod = time.Second / 60
26const suspendedFramePeriod = time.Second
27const suspendedFrameQueue = "\x00suspended" // never a wl_output name
28
29// maxPacePasses bounds the passes one tick repeats while a pass queues
30// more work; the rest is posted to the display loop.
31const maxPacePasses = 3
32
33// paceNow runs a tick from code already on the display goroutine (a client
34// commit).
35func (s *Server) paceNow() { s.paceTick(nil, nil) }
36
37// paceTolerance is how much earlier than the armed deadline a new one may be
38// and still reuse it: the ticks of one frame compute the same deadline from
39// clock reads a few microseconds apart.
40const paceTolerance = 50 * time.Microsecond
41
42// armPace schedules the next flip-less tick d from now on the pace timerfd
43// (nanosecond precision, one-shot). A deadline at most paceTolerance before
44// the armed one keeps the timer as it is: a frame's commit, flip and timer
45// ticks mostly end on the same deadline. The timer never fires before the
46// requested deadline, since the tick's checks are strict and an early tick
47// would push due work back by their minimum re-arm. The tolerance is measured
48// against the armed deadline, so it never accumulates. Display goroutine only.
49func (s *Server) armPace(d time.Duration) { s.armPaceFrom(time.Now(), d) }
50
51// armPaceFrom is armPace with the clock read at now.
52func (s *Server) armPaceFrom(now time.Time, d time.Duration) {
53 deadline := now.Add(max(d, 0))
54 if s.paceArmed {
55 if diff := deadline.Sub(s.paceDeadline); diff >= -paceTolerance && diff <= 0 {
56 return
57 }
58 }
59 spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(max(d, time.Nanosecond)))}
60 err := unix.TimerfdSettime(s.paceFD, 0, &spec, nil)
61 s.warnPace("arm", err)
62 // A failed arm leaves the old state unknown: arm again next time.
63 s.paceArmed, s.paceDeadline = err == nil, deadline
64}
65
66// stopPace disarms the pace timerfd, unless it is not armed (never armed, or
67// its one-shot expiry was consumed by onPaceTimer). Display goroutine only.
68func (s *Server) stopPace() {
69 if !s.paceArmed {
70 return
71 }
72 var spec unix.ItimerSpec
73 err := unix.TimerfdSettime(s.paceFD, 0, &spec, nil)
74 s.warnPace("stop", err)
75 // A failed stop keeps the state: the next stop tries again.
76 s.paceArmed = err != nil
77}
78
79// warnPace logs the first failure of a pace timerfd operation (later ones
80// would repeat at frame rate). A failed arm leaves frame callbacks waiting
81// for the next commit or flip. Display goroutine only.
82func (s *Server) warnPace(op string, err error) {
83 if err == nil || s.paceWarned {
84 return
85 }
86 s.paceWarned = true
87 s.log.Warn().Err(err).Str("op", op).Msg("pace timer")
88}
89
90// onPaceTimer is the display loop's callback for the pace timerfd: the
91// deadline armed by the last tick passed.
92func (s *Server) onPaceTimer() {
93 var buf [8]byte
94 // Level-triggered: consume the expiry. EAGAIN: the timer was disarmed
95 // after the loop saw it readable.
96 n, err := unix.Read(s.paceFD, buf[:])
97 switch {
98 case err == nil && n == len(buf):
99 // The one-shot expired: the timer is disarmed until the next arm.
100 s.paceArmed = false
101 case err != nil && !errors.Is(err, unix.EAGAIN):
102 s.warnPace("read", err)
103 }
104 s.paceTick(nil, nil)
105}
106
107// paceSignalled is the posted tick: a wake (fence, overflow) made while the
108// display goroutine was elsewhere.
109func (s *Server) paceSignalled() {
110 s.paceSignal.Store(false)
111 s.paceTick(nil, nil)
112}
113
114// paceTick answers the flips in reports, fires the due frame callbacks,
115// releases buffers and applies the ready FIFO commits, then arms the timer
116// for the next deadline (stopped when nothing waits). Display goroutine only.
117func (s *Server) paceTick(flipped map[string]bool, reports []ports.OutputPresented) {
118 if s.ctx != nil && s.ctx.Err() != nil {
119 return
120 }
121 wait, idle := defaultFramePeriod, false
122 for pass := 0; ; pass++ {
123 s.paceDirty = false
124 s.paceDepth++
125 wait, idle = s.pacePass(flipped, reports)
126 s.paceDepth--
127 if !s.paceDirty {
128 break
129 }
130 // The pass queued work behind its own checks (applied commits
131 // queue callbacks, released buffers): look again, as a flip-less
132 // tick, instead of posting one.
133 flipped, reports = nil, nil
134 if pass+1 == maxPacePasses {
135 s.paceDirty = false
136 s.signalPacer()
137 break
138 }
139 }
140 if idle {
141 // Nothing waits: sleep until a commit queues a callback
142 // (wakePacer) or a flip.
143 s.stopPace()
144 return
145 }
146 s.armPace(wait)
147}
148
149// pacePass is one pass of paceTick; it returns the time to the next
150// deadline and whether nothing waits.
151func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresented) (wait time.Duration, idle bool) {
152 // Each flip answers its own presentation feedbacks, in order: flips
153 // are never merged here.
154 for _, p := range reports {
155 if p.Flip != nil {
156 s.flips[p.Output] = *p.Flip
157 s.presentFlip(p.Output, p.Flip)
158 s.castFlip(p.Output, p.Flip)
159 }
160 }
161 s.dropFeedbacks(time.Now())
162 var due []string
163 due, wait, idle = s.dueFrames(time.Now(), flipped)
164 if len(s.feedbacks) > 0 {
165 // Unanswered feedbacks time out: keep ticking.
166 wait, idle = min(wait, feedbackTimeout), false
167 }
168 for _, name := range due {
169 s.sendFrames(name, flipped[name])
170 }
171 if s.releaseHeld(time.Now(), reports) && idle {
172 // Held buffers wait for a flip or heldTimeout.
173 wait, idle = heldTimeout, false
174 }
175 // Fifo barriers and queued commits wait for a refresh.
176 if fw, waiting := s.tickFifo(time.Now(), flipped); waiting {
177 wait, idle = min(wait, fw), false
178 }
179 return wait, idle
180}
181
182// toplevelRoot resolves subsurfaces and popups to their parent toplevel.
183func toplevelRoot(surf *surface) *surface {
184 surf = surf.root()
185 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
186 surf = surf.xdg.window.popup.parent.xdg.surface.root()
187 }
188 return surf
189}
190
191// invisible reports that no display shows the toplevel tree: the physical
192// truth, used for scanout and presentation feedback. A tree rendered only
193// for a capture session is invisible.
194func (s *Server) invisible(surf *surface) bool {
195 root := toplevelRoot(surf)
196 return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible
197}
198
199// suspended reports that nothing draws the toplevel tree, not even a capture
200// session: it is throttled and marked suspended. A captured tree keeps the
201// frame callbacks of its output so the capture keeps moving.
202func (s *Server) suspended(surf *surface) bool {
203 root := toplevelRoot(surf)
204 if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast {
205 return false
206 }
207 return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured
208}
209
210// frameOutput is the pacing class for a surface; "" is the 60 Hz
211// outputless class, not the invisible toplevel class.
212func (s *Server) frameOutput(surf *surface) string {
213 surf = toplevelRoot(surf)
214 switch {
215 case surf.xdg != nil && surf.xdg.window != nil:
216 if s.suspended(surf) {
217 return suspendedFrameQueue
218 }
219 return surf.xdg.window.last.Output
220 case surf.layer != nil && surf.layer.output != nil:
221 return surf.layer.output.name()
222 case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed:
223 return surf.lock.output.name()
224 }
225 return ""
226}
227
228// queueFrames adds a committed surface's callbacks to its output's queue
229// ("" when it has none) and wakes the pacer.
230func (s *Server) queueFrames(surf *surface, callbacks []*wayland.Callback) {
231 name := s.frameOutput(surf)
232 s.awaiting[name] = append(s.awaiting[name], callbacks...)
233 if s.frameOwners == nil {
234 s.frameOwners = make(map[*wayland.Callback]*surface)
235 }
236 for _, cb := range callbacks {
237 s.frameOwners[cb] = surf
238 }
239 s.wakePacer()
240}
241
242// relocateCallbacks reclassifies callbacks committed before a visibility change.
243func (s *Server) relocateCallbacks(root *surface) {
244 moved := make(map[string][]*wayland.Callback)
245 for name, callbacks := range s.awaiting {
246 kept := callbacks[:0]
247 for _, cb := range callbacks {
248 owner := s.frameOwners[cb]
249 if owner == nil || toplevelRoot(owner) != root || s.frameOutput(owner) == name {
250 kept = append(kept, cb)
251 continue
252 }
253 dest := s.frameOutput(owner)
254 moved[dest] = append(moved[dest], cb)
255 }
256 clear(callbacks[len(kept):]) // the filtered tail must not pin callbacks
257 if len(kept) == 0 {
258 delete(s.awaiting, name)
259 } else {
260 s.awaiting[name] = kept
261 }
262 }
263 for name, callbacks := range moved {
264 s.awaiting[name] = append(s.awaiting[name], callbacks...)
265 if name != suspendedFrameQueue {
266 delete(s.frameDue, name) // resume without waiting for the old phase
267 }
268 }
269 s.wakePacer()
270}
271
272// dueFrames returns the queues to fire now: flipped outputs and those past
273// their deadline. wait is the time to the next deadline; idle is set when
274// no callback waits after firing. fire is the tick's scratch, valid until
275// the next call (display goroutine only).
276func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []string, wait time.Duration, idle bool) {
277 fire = s.frameFire[:0]
278 if s.framePeriods == nil {
279 s.framePeriods = make(map[string]time.Duration, len(s.outputs)+1)
280 }
281 periods := s.framePeriods
282 clear(periods)
283 for _, o := range s.outputs {
284 periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
285 }
286 // Callbacks of surfaces whose output is gone go with the outputless ones.
287 for name, callbacks := range s.awaiting {
288 if _, ok := periods[name]; !ok && name != "" && name != suspendedFrameQueue {
289 delete(s.awaiting, name)
290 if len(callbacks) > 0 {
291 s.awaiting[""] = append(s.awaiting[""], callbacks...)
292 }
293 }
294 }
295 periods[""] = defaultFramePeriod
296 periods[suspendedFrameQueue] = suspendedFramePeriod
297 for name := range s.frameDue {
298 if _, ok := periods[name]; !ok {
299 delete(s.frameDue, name)
300 }
301 }
302 for name, p := range periods {
303 due, ok := s.frameDue[name]
304 switch {
305 case flipped[name] && name != "":
306 // A flipping output is paced by its flips; its deadline only
307 // catches a missed one.
308 fire = append(fire, name)
309 s.frameDue[name] = now.Add(p + p/2)
310 case !ok || !now.Before(due):
311 if len(s.awaiting[name]) == 0 {
312 // Idle: the next callback goes out as soon as it comes.
313 delete(s.frameDue, name)
314 continue
315 }
316 fire = append(fire, name)
317 // Keep the phase; after a stall, restart from now.
318 next := due.Add(p)
319 if !ok || !next.After(now) {
320 next = now.Add(p)
321 }
322 s.frameDue[name] = next
323 }
324 }
325 waiting := false
326 wait = suspendedFramePeriod
327 for name, callbacks := range s.awaiting {
328 if len(callbacks) == 0 || slices.Contains(fire, name) {
329 continue
330 }
331 waiting = true
332 if due, ok := s.frameDue[name]; ok {
333 wait = min(wait, due.Sub(now))
334 } else {
335 wait = 0
336 }
337 }
338 s.frameFire = fire
339 return fire, max(wait, time.Millisecond), !waiting
340}
341
342// paces reports whether a report paces frame callbacks: a flip of a real
343// display. Software flips (headless) answer presentation feedback, but
344// callbacks keep the output's refresh period.
345func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock }
346
347// sendFrames fires an output's callbacks; flipped: a flip of it fired them.
348func (s *Server) sendFrames(name string, flipped bool) {
349 callbacks := s.awaiting[name]
350 if len(callbacks) == 0 {
351 return
352 }
353 // done carries the flip's time when the output flips (the frame the
354 // client drew for is on screen), else now.
355 ms := uint32(time.Since(s.started).Milliseconds())
356 if f, ok := s.flips[name]; ok && flipped && f.When > 0 {
357 ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds())
358 }
359 for _, cb := range callbacks {
360 delete(s.frameOwners, cb)
361 if !cb.Resource.Alive() {
362 continue
363 }
364 cb.SendDone(ms)
365 cb.Destroy()
366 s.frames++
367 }
368 // The queue keeps its backing array for the next frame (an empty queue
369 // reads like an absent one). Nothing above queues a callback.
370 clear(callbacks)
371 s.awaiting[name] = callbacks[:0]
372}
373
374// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
375func framePeriod(refreshMilli int) time.Duration {
376 if refreshMilli <= 0 {
377 return defaultFramePeriod
378 }
379 return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
380}
381
382// monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps.
383func monotonic(t time.Time) time.Duration {
384 var ts unix.Timespec
385 if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil {
386 return 0
387 }
388 return time.Duration(ts.Nano()) - time.Since(t)
389}