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