frames.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close