main df9bf83

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).
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}

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