v0.1.0 d9218d6

frames.go RawHistoryFolder

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}

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