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