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// 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}

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 25

Tags 10

↑↓ moveenter openesc close