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}