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}