drmkit_scene_set/partition.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Planning how many ioctls one frame across several outputs costs.
5
6/// How to split a frame across the outputs it touches.
7///
8/// The policy shapes the groups [`partition_for_policy`] returns; each group
9/// is one atomic commit for the caller to make.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11#[non_exhaustive]
12pub enum NarrowPolicy {
13 /// Never split: one group, so one atomic commit, for every engaged scene.
14 ///
15 /// The only policy that makes a multi-output frame **atomic** — either
16 /// every output shows the new frame or none does. What it costs is that
17 /// one output's refusal takes the others down with it, and that a modeset
18 /// anywhere forces `ALLOW_MODESET` on the whole commit.
19 Combined,
20
21 /// Split only when the outputs disagree about modesetting.
22 ///
23 /// The default, and the reason the policy exists. A modeset is expensive
24 /// and can blank an output; a steady frame is neither. Committing them
25 /// together makes every steady output pay the modeset's cost, so when the
26 /// set is mixed they go separately — **modeset first**, since a steady
27 /// frame on an output that is about to be reconfigured is wasted work.
28 /// When every engaged scene agrees, one commit, and the atomicity is kept.
29 #[default]
30 AutoOnModeset,
31
32 /// One commit per engaged scene, always.
33 ///
34 /// Gives up cross-output atomicity to contain failure: an output that
35 /// refuses its frame does not stop the others showing theirs. For a
36 /// caller that would rather have three screens right and one wrong than
37 /// four screens stale.
38 PerCrtc,
39}
40
41/// What one slot in the set looks like this frame.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43#[non_exhaustive]
44pub struct SlotState {
45 /// Whether this slot holds no scene.
46 ///
47 /// A set keeps its indices stable across removal, so a removed scene
48 /// leaves a hole rather than shifting everything after it — a caller's
49 /// stored index would otherwise silently start naming a different output.
50 pub is_hole: bool,
51 /// Whether this scene needs `ALLOW_MODESET` this frame.
52 pub wants_modeset: bool,
53}
54
55/// Group the engaged slots into one commit each.
56///
57/// Holes are never in a group: there is nothing to commit for them, and an
58/// empty group would be an ioctl that programs nothing.
59#[must_use]
60pub fn partition_for_policy(slots: &[SlotState], policy: NarrowPolicy) -> Vec<Vec<usize>> {
61 let engaged = || {
62 slots
63 .iter()
64 .enumerate()
65 .filter(|(_, slot)| !slot.is_hole)
66 .map(|(index, _)| index)
67 };
68
69 match policy {
70 NarrowPolicy::PerCrtc => engaged().map(|index| vec![index]).collect(),
71
72 NarrowPolicy::Combined => {
73 let combined: Vec<usize> = engaged().collect();
74 if combined.is_empty() {
75 Vec::new()
76 } else {
77 vec![combined]
78 }
79 }
80
81 NarrowPolicy::AutoOnModeset => {
82 let (modeset, steady): (Vec<usize>, Vec<usize>) =
83 engaged().partition(|index| slots[*index].wants_modeset);
84
85 match (modeset.is_empty(), steady.is_empty()) {
86 // Nothing engaged: no commits at all.
87 (true, true) => Vec::new(),
88 // Mixed: modeset first, so a steady frame is not committed to
89 // an output that is about to be reconfigured out from under it.
90 (false, false) => vec![modeset, steady],
91 // Uniform either way: one commit, and the set stays atomic.
92 (false, true) => vec![modeset],
93 (true, false) => vec![steady],
94 }
95 }
96 }
97}