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}