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
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}