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