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