Skip to main content

drmkit_scene/
source.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Where a layer's content comes from.
5//!
6//! Port of `src/scene/buffer_source.hpp`.
7//!
8//! # Design invariants
9//!
10//! Carried from the C++, because the scene depends on all of them:
11//!
12//! - [`SourceFormat`] describes what the buffer **is** — format, modifier,
13//!   intrinsic size. It belongs to the source; the scene never mutates it.
14//!   How the buffer is *displayed* — source and destination rectangles,
15//!   rotation, alpha, stacking — lives on the layer instead, because those are
16//!   plane properties in KMS, not properties of a buffer. Keeping the two apart
17//!   mirrors the KMS boundary exactly and makes it natural to show one buffer
18//!   several ways.
19//!
20//! - [`release`](LayerBufferSource::release) is **infallible**. The source owns
21//!   everything reachable through an [`AcquiredBuffer`], so releasing cannot
22//!   fail by construction. A source whose release path genuinely can fail — a
23//!   remote producer, a network-backed surface — must log and leak rather than
24//!   propagate: the scene's cleanup paths cannot observe a release failure and
25//!   still be simple enough to be correct.
26//!
27//! - [`bind_to_plane`](LayerBufferSource::bind_to_plane) and
28//!   [`unbind_from_plane`](LayerBufferSource::unbind_from_plane) are the
29//!   insertion points for [`BindingModel::DriverOwnsBinding`]. The no-op
30//!   defaults are correct for every source that submits its own `FB_ID`.
31
32use drmkit_sync::SyncFence;
33
34/// How the scene and a source divide responsibility for `FB_ID`.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
36#[non_exhaustive]
37pub enum BindingModel {
38    /// The scene writes `FB_ID` into the atomic commit.
39    ///
40    /// Covers dumb buffers, GBM buffer objects, imported DMA-BUFs, V4L2
41    /// capture buffers — everything that becomes a DRM framebuffer.
42    #[default]
43    SceneSubmitsFbId,
44
45    /// The driver binds a producer directly to the plane, and the scene must
46    /// **not** write `FB_ID` for this layer.
47    ///
48    /// EGL Streams and relatives. The hook exists in the minimal cut as
49    /// dormant surface (DR-4) so the scene does not need rewriting when the
50    /// streams tier lands; no source in the minimal cut reports it.
51    DriverOwnsBinding,
52}
53
54/// What a source's buffer intrinsically is.
55///
56/// Width and height are the buffer's own dimensions, not the on-screen
57/// rectangle.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59#[allow(
60    clippy::exhaustive_structs,
61    reason = "built by callers with `..Default::default()`, which a new field does not break"
62)]
63pub struct SourceFormat {
64    /// DRM `FourCC`.
65    pub fourcc: u32,
66    /// Layout modifier.
67    pub modifier: u64,
68    /// Buffer width in pixels.
69    pub width: u32,
70    /// Buffer height in pixels.
71    pub height: u32,
72}
73
74/// A region of a buffer that changed since the slot was last scanned out, in
75/// buffer pixels with a top-left origin.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77#[allow(
78    clippy::exhaustive_structs,
79    reason = "a value defined by its fields; there is nothing to add to it"
80)]
81pub struct DamageRect {
82    /// Left edge.
83    pub x: i32,
84    /// Top edge.
85    pub y: i32,
86    /// Width.
87    pub w: u32,
88    /// Height.
89    pub h: u32,
90}
91
92/// A buffer handed over by [`LayerBufferSource::acquire`], valid until the
93/// matching [`release`](LayerBufferSource::release).
94///
95/// Move-only, because it carries an owned fence. That is load-bearing: it means
96/// the two-deep release queue **cannot** hand a buffer back early by accident —
97/// doing so is a move, and using it afterwards does not compile. Invariant 1's
98/// ownership half is enforced by the type; only its *timing* half needs a test.
99///
100/// Releasing a buffer and then using it is a compile error, which is the whole
101/// point:
102///
103/// ```compile_fail
104/// # use drmkit_scene::AcquiredBuffer;
105/// fn release(_buffer: AcquiredBuffer) {}
106///
107/// let buffer = AcquiredBuffer { fb_id: 1, ..AcquiredBuffer::default() };
108/// release(buffer);
109/// // The buffer is back with its source now; the display engine may already
110/// // have stopped scanning it out. Reading it here would be exactly the
111/// // use-after-release invariant 1 exists to prevent.
112/// assert_eq!(buffer.fb_id, 1);
113/// ```
114///
115/// Ordinary moves are of course fine:
116///
117/// ```
118/// # use drmkit_scene::AcquiredBuffer;
119/// let buffer = AcquiredBuffer { fb_id: 1, ..AcquiredBuffer::default() };
120/// let moved = buffer;
121/// assert_eq!(moved.fb_id, 1);
122/// ```
123#[derive(Debug, Default)]
124#[allow(
125    clippy::exhaustive_structs,
126    reason = "built by callers with `..Default::default()`, which a new field does not break"
127)]
128pub struct AcquiredBuffer {
129    /// The DRM framebuffer the scene will write to the plane's `FB_ID`.
130    ///
131    /// Non-zero for a [`BindingModel::SceneSubmitsFbId`] source. **Zero** for
132    /// [`BindingModel::DriverOwnsBinding`], where the scene skips the write.
133    pub fb_id: u32,
134
135    /// A source-private token echoed back verbatim to `release` — typically a
136    /// slot index in a ring.
137    ///
138    /// The C++ carries a `void*` here. A `u64` covers every in-tree use (all of
139    /// them store an index) without inviting a raw pointer into a type that
140    /// crosses the source boundary.
141    pub token: u64,
142
143    /// An optional render-done fence: the pixels are valid only once it
144    /// signals.
145    ///
146    /// Set it when the producer renders asynchronously rather than CPU-blocking
147    /// before returning. The scene takes ownership: where the assigned plane
148    /// advertises `IN_FENCE_FD` it hands the descriptor to KMS so the kernel
149    /// waits before scanout; otherwise it waits on the CPU before committing.
150    /// Either way the fence rides this buffer's lifecycle and closes on release
151    /// or drop — the kernel does not take ownership of `IN_FENCE_FD`.
152    ///
153    /// Sources that produce ready buffers synchronously leave this `None`.
154    pub acquire_fence: Option<SyncFence>,
155
156    /// Per-frame dirty regions.
157    ///
158    /// **Empty means full-frame** — the scene emits no `FB_DAMAGE_CLIPS` and
159    /// assumes everything changed, which is correct but not power-optimal. A
160    /// non-empty list names the changed regions; the scene clamps the count and
161    /// falls back to full-frame above its bound, and omits the blob entirely on
162    /// planes or drivers without the property.
163    pub damage: Vec<DamageRect>,
164}
165
166/// Why an [`acquire`](LayerBufferSource::acquire) did not produce a buffer.
167#[derive(Debug, thiserror::Error)]
168#[non_exhaustive]
169pub enum SourceError {
170    /// **Flow control, not a failure.** The source has no frame to contribute
171    /// this vblank.
172    ///
173    /// The scene skips the layer for this commit and calls `acquire` again on
174    /// the next one; the layer is counted in
175    /// [`layers_skipped_no_frame`](crate::CommitReport::layers_skipped_no_frame),
176    /// never in the failure paths. A live source returns this before its first
177    /// sample lands, and any time the producer falls behind with no cached
178    /// frame to re-issue.
179    ///
180    /// This is DR-6: a distinct variant, never
181    /// [`io::ErrorKind::WouldBlock`](std::io::ErrorKind::WouldBlock). Squinting
182    /// at an I/O error to recover flow control is exactly how the EAGAIN
183    /// contract gets lost in a refactor — and losing it turns a stalled
184    /// producer into an aborted commit.
185    #[error("no frame available this vblank")]
186    WouldBlock,
187
188    /// The operation is not supported by this source.
189    ///
190    /// Returned by the default [`map`](LayerBufferSource::map) and
191    /// [`export_dma_buf`](LayerBufferSource::export_dma_buf). A source that
192    /// supports neither is **uncompositable**: when the allocator cannot place
193    /// it, the scene has no way to rescue it and the layer stays dropped for
194    /// the frame.
195    #[error("not supported by this source")]
196    Unsupported,
197
198    /// A real failure. Aborts the commit.
199    #[error("source failed: {0}")]
200    Failed(#[from] rustix::io::Errno),
201}
202
203/// A borrowed view of a buffer's DMA-BUF planes, for a consumer that imports
204/// the buffer directly rather than reading CPU pixels.
205///
206/// The descriptors are **borrowed**: they stay owned by the source and are
207/// valid only until the matching release. The consumer must not close them — an
208/// image import references them internally, so no duplication is needed.
209#[derive(Debug, Clone)]
210#[allow(
211    clippy::exhaustive_structs,
212    reason = "built by callers, with no default to fill a new field from"
213)]
214pub struct DmaBufDesc<'a> {
215    /// One descriptor per plane. A single-descriptor multi-plane format such as
216    /// `NV12` repeats the same descriptor with distinct offsets.
217    pub fds: Vec<std::os::fd::BorrowedFd<'a>>,
218    /// Byte offset of each plane.
219    pub offsets: Vec<u32>,
220    /// Row stride of each plane.
221    pub pitches: Vec<u32>,
222    /// Shape, matching [`LayerBufferSource::format`].
223    pub format: SourceFormat,
224}
225
226/// Where a layer's content comes from.
227///
228/// Owning a source is owning the DRM handles it holds: dropping a layer drops
229/// its source and the buffers with it.
230pub trait LayerBufferSource {
231    /// Produce this frame's buffer.
232    ///
233    /// On success the source has arranged that `fb_id` names a ready-to-scan-out
234    /// framebuffer. The scene pairs the result with a later
235    /// [`release`](Self::release) once the commit completes.
236    ///
237    /// # Errors
238    ///
239    /// [`SourceError::WouldBlock`] is **flow control** and expected — see that
240    /// variant. Any other error aborts the commit.
241    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError>;
242
243    /// Return a buffer to the free pool.
244    ///
245    /// Infallible by contract. The scene calls this after page-flip completion,
246    /// on commit failure, and during shutdown; there is deliberately no error
247    /// channel, so those cleanup paths stay simple enough to be correct.
248    fn release(&mut self, acquired: AcquiredBuffer);
249
250    /// Release carrying the `OUT_FENCE` of the commit that displaced this
251    /// buffer.
252    ///
253    /// Once it signals, the buffer is off-screen and safe to render into again,
254    /// so a GPU producer can wait on it GPU-side instead of blocking the CPU
255    /// until the later release edge. `None` when the CRTC has no
256    /// `OUT_FENCE_PTR` or the source did not opt in.
257    ///
258    /// The default forwards to [`release`](Self::release), so only a source
259    /// that wants the fence overrides it — see
260    /// [`wants_release_fence`](Self::wants_release_fence).
261    fn release_with_fence(&mut self, acquired: AcquiredBuffer, release_fence: Option<SyncFence>) {
262        let _ = release_fence;
263        self.release(acquired);
264    }
265
266    /// Whether this source wants the per-buffer release fence.
267    ///
268    /// The scene only requests an internal `OUT_FENCE` when some live source
269    /// says yes, so this costs nothing otherwise.
270    fn wants_release_fence(&self) -> bool {
271        false
272    }
273
274    /// Whether a producer submitted something new since the last commit.
275    ///
276    /// Drives the scene's whole-commit skip: when every live source says no and
277    /// no layer is dirty, no atomic commit is issued at all — a power win, and
278    /// what lets a self-refresh panel stay in self-refresh.
279    ///
280    /// **The default is `true`, deliberately.** A source that cannot tell
281    /// whether its buffer changed — a CPU producer painting into a dumb buffer
282    /// the scene cannot introspect — must say "changed", or a real update is
283    /// silently skipped. Only a source that *knows* it is idle returns false.
284    fn has_fresh_content(&self) -> bool {
285        true
286    }
287
288    /// The scene acquires from this source no more: its layer was removed.
289    ///
290    /// Its in-flight buffers still come back through [`release`](Self::release)
291    /// or [`release_with_fence`](Self::release_with_fence), after the commits
292    /// that displace them. A source that holds its on-screen buffer back from
293    /// release until a newer frame supersedes it -- `ExternalDmaBufRing`,
294    /// `ExternalDmaBufPool` -- must let that one go with them: no newer frame
295    /// is coming, and the producer would never get the buffer back. A client
296    /// that makes a new pool per geometry would lose one buffer to every
297    /// resize until it ran out and stalled.
298    ///
299    /// The default does nothing.
300    fn on_retired(&mut self) {}
301
302    /// Which binding contract this source participates in.
303    fn binding_model(&self) -> BindingModel {
304        BindingModel::SceneSubmitsFbId
305    }
306
307    /// The buffer's shape, read during layer lowering to populate the
308    /// properties the allocator consults.
309    fn format(&self) -> SourceFormat;
310
311    /// Acquire a CPU view of the pixel storage.
312    ///
313    /// Composition fallback reads through this to rescue layers the allocator
314    /// could not place. A source whose pixels live only in GPU memory returns
315    /// [`SourceError::Unsupported`] — and is then uncompositable unless it can
316    /// [`export_dma_buf`](Self::export_dma_buf) instead.
317    ///
318    /// # Errors
319    ///
320    /// [`SourceError::Unsupported`] by default.
321    fn map(
322        &mut self,
323        access: drmkit_dumb::MapAccess,
324    ) -> Result<drmkit_dumb::Mapping<'_>, SourceError> {
325        let _ = access;
326        Err(SourceError::Unsupported)
327    }
328
329    /// Export the currently-acquired buffer's DMA-BUF planes.
330    ///
331    /// The escape from the map-uncompositable trap: a source whose pixels never
332    /// reach CPU memory — a V4L2 capture buffer — can still be composited by
333    /// importing its DMA-BUF, so the layer no longer blanks when the allocator
334    /// cannot place it.
335    ///
336    /// # Errors
337    ///
338    /// [`SourceError::Unsupported`] by default.
339    fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
340        Err(SourceError::Unsupported)
341    }
342
343    /// Bind a driver-owned producer to a plane.
344    ///
345    /// Called after the allocator settles on a plane and before the first
346    /// acquire for it. The no-op default is correct for every
347    /// [`BindingModel::SceneSubmitsFbId`] source.
348    ///
349    /// # Errors
350    ///
351    /// Never, by default.
352    fn bind_to_plane(&mut self, plane_id: u32) -> Result<(), SourceError> {
353        let _ = plane_id;
354        Ok(())
355    }
356
357    /// Unbind from a plane, when the layer is reassigned or retired.
358    fn unbind_from_plane(&mut self, plane_id: u32) {
359        let _ = plane_id;
360    }
361
362    /// The session is losing DRM master; issue no ioctls until resumed.
363    fn on_session_paused(&mut self) {}
364
365    /// The session is back with a fresh device.
366    ///
367    /// A source holding descriptor-bound state must drop every handle tied to
368    /// the dead descriptor — ioctls against it will fail — and re-allocate
369    /// equivalent buffers. Dimensions and formats are preserved: callers rely
370    /// on [`format`](Self::format) returning the same value afterwards.
371    ///
372    /// # Errors
373    ///
374    /// Whatever re-allocation failed with.
375    fn on_session_resumed(&mut self, device: &drmkit_core::Device) -> Result<(), SourceError> {
376        let _ = device;
377        Ok(())
378    }
379}