main 5d6fa26

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

Tags 10

↑↓ moveenter openesc close