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}