v0.8.0 446d1e3

frames.go RawHistoryFolder

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

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