Skip to main content

drmkit_present/
dumb_sink.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Putting a finished CPU frame on screen, repeatedly.
5
6use std::cell::RefCell;
7use std::rc::Rc;
8
9use drmkit_core::{AtomicCommitFlags, AtomicRequest, Device, Mode};
10use drmkit_modeset::ModeInfo as _;
11use drmkit_planes::PlaneRegistry;
12use drmkit_scene::{
13    AcquiredBuffer, BindingModel, CommitKind, CommitReport, DamageRect, DeviceCommitter,
14    DisplayParams, KernelResult, LayerBufferSource, LayerHandle, LayerScene, Modeset,
15    PlanePropertyMap, SourceError, SourceFormat, arm_acquire_fences, emit_frame_damaged,
16};
17
18use crate::{DumbRingSource, PaintError, Rect};
19
20/// What went wrong presenting a frame.
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum PresentError {
24    /// The source buffer is shorter than `height * stride` says it is.
25    #[error("a {height}-row frame at stride {stride} needs {needed} bytes, got {got}")]
26    ShortFrame {
27        /// Rows the sink expects.
28        height: u32,
29        /// Bytes per row the caller declared.
30        stride: u32,
31        /// Bytes that implies.
32        needed: usize,
33        /// Bytes actually supplied.
34        got: usize,
35    },
36
37    /// Every ring slot is busy. Retry once a flip has completed.
38    #[error("every ring slot is busy")]
39    WouldBlock,
40
41    /// Painting the frame into a slot failed.
42    #[error("painting: {0}")]
43    Paint(PaintError),
44
45    /// The scene could not build this frame.
46    #[error("building the frame: {0}")]
47    Build(#[from] drmkit_scene::SceneError),
48
49    /// A property write or the commit itself was refused.
50    #[error("committing: {0}")]
51    Commit(#[from] drmkit_core::CoreError),
52
53    /// Setting the scene up against this output failed.
54    #[error("binding the output: {0}")]
55    Bind(String),
56}
57
58impl From<PaintError> for PresentError {
59    fn from(error: PaintError) -> Self {
60        match error {
61            PaintError::WouldBlock => Self::WouldBlock,
62            other => Self::Paint(other),
63        }
64    }
65}
66
67/// The ring, shared between the scene that owns the layer and the sink that
68/// paints into it.
69///
70/// The reference keeps a raw `DumbRingSource*` alongside the scene that owns
71/// it. There is no borrow that can express that here -- the scene owns the
72/// source and the sink outlives neither -- so the ring is shared and the layer
73/// holds a handle to it. `RefCell` rather than a lock because the whole spine
74/// is single-threaded by contract, and a lock would suggest otherwise.
75#[derive(Clone)]
76struct SharedRing(Rc<RefCell<DumbRingSource>>);
77
78impl LayerBufferSource for SharedRing {
79    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
80        self.0.borrow_mut().acquire()
81    }
82
83    fn release(&mut self, acquired: AcquiredBuffer) {
84        self.0.borrow_mut().release(acquired);
85    }
86
87    fn binding_model(&self) -> BindingModel {
88        self.0.borrow().binding_model()
89    }
90
91    fn format(&self) -> SourceFormat {
92        self.0.borrow().format()
93    }
94
95    fn on_session_paused(&mut self) {
96        self.0.borrow_mut().on_session_paused();
97    }
98
99    fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
100        self.0.borrow_mut().on_session_resumed(device)
101    }
102}
103
104/// How a sink is configured.
105#[derive(Debug, Clone, Copy)]
106#[allow(
107    clippy::exhaustive_structs,
108    reason = "built by callers with `..Default::default()`, which a new field does not break"
109)]
110pub struct Config {
111    /// The format to scan out, or `None` to negotiate one the plane takes,
112    /// falling back to `XRGB8888` when none of the defaults match or the planes
113    /// cannot be queried.
114    pub format: Option<u32>,
115    /// Ring depth. Three is what the buffer-age path wants: one scanning, one
116    /// in flight, one free to paint into. `0` means three.
117    pub buffers: usize,
118}
119
120impl Default for Config {
121    fn default() -> Self {
122        Self {
123            format: None,
124            buffers: 3,
125        }
126    }
127}
128
129/// A software present path: hand it a finished CPU frame, it puts it on screen.
130///
131/// For callers that render a whole frame on the CPU — a software rasterizer, a
132/// plotting library, an instrument panel — and want none of GL, Vulkan or GBM.
133/// It bundles a single-layer scene over a [`DumbRingSource`], so it inherits
134/// the plane allocation, damage and release discipline rather than re-rolling
135/// a modeset and page-flip loop.
136///
137/// # What it does not do
138///
139/// It does not dispatch the page-flip event. The reference's `present()` takes
140/// a `PageFlip*` and the caller dispatches it; here the split is the same, and
141/// visible: [`present`](Self::present) commits, the caller dispatches its own
142/// [`PageFlip`](drmkit_modeset::PageFlip), and then calls
143/// [`flip_landed`](Self::flip_landed).
144///
145/// That is one more call than the C++ needs, and it is the honest shape. A
146/// present loop has other descriptors to poll — input, a timer, a socket — and
147/// a sink that blocked on vblank inside `present` would own the loop rather
148/// than serve it.
149///
150/// # Teardown
151///
152/// Dropping the sink while a flip it armed is still outstanding waits, up to
153/// 100 ms, for the CRTC's vblank sequence to move past that commit or for its
154/// flip event to be queued, before the scene lets go of its buffers. On a CRTC
155/// with no vblank counter the queued event is the only thing it waits on. It
156/// never reads the event queue, so the event stays queued for the caller.
157/// Dispatching it and calling [`flip_landed`](Self::flip_landed) first means
158/// there is nothing to wait for. The device must outlive the sink: the wait
159/// polls it.
160pub struct DumbScanoutSink {
161    scene: LayerScene,
162    ring: Rc<RefCell<DumbRingSource>>,
163    registry: PlaneRegistry,
164    map: PlanePropertyMap,
165    crtc_id: u32,
166    crtc_index: u32,
167    connector_id: u32,
168    mode: Mode,
169    width: u32,
170    height: u32,
171    format: u32,
172    /// Cleared after the first successful commit: the mode is set once, and
173    /// `ALLOW_MODESET` on every frame would hide a mistake that silently
174    /// re-modesets — which is a full-frame stall.
175    needs_modeset: bool,
176    layer: LayerHandle,
177    /// The descriptor teardown's flip wait reads the vblank sequence on. Held
178    /// raw because `Drop` has no device argument -- the ring's dumb buffers
179    /// hold it the same way, for the same reason.
180    device_fd: std::os::fd::RawFd,
181    /// What teardown waits on for the last commit's flip: the CRTC's vblank
182    /// sequence right after it, or only its event where the CRTC has no
183    /// counter. `None` when that commit armed no flip.
184    armed_flip_seq: Option<crate::armed_flip::ArmedFlip>,
185}
186
187/// How many bytes a `height`-row frame at `stride` occupies.
188///
189/// `None` when that product does not fit a `usize`, which only a 32-bit host
190/// can reach. Returning `None` rather than a wrapped product matters: the
191/// wrapped value would be small, the caller's length check would pass, and
192/// the row copy would then index past the frame it was handed.
193pub(crate) fn frame_span(height: u32, stride: u32) -> Option<usize> {
194    usize::try_from(height)
195        .ok()?
196        .checked_mul(usize::try_from(stride).ok()?)
197}
198
199impl DumbScanoutSink {
200    /// Build a sink over an output that has already been chosen.
201    ///
202    /// `crtc_index` is the CRTC's position in the device's resource list, which
203    /// is what the plane registry indexes by — not the same number as
204    /// `crtc_id`, and mixing them up gives a registry view of the wrong pipe.
205    ///
206    /// # Errors
207    ///
208    /// [`PresentError::Bind`] if the planes cannot be probed or the layer
209    /// cannot be added; [`PresentError::Paint`] if the format has no known
210    /// bits-per-pixel or the mode has a zero width or height.
211    pub fn create(
212        device: &Device,
213        crtc_id: u32,
214        crtc_index: u32,
215        connector_id: u32,
216        mode: &Mode,
217        config: &Config,
218    ) -> Result<Self, PresentError> {
219        let format = config.format.unwrap_or_else(|| {
220            // Fall back to XRGB8888 when the planes cannot be queried: it is
221            // what nearly every controller takes, and the commit will say so
222            // if this one does not.
223            crate::negotiate_scanout_format(device, crtc_id, &[])
224                .unwrap_or(drmkit_fmt::fourcc::XRGB8888)
225        });
226        let (width, height) = (mode.width(), mode.height());
227        let buffers = if config.buffers == 0 {
228            3
229        } else {
230            config.buffers
231        };
232
233        let ring = Rc::new(RefCell::new(DumbRingSource::new(
234            width, height, format, buffers,
235        )?));
236
237        let registry = PlaneRegistry::probe(device)
238            .map_err(|error| PresentError::Bind(format!("probing planes: {error}")))?;
239        let mut map = PlanePropertyMap::new();
240        for plane in registry.for_crtc(crtc_index) {
241            map.learn_plane(device, plane.id)
242                .map_err(|error| PresentError::Bind(format!("plane {}: {error}", plane.id)))?;
243        }
244
245        let mut scene = LayerScene::new(crtc_id);
246        let layer = scene.add_layer(Box::new(SharedRing(Rc::clone(&ring))));
247        scene
248            .layer_mut(layer)
249            .ok_or_else(|| PresentError::Bind("the layer vanished".to_owned()))?
250            .set_display(DisplayParams {
251                src_rect: drmkit_planes::Rect {
252                    x: 0,
253                    y: 0,
254                    w: width,
255                    h: height,
256                },
257                dst_rect: drmkit_planes::Rect {
258                    x: 0,
259                    y: 0,
260                    w: width,
261                    h: height,
262                },
263                ..DisplayParams::default()
264            });
265
266        Ok(Self {
267            scene,
268            ring,
269            registry,
270            map,
271            crtc_id,
272            crtc_index,
273            connector_id,
274            mode: *mode,
275            width,
276            height,
277            format,
278            needs_modeset: true,
279            layer,
280            device_fd: device.raw_fd(),
281            armed_flip_seq: None,
282        })
283    }
284
285    /// Copy a finished CPU frame in and commit one flip.
286    ///
287    /// `src` is the whole frame in [`format`](Self::format) at `src_stride`
288    /// bytes per row. `damage` is what changed since the previous present, in
289    /// destination pixels; empty means the whole frame.
290    ///
291    /// The frame is always copied in full regardless of `damage`, because a
292    /// reused ring slot holds an older frame and copying only the damage would
293    /// leave the rest of it stale. `damage` is what gets *reported*: it becomes
294    /// this commit's `FB_DAMAGE_CLIPS` as given, as [`DumbRingSource`] reports
295    /// whatever its paint callback touched.
296    ///
297    /// Under-reporting damage leaves stale pixels on screen. It is a hint to
298    /// the driver, not a description of the copy.
299    ///
300    /// # Errors
301    ///
302    /// [`PresentError::WouldBlock`] when every ring slot is busy, which is flow
303    /// control rather than failure. [`PresentError::ShortFrame`] when `src` is
304    /// smaller than `height * src_stride`. Otherwise whatever the build or
305    /// commit refused.
306    pub fn present(
307        &mut self,
308        device: &Device,
309        src: &[u8],
310        src_stride: u32,
311        damage: &[DamageRect],
312        flags: AtomicCommitFlags,
313    ) -> Result<CommitReport, PresentError> {
314        let needed = frame_span(self.height, src_stride);
315        if needed.is_none_or(|needed| src.len() < needed) {
316            return Err(PresentError::ShortFrame {
317                height: self.height,
318                stride: src_stride,
319                needed: needed.unwrap_or(usize::MAX),
320                got: src.len(),
321            });
322        }
323
324        let (width, height) = (self.width, self.height);
325        self.ring.borrow_mut().paint(device, |mapping, _repaint| {
326            for y in 0..height {
327                let Some(row) = mapping.row_mut(y) else {
328                    break;
329                };
330                let from = y as usize * src_stride as usize;
331                let bytes = row.len().min(src_stride as usize);
332                row[..bytes].copy_from_slice(&src[from..from + bytes]);
333            }
334            if damage.is_empty() {
335                vec![Rect {
336                    x: 0,
337                    y: 0,
338                    width,
339                    height,
340                }]
341            } else {
342                damage
343                    .iter()
344                    .map(|d| Rect {
345                        x: d.x,
346                        y: d.y,
347                        width: d.w,
348                        height: d.h,
349                    })
350                    .collect()
351            }
352        })?;
353
354        self.commit(device, flags)
355    }
356
357    /// Build, emit and commit one frame from what the ring is holding.
358    fn commit(
359        &mut self,
360        device: &Device,
361        flags: AtomicCommitFlags,
362    ) -> Result<CommitReport, PresentError> {
363        // The modeset is a local, not a field: it holds a `PropertyBlob`
364        // borrowed from the device, and storing one would put that lifetime on
365        // this type and into everything holding it. It is needed once.
366        let modeset = if self.needs_modeset {
367            Some(
368                Modeset::learn(device, self.crtc_id, self.connector_id, &self.mode)
369                    .map_err(|error| PresentError::Bind(format!("learning the mode: {error}")))?,
370            )
371        } else {
372            None
373        };
374
375        let mut flags = flags;
376        if modeset.is_some() {
377            flags |= AtomicCommitFlags::ALLOW_MODESET;
378        }
379
380        let mut committer = DeviceCommitter::new(
381            device,
382            &self.map,
383            &self.registry,
384            self.crtc_index,
385            AtomicCommitFlags::empty(),
386            modeset.as_ref(),
387        );
388
389        // A flip is armed only if this commit actually asked for the event.
390        // Saying otherwise leaves the scene waiting for a completion that will
391        // never arrive: it holds every acquisition open, and teardown then
392        // trips invariant 5 over a flip that was never in flight.
393        let arms_flip = flags.contains(AtomicCommitFlags::PAGE_FLIP_EVENT);
394        let mut build = self.scene.build_frame(
395            &self.registry,
396            self.crtc_index,
397            CommitKind::Real { arms_flip },
398            &mut committer,
399        )?;
400
401        let mut request = AtomicRequest::with_capacity(64);
402        // The blobs are bound to a local that outlives the commit: the
403        // request holds only their ids, and dropping them first would leave it
404        // pointing at kernel objects that no longer exist.
405        let (_, _damage_blobs) = emit_frame_damaged(
406            &mut request,
407            &self.map,
408            &mut build,
409            modeset.as_ref(),
410            Some(device),
411        )?;
412        arm_acquire_fences(&mut build, &mut request, &self.map, true)?;
413
414        let programmed: Vec<_> = build
415            .plan()
416            .iter()
417            .map(|entry| (entry.plane_id, entry.color))
418            .collect();
419        let result = match request.commit(device, flags) {
420            Ok(()) => KernelResult::Ok,
421            Err(error) => {
422                // The scene still has to be told, or its acquisitions leak and
423                // the next frame builds on a lie.
424                self.scene.finalize_frame(build, KernelResult::Rejected);
425                return Err(PresentError::Commit(error));
426            }
427        };
428        for (plane_id, color) in programmed {
429            self.map.note_color_committed(plane_id, color);
430        }
431        self.needs_modeset = false;
432        self.armed_flip_seq = crate::armed_flip::armed_sequence(device, self.crtc_id, arms_flip);
433        Ok(self.scene.finalize_frame(build, result))
434    }
435
436    /// Tell the scene the flip it armed has landed.
437    ///
438    /// Call it after dispatching the [`PageFlip`](drmkit_modeset::PageFlip)
439    /// the commit's event was routed to. Until it is called the scene believes
440    /// a flip is outstanding, which is what keeps teardown from releasing a
441    /// buffer the display engine is still reading — invariant 5.
442    pub fn flip_landed(&mut self) {
443        self.scene.flip_landed();
444    }
445
446    /// The frame size this sink scans out.
447    #[must_use]
448    pub const fn size(&self) -> (u32, u32) {
449        (self.width, self.height)
450    }
451
452    /// The `FourCC` a caller must render in — the requested format, or the
453    /// negotiated one.
454    #[must_use]
455    pub const fn format(&self) -> u32 {
456        self.format
457    }
458
459    /// The layer the frame is presented on, for a caller that wants to change
460    /// its placement.
461    #[must_use]
462    pub const fn layer(&self) -> LayerHandle {
463        self.layer
464    }
465
466    /// The scene underneath, for teardown and advanced wiring.
467    #[must_use]
468    pub const fn scene(&self) -> &LayerScene {
469        &self.scene
470    }
471
472    /// As [`scene`](Self::scene).
473    pub const fn scene_mut(&mut self) -> &mut LayerScene {
474        &mut self.scene
475    }
476}
477
478impl Drop for DumbScanoutSink {
479    fn drop(&mut self) {
480        // SAFETY: the descriptor belongs to the device this sink was built
481        // against, which outlives it -- the ring's buffers free themselves on
482        // it too. The borrow is only for this body.
483        let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(self.device_fd) };
484        // A flip the last commit armed may still be reading a ring slot the
485        // scene releases when it drops. Let it land first, bounded, without
486        // touching the event queue.
487        crate::armed_flip::settle(
488            &mut self.scene,
489            &borrowed,
490            self.crtc_id,
491            self.armed_flip_seq,
492        );
493    }
494}