drmkit_present/dumb_ring.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! A multi-buffered CPU source, and the canonical [`BufferRing`] consumer.
5
6use drmkit_core::Device;
7use drmkit_dumb::{Buffer, Config, DumbError, MapAccess, Mapping};
8use drmkit_scene::{
9 AcquiredBuffer, BindingModel, DamageRect, LayerBufferSource, SourceError, SourceFormat,
10};
11
12use crate::{BufferRing, Rect, Repaint};
13
14/// What went wrong painting a frame.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum PaintError {
18 /// Every slot is busy. Retry after the next flip completes.
19 ///
20 /// Flow control, not a failure: the producer is ahead of the display.
21 #[error("every ring slot is busy")]
22 WouldBlock,
23
24 /// A slot's buffer could not be allocated.
25 #[error("allocating a ring slot: {0}")]
26 Allocate(#[from] DumbError),
27
28 /// The format's bits-per-pixel is not one this can size an allocation
29 /// from.
30 #[error("no known bits-per-pixel for fourcc {0:#010x}")]
31 UnknownFormat(u32),
32}
33
34/// A ring of CPU-mapped dumb buffers, presented as a scene layer's source.
35///
36/// This is what turns [`BufferRing`]'s bookkeeping into something a producer
37/// can use: [`paint`](Self::paint) hands a callback the leased buffer's
38/// mapping *and the region it must repaint*, and whatever the callback says it
39/// changed becomes the next commit's `FB_DAMAGE_CLIPS`.
40///
41/// # Why it lives here and not in `drmkit-scene-sources`
42///
43/// It depends on [`BufferRing`], and the scene must not depend on the present
44/// spine — the producer seam points the other way. It is still an ordinary
45/// [`LayerBufferSource`], so a scene layer holds one directly without knowing
46/// where it came from.
47///
48/// # The contract, for a CPU producer
49///
50/// Once per frame, call `paint`. The callback gets the stale region: [`Repaint::Full`]
51/// for a slot that is fresh or too old to track, otherwise the union of damage
52/// since *this slot* was last on screen — which is not the same as what changed
53/// this frame, and is the whole reason the ring exists. Paint at least that
54/// much, return what you actually touched, and the scene's next commit scans
55/// out the slot and reports exactly that as its damage.
56///
57/// Under-reporting leaves stale pixels on screen. Over-reporting costs
58/// bandwidth and nothing else, which is why every path that cannot prove the
59/// stale region returns `Full`.
60///
61/// # Threading
62///
63/// Single-threaded: drive `paint` and the scene commit from one loop.
64pub struct DumbRingSource {
65 format: SourceFormat,
66 bpp: u32,
67 ring: BufferRing,
68 buffers: Vec<Option<Buffer>>,
69 /// The slot `paint` committed, waiting for the scene to acquire it. Cleared
70 /// on acquire, so every commit needs a fresh paint.
71 pending: Option<(usize, Vec<DamageRect>)>,
72}
73
74impl DumbRingSource {
75 /// A ring of up to `max_slots` linear dumb buffers.
76 ///
77 /// Nothing is allocated here. Slots are allocated by [`paint`](Self::paint)
78 /// as the ring grows under contention, which on a 4K display is the
79 /// difference between reserving one buffer and reserving three.
80 ///
81 /// # Errors
82 ///
83 /// [`PaintError::UnknownFormat`] if `fourcc`'s bits-per-pixel is not known,
84 /// since that is what sizes the allocation.
85 pub fn new(width: u32, height: u32, fourcc: u32, max_slots: usize) -> Result<Self, PaintError> {
86 let bpp = drmkit_fmt::format_bpp(fourcc);
87 if bpp == 0 {
88 return Err(PaintError::UnknownFormat(fourcc));
89 }
90 Ok(Self {
91 format: SourceFormat {
92 fourcc,
93 modifier: drmkit_fmt::Modifier::LINEAR.0,
94 width,
95 height,
96 },
97 bpp,
98 ring: BufferRing::new(max_slots, BufferRing::DEFAULT_MAX_DAMAGE_RECTS),
99 buffers: Vec::new(),
100 pending: None,
101 })
102 }
103
104 /// Paint the next frame.
105 ///
106 /// The callback receives the leased buffer's writable mapping and the
107 /// region it must repaint, and returns the region it actually changed, in
108 /// buffer pixels.
109 ///
110 /// `device` is a parameter rather than something the source holds. The
111 /// reference keeps a raw `const drm::Device*` and swaps it on session
112 /// resume; storing a borrow here would put a lifetime on the type and into
113 /// every scene that holds one, and storing a pointer is the thing Rust is
114 /// for avoiding. Passing it per call also means there is no stale device to
115 /// swap when a session resumes — the next paint simply gets the new one.
116 ///
117 /// # Errors
118 ///
119 /// [`PaintError::WouldBlock`] when every slot is busy, which is flow
120 /// control rather than failure. Otherwise whatever allocating a slot
121 /// failed with.
122 pub fn paint<F>(&mut self, device: &Device, paint: F) -> Result<(), PaintError>
123 where
124 F: FnOnce(&mut Mapping<'_>, &Repaint) -> Vec<Rect>,
125 {
126 let lease = self.ring.acquire().ok_or(PaintError::WouldBlock)?;
127 let buffer = self.ensure_slot(device, lease.slot)?;
128 let painted = {
129 let mut mapping = buffer.map(MapAccess::Write);
130 paint(&mut mapping, &lease.repaint)
131 };
132
133 let damage = painted
134 .iter()
135 .map(|r| DamageRect {
136 x: r.x,
137 y: r.y,
138 w: r.width,
139 h: r.height,
140 })
141 .collect();
142 // The ring is told what changed *this frame*; it works out for itself
143 // what any other slot therefore owes.
144 self.ring.present(lease.slot, &painted);
145 self.pending = Some((lease.slot, damage));
146 Ok(())
147 }
148
149 /// How many slots have been allocated so far.
150 #[must_use]
151 pub fn allocated(&self) -> usize {
152 self.buffers.iter().filter(|b| b.is_some()).count()
153 }
154
155 /// The slot's buffer, allocating it if this is its first use.
156 ///
157 /// Returns the buffer rather than `()` so the caller has no unreachable
158 /// unwrap to justify.
159 fn ensure_slot(&mut self, device: &Device, slot: usize) -> Result<&mut Buffer, PaintError> {
160 if self.buffers.len() <= slot {
161 self.buffers.resize_with(slot + 1, || None);
162 }
163 if self.buffers[slot].is_none() {
164 self.buffers[slot] = Some(Self::allocate(device, &self.format, self.bpp)?);
165 }
166 Ok(self.buffers[slot]
167 .as_mut()
168 .unwrap_or_else(|| unreachable!("just allocated")))
169 }
170
171 /// One slot's buffer.
172 fn allocate(device: &Device, format: &SourceFormat, bpp: u32) -> Result<Buffer, PaintError> {
173 let buffer = Buffer::create(
174 device,
175 &Config {
176 width: format.width,
177 height: format.height,
178 fourcc: format.fourcc,
179 bpp,
180 add_fb: true,
181 },
182 )?;
183 Ok(buffer)
184 }
185}
186
187impl LayerBufferSource for DumbRingSource {
188 /// Hand the scene the slot the last `paint` committed.
189 ///
190 /// [`SourceError::WouldBlock`] when nothing has been painted since the last
191 /// acquire — the same signal a camera gives before its first frame, and
192 /// handled the same way: the layer keeps what it had rather than the frame
193 /// failing.
194 fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
195 let (slot, damage) = self.pending.take().ok_or(SourceError::WouldBlock)?;
196 let fb_id = self
197 .buffers
198 .get(slot)
199 .and_then(Option::as_ref)
200 .and_then(Buffer::fb_id)
201 .ok_or(SourceError::WouldBlock)?;
202 Ok(AcquiredBuffer {
203 fb_id,
204 token: u64::try_from(slot).unwrap_or(u64::MAX),
205 acquire_fence: None,
206 damage,
207 })
208 }
209
210 /// Return a slot to the ring once its flip has landed.
211 fn release(&mut self, acquired: AcquiredBuffer) {
212 // A token that does not fit is not a slot this ring handed out, and
213 // `release` ignores an out-of-range one.
214 let slot = usize::try_from(acquired.token).unwrap_or(usize::MAX);
215 self.ring.release(slot);
216 }
217
218 fn binding_model(&self) -> BindingModel {
219 BindingModel::SceneSubmitsFbId
220 }
221
222 fn format(&self) -> SourceFormat {
223 self.format
224 }
225
226 /// Drop every buffer without touching the dead descriptor.
227 ///
228 /// The GEM handles and framebuffer ids belonged to it and the kernel
229 /// reclaims them when it closes; issuing ioctls against it would fail and
230 /// change nothing.
231 fn on_session_paused(&mut self) {
232 for slot in &mut self.buffers {
233 if let Some(buffer) = slot.as_mut() {
234 buffer.forget();
235 }
236 *slot = None;
237 }
238 self.pending = None;
239 }
240
241 /// Nothing to do: the next `paint` allocates against the device it is
242 /// given.
243 ///
244 /// The reference re-points its stored device here. Not holding one is what
245 /// makes this empty.
246 fn on_session_resumed(&mut self, _device: &Device) -> Result<(), SourceError> {
247 Ok(())
248 }
249}