drmkit_scene/release.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Deferred buffer release.
5//!
6//! Ports the acquisition ring from `src/scene/layer_scene.cpp`. This is where
7//! **invariants 1, 2 and 5** live.
8
9use drmkit_planes::LayerId;
10use drmkit_sync::SyncFence;
11
12use crate::source::AcquiredBuffer;
13
14/// One buffer held on behalf of a layer.
15#[derive(Debug)]
16#[non_exhaustive]
17pub struct Acquisition {
18 /// Which layer acquired it.
19 pub layer: LayerId,
20 /// The buffer itself.
21 pub buffer: AcquiredBuffer,
22 /// The `OUT_FENCE` of the commit that displaced it, stamped on when it
23 /// leaves the screen and handed back with the buffer.
24 ///
25 /// `None` when the CRTC has no `OUT_FENCE_PTR` or no source opted in.
26 pub release_fence: Option<SyncFence>,
27}
28
29impl Acquisition {
30 /// Hold `buffer` on behalf of `layer`.
31 #[must_use]
32 pub const fn new(layer: LayerId, buffer: AcquiredBuffer) -> Self {
33 Self {
34 layer,
35 buffer,
36 release_fence: None,
37 }
38 }
39}
40
41/// Why a set of buffers is being handed back.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum ReleaseReason {
45 /// They stopped being scanned out: a later commit's flip completed.
46 ///
47 /// The ordinary path, and the one invariant 1 governs.
48 Retired,
49 /// The commit that would have shown them was rejected.
50 ///
51 /// Invariant 2: no flip is in flight to gate them on, so they go back at
52 /// once and the producer can reuse them immediately.
53 CommitFailed,
54 /// A test commit, which applies nothing to hardware.
55 TestOnly,
56 /// The scene is being torn down, paused, or rebound.
57 Drained,
58}
59
60/// The buffers to hand back, and why.
61#[derive(Debug)]
62#[non_exhaustive]
63pub struct Released {
64 /// The buffers, oldest first.
65 pub acquisitions: Vec<Acquisition>,
66 /// Why they are being returned.
67 pub reason: ReleaseReason,
68}
69
70impl Released {
71 /// Whether anything is being returned.
72 #[must_use]
73 pub fn is_empty(&self) -> bool {
74 self.acquisitions.is_empty()
75 }
76
77 /// How many buffers are being returned.
78 #[must_use]
79 pub fn len(&self) -> usize {
80 self.acquisitions.len()
81 }
82}
83
84/// The two-deep ring of in-flight acquisitions.
85///
86/// # Invariant 1: release after the flip, not after the ioctl
87///
88/// A buffer goes back to its source only once the page flip that **stops**
89/// scanning it out has completed — two commits deep, never when `commit`'s
90/// ioctl returns. Releasing at ioctl return would let a producer overwrite a
91/// buffer the display engine is still reading, which tears on hardware whose
92/// producer driver attaches no reservation fence.
93///
94/// Three generations are tracked:
95///
96/// | Generation | Frame | State |
97/// |---|---|---|
98/// | `current` | N | just committed, about to be scanned out |
99/// | `previous` | N-1 | on screen; the flip for N displaces it |
100/// | `two_ago` | N-2 | off screen — safe to return |
101///
102/// A successful commit returns `two_ago`, then rotates. That is the whole
103/// mechanism.
104///
105/// # Why this is not the scene's own field
106///
107/// The queue never calls a source. It *yields* the buffers to hand back and
108/// lets the caller route each to the right source, because a source is owned
109/// by its layer and the queue has no business borrowing the layer table. It
110/// also means the entire contract is testable with no device, no source, and
111/// no kernel — which is what these tests do.
112#[derive(Debug, Default)]
113pub struct ReleaseQueue {
114 /// Frame N-1: on screen now.
115 previous: Vec<Acquisition>,
116 /// Frame N-2: off screen, returned by the next successful commit.
117 two_ago: Vec<Acquisition>,
118}
119
120impl ReleaseQueue {
121 /// An empty queue.
122 #[must_use]
123 pub const fn new() -> Self {
124 Self {
125 previous: Vec::new(),
126 two_ago: Vec::new(),
127 }
128 }
129
130 /// How many buffers are held in flight.
131 #[must_use]
132 pub fn in_flight(&self) -> usize {
133 self.previous.len() + self.two_ago.len()
134 }
135
136 /// Whether any buffer is held.
137 #[must_use]
138 pub fn is_empty(&self) -> bool {
139 self.previous.is_empty() && self.two_ago.is_empty()
140 }
141
142 /// Record a **successful real commit** and return the buffers it retired.
143 ///
144 /// **Invariant 1.** The returned buffers are frame N-2's: the flip just
145 /// armed displaces frame N-1, so N-2 is what has finished scanning out.
146 /// Frame N's buffers stay held.
147 ///
148 /// `release_fence` is this commit's `OUT_FENCE`. It signals when the
149 /// buffers currently on screen — frame N-1 — leave it, so it is stamped
150 /// onto those and rides down to be handed back next rotation. A source that
151 /// wants it can then wait GPU-side instead of blocking the CPU until the
152 /// later release edge.
153 ///
154 /// `wants_fence` decides per layer whether to stamp, so a scene with no
155 /// interested source pays nothing.
156 pub fn commit_succeeded<F>(
157 &mut self,
158 current: Vec<Acquisition>,
159 release_fence: Option<&SyncFence>,
160 mut wants_fence: F,
161 ) -> Released
162 where
163 F: FnMut(LayerId) -> bool,
164 {
165 if let Some(fence) = release_fence {
166 for acquisition in &mut self.previous {
167 if wants_fence(acquisition.layer)
168 && let Some(fd) = fence.as_fd()
169 && let Ok(duplicate) = SyncFence::import(fd)
170 {
171 acquisition.release_fence = Some(duplicate);
172 }
173 }
174 }
175
176 // Frame N-2 has finished scanning out.
177 let retired = std::mem::take(&mut self.two_ago);
178 // Rotate: N-1 becomes N-2, N becomes N-1.
179 self.two_ago = std::mem::take(&mut self.previous);
180 self.previous = current;
181
182 Released {
183 acquisitions: retired,
184 reason: ReleaseReason::Retired,
185 }
186 }
187
188 /// Return frame N-2's buffers now, ahead of the next commit.
189 ///
190 /// Only sound once the flip of the most recent real commit has completed:
191 /// that flip is what took them off screen. [`FrameLifecycle`] gates it on
192 /// the commit's sequence for that reason; see
193 /// [`LayerScene::release_displaced`](crate::LayerScene::release_displaced).
194 ///
195 /// [`FrameLifecycle`]: crate::FrameLifecycle
196 pub fn release_displaced(&mut self) -> Released {
197 Released {
198 acquisitions: std::mem::take(&mut self.two_ago),
199 reason: ReleaseReason::Retired,
200 }
201 }
202
203 /// Record a **failed** commit and return this frame's buffers at once.
204 ///
205 /// **Invariant 2.** No flip is in flight to gate them on, so deferring
206 /// would just stall the source's ring. Self-healing producer rings depend
207 /// on reusing their buffers immediately after a rejection; the dropped
208 /// frame heals on the next one.
209 ///
210 /// The in-flight generations are **not** disturbed: the previous commit's
211 /// buffers are still on screen, and a failed commit did not displace them.
212 pub fn commit_failed(&mut self, current: Vec<Acquisition>) -> Released {
213 Released {
214 acquisitions: current,
215 reason: ReleaseReason::CommitFailed,
216 }
217 }
218
219 /// Record a **test** commit and return its buffers at once.
220 ///
221 /// A `TEST_ONLY` applies nothing to hardware, so nothing in the scanout
222 /// pipeline references these buffers and deferring would only stall the
223 /// ring. Distinguished from [`commit_failed`](Self::commit_failed) because
224 /// the caller reports them differently, not because the buffers are handled
225 /// differently.
226 pub fn test_committed(&mut self, current: Vec<Acquisition>) -> Released {
227 Released {
228 acquisitions: current,
229 reason: ReleaseReason::TestOnly,
230 }
231 }
232
233 /// Return **everything** held, oldest first.
234 ///
235 /// **Invariant 5's counterpart.** For teardown, session pause, and rebind —
236 /// paths where no flip is in flight, so holding buffers back from their
237 /// sources would simply leak them.
238 ///
239 /// This does not wait on the kernel and must not: the scene's drop stays
240 /// non-blocking, and landing an armed flip first is the caller's job. See
241 /// [`ScenePendingFlip`].
242 pub fn drain(&mut self) -> Released {
243 let mut acquisitions = std::mem::take(&mut self.two_ago);
244 acquisitions.append(&mut self.previous);
245 Released {
246 acquisitions,
247 reason: ReleaseReason::Drained,
248 }
249 }
250}
251
252/// Tracks whether a commit armed a page-flip event that has not been
253/// dispatched.
254///
255/// # Invariant 5: teardown does not wait on the kernel
256///
257/// Dropping a scene releases its buffers and framebuffers **without** waiting.
258/// If the last real commit armed `DRM_MODE_PAGE_FLIP_EVENT` and that event has
259/// not been dispatched, the flip still references a framebuffer the drop tears
260/// down — the RmFB-on-in-flight-FB hazard. The caller lands it first, by
261/// draining or by dispatching the event itself.
262///
263/// The C++ can only document that. Here the scene's `Drop` consults this and
264/// warns when a flip is still armed, which is observability the C++ cannot
265/// cheaply add — it turns a hazard that shows up as a rare tear into something
266/// loud in development.
267#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
268pub struct ScenePendingFlip {
269 armed: bool,
270}
271
272impl ScenePendingFlip {
273 /// Nothing armed.
274 #[must_use]
275 pub const fn new() -> Self {
276 Self { armed: false }
277 }
278
279 /// Record that a commit armed a page-flip event.
280 pub const fn arm(&mut self) {
281 self.armed = true;
282 }
283
284 /// Record that the event was dispatched.
285 pub const fn landed(&mut self) {
286 self.armed = false;
287 }
288
289 /// Whether a flip is still outstanding.
290 ///
291 /// Dropping the scene while this is true is the hazard invariant 5
292 /// describes.
293 #[must_use]
294 pub const fn is_armed(&self) -> bool {
295 self.armed
296 }
297}