drmkit_scene_set/lib.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Several scenes, one frame.
5//!
6//! Port of `src/scene/scene_set.{hpp,cpp}`.
7//!
8//! A [`LayerScene`] drives one CRTC. A [`SceneSet`] holds several, mirrors
9//! layers across them, and plans how many atomic commits a frame across them
10//! costs — which is the whole question, because one commit across every output
11//! is *atomic* and several are not, while one commit means one output's
12//! refusal takes the rest down with it. See [`NarrowPolicy`]. The set plans;
13//! making the commits is the caller's.
14//!
15//! # Mirrored layers
16//!
17//! [`add_layer`](SceneSet::add_layer) takes one source and a target per
18//! scene, so the same content reaches several outputs — a presentation
19//! mirrored to a projector, a status bar on every screen. Each target carries
20//! its own geometry, since the outputs rarely share a resolution.
21
22mod partition;
23pub use partition::{NarrowPolicy, SlotState, partition_for_policy};
24
25use std::rc::Rc;
26
27use drmkit_scene::{DisplayParams, LayerBufferSource, LayerHandle, LayerScene};
28
29mod shared;
30use shared::SharedSource;
31
32#[cfg(test)]
33mod tests;
34
35/// Where one mirrored layer lands on one scene.
36#[derive(Debug, Clone, Copy)]
37#[allow(
38 clippy::exhaustive_structs,
39 reason = "built by callers, with no default to fill a new field from"
40)]
41pub struct Target {
42 /// Which scene in the set, by index.
43 pub scene_index: usize,
44 /// How to display it there.
45 ///
46 /// Per target, not per layer: two outputs rarely share a resolution, and
47 /// mirroring at one geometry would letterbox or crop the other.
48 pub display: DisplayParams,
49 /// Force this copy through composition rather than onto a plane.
50 ///
51 /// The copy is blended from the shared source's pixels, read through
52 /// [`with_mapping`](drmkit_scene::LayerBufferSource::with_mapping), so the
53 /// source must offer a CPU view.
54 pub force_composited: bool,
55}
56
57/// One layer across one or more scenes.
58#[allow(
59 clippy::exhaustive_structs,
60 reason = "built by callers, with no default to fill a new field from"
61)]
62pub struct LayerSpec {
63 /// The content. Shared across every target — that is what makes it a
64 /// mirror rather than two layers that happen to look alike.
65 pub source: Rc<std::cell::RefCell<dyn LayerBufferSource>>,
66 /// Where it goes. At least one.
67 pub targets: Vec<Target>,
68}
69
70impl std::fmt::Debug for LayerSpec {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("LayerSpec")
73 .field("targets", &self.targets.len())
74 .finish_non_exhaustive()
75 }
76}
77
78/// A handle to a layer that may span several scenes.
79///
80/// Distinct from [`LayerHandle`], which names a layer within one scene: this
81/// names the *set* of them a single [`add_layer`](SceneSet::add_layer)
82/// created, so removing it removes every copy.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub struct SetLayerHandle {
85 id: u32,
86 generation: u32,
87}
88
89impl SetLayerHandle {
90 /// Whether this handle ever named anything.
91 ///
92 /// A default handle does not. Checked rather than assumed because the
93 /// default is what a caller gets from `SetLayerHandle::default()`, and
94 /// removing with one must not remove whatever happens to be at index
95 /// zero.
96 #[must_use]
97 pub const fn is_valid(self) -> bool {
98 self.id != 0
99 }
100}
101
102/// Why a set operation was refused.
103#[derive(Debug, thiserror::Error, PartialEq, Eq)]
104#[non_exhaustive]
105pub enum SetError {
106 /// A layer was added with no targets.
107 ///
108 /// Refused rather than accepted as a no-op: a caller that built an empty
109 /// target list has a bug upstream of here, and silently creating a layer
110 /// that appears nowhere hides it until someone asks why the screen is
111 /// blank.
112 #[error("a layer needs at least one target")]
113 NoTargets,
114
115 /// A target named a scene the set does not have.
116 #[error("scene index {index} is out of range ({count} scenes)")]
117 NoSuchScene {
118 /// What was asked for.
119 index: usize,
120 /// How many the set holds, holes included.
121 count: usize,
122 },
123}
124
125/// One layer's copies, so removal can find them all.
126struct SetLayer {
127 generation: u32,
128 /// Which scene each copy lives in, and its handle there.
129 copies: Vec<(usize, LayerHandle)>,
130}
131
132/// Several scenes, with mirrored layers and a plan for how a frame across
133/// them is committed.
134pub struct SceneSet {
135 /// `None` is a hole: a removed scene, kept so indices stay stable.
136 scenes: Vec<Option<LayerScene>>,
137 layers: Vec<Option<SetLayer>>,
138 /// Emptied layer slots, reused most recent first, as the reference reuses
139 /// them, so adding and removing layers does not grow the set.
140 free_layers: Vec<usize>,
141 next_generation: u32,
142}
143
144impl std::fmt::Debug for SceneSet {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("SceneSet")
147 .field("slots", &self.scenes.len())
148 .field("engaged", &self.scenes.iter().flatten().count())
149 .finish_non_exhaustive()
150 }
151}
152
153impl SceneSet {
154 /// A set over `scenes`, in the order given.
155 ///
156 /// The index is the caller's handle to an output for the set's lifetime,
157 /// so the order is theirs to choose and is never rearranged.
158 #[must_use]
159 pub fn new(scenes: Vec<LayerScene>) -> Self {
160 Self {
161 scenes: scenes.into_iter().map(Some).collect(),
162 layers: Vec::new(),
163 free_layers: Vec::new(),
164 next_generation: 1,
165 }
166 }
167
168 /// How many slots the set has, holes included.
169 ///
170 /// Not how many scenes: a hole still occupies an index, because that is
171 /// what keeps every other index meaning what it did.
172 #[must_use]
173 pub fn slot_count(&self) -> usize {
174 self.scenes.len()
175 }
176
177 /// How many slots hold a scene.
178 #[must_use]
179 pub fn scene_count(&self) -> usize {
180 self.scenes.iter().flatten().count()
181 }
182
183 /// The scene at `index`, if that slot holds one.
184 #[must_use]
185 pub fn scene(&self, index: usize) -> Option<&LayerScene> {
186 self.scenes.get(index)?.as_ref()
187 }
188
189 /// The scene at `index`, mutably.
190 pub fn scene_mut(&mut self, index: usize) -> Option<&mut LayerScene> {
191 self.scenes.get_mut(index)?.as_mut()
192 }
193
194 /// Add a scene, reusing a hole if there is one.
195 ///
196 /// Reusing keeps the set from growing without bound as outputs come and
197 /// go — a laptop docking and undocking all day would otherwise leave a
198 /// slot behind each time.
199 pub fn add_scene(&mut self, scene: LayerScene) -> usize {
200 if let Some(index) = self.scenes.iter().position(Option::is_none) {
201 self.scenes[index] = Some(scene);
202 return index;
203 }
204 self.scenes.push(Some(scene));
205 self.scenes.len() - 1
206 }
207
208 /// Remove the scene at `index`, leaving a hole.
209 ///
210 /// Out of range is a no-op, and so is a slot that is already a hole: both
211 /// are what a caller reacting to an output that has gone away will do
212 /// twice.
213 pub fn remove_scene(&mut self, index: usize) {
214 if let Some(slot) = self.scenes.get_mut(index) {
215 *slot = None;
216 }
217 }
218
219 /// Add one layer to every target scene.
220 ///
221 /// # Errors
222 ///
223 /// [`SetError::NoTargets`] for an empty target list, and
224 /// [`SetError::NoSuchScene`] if a target names a slot the set does not
225 /// have or that holds no scene. **Checked before anything is added**, so a
226 /// spec naming three scenes and one bad index leaves the set as it was
227 /// rather than half-populated.
228 pub fn add_layer(&mut self, spec: &LayerSpec) -> Result<SetLayerHandle, SetError> {
229 if spec.targets.is_empty() {
230 return Err(SetError::NoTargets);
231 }
232 for target in &spec.targets {
233 if self.scene(target.scene_index).is_none() {
234 return Err(SetError::NoSuchScene {
235 index: target.scene_index,
236 count: self.scenes.len(),
237 });
238 }
239 }
240
241 let mut copies = Vec::with_capacity(spec.targets.len());
242 for target in &spec.targets {
243 let shared = SharedSource(Rc::clone(&spec.source));
244 // Every index was checked above and nothing since then can have
245 // removed a scene, so a missing slot here would be a bug in this
246 // function rather than a caller error -- skipping it silently
247 // would leave the layer on fewer outputs than asked for.
248 let Some(scene) = self.scene_mut(target.scene_index) else {
249 continue;
250 };
251 let handle = scene.add_layer(Box::new(shared));
252 if let Some(layer) = scene.layer_mut(handle) {
253 layer.set_display(target.display);
254 layer.set_force_composited(target.force_composited);
255 }
256 copies.push((target.scene_index, handle));
257 }
258
259 let generation = self.next_generation;
260 self.next_generation = self.next_generation.wrapping_add(1).max(1);
261 let layer = Some(SetLayer { generation, copies });
262 let index = if let Some(index) = self.free_layers.pop() {
263 self.layers[index] = layer;
264 index
265 } else {
266 self.layers.push(layer);
267 self.layers.len() - 1
268 };
269
270 Ok(SetLayerHandle {
271 // One-based: zero is the "never issued" value a default handle
272 // carries, and it must not name the first layer.
273 id: u32::try_from(index + 1).unwrap_or(u32::MAX),
274 generation,
275 })
276 }
277
278 /// Remove every copy of a layer.
279 ///
280 /// A handle that was never issued, or was issued and already removed, is
281 /// ignored. The generation is what makes the second case safe: a slot
282 /// reused by a later layer does not answer to the old handle.
283 pub fn remove_layer(&mut self, handle: SetLayerHandle) {
284 if !handle.is_valid() {
285 return;
286 }
287 let Some(index) = (handle.id as usize).checked_sub(1) else {
288 return;
289 };
290 let Some(slot) = self.layers.get_mut(index) else {
291 return;
292 };
293 let Some(layer) = slot.as_ref() else { return };
294 if layer.generation != handle.generation {
295 return;
296 }
297
298 let Some(taken) = slot.take() else { return };
299 self.free_layers.push(index);
300 let copies = taken.copies;
301 for (scene_index, layer_handle) in copies {
302 if let Some(scene) = self.scene_mut(scene_index) {
303 scene.remove_layer(layer_handle);
304 }
305 }
306 }
307
308 /// What each slot looks like this frame, for the partition.
309 ///
310 /// `wants_modeset` is the caller's to say: the scene does not know whether
311 /// the mode has been set, since the commit path owns the `Modeset` and
312 /// this type does not commit for the caller.
313 #[must_use]
314 pub fn slot_states(&self, wants_modeset: &[usize]) -> Vec<SlotState> {
315 self.scenes
316 .iter()
317 .enumerate()
318 .map(|(index, slot)| SlotState {
319 is_hole: slot.is_none(),
320 wants_modeset: wants_modeset.contains(&index),
321 })
322 .collect()
323 }
324
325 /// How this frame splits into commits.
326 ///
327 /// The groups are slot indices, in the order they should be committed.
328 #[must_use]
329 pub fn plan_commits(&self, wants_modeset: &[usize], policy: NarrowPolicy) -> Vec<Vec<usize>> {
330 partition_for_policy(&self.slot_states(wants_modeset), policy)
331 }
332}