1package wayland
2
3import (
4 "context"
5 "golang.org/x/sys/unix"
6 "slices"
7 "time"
8
9 "github.com/bnema/neferwl/internal/ports"
10
11 "github.com/bnema/purego-libwayland/protocol/wayland"
12)
13
14// Frame callbacks (wl_surface.frame) tell a client when to draw its next
15// frame. They are paced per output: on its page flip when it flips, else
16// at its refresh rate, so a 165 Hz output lets clients draw 165 frames a
17// second and a V-Sync game follows the screen. Surfaces with no output
18// (hidden windows, cursors, unplaced surfaces) are paced at 60 Hz. The
19// pacer sleeps while no callback waits.
20
21// defaultFramePeriod paces surfaces without an output (60 Hz).
22const defaultFramePeriod = time.Second / 60
23
24// pace fires the due frame callbacks on page flips and deadlines.
25func (s *Server) pace(ctx context.Context) {
26 timer := time.NewTimer(defaultFramePeriod)
27 defer timer.Stop()
28 presented := s.channels.Presented
29 for {
30 flipped := map[string]bool{}
31 var reports []ports.OutputPresented
32 select {
33 case <-ctx.Done():
34 return
35 case <-s.display.Stopped():
36 return
37 case p := <-presented:
38 flipped[p.Output] = flipped[p.Output] || paces(p)
39 reports = append(reports, p)
40 case <-s.frameReady:
41 case <-timer.C:
42 }
43 // Coalesce flips queued while the display was busy: one burst of
44 // callbacks per output, not one per stale flip.
45 drain:
46 for {
47 select {
48 case p := <-presented:
49 flipped[p.Output] = flipped[p.Output] || paces(p)
50 reports = append(reports, p)
51 default:
52 break drain
53 }
54 }
55 wait, idle := defaultFramePeriod, false
56 if !s.display.Do(func() {
57 if ctx.Err() == nil {
58 // Each flip answers its own presentation feedbacks, in
59 // order: flips are never merged here.
60 for _, p := range reports {
61 if p.Flip != nil {
62 s.flips[p.Output] = *p.Flip
63 s.presentFlip(p.Output, p.Flip)
64 }
65 }
66 s.dropFeedbacks(time.Now())
67 if len(s.feedbacks) > 0 {
68 wait = feedbackTimeout
69 }
70 var due []string
71 var dueWait time.Duration
72 due, dueWait, idle = s.dueFrames(time.Now(), flipped)
73 if len(s.feedbacks) > 0 {
74 // Unanswered feedbacks time out: keep ticking.
75 dueWait, idle = min(dueWait, feedbackTimeout), false
76 }
77 wait = dueWait
78 for _, name := range due {
79 s.sendFrames(name, flipped[name])
80 }
81 if s.releaseHeld(time.Now(), reports) && idle {
82 // Held buffers wait for a flip or heldTimeout.
83 wait, idle = heldTimeout, false
84 }
85 // Fifo barriers and queued commits wait for a refresh.
86 if fw, waiting := s.tickFifo(time.Now(), flipped); waiting {
87 wait, idle = min(wait, fw), false
88 }
89 }
90 }) {
91 return
92 }
93 if idle {
94 // Nothing waits: sleep until a commit queues a callback
95 // (frameReady) or a flip.
96 timer.Stop()
97 continue
98 }
99 timer.Reset(wait)
100 }
101}
102
103// frameOutput is the output showing a surface, "" when none does (a hidden
104// window, a cursor): unlike outputOfSurface, no fallback to the focused one.
105func (s *Server) frameOutput(surf *surface) string {
106 surf = surf.root()
107 for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil {
108 surf = surf.xdg.window.popup.parent.xdg.surface.root()
109 }
110 switch {
111 case surf.xdg != nil && surf.xdg.window != nil:
112 return surf.xdg.window.last.Output
113 case surf.layer != nil && surf.layer.output != nil:
114 return surf.layer.output.name()
115 }
116 return ""
117}
118
119// queueFrames adds a committed surface's callbacks to its output's queue
120// ("" when it has none) and wakes the pacer.
121func (s *Server) queueFrames(name string, callbacks []*wayland.Callback) {
122 s.awaiting[name] = append(s.awaiting[name], callbacks...)
123 select {
124 case s.frameReady <- struct{}{}:
125 default:
126 }
127}
128
129// dueFrames returns the queues to fire now: flipped outputs and those past
130// their deadline. wait is the time to the next deadline; idle is set when
131// no callback waits after firing.
132func (s *Server) dueFrames(now time.Time, flipped map[string]bool) (fire []string, wait time.Duration, idle bool) {
133 periods := make(map[string]time.Duration, len(s.outputs)+1)
134 for _, o := range s.outputs {
135 periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
136 }
137 // Callbacks of surfaces whose output is gone go with the outputless ones.
138 for name, callbacks := range s.awaiting {
139 if _, ok := periods[name]; !ok && name != "" {
140 delete(s.awaiting, name)
141 s.awaiting[""] = append(s.awaiting[""], callbacks...)
142 }
143 }
144 periods[""] = defaultFramePeriod
145 for name := range s.frameDue {
146 if _, ok := periods[name]; !ok {
147 delete(s.frameDue, name)
148 }
149 }
150 for name, p := range periods {
151 due, ok := s.frameDue[name]
152 switch {
153 case flipped[name] && name != "":
154 // A flipping output is paced by its flips; its deadline only
155 // catches a missed one.
156 fire = append(fire, name)
157 s.frameDue[name] = now.Add(p + p/2)
158 case !ok || !now.Before(due):
159 if len(s.awaiting[name]) == 0 {
160 // Idle: the next callback goes out as soon as it comes.
161 delete(s.frameDue, name)
162 continue
163 }
164 fire = append(fire, name)
165 // Keep the phase; after a stall, restart from now.
166 next := due.Add(p)
167 if !ok || !next.After(now) {
168 next = now.Add(p)
169 }
170 s.frameDue[name] = next
171 }
172 }
173 waiting := false
174 wait = defaultFramePeriod
175 for name, callbacks := range s.awaiting {
176 if len(callbacks) == 0 || slices.Contains(fire, name) {
177 continue
178 }
179 waiting = true
180 if due, ok := s.frameDue[name]; ok {
181 wait = min(wait, due.Sub(now))
182 } else {
183 wait = 0
184 }
185 }
186 return fire, max(wait, time.Millisecond), !waiting
187}
188
189// paces reports whether a report paces frame callbacks: a flip of a real
190// display. Software flips (headless) answer presentation feedback, but
191// callbacks keep the output's refresh period.
192func paces(p ports.OutputPresented) bool { return p.Flip != nil && p.Flip.HardwareClock }
193
194// sendFrames fires an output's callbacks; flipped: a flip of it fired them.
195func (s *Server) sendFrames(name string, flipped bool) {
196 callbacks := s.awaiting[name]
197 delete(s.awaiting, name)
198 if len(callbacks) == 0 {
199 return
200 }
201 // done carries the flip's time when the output flips (the frame the
202 // client drew for is on screen), else now.
203 ms := uint32(time.Since(s.started).Milliseconds())
204 if f, ok := s.flips[name]; ok && flipped && f.When > 0 {
205 ms = uint32(max(f.When-monotonic(s.started), 0).Milliseconds())
206 }
207 for _, cb := range callbacks {
208 if !cb.Resource.Alive() {
209 continue
210 }
211 cb.SendDone(ms)
212 cb.Destroy()
213 s.frames++
214 }
215}
216
217// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
218func framePeriod(refreshMilli int) time.Duration {
219 if refreshMilli <= 0 {
220 return defaultFramePeriod
221 }
222 return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
223}
224
225// monotonic is t on CLOCK_MONOTONIC, the clock of flip timestamps.
226func monotonic(t time.Time) time.Duration {
227 var ts unix.Timespec
228 if unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) != nil {
229 return 0
230 }
231 return time.Duration(ts.Nano()) - time.Since(t)
232}