Skip to main content

drmkit_present/
buffer_ring.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The scanout slot pool, and the damage bookkeeping that makes partial
5//! repaint correct.
6
7use std::collections::VecDeque;
8
9/// A damage rectangle, in buffer pixels.
10///
11/// Deliberately not `drmkit_planes::Rect`. That one is a plane's source or
12/// destination rectangle in CRTC coordinates; this is a region of a buffer the
13/// producer owns, and the two spaces coincide only when a layer happens to be
14/// unscaled and at the origin. Sharing the type would invite passing one where
15/// the other is meant, which is a bug no compiler would catch.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17#[allow(
18    clippy::exhaustive_structs,
19    reason = "a value defined by its fields; there is nothing to add to it"
20)]
21pub struct Rect {
22    /// Left edge.
23    pub x: i32,
24    /// Top edge.
25    pub y: i32,
26    /// Width in pixels.
27    pub width: u32,
28    /// Height in pixels.
29    pub height: u32,
30}
31
32/// What a reused slot must repaint before its contents are a correct frame.
33///
34/// An enum rather than the reference's `{ bool full; vector<Rect> region; }`,
35/// because that struct can hold a full repaint *and* a region, and a caller
36/// reading the region without checking the flag under-paints. Here the region
37/// only exists when it is the answer.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[allow(
40    clippy::exhaustive_enums,
41    reason = "every producer handles each kind; a new one has to break them so none repaints the wrong region"
42)]
43pub enum Repaint {
44    /// Repaint the whole buffer: the slot has never been presented, or its
45    /// damage history has scrolled away, or the union grew past the cap.
46    Full,
47    /// Repaint exactly this union of damage since the slot was last presented.
48    ///
49    /// In present order, oldest first, and not merged or deduplicated -- the
50    /// producer knows more about its own cost model than this does.
51    Region(Vec<Rect>),
52}
53
54/// A slot handed to the producer for the next frame.
55#[derive(Debug, Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub struct Lease {
58    /// Index into the producer's own buffer array.
59    pub slot: usize,
60    /// The ring grew to make this slot, so no buffer exists for it yet.
61    pub fresh: bool,
62    /// What the producer must draw to make this slot a correct frame.
63    pub repaint: Repaint,
64}
65
66/// Where a slot is in its lifecycle.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68enum State {
69    /// Nothing holds it; `acquire` may hand it out.
70    Free,
71    /// Leased to the producer, which is drawing into it.
72    InFlight,
73    /// On screen. Never handed out, never released.
74    Scanning,
75    /// Replaced, but its flip has not landed, so the display engine may still
76    /// be reading it.
77    PendingRelease,
78}
79
80#[derive(Debug, Clone, Copy)]
81struct Slot {
82    state: State,
83    /// Present sequence when this slot was last presented. Zero means never,
84    /// which is why sequences start at one.
85    last_present: u64,
86}
87
88/// A producer-agnostic pool of scanout slots with buffer age and damage
89/// accumulation.
90///
91/// The ring owns slot *lifecycle* and staleness; the producer owns the actual
92/// buffers and indexes them by slot. That split is what lets the same ring sit
93/// under a dumb buffer, a GBM surface or a Vulkan swapchain without knowing
94/// which.
95///
96/// # What it is for
97///
98/// Redrawing a whole frame when three pixels changed wastes bandwidth, and on
99/// a memory-bound part that is the difference between hitting vblank and
100/// missing it. Partial repaint avoids it, but only if the producer knows how
101/// stale the buffer it has just been handed is -- a buffer two frames old needs
102/// everything that changed in those two frames repainted, not just this
103/// frame's damage. That is the `EGL_EXT_buffer_age` contract, and this is the
104/// bookkeeping behind it.
105///
106/// Getting it wrong under-paints: the frame is composed of pixels from
107/// different moments, and the error persists until something forces a full
108/// repaint. So every path that cannot prove what a slot needs returns
109/// [`Repaint::Full`].
110///
111/// # Threading
112///
113/// Not synchronized, by design. Drive `acquire`, `present` and `release` from
114/// the one thread the producer renders on -- the present loop. `&mut self` on
115/// the mutating methods makes sharing it across threads a compile error rather
116/// than a race.
117#[derive(Debug)]
118pub struct BufferRing {
119    slots: Vec<Slot>,
120    /// Per-frame damage, oldest first. `damage[0]` belongs to present sequence
121    /// `base_seq + 1`, and the back to `present_seq`.
122    damage: VecDeque<Vec<Rect>>,
123    /// Last committed present sequence. Zero means nothing presented yet.
124    present_seq: u64,
125    /// The sequence just before `damage.front()`, so history that has scrolled
126    /// away is detectable rather than silently mis-indexed.
127    base_seq: u64,
128    max_slots: usize,
129    max_damage_rects: usize,
130}
131
132impl BufferRing {
133    /// How deep a ring grows unless the caller says otherwise.
134    pub const DEFAULT_MAX_SLOTS: usize = 3;
135    /// How many rectangles a union may hold unless the caller says otherwise.
136    /// See [`BufferRing::new`] for what happens when a union exceeds this.
137    pub const DEFAULT_MAX_DAMAGE_RECTS: usize = 16;
138
139    /// A ring that grows to `max_slots`, with unions capped at
140    /// `max_damage_rects`.
141    ///
142    /// `max_slots` is clamped to at least one: a ring with no slots can never
143    /// hand one out, and a caller asking for zero has made an arithmetic
144    /// mistake rather than a request.
145    ///
146    /// The rect cap is a bandwidth-versus-bookkeeping knob. A union that
147    /// exceeds it collapses to [`Repaint::Full`], on the reasoning that
148    /// scattered small rectangles cost more in per-rect overhead than the
149    /// bandwidth they save.
150    #[must_use]
151    pub fn new(max_slots: usize, max_damage_rects: usize) -> Self {
152        Self {
153            slots: Vec::new(),
154            damage: VecDeque::new(),
155            present_seq: 0,
156            base_seq: 0,
157            max_slots: max_slots.max(1),
158            max_damage_rects,
159        }
160    }
161
162    /// How many slots the ring has grown so far.
163    #[must_use]
164    pub fn len(&self) -> usize {
165        self.slots.len()
166    }
167
168    /// Whether the ring has grown any slots yet.
169    #[must_use]
170    pub fn is_empty(&self) -> bool {
171        self.slots.is_empty()
172    }
173
174    /// The cap this ring grows to.
175    #[must_use]
176    pub const fn max_slots(&self) -> usize {
177        self.max_slots
178    }
179
180    /// How many slots the producer is currently drawing into.
181    #[must_use]
182    pub fn in_flight(&self) -> usize {
183        self.slots
184            .iter()
185            .filter(|slot| slot.state == State::InFlight)
186            .count()
187    }
188
189    /// How many presents have happened since this slot was last presented.
190    ///
191    /// Zero means either that it has never been presented, so its contents are
192    /// undefined, or that it is the slot presented last, which is current.
193    /// [`acquire`](Self::acquire) is what tells them apart where it matters to
194    /// a producer: it never hands out the slot presented last, and answers
195    /// [`Repaint::Full`] for one never presented.
196    #[must_use]
197    pub fn age(&self, slot: usize) -> u32 {
198        match self.slots.get(slot) {
199            Some(s) if s.last_present != 0 => {
200                u32::try_from(self.present_seq - s.last_present).unwrap_or(u32::MAX)
201            }
202            _ => 0,
203        }
204    }
205
206    /// Take a slot for the next frame, or `None` if every one is busy.
207    ///
208    /// Prefers the freshest free slot, because freshest means least to
209    /// repaint. Grows the ring if nothing is free and it is below capacity.
210    /// Never returns the scanning slot.
211    ///
212    /// `None` is not an error: it means the producer is ahead of the display
213    /// and should retry after the next vblank.
214    pub fn acquire(&mut self) -> Option<Lease> {
215        let best = self
216            .slots
217            .iter()
218            .enumerate()
219            .filter(|(_, slot)| slot.state == State::Free)
220            .max_by_key(|(_, slot)| slot.last_present)
221            .map(|(index, _)| index);
222
223        if let Some(index) = best {
224            self.slots[index].state = State::InFlight;
225            return Some(Lease {
226                slot: index,
227                fresh: false,
228                repaint: self.repaint_for(self.slots[index].last_present),
229            });
230        }
231
232        if self.slots.len() < self.max_slots {
233            let index = self.slots.len();
234            self.slots.push(Slot {
235                state: State::InFlight,
236                last_present: 0,
237            });
238            return Some(Lease {
239                slot: index,
240                fresh: true,
241                repaint: Repaint::Full,
242            });
243        }
244
245        None
246    }
247
248    /// Commit a slot as the new scanout buffer.
249    ///
250    /// `frame_damage` is what changed in this frame against the previous one --
251    /// not against whatever was in this slot, which is what the ring works out
252    /// for itself.
253    ///
254    /// The outgoing scanning slot becomes releasable rather than free: its flip
255    /// has not landed, so the display engine may still be reading it. Freeing
256    /// it here is the mistake `drmkit-scene`'s invariant 1 (release only after
257    /// the replacing flip lands) forbids, in a different place.
258    pub fn present(&mut self, slot: usize, frame_damage: &[Rect]) {
259        if slot >= self.slots.len() {
260            return;
261        }
262        for s in &mut self.slots {
263            if s.state == State::Scanning {
264                s.state = State::PendingRelease;
265            }
266        }
267        self.present_seq += 1;
268        self.damage.push_back(frame_damage.to_vec());
269        // Only as much history as the deepest reusable slot could need. Older
270        // frames falling off is safe: a slot that stale gets a full repaint.
271        while self.damage.len() > self.max_slots {
272            self.damage.pop_front();
273            self.base_seq += 1;
274        }
275        self.slots[slot].state = State::Scanning;
276        self.slots[slot].last_present = self.present_seq;
277    }
278
279    /// Return a replaced slot to the free pool.
280    ///
281    /// Call it once the flip that replaced this slot has landed, or once its
282    /// render has been abandoned. The scanning slot is ignored rather than
283    /// freed -- releasing what is on screen is not a request this can honor.
284    pub fn release(&mut self, slot: usize) {
285        if let Some(s) = self.slots.get_mut(slot)
286            && s.state != State::Scanning
287        {
288            s.state = State::Free;
289        }
290    }
291
292    /// What a slot last presented at `last_present` must repaint.
293    fn repaint_for(&self, last_present: u64) -> Repaint {
294        // Never presented, or its damage has scrolled out of history.
295        if last_present == 0 || last_present < self.base_seq {
296            return Repaint::Full;
297        }
298        // Sequence S sits at index (S - base_seq - 1), so the first frame this
299        // slot missed -- last_present + 1 -- is at (last_present - base_seq).
300        let from = usize::try_from(last_present - self.base_seq).unwrap_or(usize::MAX);
301        let mut region = Vec::new();
302        for frame in self.damage.iter().skip(from) {
303            for rect in frame {
304                region.push(*rect);
305                if region.len() > self.max_damage_rects {
306                    return Repaint::Full;
307                }
308            }
309        }
310        Repaint::Region(region)
311    }
312}
313
314impl Default for BufferRing {
315    fn default() -> Self {
316        Self::new(Self::DEFAULT_MAX_SLOTS, Self::DEFAULT_MAX_DAMAGE_RECTS)
317    }
318}