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// pace fires the due frame callbacks on page flips and deadlines.
28func (s *Server) pace(ctx context.Context) {
29 timer := time.NewTimer(defaultFramePeriod)
30 defer timer.Stop()
31 presented := s.channels.Presented
32 if s.flipped == nil {
33 s.flipped = make(map[string]bool)
34 }
35 for {
36 clear(s.flipped)
37 flipped := s.flipped
38 s.frameReports = s.frameReports[:0]
39 reports := s.frameReports
40 select {
41 case <-ctx.Done():
42 return
43 case <-s.display.Stopped():
44 return
45 case p := <-presented:
46 flipped[p.Output] = flipped[p.Output] || paces(p)
47 reports = append(reports, p)
48 case <-s.frameReady:
49 case <-timer.C:
50 }
51 // Coalesce flips queued while the display was busy: one burst of
52 // callbacks per output, not one per stale flip.
53 drain:
54 for {
55 select {
56 case p := <-presented:
57 flipped[p.Output] = flipped[p.Output] || paces(p)
58 reports = append(reports, p)
59 default:
60 break drain
61 }
62 }
63 s.frameReports = reports
64 wait, idle := defaultFramePeriod, false
65 if !s.display.Do(func() {
66 if ctx.Err() == nil {
67 // Each flip answers its own presentation feedbacks, in
68 // order: flips are never merged here.
69 for _, p := range reports {
70 if p.Flip != nil {
71 s.flips[p.Output] = *p.Flip
72 s.presentFlip(p.Output, p.Flip)
73 }
74 }
75 s.dropFeedbacks(time.Now())
76 if len(s.feedbacks) > 0 {
77 wait = feedbackTimeout
78 }
79 var due []string
80 var dueWait time.Duration
81 due, dueWait, idle = s.dueFrames(time.Now(), flipped)
82 if len(s.feedbacks) > 0 {
83 // Unanswered feedbacks time out: keep ticking.
84 dueWait, idle = min(dueWait, feedbackTimeout), false
85 }
86 wait = dueWait
87 for _, name := range due {
88 s.sendFrames(name, flipped[name])
89 }
90 if s.releaseHeld(time.Now(), reports) && idle {
91 // Held buffers wait for a flip or heldTimeout.
92 wait, idle = heldTimeout, false
93 }
94 // Fifo barriers and queued commits wait for a refresh.
95 if fw, waiting := s.tickFifo(time.Now(), flipped); waiting {
96 wait, idle = min(wait, fw), false
97 }
98 }
99 }) {
100 return
101 }
102 if idle {
103 // Nothing waits: sleep until a commit queues a callback
104 // (frameReady) or a flip.
105 timer.Stop()
106 continue
107 }
108 timer.Reset(wait)
109 }
110}
111
112// toplevelRoot resolves subsurfaces and popups to their parent toplevel.
113func toplevelRoot(surf *surface) *surface {
114 surf = surf.root()
115 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
116 surf = surf.xdg.window.popup.parent.xdg.surface.root()
117 }
118 return surf
119}
120
121// invisible reports that no display shows the toplevel tree: the physical
122// truth, used for scanout and presentation feedback. A tree rendered only
123// for a capture session is invisible.
124func (s *Server) invisible(surf *surface) bool {
125 root := toplevelRoot(surf)
126 return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible
127}
128
129// suspended reports that nothing draws the toplevel tree, not even a capture
130// session: it is throttled and marked suspended. A captured tree keeps the
131// frame callbacks of its output so the capture keeps moving.
132func (s *Server) suspended(surf *surface) bool {
133 root := toplevelRoot(surf)
134 if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast {
135 return false
136 }
137 return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured
138}
139
140// frameOutput is the pacing class for a surface; "" is the 60 Hz
141// outputless class, not the invisible toplevel class.
142func (s *Server) frameOutput(surf *surface) string {
143 surf = toplevelRoot(surf)
144 switch {
145 case surf.xdg != nil && surf.xdg.window != nil:
146 if s.suspended(surf) {
147 return suspendedFrameQueue
148 }
149 return surf.xdg.window.last.Output
150 case surf.layer != nil && surf.layer.output != nil:
151 return surf.layer.output.name()
152 case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed:
153 return surf.lock.output.name()
154 }
155 return ""
156}
157
158// queueFrames adds a committed surface's callbacks to its output's queue
159// ("" when it has none) and wakes the pacer.
160func (s *Server) queueFrames(surf *surface, callbacks []*wayland.Callback) {
161 name := s.frameOutput(surf)
162 s.awaiting[name] = append(s.awaiting[name], callbacks...)
163 if s.frameOwners == nil {
164 s.frameOwners = make(map[*wayland.Callback]*surface)
165 }
166 for _, cb := range callbacks {
167 s.frameOwners[cb] = surf
168 }
169 s.wakePacer()
170}
171
172// relocateCallbacks reclassifies callbacks committed before a visibility change.
173func (s *Server) relocateCallbacks(root *surface) {
174 moved := make(map[string][]*wayland.Callback)
175 for name, callbacks := range s.awaiting {
176 kept := callbacks[:0]
177 for _, cb := range callbacks {
178 owner := s.frameOwners[cb]
179 if owner == nil || toplevelRoot(owner) != root || s.frameOutput(owner) == name {
180 kept = append(kept, cb)
181 continue
182 }
183 dest := s.frameOutput(owner)
184 moved[dest] = append(moved[dest], cb)
185 }
186 if len(kept) == 0 {
187 delete(s.awaiting, name)
188 } else {
189 s.awaiting[name] = kept
190 }
191 }
192 for name, callbacks := range moved {
193 s.awaiting[name] = append(s.awaiting[name], callbacks...)
194 if name != suspendedFrameQueue {
195 delete(s.frameDue, name) // resume without waiting for the old phase
196 }
197 }
198 s.wakePacer()
199}
200
201// dueFrames returns the queues to fire now: flipped outputs and those past
202// their deadline. wait is the time to the next deadline; idle is set when
203// no callback waits after firing.
204func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []string, wait time.Duration, idle bool) {
205 if s.framePeriods == nil {
206 s.framePeriods = make(map[string]time.Duration, len(s.outputs)+1)
207 }
208 periods := s.framePeriods
209 clear(periods)
210 for _, o := range s.outputs {
211 periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
212 }
213 // Callbacks of surfaces whose output is gone go with the outputless ones.
214 for name, callbacks := range s.awaiting {
215 if _, ok := periods[name]; !ok && name != "" && name != suspendedFrameQueue {
216 delete(s.awaiting, name)
217 s.awaiting[""] = append(s.awaiting[""], callbacks...)
218 }
219 }
220 periods[""] = defaultFramePeriod
221 periods[suspendedFrameQueue] = suspendedFramePeriod
222 for name := range s.frameDue {
223 if _, ok := periods[name]; !ok {
224 delete(s.frameDue, name)
225 }
226 }
227 for name, p := range periods {
228 due, ok := s.frameDue[name]
229 switch {
230 case flipped[name] && name != "":
231 // A flipping output is paced by its flips; its deadline only
232 // catches a missed one.
233 fire = append(fire, name)
234 s.frameDue[name] = now.Add(p + p/2)
235 case !ok || !now.Before(due):
236 if len(s.awaiting[name]) == 0 {
237 // Idle: the next callback goes out as soon as it comes.
238 delete(s.frameDue, name)
239 continue
240 }
241 fire = append(fire, name)
242 // Keep the phase; after a stall, restart from now.
243 next := due.Add(p)
244 if !ok || !next.After(now) {
245 next = now.Add(p)
246 }
247 s.frameDue[name] = next
248 }
249 }
250 waiting := false
251 wait = suspendedFramePeriod
252 for name, callbacks := range s.awaiting {
253 if len(callbacks) == 0 || slices.Contains(fire, name) {
254 continue
255 }
256 waiting = true
257 if due, ok := s.frameDue[name]; ok {
258 wait = min(wait, due.Sub(now))
259 } else {
260 wait = 0
261 }
262 }
263 return fire, max(wait, time.Millisecond), !waiting
264}
265
266// paces reports whether a report paces frame callbacks: a flip of a real
267// display. Software flips (headless) answer presentation feedback, but
268// callbacks keep the output's refresh period.
269func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock }
270
271// sendFrames fires an output's callbacks; flipped: a flip of it fired them.
272func (s *Server) sendFrames(name string, flipped bool) {
273 callbacks := s.awaiting[name]
274 delete(s.awaiting, name)
275 if len(callbacks) == 0 {
276 return
277 }
278 // done carries the flip's time when the output flips (the frame the
279 // client drew for is on screen), else now.
280 ms := uint32(time.Since(s.started).Milliseconds())
281 if f, ok := s.flips[name]; ok && flipped && f.When > 0 {
282 ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds())
283 }
284 for _, cb := range callbacks {
285 delete(s.frameOwners, cb)
286 if !cb.Resource.Alive() {
287 continue
288 }
289 cb.SendDone(ms)
290 cb.Destroy()
291 s.frames++
292 }
293}
294
295// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
296func framePeriod(refreshMilli int) time.Duration {
297 if refreshMilli <= 0 {
298 return defaultFramePeriod
299 }
300 return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
301}
302
303// monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps.
304func monotonic(t time.Time) time.Duration {
305 var ts unix.Timespec
306 if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil {
307 return 0
308 }
309 return time.Duration(ts.Nano()) - time.Since(t)
310}