detached 7703bb9

fix(drm): separate the frame wait reason from the memory-refusal run

commit
7703bb9e384ac91419bb87626e931a3226b6e64c
author
brice · · 2026-10-22 12:04 UTC
parent
d707400
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +56 -3
M internal/adapters/drm/frame_lifecycle.go +19 -3
@@ -13,6 +13,7 @@ import (
1313 // deadline ticks. Only Run uses the timer; the transitions take plain values.
1414 type frameLifecycle struct {
1515 pending bool
16+ waitKind frameWaitKind
1617 pendingFrame pendingFrame
1718 serials *atomic.Uint64 // shared across outputs on the card
1819 pendingSerial, nextSerial uint64
@@ -27,6 +28,16 @@ type frameLifecycle struct {
2728 stuckTimer *time.Timer
2829 }
2930
31+// frameWaitKind is the current wait, independent of refusal-run summaries.
32+type frameWaitKind uint8
33+
34+const (
35+ waitNone frameWaitKind = iota
36+ waitFlip
37+ waitBusy
38+ waitNoMem
39+)
40+
3041 // pendingFrame is what the pending frame commit shows.
3142 type pendingFrame struct {
3243 security ports.SecurityState // admission epoch checked again at KMS boundary
@@ -94,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 {
94105 func (l *frameLifecycle) endPending() {
95106 l.pendingFrame.closeFences()
96107 l.pending, l.pendingFrame = false, pendingFrame{}
108+ l.waitKind = waitNone
97109 }
98110
99111 // begin records a successful nonblocking commit awaiting its event.
@@ -101,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) {
101113 l.pendingSerial = l.nextSerial
102114 l.pendingFrame.closeFences()
103115 l.pending, l.pendingFrame, l.flipStart = true, f, now
116+ l.waitKind = waitFlip
104117 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false
105118 l.busySince = time.Time{}
106119 }
@@ -126,6 +139,7 @@ func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) {
126139 l.lastNomem = now
127140 l.nomemAttempts++
128141 l.pending, l.pendingSerial, l.flipStart = true, 0, now
142+ l.waitKind = waitNoMem
129143 l.stuckAt = now.Add(l.nomemBackoff)
130144 return l.nomemAttempts, newRun
131145 }
@@ -158,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool {
158172 l.endPending()
159173 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)
160174 l.pendingSerial = 0
175+ l.waitKind = waitBusy
161176 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry
162177 if newRun {
163178 l.busySince = now
@@ -176,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool {
176191 // EBUSY there is no known serial, so any CRTC event ends that wait. The
177192 // returned start lets Output measure flip age after pending fences close.
178193 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {
179- if !l.pending || l.pendingSerial == 0 && !l.nomemSince.IsZero() || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
194+ if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
180195 return pendingFrame{}, time.Time{}, false
181196 }
182197 f, start := l.pendingFrame, l.flipStart
@@ -211,11 +226,12 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult
211226 }
212227 return timeoutEarly
213228 }
229+ kind := l.waitKind
214230 l.endPending()
215- if l.pendingSerial != 0 {
231+ if kind == waitFlip {
216232 return timeoutMissing
217233 }
218- if !l.nomemSince.IsZero() {
234+ if kind == waitNoMem {
219235 return timeoutRetry
220236 }
221237 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {
@@ -13,6 +13,7 @@ import (
13 // deadline ticks. Only Run uses the timer; the transitions take plain values.13 // deadline ticks. Only Run uses the timer; the transitions take plain values.
14 type frameLifecycle struct {14 type frameLifecycle struct {
15 pending bool15 pending bool
16+ waitKind frameWaitKind
16 pendingFrame pendingFrame17 pendingFrame pendingFrame
17 serials *atomic.Uint64 // shared across outputs on the card18 serials *atomic.Uint64 // shared across outputs on the card
18 pendingSerial, nextSerial uint6419 pendingSerial, nextSerial uint64
@@ -27,6 +28,16 @@ type frameLifecycle struct {
27 stuckTimer *time.Timer28 stuckTimer *time.Timer
28 }29 }
29 30
31+// frameWaitKind is the current wait, independent of refusal-run summaries.
32+type frameWaitKind uint8
33+
34+const (
35+ waitNone frameWaitKind = iota
36+ waitFlip
37+ waitBusy
38+ waitNoMem
39+)
40+
30 // pendingFrame is what the pending frame commit shows.41 // pendingFrame is what the pending frame commit shows.
31 type pendingFrame struct {42 type pendingFrame struct {
32 security ports.SecurityState // admission epoch checked again at KMS boundary43 security ports.SecurityState // admission epoch checked again at KMS boundary
@@ -94,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 {
94 func (l *frameLifecycle) endPending() {105 func (l *frameLifecycle) endPending() {
95 l.pendingFrame.closeFences()106 l.pendingFrame.closeFences()
96 l.pending, l.pendingFrame = false, pendingFrame{}107 l.pending, l.pendingFrame = false, pendingFrame{}
108+ l.waitKind = waitNone
97 }109 }
98 110
99 // begin records a successful nonblocking commit awaiting its event.111 // begin records a successful nonblocking commit awaiting its event.
@@ -101,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) {
101 l.pendingSerial = l.nextSerial113 l.pendingSerial = l.nextSerial
102 l.pendingFrame.closeFences()114 l.pendingFrame.closeFences()
103 l.pending, l.pendingFrame, l.flipStart = true, f, now115 l.pending, l.pendingFrame, l.flipStart = true, f, now
116+ l.waitKind = waitFlip
104 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false117 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false
105 l.busySince = time.Time{}118 l.busySince = time.Time{}
106 }119 }
@@ -126,6 +139,7 @@ func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) {
126 l.lastNomem = now139 l.lastNomem = now
127 l.nomemAttempts++140 l.nomemAttempts++
128 l.pending, l.pendingSerial, l.flipStart = true, 0, now141 l.pending, l.pendingSerial, l.flipStart = true, 0, now
142+ l.waitKind = waitNoMem
129 l.stuckAt = now.Add(l.nomemBackoff)143 l.stuckAt = now.Add(l.nomemBackoff)
130 return l.nomemAttempts, newRun144 return l.nomemAttempts, newRun
131 }145 }
@@ -158,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool {
158 l.endPending()172 l.endPending()
159 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)173 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)
160 l.pendingSerial = 0174 l.pendingSerial = 0
175+ l.waitKind = waitBusy
161 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry176 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry
162 if newRun {177 if newRun {
163 l.busySince = now178 l.busySince = now
@@ -176,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool {
176 // EBUSY there is no known serial, so any CRTC event ends that wait. The191 // EBUSY there is no known serial, so any CRTC event ends that wait. The
177 // returned start lets Output measure flip age after pending fences close.192 // returned start lets Output measure flip age after pending fences close.
178 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {193 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {
179- if !l.pending || l.pendingSerial == 0 && !l.nomemSince.IsZero() || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {194+ if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
180 return pendingFrame{}, time.Time{}, false195 return pendingFrame{}, time.Time{}, false
181 }196 }
182 f, start := l.pendingFrame, l.flipStart197 f, start := l.pendingFrame, l.flipStart
@@ -211,11 +226,12 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult
211 }226 }
212 return timeoutEarly227 return timeoutEarly
213 }228 }
229+ kind := l.waitKind
214 l.endPending()230 l.endPending()
215- if l.pendingSerial != 0 {231+ if kind == waitFlip {
216 return timeoutMissing232 return timeoutMissing
217 }233 }
218- if !l.nomemSince.IsZero() {234+ if kind == waitNoMem {
219 return timeoutRetry235 return timeoutRetry
220 }236 }
221 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {237 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {
M internal/adapters/drm/frame_lifecycle_test.go +37 -0
@@ -124,3 +124,40 @@ func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) {
124124 t.Fatal("modeset did not clear run")
125125 }
126126 }
127+
128+func TestFrameLifecycleNoMemThenBusyEvent(t *testing.T) {
129+ l := frameLifecycle{}
130+ start := time.Unix(100, 0)
131+ l.nomem(start)
132+ if got := l.timeout(l.stuckAt, true); got != timeoutRetry {
133+ t.Fatalf("memory wait: %v", got)
134+ }
135+ l.busy(start.Add(nomemRetryMin))
136+ if _, _, ok := l.flip(123 << userKindBits); !ok || l.pending || l.waitKind != waitNone {
137+ t.Fatal("memory history prevented a CRTC event from ending EBUSY")
138+ }
139+ if l.nomemAttempts != 1 || l.nomemSince != start {
140+ t.Fatal("busy event lost memory recovery summary")
141+ }
142+}
143+
144+func TestFrameLifecycleNoMemThenBusyLimit(t *testing.T) {
145+ l := frameLifecycle{stuckAfter: 200 * time.Millisecond}
146+ start := time.Unix(100, 0)
147+ l.nomem(start)
148+ l.timeout(l.stuckAt, true)
149+ busyStart := start.Add(nomemRetryMin)
150+ for i := range 4 {
151+ l.busy(busyStart.Add(time.Duration(i) * busyRetry))
152+ want := timeoutRetry
153+ if i == 3 {
154+ want = timeoutBusy
155+ }
156+ if got := l.timeout(l.stuckAt, true); got != want {
157+ t.Fatalf("busy attempt %d: got %v want %v", i+1, got, want)
158+ }
159+ }
160+ if l.nomemAttempts != 1 || l.nomemSince != start {
161+ t.Fatal("busy timeout lost memory recovery summary")
162+ }
163+}
@@ -124,3 +124,40 @@ func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) {
124 t.Fatal("modeset did not clear run")124 t.Fatal("modeset did not clear run")
125 }125 }
126 }126 }
127+
128+func TestFrameLifecycleNoMemThenBusyEvent(t *testing.T) {
129+ l := frameLifecycle{}
130+ start := time.Unix(100, 0)
131+ l.nomem(start)
132+ if got := l.timeout(l.stuckAt, true); got != timeoutRetry {
133+ t.Fatalf("memory wait: %v", got)
134+ }
135+ l.busy(start.Add(nomemRetryMin))
136+ if _, _, ok := l.flip(123 << userKindBits); !ok || l.pending || l.waitKind != waitNone {
137+ t.Fatal("memory history prevented a CRTC event from ending EBUSY")
138+ }
139+ if l.nomemAttempts != 1 || l.nomemSince != start {
140+ t.Fatal("busy event lost memory recovery summary")
141+ }
142+}
143+
144+func TestFrameLifecycleNoMemThenBusyLimit(t *testing.T) {
145+ l := frameLifecycle{stuckAfter: 200 * time.Millisecond}
146+ start := time.Unix(100, 0)
147+ l.nomem(start)
148+ l.timeout(l.stuckAt, true)
149+ busyStart := start.Add(nomemRetryMin)
150+ for i := range 4 {
151+ l.busy(busyStart.Add(time.Duration(i) * busyRetry))
152+ want := timeoutRetry
153+ if i == 3 {
154+ want = timeoutBusy
155+ }
156+ if got := l.timeout(l.stuckAt, true); got != want {
157+ t.Fatalf("busy attempt %d: got %v want %v", i+1, got, want)
158+ }
159+ }
160+ if l.nomemAttempts != 1 || l.nomemSince != start {
161+ t.Fatal("busy timeout lost memory recovery summary")
162+ }
163+}

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