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}