feat/hints a471fa4

frame_lifecycle.go RawHistoryFolder

1package drm
2
3import (
4	"os"
5	"sync/atomic"
6	"time"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// frameLifecycle is owned by Output.Run. KMS and the event reader do not
12// schedule frames: Run feeds this state with commit results, flip serials and
13// deadline ticks. Only Run uses the timer; the transitions take plain values.
14type frameLifecycle struct {
15	pending                   bool
16	pendingFrame              pendingFrame
17	serials                   *atomic.Uint64 // shared across outputs on the card
18	pendingSerial, nextSerial uint64
19	flipStart                 time.Time
20	stuckAfter                time.Duration
21	stuckAt                   time.Time
22	fenceWarned               bool
23	busySince, lastBusy       time.Time
24	stuckTimer                *time.Timer
25}
26
27// pendingFrame is what the pending frame commit shows.
28type pendingFrame struct {
29	security ports.SecurityState // admission epoch checked again at KMS boundary
30	frame    bool
31	queued   uint64
32	zeroCopy ports.WindowID // window shown without composition
33	async    bool
34	// animating: the scene shows a running animation; its flip wakes core.
35	animating bool
36	shows     map[ports.WindowID]uint64
37	// fences are duplicates of the fences the commit waits on, owned until
38	// the commit ends. composed is set when the frame was rendered.
39	fences   []*os.File
40	composed bool
41	// fenceReady: every fence had signalled at commit (flip tracing only).
42	fenceReady bool
43	// wantedAt is when the frame was first wanted (CLOCK_MONOTONIC, 0:
44	// unknown), for the flip stats.
45	wantedAt time.Duration
46	// gapEnd is when the VRR flip gap that held the frame ended (0: none).
47	gapEnd time.Duration
48}
49
50// dueAt is when the frame could first commit: wanted, and after the
51// deliberate VRR flip gap (0: unknown).
52func (f *pendingFrame) dueAt() time.Duration { return max(f.wantedAt, f.gapEnd) }
53
54func (f *pendingFrame) closeFences() {
55	for _, fd := range f.fences {
56		fd.Close()
57	}
58	f.fences = nil
59}
60
61// Commit event userData: the commit serial above userKindBits, the kind below.
62const (
63	userFrame    = 1
64	userState    = 2
65	userKindBits = 2
66)
67
68const (
69	stuckTimeout = time.Second
70	busyRetry    = 50 * time.Millisecond
71)
72
73// stuckLimit returns the configured recovery deadline or its default.
74func (l *frameLifecycle) stuckLimit() time.Duration {
75	if l.stuckAfter > 0 {
76		return l.stuckAfter
77	}
78	return stuckTimeout
79}
80
81// userData numbers a commit across all outputs of the card.
82func (l *frameLifecycle) userData(kind uint64) uint64 {
83	l.nextSerial = l.serials.Add(1)
84	return l.nextSerial<<userKindBits | kind
85}
86
87// endPending releases the in-flight commit and its duplicated fences.
88func (l *frameLifecycle) endPending() {
89	l.pendingFrame.closeFences()
90	l.pending, l.pendingFrame = false, pendingFrame{}
91}
92
93// begin records a successful nonblocking commit awaiting its event.
94func (l *frameLifecycle) begin(f pendingFrame, now time.Time) {
95	l.pendingSerial = l.nextSerial
96	l.pendingFrame.closeFences()
97	l.pending, l.pendingFrame, l.flipStart = true, f, now
98	l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false
99	l.busySince = time.Time{}
100}
101
102// pendingCommit reports whether the output must wait before another commit.
103func (l *frameLifecycle) pendingCommit() bool { return l.pending }
104
105// resetAfterModeset ends a replaced commit and clears the old busy run.
106func (l *frameLifecycle) resetAfterModeset() {
107	l.endPending()
108	l.busySince = time.Time{}
109}
110
111// deadlineReached reports whether a pending commit is ready for recovery.
112func (l *frameLifecycle) deadlineReached(now time.Time) bool {
113	return l.pending && !now.Before(l.stuckAt)
114}
115
116// deadlineInfo is the pending commit's log context and fences at a tick.
117// The caller polls the fences; the transition itself consumes a plain bool.
118func (l *frameLifecycle) deadlineInfo(now time.Time) (serial uint64, frame bool, age, busy time.Duration, fences []*os.File) {
119	return l.pendingSerial, l.pendingFrame.frame, now.Sub(l.flipStart), now.Sub(l.busySince), l.pendingFrame.fences
120}
121
122// busy records an EBUSY refusal. No commit of ours is known to be in
123// flight: any event of this CRTC may unblock the retry.
124// It returns true when this is a new run, for the caller's log.
125func (l *frameLifecycle) busy(now time.Time) bool {
126	l.endPending()
127	l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)
128	l.pendingSerial = 0
129	newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry
130	if newRun {
131		l.busySince = now
132	}
133	l.lastBusy = now
134	return newRun
135}
136
137// ours reports whether user is the event of the pending commit with a
138// known serial (not the end of an EBUSY wait).
139func (l *frameLifecycle) ours(user uint64) bool {
140	return l.pending && l.pendingSerial != 0 && user>>userKindBits == l.pendingSerial
141}
142
143// flip consumes only the event for the pending commit; while waiting after
144// EBUSY there is no known serial, so any CRTC event ends that wait. The
145// returned start lets Output measure flip age after pending fences close.
146func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {
147	if !l.pending || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
148		return pendingFrame{}, time.Time{}, false
149	}
150	f, start := l.pendingFrame, l.flipStart
151	l.endPending()
152	l.busySince = time.Time{}
153	return f, start, true
154}
155
156// timeoutResult describes the action after a deadline tick.
157type timeoutResult uint8
158
159const (
160	timeoutEarly   timeoutResult = iota // deadline not reached (or a warned fence still waits)
161	timeoutFence                        // first wait for an unsignalled fence
162	timeoutRetry                        // EBUSY without an event: retry the commit
163	timeoutBusy                         // EBUSY past its limit: modeset
164	timeoutMissing                      // committed event missing: modeset
165)
166
167// timeout takes the current fence status as a value. The caller polls only
168// when a known commit reaches its deadline; no clock, fence or event source
169// is mocked in transition tests.
170func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult {
171	if !l.pending || now.Before(l.stuckAt) {
172		return timeoutEarly
173	}
174	if l.pendingSerial != 0 && !fencesReady {
175		l.stuckAt = now.Add(l.stuckLimit())
176		if !l.fenceWarned {
177			l.fenceWarned = true
178			return timeoutFence
179		}
180		return timeoutEarly
181	}
182	l.endPending()
183	if l.pendingSerial != 0 {
184		return timeoutMissing
185	}
186	if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {
187		return timeoutRetry
188	}
189	return timeoutBusy
190}
191
192// startTimer creates the per-output deadline timer for Run, not kms.
193func (l *frameLifecycle) startTimer() {
194	l.stuckTimer = time.NewTimer(time.Hour)
195	l.stuckTimer.Stop()
196}
197
198// wait arms the pending deadline, or disables it while idle or away.
199// Go 1.27 guarantees a stopped/reset timer cannot deliver an old value.
200func (l *frameLifecycle) wait(enabled bool) <-chan time.Time {
201	if !enabled || !l.pending {
202		l.stuckTimer.Stop()
203		return nil
204	}
205	l.stuckTimer.Reset(max(0, time.Until(l.stuckAt)))
206	return l.stuckTimer.C
207}
208
209// stopTimer releases the timer when Run exits.
210func (l *frameLifecycle) stopTimer() { l.stuckTimer.Stop() }

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 26

Tags 10

↑↓ moveenter openesc close