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 as dormant surface (DR-4) so
49    /// the scene does not need rewriting when the streams tier lands; no
50    /// in-tree source reports it yet.
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 falls back to
161    /// full-frame above 16 rectangles, clips each to the buffer and drops any
162    /// left empty, and omits the blob entirely on planes or drivers without the
163    /// property.
164    pub damage: Vec<DamageRect>,
165}
166
167/// Why an [`acquire`](LayerBufferSource::acquire) did not produce a buffer.
168#[derive(Debug, thiserror::Error)]
169#[non_exhaustive]
170pub enum SourceError {
171    /// **Flow control, not a failure.** The source has no frame to contribute
172    /// this vblank.
173    ///
174    /// The layer keeps its plane and goes on showing its previous framebuffer
175    /// -- or is left out, if it has never produced one -- and `acquire` is
176    /// called again on the next commit; the layer is counted in
177    /// [`layers_skipped_no_frame`](crate::CommitReport::layers_skipped_no_frame),
178    /// never in the failure paths. A live source returns this before its first
179    /// sample lands, and any time the producer falls behind with no cached
180    /// frame to re-issue.
181    ///
182    /// This is DR-6: a distinct variant, never
183    /// [`io::ErrorKind::WouldBlock`](std::io::ErrorKind::WouldBlock). Squinting
184    /// at an I/O error to recover flow control is exactly how the EAGAIN
185    /// contract gets lost in a refactor — and losing it turns a stalled
186    /// producer into an aborted commit.
187    #[error("no frame available this vblank")]
188    WouldBlock,
189
190    /// The operation is not supported by this source.
191    ///
192    /// Returned by the default [`map`](LayerBufferSource::map) and
193    /// [`export_dma_buf`](LayerBufferSource::export_dma_buf). A source that
194    /// does not support `map` is **uncompositable**: when the allocator cannot place
195    /// it, the scene has no way to rescue it and the layer stays dropped for
196    /// the frame.
197    #[error("not supported by this source")]
198    Unsupported,
199
200    /// A real failure. Aborts the commit.
201    #[error("source failed: {0}")]
202    Failed(#[from] rustix::io::Errno),
203}
204
205/// A borrowed view of a buffer's DMA-BUF planes, for a consumer that imports
206/// the buffer directly rather than reading CPU pixels.
207///
208/// The descriptors are **borrowed**: they stay owned by the source and are
209/// valid only until the matching release. The consumer must not close them — an
210/// image import references them internally, so no duplication is needed.
211#[derive(Debug, Clone)]
212#[allow(
213    clippy::exhaustive_structs,
214    reason = "built by callers, with no default to fill a new field from"
215)]
216pub struct DmaBufDesc<'a> {
217    /// One descriptor per plane. A single-descriptor multi-plane format such as
218    /// `NV12` repeats the same descriptor with distinct offsets.
219    pub fds: Vec<std::os::fd::BorrowedFd<'a>>,
220    /// Byte offset of each plane.
221    pub offsets: Vec<u32>,
222    /// Row stride of each plane.
223    pub pitches: Vec<u32>,
224    /// Shape, matching [`LayerBufferSource::format`].
225    pub format: SourceFormat,
226}
227
228/// Where a layer's content comes from.
229///
230/// Owning a source is owning the DRM handles it holds: dropping a layer drops
231/// its source and the buffers with it.
232pub trait LayerBufferSource {
233    /// Produce this frame's buffer.
234    ///
235    /// On success the source has arranged that `fb_id` names a ready-to-scan-out
236    /// framebuffer. The scene pairs the result with a later
237    /// [`release`](Self::release) once the commit completes.
238    ///
239    /// # Errors
240    ///
241    /// [`SourceError::WouldBlock`] is **flow control** and expected — see that
242    /// variant. Any other error aborts the commit.
243    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError>;
244
245    /// Return a buffer to the free pool.
246    ///
247    /// Infallible by contract. The scene calls this after page-flip completion,
248    /// on commit failure, and during shutdown; there is deliberately no error
249    /// channel, so those cleanup paths stay simple enough to be correct.
250    fn release(&mut self, acquired: AcquiredBuffer);
251
252    /// Release carrying the `OUT_FENCE` of the commit that displaced this
253    /// buffer.
254    ///
255    /// Once it signals, the buffer is off-screen and safe to render into again,
256    /// so a GPU producer can wait on it GPU-side instead of blocking the CPU
257    /// until the later release edge. `None` when the CRTC has no
258    /// `OUT_FENCE_PTR` or the source did not opt in.
259    ///
260    /// The default forwards to [`release`](Self::release), so only a source
261    /// that wants the fence overrides it — see
262    /// [`wants_release_fence`](Self::wants_release_fence).
263    fn release_with_fence(&mut self, acquired: AcquiredBuffer, release_fence: Option<SyncFence>) {
264        let _ = release_fence;
265        self.release(acquired);
266    }
267
268    /// Whether this source wants the per-buffer release fence.
269    ///
270    /// The scene only requests an internal `OUT_FENCE` when some live source
271    /// says yes, so this costs nothing otherwise.
272    fn wants_release_fence(&self) -> bool {
273        false
274    }
275
276    /// Whether a producer submitted something new since the last commit.
277    ///
278    /// Drives the scene's whole-commit skip: when every live source says no and
279    /// no layer is dirty, no atomic commit is issued at all — a power win, and
280    /// what lets a self-refresh panel stay in self-refresh.
281    ///
282    /// **The default is `true`, deliberately.** A source that cannot tell
283    /// whether its buffer changed — a CPU producer painting into a dumb buffer
284    /// the scene cannot introspect — must say "changed", or a real update is
285    /// silently skipped. Only a source that *knows* it is idle returns false.
286    fn has_fresh_content(&self) -> bool {
287        true
288    }
289
290    /// The scene acquires from this source no more: its layer was removed.
291    ///
292    /// Its in-flight buffers still come back through [`release`](Self::release)
293    /// or [`release_with_fence`](Self::release_with_fence), after the commits
294    /// that displace them. A source that holds its on-screen buffer back from
295    /// release until a newer frame supersedes it -- `ExternalDmaBufRing`,
296    /// `ExternalDmaBufPool` -- must let that one go with them: no newer frame
297    /// is coming, and the producer would never get the buffer back. A client
298    /// that makes a new pool per geometry would lose one buffer to every
299    /// resize until it ran out and stalled.
300    ///
301    /// The default does nothing.
302    fn on_retired(&mut self) {}
303
304    /// Which binding contract this source participates in.
305    fn binding_model(&self) -> BindingModel {
306        BindingModel::SceneSubmitsFbId
307    }
308
309    /// The buffer's shape, read during layer lowering to populate the
310    /// properties the allocator consults.
311    fn format(&self) -> SourceFormat;
312
313    /// Acquire a CPU view of the pixel storage.
314    ///
315    /// Composition fallback reads through this, by way of
316    /// [`with_mapping`](Self::with_mapping), to rescue layers the allocator
317    /// could not place. A source whose pixels live only in GPU memory returns
318    /// [`SourceError::Unsupported`] — and is then uncompositable: the scene's
319    /// CPU composition reads only through these.
320    ///
321    /// # Errors
322    ///
323    /// [`SourceError::Unsupported`] by default.
324    fn map(
325        &mut self,
326        access: drmkit_dumb::MapAccess,
327    ) -> Result<drmkit_dumb::Mapping<'_>, SourceError> {
328        let _ = access;
329        Err(SourceError::Unsupported)
330    }
331
332    /// Run `f` over a CPU view of the pixel storage, writable when `access`
333    /// allows it.
334    ///
335    /// What the scene's composition reads through. The default maps with
336    /// [`map`](Self::map) and hands that view to `f`, so a source that
337    /// implements `map` needs nothing here. A source that can lend its pixels
338    /// only for a bounded time -- one shared behind a `RefCell`, whose borrow
339    /// cannot outlive a call -- overrides this instead, and holds whatever it
340    /// must for exactly as long as `f` runs.
341    ///
342    /// # Errors
343    ///
344    /// Whatever [`map`](Self::map) fails with; `f` is not called then.
345    fn with_mapping(
346        &mut self,
347        access: drmkit_dumb::MapAccess,
348        f: &mut dyn FnMut(&mut drmkit_dumb::Mapping<'_>),
349    ) -> Result<(), SourceError> {
350        let mut mapping = self.map(access)?;
351        f(&mut mapping);
352        Ok(())
353    }
354
355    /// Export the currently-acquired buffer's DMA-BUF planes.
356    ///
357    /// For a consumer that imports DMA-BUFs: a source whose pixels never reach
358    /// CPU memory — a V4L2 capture buffer — can be handed on this way. Upstream
359    /// composites such a layer on the GPU; that compositor is not ported yet,
360    /// and the scene's CPU composition reads only through
361    /// [`with_mapping`](Self::with_mapping).
362    ///
363    /// # Errors
364    ///
365    /// [`SourceError::Unsupported`] by default.
366    fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
367        Err(SourceError::Unsupported)
368    }
369
370    /// Bind a driver-owned producer to a plane.
371    ///
372    /// For a [`BindingModel::DriverOwnsBinding`] source, after the allocator
373    /// settles on a plane and before the first acquire for it. Dormant like
374    /// that variant: nothing in the scene calls it yet. The no-op default is correct for every
375    /// [`BindingModel::SceneSubmitsFbId`] source.
376    ///
377    /// # Errors
378    ///
379    /// Never, by default.
380    fn bind_to_plane(&mut self, plane_id: u32) -> Result<(), SourceError> {
381        let _ = plane_id;
382        Ok(())
383    }
384
385    /// Unbind from a plane, when the layer is reassigned or retired. Dormant,
386    /// as [`bind_to_plane`](Self::bind_to_plane) is.
387    fn unbind_from_plane(&mut self, plane_id: u32) {
388        let _ = plane_id;
389    }
390
391    /// The session is losing DRM master; issue no ioctls until resumed.
392    fn on_session_paused(&mut self) {}
393
394    /// The session is back with a fresh device.
395    ///
396    /// A source holding descriptor-bound state must drop every handle tied to
397    /// the dead descriptor — ioctls against it will fail — and re-allocate
398    /// equivalent buffers. Dimensions and formats are preserved: callers rely
399    /// on [`format`](Self::format) returning the same value afterwards.
400    ///
401    /// # Errors
402    ///
403    /// Whatever re-allocation failed with.
404    fn on_session_resumed(&mut self, device: &drmkit_core::Device) -> Result<(), SourceError> {
405        let _ = device;
406        Ok(())
407    }
408}