Skip to main content

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}