Skip to main content

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}