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() }