Skip to main content

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}