Skip to main content

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}