Skip to main content

drmkit_planes/
allocator.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The plane allocator.
5//!
6//! Port of the search in `src/planes/allocator.cpp`. Home of **invariant 4**'s
7//! FB-only fast path and of the v2.0.1 warm-start stability bonus.
8
9use std::collections::HashMap;
10
11use drmkit_fmt::{Modifier, ModifierProbeCache, Verdict};
12
13use crate::layer::Layer;
14use crate::matching::BipartiteMatching;
15use crate::plane_order::{PlaneOrder, Position, plane_order_consistent, stacks_by_plane_id};
16use crate::prop::PropTag;
17use crate::registry::{PlaneCapabilities, PlaneRegistry, PlaneType};
18use crate::scoring::{
19    ScoreContext, keep_priority, layers_intersect, plane_statically_compatible, score_pair,
20    split_independent_groups,
21};
22
23/// Stable identity for a layer across frames.
24///
25/// # Deviation: identity by value, not by address
26///
27/// The C++ allocator remembers `const Layer*` and therefore needs
28/// `forget_layer()`, which the header explains must run before a `Layer` is
29/// destroyed:
30///
31/// > without invalidation, heap reuse can give a freshly-added layer the same
32/// > address, fool the diff path into treating the two as the same logical
33/// > layer, and silently skip property writes the kernel needs.
34///
35/// Keying on a caller-assigned id removes that hazard rather than mitigating
36/// it: a recycled allocation cannot masquerade as a previous layer, provided
37/// the caller never reuses an id ([`Output`](crate::Output) and the scene
38/// never do). [`Allocator::forget_layer`] is still worth calling, to keep the
39/// fast path warm and to force a full write on the plane the layer leaves.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[allow(
42    clippy::exhaustive_structs,
43    reason = "a value defined by its fields; there is nothing to add to it"
44)]
45pub struct LayerId(pub u64);
46
47/// One layer offered to the allocator.
48#[derive(Debug, Clone, Copy)]
49#[allow(
50    clippy::exhaustive_structs,
51    reason = "a value defined by its fields; there is nothing to add to it"
52)]
53pub struct LayerRef<'a> {
54    /// Stable identity across frames.
55    pub id: LayerId,
56    /// The layer itself.
57    pub layer: &'a Layer,
58}
59
60/// Why a test commit was rejected.
61///
62/// The distinction matters: a scene suspends on lost DRM master and on nothing
63/// else (invariant 2), so flattening every rejection into one variant would
64/// break that contract.
65///
66/// Deliberately **not** `#[non_exhaustive]`. A caller must decide what each
67/// failure means — retry a smaller assignment, or stop the scene — and an open
68/// enum forces a catch-all arm, which is exactly how a new failure kind would
69/// silently inherit the wrong handling. Adding a variant should break every
70/// caller so each one chooses.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[allow(
73    clippy::exhaustive_enums,
74    reason = "every caller handles each failure; a new one has to break them so each chooses"
75)]
76pub enum TestFailure {
77    /// The kernel rejected the configuration. The ordinary case: try a smaller
78    /// assignment.
79    Rejected,
80    /// The caller is no longer DRM master. Propagated up rather than retried —
81    /// no smaller assignment will help.
82    NotMaster,
83}
84
85/// Validates tentative plane assignments.
86///
87/// # Why this is a trait
88///
89/// The C++ allocator holds a `Device` and issues `TEST_ONLY` commits itself.
90/// Doing the same here would make `drmkit-planes` depend on `drmkit-core` and
91/// give up being device-free — and the plan wants this crate `no_std`-adjacent
92/// (§5).
93///
94/// Handing the commit back to the caller keeps the split clean: the allocator
95/// decides *what* to try, the scene knows *how* to ask the kernel, and the
96/// property-name-to-id resolution stays next to the `PropertyStore` that owns
97/// it. It also makes the whole search testable against a fake, which is how the
98/// suite reaches its coverage with no hardware.
99pub trait TestCommitter {
100    /// Validate an assignment with a `TEST_ONLY` commit.
101    ///
102    /// `assignment` is `(plane_id, layer)` pairs. An implementation is expected
103    /// to disable every other CRTC-compatible plane in the same request:
104    /// without that, the test inherits stale state from the previously
105    /// committed plane and reports a spurious rejection when a layer migrates
106    /// between planes on one CRTC.
107    ///
108    /// # Errors
109    ///
110    /// [`TestFailure::Rejected`] for an ordinary kernel rejection, or
111    /// [`TestFailure::NotMaster`] when DRM master was lost.
112    fn test_assignment(&mut self, assignment: &[(u32, LayerRef<'_>)]) -> Result<(), TestFailure>;
113
114    /// Arm one more plane in every test, alongside the assignment.
115    ///
116    /// The composition canvas lands on a plane the assignment never names, so
117    /// without this the search validates a frame one plane smaller than the
118    /// one that gets committed. Where the hardware lights fewer planes than it
119    /// advertises, that difference is a frame the kernel refuses -- and a
120    /// refused frame shows nothing at all. Telling the committer about the
121    /// canvas makes the search account for it, so it drops a layer into the
122    /// composition instead of proposing something uncommittable.
123    ///
124    /// The plane is excluded from the test's disable pass for as long as it is
125    /// set. Call [`TestCommitter::clear_extra_plane`] afterwards.
126    ///
127    /// Defaults to doing nothing, which is right for a committer that accepts
128    /// everything or one that is not talking to a device.
129    fn set_extra_plane(&mut self, plane_id: u32, layer: Layer) {
130        let _ = (plane_id, layer);
131    }
132
133    /// Stop arming the plane set by [`TestCommitter::set_extra_plane`].
134    fn clear_extra_plane(&mut self) {}
135
136    /// The planes the kernel has lit on `crtc_id` right now, whoever lit
137    /// them; `None` when the committer cannot tell.
138    ///
139    /// A scene asks this while it does not yet know what the kernel holds --
140    /// before its first commit lands, after a rebind or a resume -- so the
141    /// frame can turn off a plane another client left armed, as every test
142    /// already does. `None`, the default, makes the scene turn off every plane
143    /// it does not use instead, which is right but writes more.
144    fn lit_planes(&mut self, crtc_id: u32) -> Option<Vec<u32>> {
145        let _ = crtc_id;
146        None
147    }
148}
149
150/// A committer that accepts everything. For tests that care about which
151/// assignment the search *proposes* rather than how it narrows one down.
152#[derive(Debug, Default, Clone, Copy)]
153#[allow(
154    clippy::exhaustive_structs,
155    reason = "built by callers with `..Default::default()`, which a new field does not break"
156)]
157pub struct AlwaysAccept {
158    /// How many test commits were requested.
159    pub calls: usize,
160}
161
162impl TestCommitter for AlwaysAccept {
163    fn test_assignment(&mut self, _assignment: &[(u32, LayerRef<'_>)]) -> Result<(), TestFailure> {
164        self.calls += 1;
165        Ok(())
166    }
167}
168
169/// Plane-to-layer assignment.
170///
171/// A flat vector rather than a map: CRTCs have at most a handful of planes, so
172/// a linear scan beats hashing, and reassigning one reuses the allocation
173/// instead of rehashing. Same reasoning the C++ `PlaneAssignment` records.
174#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct PlaneAssignment {
176    entries: Vec<(u32, LayerId)>,
177}
178
179impl PlaneAssignment {
180    /// An empty assignment.
181    #[must_use]
182    pub const fn new() -> Self {
183        Self {
184            entries: Vec::new(),
185        }
186    }
187
188    /// Assign a plane, replacing any previous layer on it.
189    pub fn insert(&mut self, plane_id: u32, layer: LayerId) {
190        if let Some(entry) = self.entries.iter_mut().find(|(id, _)| *id == plane_id) {
191            entry.1 = layer;
192        } else {
193            self.entries.push((plane_id, layer));
194        }
195    }
196
197    /// The layer assigned to a plane.
198    #[must_use]
199    pub fn get(&self, plane_id: u32) -> Option<LayerId> {
200        self.entries
201            .iter()
202            .find(|(id, _)| *id == plane_id)
203            .map(|(_, layer)| *layer)
204    }
205
206    /// The plane a layer is assigned to, if any.
207    #[must_use]
208    pub fn get_plane_of(&self, layer: LayerId) -> Option<u32> {
209        self.entries
210            .iter()
211            .find(|(_, id)| *id == layer)
212            .map(|(plane_id, _)| *plane_id)
213    }
214
215    /// Remove a plane's assignment.
216    pub fn remove(&mut self, plane_id: u32) -> Option<LayerId> {
217        let index = self.entries.iter().position(|(id, _)| *id == plane_id)?;
218        Some(self.entries.remove(index).1)
219    }
220
221    /// Every `(plane_id, layer)` pair.
222    #[must_use]
223    pub fn entries(&self) -> &[(u32, LayerId)] {
224        &self.entries
225    }
226
227    /// How many planes are assigned.
228    #[must_use]
229    pub fn len(&self) -> usize {
230        self.entries.len()
231    }
232
233    /// Whether nothing is assigned.
234    #[must_use]
235    pub fn is_empty(&self) -> bool {
236        self.entries.is_empty()
237    }
238
239    /// Drop every assignment.
240    pub fn clear(&mut self) {
241        self.entries.clear();
242    }
243}
244
245/// Memoized test-commit verdicts, keyed by `(plane, property_hash)`.
246#[derive(Debug, Default, Clone)]
247pub struct TestCache {
248    entries: HashMap<(u32, u64), Entry>,
249}
250
251#[derive(Debug, Clone, Copy, Default)]
252struct Entry {
253    /// The latest verdict.
254    passed: bool,
255    /// How many times this combination has been rejected. Successes do not
256    /// increment it, so it is a penalty and not a visit tally.
257    failures: u32,
258}
259
260impl TestCache {
261    /// An empty cache.
262    #[must_use]
263    pub fn new() -> Self {
264        Self::default()
265    }
266
267    /// The cached verdict, if any.
268    #[must_use]
269    pub fn lookup(&self, plane_id: u32, property_hash: u64) -> Option<bool> {
270        self.entries
271            .get(&(plane_id, property_hash))
272            .map(|entry| entry.passed)
273    }
274
275    /// Record a verdict, counting failures so scoring can back off.
276    pub fn record(&mut self, plane_id: u32, property_hash: u64, passed: bool) {
277        let entry = self.entries.entry((plane_id, property_hash)).or_default();
278        entry.passed = passed;
279        if !passed {
280            entry.failures = entry.failures.saturating_add(1);
281        }
282    }
283
284    /// How many times this combination has been rejected; zero while its
285    /// latest verdict is a pass.
286    ///
287    /// The score subtracts this. Counting every verdict instead -- what this
288    /// did until drm-cxx#267 -- decayed a plane the kernel keeps accepting as
289    /// fast as one it keeps rejecting, so the search walked away from the
290    /// plane that worked. Zero after a pass, so a combination rejected while
291    /// some other state was wrong stops being penalized once it succeeds.
292    #[must_use]
293    pub fn failure_count(&self, plane_id: u32, property_hash: u64) -> u32 {
294        self.entries
295            .get(&(plane_id, property_hash))
296            .filter(|entry| !entry.passed)
297            .map_or(0, |entry| entry.failures)
298    }
299
300    /// Drop everything.
301    pub fn clear(&mut self) {
302        self.entries.clear();
303    }
304}
305
306/// What one allocation pass did.
307#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
308#[non_exhaustive]
309pub struct Diagnostics {
310    /// `TEST_ONLY` commits issued while probing for a viable assignment.
311    ///
312    /// 1 in steady state — warm re-validation of the cached assignment. Higher
313    /// when the cache misses and the full search has to preseed, greedy, and
314    /// backtrack. **0 when the FB-only fast path skipped re-validation.**
315    pub test_commits_issued: usize,
316    /// Whether the cached allocation was reused and the `TEST_ONLY` skipped
317    /// because only content changed on already-placed layers.
318    ///
319    /// Implies `test_commits_issued == 0` for the frame. This is the counter
320    /// invariant 4 is stated in terms of.
321    pub fb_delta_fast_path: bool,
322    /// Whether the per-frame test-commit budget ran out before every layer had
323    /// been offered a plane.
324    ///
325    /// The budget bounds how many round trips to the kernel one frame may
326    /// cost. Spending it is not an error -- the layers it could not reach fall
327    /// through to composition and still reach the screen -- but it is a
328    /// *capacity* limit rather than a hardware one, and the two look identical
329    /// in `layers_composited`. On a device with many planes and a scene of
330    /// spatially disjoint layers, each layer forms its own group costing one
331    /// test commit, so a 17-layer scene exhausts the default budget of 16 and
332    /// composites its last layer with a free, compatible plane sitting unused.
333    /// Without this flag that reads as "the hardware ran out of planes".
334    pub budget_exhausted: bool,
335}
336
337/// The result of one allocation pass.
338#[derive(Debug, Clone, Default)]
339#[non_exhaustive]
340pub struct Allocation {
341    /// Which layer goes on which plane.
342    pub assignment: PlaneAssignment,
343    /// Layers without a plane, which the caller must composite: the ones the
344    /// search could not place and the force- and transient-composited ones it
345    /// never tried, in stacking order. Never the composition layer itself, nor
346    /// an externally bound or pinned layer, whose planes the scene owns.
347    pub composited: Vec<LayerId>,
348    /// What the pass did.
349    pub diagnostics: Diagnostics,
350}
351
352/// What the kernel accepted for a plane last commit.
353#[derive(Debug, Clone, Copy)]
354struct LastCommitted {
355    /// `None` once the layer is gone but the plane is still lit -- see
356    /// [`Allocator::forget_layer`].
357    layer: Option<LayerId>,
358    /// Every property the kernel took, so the next commit can write only what
359    /// actually changed.
360    properties: crate::PropertySnapshot,
361    /// `property_hash` at commit time, which excludes `FB_ID` and
362    /// `IN_FENCE_FD`. The fast path compares against this to prove geometry,
363    /// format, and modifier are unchanged since the kernel accepted them.
364    hash: u64,
365}
366
367/// Assigns layers to display planes.
368///
369/// One allocator belongs to one CRTC and is kept across frames, because most
370/// of what makes it cheap is memory of the last one. A frame goes through at
371/// most four stages, and a steady scene stops at the first:
372///
373/// 1. **The FB-only fast path.** Only framebuffer ids changed, so the kernel
374///    has already accepted this arrangement — no test commit at all. This is
375///    invariant 4, and [`Layer::property_hash`](crate::Layer::property_hash)
376///    is what makes it safe to claim.
377/// 2. **Warm start.** The layer set is unchanged but something else moved, so
378///    last frame's assignment is re-offered and validated with one test.
379/// 3. **A full search.** Bipartite matching seeds it, scored candidates order
380///    it, and backtracking drops the layer with the lowest keep-priority when
381///    the kernel refuses.
382/// 4. **Composition.** Whatever is still unplaced is handed back for the
383///    caller to blend, rather than the frame failing.
384///
385/// The search is budgeted: [`DEFAULT_MAX_TEST_COMMITS`](Self::DEFAULT_MAX_TEST_COMMITS)
386/// test commits per search -- a compositing frame runs two, see
387/// [`set_max_test_commits`](Self::set_max_test_commits) -- after which it
388/// takes the best arrangement it has proved. Exhausting the budget is
389/// reported rather than hidden, because "this
390/// device is out of planes" and "raise the budget" want different answers from
391/// a caller.
392///
393/// A refusal is never treated as a defect. The kernel is the only authority on
394/// what a device can scan out simultaneously, and a `TEST_ONLY` that comes back
395/// `EINVAL` is that authority answering — which is why the allocator asks
396/// rather than predicting.
397#[derive(Debug)]
398pub struct Allocator {
399    previous: PlaneAssignment,
400    previous_valid: bool,
401    last_committed: HashMap<u32, LastCommitted>,
402    /// Driver-quirk opt-out; see `set_force_full_property_writes`.
403    force_full_writes: bool,
404    /// Planes the caller has claimed; see `set_reserved_planes`.
405    reserved: Vec<u32>,
406    failure_cache: TestCache,
407    probe_cache: ModifierProbeCache,
408    max_test_commits: usize,
409    test_commits_this_frame: usize,
410    /// Whether the budget stopped a test this frame -- see
411    /// [`Diagnostics::budget_exhausted`].
412    budget_exhausted_this_frame: bool,
413    matcher: BipartiteMatching,
414    /// Planes that can carry the composition canvas, and the canvas as its
415    /// plane would be programmed; empty and `None` without one. See
416    /// [`set_canvas`](Self::set_canvas).
417    canvas_hosts: Vec<u32>,
418    canvas_layer: Option<Layer>,
419    /// The canvas plane for the last pass, and the one cached with
420    /// `previous`.
421    canvas_plane: Option<u32>,
422    previous_canvas_plane: Option<u32>,
423    /// Where planes take a `zpos`: the reserved plane the caller holds back
424    /// for the canvas. See [`hold_canvas_plane`](Self::hold_canvas_plane).
425    canvas_hold: Option<u32>,
426    /// The layers the last full search left unplaced, which the cached paths
427    /// composite again rather than treat as new, and the planes it had free
428    /// when it decided that.
429    previous_composited: Vec<LayerId>,
430    previous_free: Vec<u32>,
431    /// Where planes take a `zpos`: the zpos the canvas asks for after the
432    /// last pass. See [`canvas_zpos`](Self::canvas_zpos).
433    canvas_slot: Option<u64>,
434    /// How many planes this pass's CRTC can stack, where the registry knows
435    /// (P-46), and the plane the canvas is armed on beside each test.
436    stage_limit: Option<usize>,
437    canvas_armed: Option<u32>,
438}
439
440impl Default for Allocator {
441    fn default() -> Self {
442        Self::new()
443    }
444}
445
446impl Allocator {
447    /// Default per-frame test-commit budget.
448    pub const DEFAULT_MAX_TEST_COMMITS: usize = 16;
449
450    /// A fresh allocator with no cached state.
451    #[must_use]
452    pub fn new() -> Self {
453        Self {
454            previous: PlaneAssignment::new(),
455            previous_valid: false,
456            last_committed: HashMap::new(),
457            force_full_writes: false,
458            reserved: Vec::new(),
459            failure_cache: TestCache::new(),
460            probe_cache: ModifierProbeCache::new(),
461            max_test_commits: Self::DEFAULT_MAX_TEST_COMMITS,
462            test_commits_this_frame: 0,
463            budget_exhausted_this_frame: false,
464            matcher: BipartiteMatching::new(),
465            canvas_hosts: Vec::new(),
466            canvas_layer: None,
467            canvas_plane: None,
468            previous_canvas_plane: None,
469            canvas_hold: None,
470            previous_composited: Vec::new(),
471            previous_free: Vec::new(),
472            canvas_slot: None,
473            stage_limit: None,
474            canvas_armed: None,
475        }
476    }
477
478    /// The composition canvas: which planes can carry it, and its plane's
479    /// properties. Empty `hosts` or no `layer` means the caller has none.
480    ///
481    /// Where planes have no `zpos` the allocator picks the canvas plane
482    /// itself, since only a plane between the composited run's neighbors
483    /// stacks the canvas where the run asked. Elsewhere the caller names it
484    /// with [`hold_canvas_plane`](Self::hold_canvas_plane). Either way the
485    /// allocator arms `layer` there in every test it issues, so that what the
486    /// kernel accepts is the whole frame (P-18).
487    pub fn set_canvas(&mut self, hosts: &[u32], layer: Option<&Layer>) {
488        self.canvas_hosts.clear();
489        self.canvas_layer = layer.cloned();
490        if self.canvas_layer.is_some() {
491            self.canvas_hosts.extend_from_slice(hosts);
492        }
493    }
494
495    /// The plane the last pass left for the canvas, tested with it armed.
496    ///
497    /// `None` when nothing is composited, when no host fits, or where planes
498    /// take a `zpos` and the pass held no plane for it; the caller arms the
499    /// canvas here when it is `Some`. A cached pass reports the plane the
500    /// search it reuses left, since that is the plane its test armed.
501    #[must_use]
502    pub const fn canvas_plane(&self) -> Option<u32> {
503        self.canvas_plane
504    }
505
506    /// Where planes take a `zpos`: hold `plane` for the canvas this pass.
507    ///
508    /// The plane must also be reserved (see
509    /// [`set_reserved_planes`](Self::set_reserved_planes)). The search arms
510    /// the canvas there in every test, and when it leaves layers composited
511    /// reports the plane as [`canvas_plane`](Self::canvas_plane) and keeps it
512    /// with the cached assignment, so a warm start re-tests the canvas with
513    /// the layers rather than searching again. Ignored where planes stack by
514    /// id, which pick their own.
515    pub const fn hold_canvas_plane(&mut self, plane: Option<u32>) {
516        self.canvas_hold = plane;
517    }
518
519    /// Set the per-search test-commit budget.
520    ///
521    /// A frame that composites runs two searches -- the second holds a plane
522    /// back for the composition canvas and re-validates with it armed -- so
523    /// such a frame can spend up to twice this. That is deliberate: starving
524    /// the second search would leave it unable to validate, which is the one
525    /// thing it exists to do.
526    pub const fn set_max_test_commits(&mut self, max: usize) {
527        self.max_test_commits = max;
528    }
529
530    /// Drop the warm-start cache so the next pass does a full search.
531    ///
532    /// The scene calls this when a layer's content type or update hint changed:
533    /// those affect scoring but not the layer set, so warm-start — keyed on
534    /// "same layers, still valid" — would otherwise keep the stale assignment
535    /// and never move the layer to the plane its new hint prefers.
536    pub fn invalidate_allocation(&mut self) {
537        self.previous_valid = false;
538    }
539
540    /// Forget everything cached about the previous output.
541    ///
542    /// For a scene moving to a different CRTC. Both the assignment and the
543    /// committed baseline describe *that* output's planes, and a plane id
544    /// means nothing on another pipe -- diffing against a baseline from the
545    /// old one would suppress properties the new one has never been told.
546    ///
547    /// Stronger than [`invalidate_allocation`](Self::invalidate_allocation),
548    /// which only forces a fresh search and keeps the baseline.
549    pub fn forget_output(&mut self) {
550        self.previous = PlaneAssignment::new();
551        self.previous_valid = false;
552        self.canvas_plane = None;
553        self.previous_canvas_plane = None;
554        self.previous_composited.clear();
555        self.previous_free.clear();
556        self.last_committed.clear();
557        self.failure_cache = TestCache::default();
558    }
559
560    /// Record what the kernel accepted, so the next frame's fast path has a
561    /// baseline. Call after a successful real commit, never after a test.
562    ///
563    /// A `TEST_ONLY` applies nothing to hardware, so recording one would let a
564    /// later commit diff against state the kernel never took and suppress
565    /// properties it still requires.
566    pub fn record_committed(&mut self, plane_id: u32, layer: LayerRef<'_>) {
567        self.last_committed.insert(
568            plane_id,
569            LastCommitted {
570                layer: Some(layer.id),
571                properties: layer.layer.snapshot(),
572                hash: layer.layer.property_hash(),
573            },
574        );
575    }
576
577    /// Replace the committed baseline with what a real commit just applied.
578    ///
579    /// Planes absent from `applied` are dropped rather than left behind: the
580    /// commit that carried this assignment explicitly disabled every candidate
581    /// plane it did not use, so any baseline they had describes state the
582    /// kernel no longer holds.
583    ///
584    /// Call after a successful **real** commit, never after a test -- see
585    /// [`record_committed`](Self::record_committed).
586    pub fn record_commit(&mut self, applied: &[(u32, LayerRef<'_>)]) {
587        self.record_commit_stacked(applied, &[]);
588    }
589
590    /// [`record_commit`](Self::record_commit), for a commit that wrote a
591    /// stacked zpos in place of some layers' own (see
592    /// [`stacked_zpos`](crate::stacked_zpos)).
593    ///
594    /// The snapshot takes the written value, because the next frame's diff
595    /// has to compare against what the kernel holds. The hash stays the
596    /// layer's own, because the fast path compares it against the layer as
597    /// the caller will present it next frame, which is the requested zpos --
598    /// and an unchanged assignment of unchanged requests stacks the same.
599    pub fn record_commit_stacked(&mut self, applied: &[(u32, LayerRef<'_>)], zpos: &[(u32, u64)]) {
600        self.last_committed.clear();
601        for (plane_id, entry) in applied {
602            let mut properties = entry.layer.snapshot();
603            if let Some((_, written)) = zpos.iter().find(|(id, _)| id == plane_id) {
604                properties = properties.with(PropTag::Zpos, *written);
605            }
606            self.last_committed.insert(
607                *plane_id,
608                LastCommitted {
609                    layer: Some(entry.id),
610                    properties,
611                    hash: entry.layer.property_hash(),
612                },
613            );
614        }
615    }
616
617    /// What the kernel last took on `plane_id`, if this commit may diff
618    /// against it.
619    ///
620    /// `None` means every property must be written:
621    ///
622    /// * the plane has no baseline -- first use, or it was detached since;
623    /// * the baseline belongs to a **different** layer, so the incoming one
624    ///   would silently inherit any property the outgoing layer set and it
625    ///   does not (a stale rotation, a stale alpha);
626    /// * full writes were forced for a driver that mishandles partial ones.
627    #[must_use]
628    pub fn committed_baseline(
629        &self,
630        plane_id: u32,
631        layer: LayerId,
632    ) -> Option<&crate::PropertySnapshot> {
633        if self.force_full_writes {
634            return None;
635        }
636        self.last_committed
637            .get(&plane_id)
638            .filter(|baseline| baseline.layer == Some(layer))
639            .map(|baseline| &baseline.properties)
640    }
641
642    /// Hold these planes back from the search.
643    ///
644    /// A reserved plane is not offered to any layer, and the caller arms it
645    /// itself. The composition canvas needs this: with more layers than planes
646    /// the search would otherwise claim every one of them, and the canvas that
647    /// exists to rescue the overflow would have nowhere to land — so the
648    /// overflow is dropped instead of composited, which is the opposite of
649    /// what the fallback is for.
650    ///
651    /// Reserving costs a plane whether or not it ends up used, so a caller
652    /// should reserve only when it can tell the search will overflow.
653    pub fn set_reserved_planes(&mut self, planes: &[u32]) {
654        self.reserved.clear();
655        self.reserved.extend_from_slice(planes);
656    }
657
658    /// Whether the kernel currently has nothing on `plane_id`.
659    ///
660    /// A plane with no baseline was never activated, or was detached by the
661    /// commit that dropped it. Either way the kernel already has it off, and
662    /// disabling it again is a pair of property writes the kernel still has to
663    /// walk on every frame.
664    #[must_use]
665    pub fn plane_is_off(&self, plane_id: u32) -> bool {
666        self.last_committed.get(&plane_id).is_none_or(|baseline| {
667            baseline
668                .properties
669                .get(PropTag::FbId)
670                .is_none_or(|fb| fb == 0)
671        })
672    }
673
674    /// Every plane the kernel has a framebuffer on, as far as this allocator
675    /// committed it.
676    ///
677    /// What a scene leaving its CRTC has to turn off before the baseline that
678    /// records it is forgotten: nothing else knows these planes are lit.
679    #[must_use]
680    pub fn lit_planes(&self) -> Vec<u32> {
681        let mut planes: Vec<u32> = self
682            .last_committed
683            .keys()
684            .copied()
685            .filter(|plane_id| !self.plane_is_off(*plane_id))
686            .collect();
687        planes.sort_unstable();
688        planes
689    }
690
691    /// Re-emit every property on every commit, for drivers that mishandle a
692    /// partial write. Off by default; this is a quirk escape hatch, not a
693    /// tuning knob -- it multiplies per-frame property traffic.
694    pub const fn set_force_full_property_writes(&mut self, force: bool) {
695        self.force_full_writes = force;
696    }
697
698    /// Whether full property writes are being forced.
699    #[must_use]
700    pub const fn force_full_property_writes(&self) -> bool {
701        self.force_full_writes
702    }
703
704    /// Drop every trace of a layer the scene has removed.
705    ///
706    /// Without this the cached assignment still names the departed layer, so
707    /// the FB-only fast path sees "a previously-placed layer is gone" and
708    /// falls back to a tested pass on the frame after every removal -- one
709    /// wasted `TEST_ONLY` commit for a configuration that cannot have become
710    /// invalid, since dropping a layer only ever frees resources.
711    ///
712    /// The committed baseline's entry is kept and only its identity cleared,
713    /// as upstream does: it is the only record that the plane is still lit,
714    /// and a cleared identity forces a full property write for the next layer
715    /// to land there.
716    pub fn forget_layer(&mut self, layer: LayerId) {
717        while let Some(plane_id) = self.previous.get_plane_of(layer) {
718            self.previous.remove(plane_id);
719        }
720        // Clear the identity but keep the entry. The entry is the only record
721        // that the kernel still has this plane lit: dropping it would make the
722        // plane look already-off, so nothing would disable it and it would go
723        // on scanning out the departed layer's last framebuffer.
724        //
725        // Losing the identity is what forces a full property write for the
726        // next layer to land here, which is the other half of what upstream's
727        // pointer-nulling buys.
728        for baseline in self.last_committed.values_mut() {
729            if baseline.layer == Some(layer) {
730                baseline.layer = None;
731            }
732        }
733        if self.previous.is_empty() {
734            self.previous_valid = false;
735        }
736    }
737
738    /// Forget a plane's committed baseline, after detaching it.
739    pub fn forget_plane(&mut self, plane_id: u32) {
740        self.last_committed.remove(&plane_id);
741    }
742
743    /// The cached assignment from the previous pass.
744    #[must_use]
745    pub const fn previous_allocation(&self) -> &PlaneAssignment {
746        &self.previous
747    }
748
749    /// **Invariant 4.** Whether only content changed on already-placed layers.
750    ///
751    /// True when every plane committed last frame still maps to the same layer,
752    /// and that layer's current `property_hash` matches what was recorded at
753    /// commit. Since the hash excludes `FB_ID` and `IN_FENCE_FD`, a match
754    /// proves geometry, format, and modifier are identical to the assignment
755    /// the kernel already accepted — so the redundant `TEST_ONLY` can be
756    /// skipped and the real commit's own `atomic_check` left as the sole
757    /// arbiter.
758    ///
759    /// Any placement, format, or modifier edit defeats it, as does a
760    /// previously-placed layer vanishing this frame.
761    #[must_use]
762    pub fn is_fb_only_frame(&self, present: &[LayerRef<'_>]) -> bool {
763        if self.previous.is_empty() {
764            return false;
765        }
766        self.previous.entries().iter().all(|(plane_id, layer_id)| {
767            let Some(current) = present.iter().find(|entry| entry.id == *layer_id) else {
768                return false; // a previously-placed layer is gone this frame
769            };
770            self.last_committed.get(plane_id).is_some_and(|baseline| {
771                baseline.layer == Some(*layer_id) && baseline.hash == current.layer.property_hash()
772            })
773        })
774    }
775
776    /// Assign layers to planes.
777    ///
778    /// Layers that cannot be placed come back in
779    /// [`Allocation::composited`] for the caller's composition fallback.
780    ///
781    /// # Errors
782    ///
783    /// [`TestFailure::NotMaster`] if DRM master was lost mid-search. Ordinary
784    /// rejections are handled internally by trying smaller assignments.
785    pub fn allocate<C: TestCommitter>(
786        &mut self,
787        layers: &[LayerRef<'_>],
788        registry: &PlaneRegistry,
789        crtc_index: u32,
790        committer: &mut C,
791    ) -> Result<Allocation, TestFailure> {
792        self.canvas_slot = None;
793        let allocation = self.allocate_planes(layers, registry, crtc_index, committer)?;
794        if !allocation.composited.is_empty() && !stacks_by_plane_id(registry, crtc_index) {
795            let bounds = canvas_bounds(&allocation.assignment, layers);
796            self.canvas_slot = bounds.above.filter(|_| bounds.feasible());
797        }
798        Ok(allocation)
799    }
800
801    /// Where planes take a `zpos`, the zpos the composition canvas asks for
802    /// after the last [`allocate`](Self::allocate): that of the lowest placed
803    /// layer that overlaps a composited one and must cover it, for the canvas
804    /// to stack just under (drm-cxx `07226e4`, #343). `None` when no placed
805    /// layer must cover the canvas, which may then stack on top, and where
806    /// planes stack by id, where the canvas plane's id places it.
807    ///
808    /// The slot asks for a placed layer's own zpos, so the canvas has to stack
809    /// under ties: [`stacked_zpos_beneath`](crate::stacked_zpos_beneath).
810    #[must_use]
811    pub const fn canvas_zpos(&self) -> Option<u64> {
812        self.canvas_slot
813    }
814
815    fn allocate_planes<C: TestCommitter>(
816        &mut self,
817        layers: &[LayerRef<'_>],
818        registry: &PlaneRegistry,
819        crtc_index: u32,
820        committer: &mut C,
821    ) -> Result<Allocation, TestFailure> {
822        self.stage_limit = registry.blend_stage_limit(crtc_index);
823        self.test_commits_this_frame = 0;
824        self.budget_exhausted_this_frame = false;
825        self.canvas_plane = None;
826        let by_plane_id = stacks_by_plane_id(registry, crtc_index);
827
828        // Layers the allocator must not touch: the scene owns their planes.
829        let placeable: Vec<LayerRef<'_>> = layers
830            .iter()
831            .filter(|entry| {
832                !entry.layer.is_externally_bound()
833                    && !entry.layer.is_pinned()
834                    && !entry.layer.is_transient_composited()
835            })
836            .copied()
837            .collect();
838
839        // Bottom-up, so the search hands the lowest layer the lowest plane.
840        // Only a preference where planes take a written `zpos`; where they do
841        // not, the plane-order path below keeps the order by construction.
842        // Cheap here, and it cannot cost a placement: the matcher maximizes
843        // cardinality first and uses score only to break ties, so reordering
844        // the input changes which valid assignment is chosen, never whether
845        // one is found.
846        let mut placeable = placeable;
847        placeable.sort_by_key(|entry| entry.layer.property(PropTag::Zpos).unwrap_or(0));
848
849        let stacked = if by_plane_id {
850            Self::plane_stack(layers)
851        } else {
852            Vec::new()
853        };
854        // The cached assignment is still valid to the kernel after a restack,
855        // just stacked in the old order, and nothing but this notices
856        // (drm-cxx#239).
857        let order_holds = !by_plane_id || self.plane_order_holds(&stacked);
858        // Where planes take a zpos, a move or restack can leave a placed layer
859        // that would have to sit on both sides of the one canvas; the cached
860        // assignment cannot fix that, so search again (drm-cxx `07226e4`).
861        let order_holds =
862            order_holds && (by_plane_id || canvas_bounds(&self.previous, layers).feasible());
863
864        let has_new_layer =
865            self.previous_valid && self.has_new_layer(&placeable, layers, registry, crtc_index);
866        // A plane held for the canvas that the cached assignment was not
867        // tested with needs a test before the pair is reused.
868        let canvas_tested =
869            self.canvas_hold.is_none() || self.canvas_hold == self.previous_canvas_plane;
870        let canvas = self.canvas_hold.or(self.previous_canvas_plane);
871
872        // --- invariant 4: the FB-only fast path ---------------------------
873        if self.previous_valid
874            && !has_new_layer
875            && order_holds
876            && canvas_tested
877            && self.is_fb_only_frame(layers)
878        {
879            let assignment = self.previous.clone();
880            self.canvas_plane = self.previous_canvas_plane;
881            let composited = Self::to_composite(layers, &assignment);
882            return Ok(Allocation {
883                assignment,
884                composited,
885                diagnostics: Diagnostics {
886                    test_commits_issued: 0,
887                    fb_delta_fast_path: true,
888                    budget_exhausted: false,
889                },
890            });
891        }
892
893        // --- warm start: re-validate the cached assignment ----------------
894        if self.previous_valid
895            && !has_new_layer
896            && order_holds
897            && !self.previous.is_empty()
898            && self.previous_still_applies(&placeable)
899            && {
900                let planes: Vec<&PlaneCapabilities> = registry.for_crtc(crtc_index).collect();
901                stacking_consistent_all(&planes, &self.previous, &placeable)
902                    && multirect_complete(&planes, &self.previous)
903            }
904            && let Some(allocation) = self.warm_start(&placeable, layers, canvas, committer)?
905        {
906            return Ok(allocation);
907        }
908
909        let assignment = if by_plane_id {
910            self.place_in_plane_order(&stacked, layers, registry, crtc_index, committer)?
911        } else {
912            self.canvas_plane = self.canvas_hold;
913            self.arm_canvas(self.canvas_hold, committer);
914            let capped = self.cap_to_blend_limit(&placeable, layers);
915            let searched = self
916                .full_search(&capped, registry, crtc_index, committer)
917                .and_then(|searched| {
918                    self.keep_canvas_bounds(
919                        searched, &capped, layers, registry, crtc_index, committer,
920                    )
921                });
922            self.arm_canvas(None, committer);
923            searched?
924        };
925        let composited = Self::to_composite(layers, &assignment);
926        if composited.is_empty() {
927            self.canvas_plane = None;
928        }
929
930        self.previous = assignment.clone();
931        self.previous_valid = !assignment.is_empty();
932        self.previous_canvas_plane = self.canvas_plane;
933        self.previous_composited = Self::unplaced(&placeable, &assignment);
934        self.previous_free = self.free_planes(&self.previous, layers, registry, crtc_index);
935
936        Ok(Allocation {
937            assignment,
938            composited,
939            diagnostics: Diagnostics {
940                test_commits_issued: self.test_commits_this_frame,
941                fb_delta_fast_path: false,
942                budget_exhausted: self.budget_exhausted_this_frame,
943            },
944        })
945    }
946
947    /// Re-test the cached assignment, with the canvas armed on `canvas`.
948    /// `None` when the kernel refuses it, which drops the cache for a full
949    /// search.
950    fn warm_start<C: TestCommitter>(
951        &mut self,
952        placeable: &[LayerRef<'_>],
953        layers: &[LayerRef<'_>],
954        canvas: Option<u32>,
955        committer: &mut C,
956    ) -> Result<Option<Allocation>, TestFailure> {
957        let pairs = Self::pairs_for(&self.previous, placeable);
958        if pairs.len() != self.previous.len()
959            || self.stage_limit.is_some_and(|limit| {
960                pairs.len() + usize::from(canvas.is_some() && self.canvas_layer.is_some()) > limit
961            })
962        {
963            return Ok(None);
964        }
965        self.test_commits_this_frame += 1;
966        self.arm_canvas(canvas, committer);
967        let verdict = committer.test_assignment(&pairs);
968        self.arm_canvas(None, committer);
969        match verdict {
970            Ok(()) => {
971                let assignment = self.previous.clone();
972                let composited = Self::to_composite(layers, &assignment);
973                // The plane the test armed is the canvas's now, and stays
974                // paired with the cached assignment.
975                self.canvas_plane = canvas.filter(|_| !composited.is_empty());
976                self.previous_canvas_plane = self.canvas_plane;
977                Ok(Some(Allocation {
978                    assignment,
979                    composited,
980                    diagnostics: Diagnostics {
981                        test_commits_issued: self.test_commits_this_frame,
982                        fb_delta_fast_path: false,
983                        budget_exhausted: false,
984                    },
985                }))
986            }
987            Err(TestFailure::NotMaster) => Err(TestFailure::NotMaster),
988            Err(TestFailure::Rejected) => {
989                self.previous_valid = false;
990                Ok(None)
991            }
992        }
993    }
994
995    /// A full search's assignment, or one the one canvas can stack with.
996    ///
997    /// One canvas stacks at one zpos. When a placed layer would have to sit
998    /// on both sides of it, composite one contiguous zpos run instead;
999    /// failing that, move the caught layers into the composition and keep the
1000    /// smaller set if the kernel takes it. Otherwise the search's assignment
1001    /// stands, and the canvas stacks on top. Port of upstream's
1002    /// `full_search` tail (`07226e4`).
1003    fn keep_canvas_bounds<C: TestCommitter>(
1004        &mut self,
1005        searched: PlaneAssignment,
1006        placeable: &[LayerRef<'_>],
1007        layers: &[LayerRef<'_>],
1008        registry: &PlaneRegistry,
1009        crtc_index: u32,
1010        committer: &mut C,
1011    ) -> Result<PlaneAssignment, TestFailure> {
1012        if canvas_bounds(&searched, layers).feasible() {
1013            return Ok(searched);
1014        }
1015        if let Some(run) =
1016            self.place_around_run(searched.len(), layers, registry, crtc_index, committer)?
1017        {
1018            return Ok(run);
1019        }
1020        let planes: Vec<&PlaneCapabilities> = registry.for_crtc(crtc_index).collect();
1021        let mut resolved = searched.clone();
1022        if resolve_canvas_bounds(&mut resolved, layers, &planes)
1023            && (resolved.is_empty() || self.try_test(&resolved, placeable, committer)?)
1024        {
1025            return Ok(resolved);
1026        }
1027        Ok(searched)
1028    }
1029
1030    /// Place every layer outside one contiguous zpos run of at least the
1031    /// layers the search left unplaced: for each run length, smallest first,
1032    /// the run with the lowest total keep-priority, the higher on a tie (a
1033    /// run at the top keeps the canvas on top). Layers the caller or the
1034    /// scene sent to the canvas must be in the run. `None` when no run leaves
1035    /// the canvas bounds feasible within the test budget.
1036    fn place_around_run<C: TestCommitter>(
1037        &mut self,
1038        placed: usize,
1039        layers: &[LayerRef<'_>],
1040        registry: &PlaneRegistry,
1041        crtc_index: u32,
1042        committer: &mut C,
1043    ) -> Result<Option<PlaneAssignment>, TestFailure> {
1044        let mut candidates: Vec<LayerRef<'_>> = layers
1045            .iter()
1046            .filter(|entry| {
1047                !entry.layer.is_composition_layer()
1048                    && !entry.layer.is_externally_bound()
1049                    && !entry.layer.is_pinned()
1050            })
1051            .copied()
1052            .collect();
1053        candidates.sort_by_key(|entry| entry.layer.property(PropTag::Zpos).unwrap_or(0));
1054        let n = candidates.len();
1055        let forced: Vec<usize> = (0..n)
1056            .filter(|&i| forced_composited(candidates[i].layer))
1057            .collect();
1058        let available: Vec<&PlaneCapabilities> = registry
1059            .for_crtc(crtc_index)
1060            .filter(|plane| !self.reserved.contains(&plane.id))
1061            .collect();
1062
1063        for width in (n - placed.min(n)).max(1)..n {
1064            if self.test_commits_this_frame >= self.max_test_commits {
1065                self.budget_exhausted_this_frame = true;
1066                break;
1067            }
1068            let mut best: Option<(usize, i64)> = None;
1069            for start in 0..=n - width {
1070                let covers_forced = forced.iter().all(|&i| (start..start + width).contains(&i));
1071                if !covers_forced {
1072                    continue;
1073                }
1074                let cost: i64 = candidates[start..start + width]
1075                    .iter()
1076                    .map(|entry| i64::from(keep_priority(entry.layer)))
1077                    .sum();
1078                if best.is_none_or(|(_, best_cost)| cost <= best_cost) {
1079                    best = Some((start, cost));
1080                }
1081            }
1082            let Some((start, _)) = best else {
1083                continue;
1084            };
1085            let outside: Vec<LayerRef<'_>> = candidates
1086                .iter()
1087                .enumerate()
1088                .filter(|(i, _)| !(start..start + width).contains(i))
1089                .map(|(_, entry)| *entry)
1090                .collect();
1091            let assignment = self.place_group(&outside, &available, crtc_index, committer)?;
1092            if !assignment.is_empty() && canvas_bounds(&assignment, layers).feasible() {
1093                return Ok(Some(assignment));
1094            }
1095        }
1096        Ok(None)
1097    }
1098
1099    /// Whether a layer present this frame needs a full search to have a
1100    /// fair shot at a plane: the previous frame did not place it, and either
1101    /// did not composite it either, or something the last search decided on
1102    /// has changed in a way that could let it place more.
1103    ///
1104    /// Both cached paths iterate the *previous* assignment to decide what to
1105    /// emit, so neither can place a layer that is not already in it:
1106    /// warm-start would succeed with the old set, the new layer would be
1107    /// composited, and the cached assignment would never grow -- so the same
1108    /// fate would hit every subsequent frame.
1109    ///
1110    /// A layer composited last frame is not new (drm-cxx `6e58d23`): counting
1111    /// it as one forced a full search on every frame composition was active.
1112    /// The cached paths composite it again, on the last search's verdict --
1113    /// unless a composited layer has gone, which can leave the rest few enough
1114    /// to fit, or a plane has come free that the search did not have. Upstream
1115    /// fixed the same trap in drm-cxx#341, searching after any shrink that
1116    /// leaves composition running.
1117    ///
1118    /// A free plane alone is no reason: on a controller that lights fewer
1119    /// planes than it offers (RK3566 VOP2: three eligible, two usable) the
1120    /// search composited those layers with that plane free, and would again.
1121    fn has_new_layer(
1122        &self,
1123        placeable: &[LayerRef<'_>],
1124        layers: &[LayerRef<'_>],
1125        registry: &PlaneRegistry,
1126        crtc_index: u32,
1127    ) -> bool {
1128        let mut unplaced = 0;
1129        for entry in placeable {
1130            if self.previous.get_plane_of(entry.id).is_some() {
1131                continue;
1132            }
1133            if !self.previous_composited.contains(&entry.id) {
1134                return true;
1135            }
1136            unplaced += 1;
1137        }
1138        if unplaced == 0 {
1139            return false;
1140        }
1141        unplaced < self.previous_composited.len()
1142            || self
1143                .free_planes(&self.previous, layers, registry, crtc_index)
1144                .iter()
1145                .any(|plane| !self.previous_free.contains(plane))
1146    }
1147
1148    /// The planes on `crtc_index` a search could still use beside
1149    /// `assignment`: not a cursor, not reserved, not a pinned layer's, not the canvas's.
1150    fn free_planes(
1151        &self,
1152        assignment: &PlaneAssignment,
1153        layers: &[LayerRef<'_>],
1154        registry: &PlaneRegistry,
1155        crtc_index: u32,
1156    ) -> Vec<u32> {
1157        let pinned: Vec<u32> = layers
1158            .iter()
1159            .filter(|entry| entry.layer.is_pinned())
1160            .filter_map(|entry| entry.layer.assigned_plane())
1161            .collect();
1162        registry
1163            .for_crtc(crtc_index)
1164            .filter(|plane| {
1165                plane.plane_type != PlaneType::Cursor
1166                    && !self.reserved.contains(&plane.id)
1167                    && !pinned.contains(&plane.id)
1168                    && assignment.get(plane.id).is_none()
1169                    // The canvas's plane is taken, held back or not.
1170                    && self.previous_canvas_plane != Some(plane.id)
1171            })
1172            .map(|plane| plane.id)
1173            .collect()
1174    }
1175
1176    /// Whether every layer in the cached assignment is still present.
1177    fn previous_still_applies(&self, present: &[LayerRef<'_>]) -> bool {
1178        self.previous
1179            .entries()
1180            .iter()
1181            .all(|(_, id)| present.iter().any(|entry| entry.id == *id))
1182    }
1183
1184    /// Resolve an assignment's layer ids against this frame's layers.
1185    fn pairs_for<'a>(
1186        assignment: &PlaneAssignment,
1187        present: &[LayerRef<'a>],
1188    ) -> Vec<(u32, LayerRef<'a>)> {
1189        assignment
1190            .entries()
1191            .iter()
1192            .filter_map(|(plane_id, layer_id)| {
1193                present
1194                    .iter()
1195                    .find(|entry| entry.id == *layer_id)
1196                    .map(|entry| (*plane_id, *entry))
1197            })
1198            .collect()
1199    }
1200
1201    /// What the caller must composite: every layer without a plane, as
1202    /// upstream marks them after each pass -- transient-composited layers
1203    /// included, which the search never sees -- except the composition
1204    /// layer itself and the layers whose planes the scene owns. In stacking
1205    /// order, ties in the order given.
1206    fn to_composite(layers: &[LayerRef<'_>], assignment: &PlaneAssignment) -> Vec<LayerId> {
1207        let mut unplaced: Vec<&LayerRef<'_>> = layers
1208            .iter()
1209            .filter(|entry| {
1210                !entry.layer.is_composition_layer()
1211                    && !entry.layer.is_externally_bound()
1212                    && !entry.layer.is_pinned()
1213                    && !assignment.entries().iter().any(|(_, id)| *id == entry.id)
1214            })
1215            .collect();
1216        unplaced.sort_by_key(|entry| entry.layer.property(PropTag::Zpos).unwrap_or(0));
1217        unplaced.into_iter().map(|entry| entry.id).collect()
1218    }
1219
1220    /// Searched layers that did not get a plane: the cache's record of what
1221    /// the last search composited, which is what the next frame's
1222    /// [`has_new_layer`](Self::has_new_layer) compares against.
1223    fn unplaced(placeable: &[LayerRef<'_>], assignment: &PlaneAssignment) -> Vec<LayerId> {
1224        placeable
1225            .iter()
1226            .filter(|entry| !assignment.entries().iter().any(|(_, id)| *id == entry.id))
1227            .map(|entry| entry.id)
1228            .collect()
1229    }
1230
1231    /// Every layer that reaches the screen through the allocator, in the one
1232    /// order planes stacked by id can show them: transient-composited layers
1233    /// too, which land on the canvas.
1234    ///
1235    /// Equal zpos asks for no order, so ties go lowest keep-priority first:
1236    /// the composited run has to be contiguous, and this is what lets it take
1237    /// the low-priority layers of a tie rather than whichever the caller added
1238    /// first. Upstream keeps caller order there. Stable, so equal priorities
1239    /// keep caller order.
1240    fn plane_stack<'a>(layers: &[LayerRef<'a>]) -> Vec<LayerRef<'a>> {
1241        let mut stacked: Vec<LayerRef<'a>> = layers
1242            .iter()
1243            .filter(|entry| !entry.layer.is_externally_bound() && !entry.layer.is_pinned())
1244            .copied()
1245            .collect();
1246        stacked.sort_by_key(|entry| {
1247            (
1248                entry.layer.property(PropTag::Zpos).unwrap_or(0),
1249                keep_priority(entry.layer),
1250            )
1251        });
1252        stacked
1253    }
1254
1255    /// Whether the cached assignment and canvas plane still stack `stacked`
1256    /// in zpos order, on a CRTC whose planes stack by id.
1257    ///
1258    /// Without a canvas a layer left off the planes is not on screen at all,
1259    /// so it constrains nothing. Upstream fails the check there, which costs
1260    /// a canvas-less scene with a dropped layer its warm start every frame.
1261    fn plane_order_holds(&self, stacked: &[LayerRef<'_>]) -> bool {
1262        let with_canvas = !self.canvas_hosts.is_empty();
1263        let positions: Vec<Position> = stacked
1264            .iter()
1265            .filter_map(|entry| {
1266                let plane = self.previous.get_plane_of(entry.id);
1267                if plane.is_none() && !with_canvas {
1268                    return None;
1269                }
1270                Some(Position {
1271                    zpos: entry.layer.property(PropTag::Zpos).unwrap_or(0),
1272                    plane: plane.or(self.previous_canvas_plane),
1273                    on_canvas: plane.is_none(),
1274                })
1275            })
1276            .collect();
1277        plane_order_consistent(&positions)
1278    }
1279
1280    /// Place zpos-ordered `stacked` on a CRTC whose planes stack by id.
1281    ///
1282    /// Layers map onto planes in id order, and those left over form one
1283    /// contiguous zpos run on [`canvas_plane`](Self::canvas_plane), between
1284    /// the run's neighbors. Most layers placed wins, then the lowest total
1285    /// keep-priority composited; a refused `TEST_ONLY` retries with one placed
1286    /// layer fewer. The spatial split does not apply -- the canvas spans every
1287    /// group -- and cursor planes are left out: their size and update rules
1288    /// are the cursor path's.
1289    ///
1290    /// Conservative for disjoint layers, which keep plane order too.
1291    fn place_in_plane_order<C: TestCommitter>(
1292        &mut self,
1293        stacked: &[LayerRef<'_>],
1294        layers: &[LayerRef<'_>],
1295        registry: &PlaneRegistry,
1296        crtc_index: u32,
1297        committer: &mut C,
1298    ) -> Result<PlaneAssignment, TestFailure> {
1299        // A pinned layer's plane is the scene's.
1300        let pinned: Vec<u32> = layers
1301            .iter()
1302            .filter(|entry| entry.layer.is_pinned())
1303            .filter_map(|entry| entry.layer.assigned_plane())
1304            .collect();
1305        // Left out rather than modeled: a multirect virtual plane, which needs
1306        // its parent armed alongside.
1307        let mut planes: Vec<&PlaneCapabilities> = registry
1308            .for_crtc(crtc_index)
1309            .filter(|plane| {
1310                plane.plane_type != PlaneType::Cursor
1311                    && plane.multirect_parent.is_none()
1312                    && !self.reserved.contains(&plane.id)
1313                    && !pinned.contains(&plane.id)
1314            })
1315            .collect();
1316        planes.sort_by_key(|plane| plane.id);
1317        if stacked.is_empty() {
1318            return Ok(PlaneAssignment::new());
1319        }
1320
1321        let order = PlaneOrder::new(stacked.len(), planes.len(), |i, j| {
1322            let (plane, layer) = (planes[j], stacked[i].layer);
1323            plane_statically_compatible(plane, layer, crtc_index)
1324                && !self.probe_rejected(crtc_index, plane.id, layer)
1325                && self.failure_cache.lookup(plane.id, layer.property_hash()) != Some(false)
1326        });
1327        let forced = stacked
1328            .iter()
1329            .position(|entry| forced_composited(entry.layer))
1330            .zip(
1331                stacked
1332                    .iter()
1333                    .rposition(|entry| forced_composited(entry.layer)),
1334            )
1335            .map(|(first, last)| first..last + 1);
1336        let host_ids = self.canvas_hosts.clone();
1337        let hosts = |j: usize| host_ids.contains(&planes[j].id);
1338        let hosts_canvas: Option<&dyn Fn(usize) -> bool> = (!host_ids.is_empty()).then_some(&hosts);
1339        let cost = |i: usize| i64::from(keep_priority(stacked[i].layer));
1340
1341        let mut max_placed = stacked.len();
1342        loop {
1343            let Some(choice) = order.choose(max_placed, forced.as_ref(), hosts_canvas, cost) else {
1344                return Ok(PlaneAssignment::new());
1345            };
1346            let mut assignment = PlaneAssignment::new();
1347            for (i, entry) in stacked.iter().enumerate() {
1348                if let Some(j) = order.plane_of(i, &choice) {
1349                    assignment.insert(planes[j].id, entry.id);
1350                }
1351            }
1352            self.canvas_plane = choice.canvas.map(|j| planes[j].id);
1353            if assignment.is_empty() {
1354                return Ok(assignment);
1355            }
1356            self.arm_canvas(self.canvas_plane, committer);
1357            let passed = self.try_test(&assignment, stacked, committer);
1358            self.arm_canvas(None, committer);
1359            if passed? {
1360                return Ok(assignment);
1361            }
1362            if self.test_commits_this_frame >= self.max_test_commits {
1363                // Out of tests: composite everything rather than arm an
1364                // untested stack.
1365                self.budget_exhausted_this_frame = true;
1366                self.canvas_plane = order
1367                    .choose(0, forced.as_ref(), hosts_canvas, cost)
1368                    .and_then(|all| all.canvas)
1369                    .map(|j| planes[j].id);
1370                return Ok(PlaneAssignment::new());
1371            }
1372            max_placed = assignment.len() - 1;
1373        }
1374    }
1375
1376    /// Arm the canvas on `plane` in the tests that follow, or disarm it.
1377    fn arm_canvas<C: TestCommitter>(&mut self, plane: Option<u32>, committer: &mut C) {
1378        if let (Some(plane_id), Some(layer)) = (plane, &self.canvas_layer) {
1379            committer.set_extra_plane(plane_id, layer.clone());
1380            self.canvas_armed = Some(plane_id);
1381        } else {
1382            committer.clear_extra_plane();
1383            self.canvas_armed = None;
1384        }
1385    }
1386
1387    /// Whether `planes` placed planes, with the canvas when it is armed, are
1388    /// more than the CRTC can stack. Such an assignment is refused without
1389    /// asking the kernel: the answer is known, and on the SA8155P each such
1390    /// test came back `EINVAL` and spent the search's budget getting there
1391    /// (P-46).
1392    fn over_stage_limit(&self, planes: usize) -> bool {
1393        self.stage_limit
1394            .is_some_and(|limit| planes + usize::from(self.canvas_armed.is_some()) > limit)
1395    }
1396
1397    /// The placeable layers most worth a plane on a CRTC that blends fewer
1398    /// planes than it offers, as the reference's `cap_to_blend_limit` keeps
1399    /// them (drm-cxx#363).
1400    ///
1401    /// Pinned and externally bound layers hold stages of their own, and one is
1402    /// left for the canvas when anything composites -- a layer sent there, or
1403    /// more layers than fit. The rest go by [`keep_priority`], highest first,
1404    /// ties in the caller's order, and the layers past the budget are left out
1405    /// of the search, to be composited. Without this the search learned the
1406    /// limit only by having assignments refused, and the layers it dropped
1407    /// were the most constrained rather than the least important: on the
1408    /// SA8155P, high-priority layers were composited while low-priority ones
1409    /// held planes.
1410    ///
1411    /// Only on a CRTC whose planes take a `zpos`: the full search is not run
1412    /// where planes stack by id.
1413    fn cap_to_blend_limit<'l>(
1414        &self,
1415        placeable: &[LayerRef<'l>],
1416        layers: &[LayerRef<'l>],
1417    ) -> Vec<LayerRef<'l>> {
1418        let Some(limit) = self.stage_limit else {
1419            return placeable.to_vec();
1420        };
1421        let mut held = 0;
1422        let mut composites = false;
1423        for entry in layers {
1424            let layer = entry.layer;
1425            if layer.is_composition_layer() {
1426                continue;
1427            }
1428            if layer.is_pinned() || layer.is_externally_bound() {
1429                held += 1;
1430            } else if forced_composited(layer) {
1431                composites = true;
1432            }
1433        }
1434        // Layers the caller sent to composition are not candidates, as they
1435        // are not in the reference's placeable set.
1436        let candidates: Vec<LayerRef<'l>> = placeable
1437            .iter()
1438            .filter(|entry| !forced_composited(entry.layer))
1439            .copied()
1440            .collect();
1441        if held + candidates.len() > limit {
1442            composites = true;
1443        }
1444        let stages = limit.saturating_sub(held);
1445        let budget = if composites {
1446            stages.saturating_sub(1)
1447        } else {
1448            stages
1449        };
1450        if candidates.len() <= budget {
1451            return placeable.to_vec();
1452        }
1453
1454        let order = |entry: &LayerRef<'_>| {
1455            layers
1456                .iter()
1457                .position(|other| other.id == entry.id)
1458                .unwrap_or(usize::MAX)
1459        };
1460        let mut ranked = candidates;
1461        ranked.sort_by_key(|entry| (core::cmp::Reverse(keep_priority(entry.layer)), order(entry)));
1462        let over: Vec<LayerId> = ranked[budget..].iter().map(|entry| entry.id).collect();
1463        placeable
1464            .iter()
1465            .filter(|entry| !over.contains(&entry.id))
1466            .copied()
1467            .collect()
1468    }
1469
1470    /// Full search: split into independent groups, place each from a shared
1471    /// plane pool.
1472    fn full_search<C: TestCommitter>(
1473        &mut self,
1474        placeable: &[LayerRef<'_>],
1475        registry: &PlaneRegistry,
1476        crtc_index: u32,
1477        committer: &mut C,
1478    ) -> Result<PlaneAssignment, TestFailure> {
1479        let mut available: Vec<&PlaneCapabilities> = registry
1480            .for_crtc(crtc_index)
1481            .filter(|plane| !self.reserved.contains(&plane.id))
1482            .collect();
1483        let layer_refs: Vec<&Layer> = placeable.iter().map(|entry| entry.layer).collect();
1484        let groups = split_independent_groups(&layer_refs);
1485
1486        let mut result = PlaneAssignment::new();
1487        for group in groups {
1488            let members: Vec<LayerRef<'_>> = group.iter().map(|&i| placeable[i]).collect();
1489            let placed = self.place_group(&members, &available, crtc_index, committer)?;
1490
1491            for (plane_id, layer_id) in placed.entries() {
1492                result.insert(*plane_id, *layer_id);
1493            }
1494            // Planes this group took are gone for the next one.
1495            available.retain(|plane| placed.get(plane.id).is_none());
1496        }
1497
1498        Ok(result)
1499    }
1500
1501    /// Place one spatially independent group: preseed, greedy, then backtrack.
1502    fn place_group<C: TestCommitter>(
1503        &mut self,
1504        members: &[LayerRef<'_>],
1505        planes: &[&PlaneCapabilities],
1506        crtc_index: u32,
1507        committer: &mut C,
1508    ) -> Result<PlaneAssignment, TestFailure> {
1509        // Highest keep-priority first, so when planes are scarce the matching
1510        // and the greedy pass keep the higher-priority layers. The matching is
1511        // weight-agnostic about *which* layer it drops, so input order is what
1512        // decides who survives. Stable, so equal priorities keep caller order.
1513        let mut ordered: Vec<LayerRef<'_>> = members.to_vec();
1514        ordered.sort_by(|a, b| keep_priority(b.layer).cmp(&keep_priority(a.layer)));
1515
1516        // --- bipartite preseed --------------------------------------------
1517        let mut assignment = self.preseed(&ordered, planes, crtc_index);
1518        if assignment.is_empty() {
1519            return Ok(assignment);
1520        }
1521        // The matching knows nothing about stacking, so an inverted preseed
1522        // goes straight to the greedy pass rather than to a TEST that would
1523        // accept it. Likewise a multirect virtual plane matched without its
1524        // parent, which the TEST would refuse.
1525        if stacking_consistent_all(planes, &assignment, &ordered)
1526            && multirect_complete(planes, &assignment)
1527            && self.try_test(&assignment, &ordered, committer)?
1528        {
1529            return Ok(assignment);
1530        }
1531
1532        // --- greedy from ranked candidates ---------------------------------
1533        assignment.clear();
1534        let mut candidates = self.rank_candidates(&ordered, planes, crtc_index);
1535        candidates.sort_by(|a, b| b.2.cmp(&a.2));
1536
1537        // Ordinary planes first, then multirect virtual planes, each only
1538        // once the first pass has taken its parent.
1539        let (ordinary, virtual_planes): (Vec<_>, Vec<_>) =
1540            candidates.into_iter().partition(|(plane_id, _, _)| {
1541                caps_in(planes, *plane_id).is_none_or(|plane| plane.multirect_parent.is_none())
1542            });
1543        let mut used_planes: Vec<u32> = Vec::new();
1544        let mut used_layers: Vec<LayerId> = Vec::new();
1545        for (plane_id, layer, _) in ordinary.into_iter().chain(virtual_planes) {
1546            if used_planes.contains(&plane_id) || used_layers.contains(&layer.id) {
1547                continue;
1548            }
1549            if self
1550                .failure_cache
1551                .lookup(plane_id, layer.layer.property_hash())
1552                == Some(false)
1553            {
1554                continue;
1555            }
1556            if self.probe_rejected(crtc_index, plane_id, layer.layer) {
1557                continue;
1558            }
1559            // The kernel accepts an inverted stack, so no TEST would catch
1560            // one: a layer is placed only where it stacks in the order its
1561            // zpos asks relative to what is already placed.
1562            if !fits_stacking(planes, &assignment, &ordered, plane_id, layer.layer)
1563                || !fits_multirect(planes, &assignment, plane_id)
1564            {
1565                continue;
1566            }
1567            assignment.insert(plane_id, layer.id);
1568            used_planes.push(plane_id);
1569            used_layers.push(layer.id);
1570        }
1571
1572        if assignment.is_empty() {
1573            return Ok(assignment);
1574        }
1575        if self.try_test(&assignment, &ordered, committer)? {
1576            return Ok(assignment);
1577        }
1578
1579        // --- backtrack: drop lowest keep-priority first ---------------------
1580        let mut by_priority: Vec<(u32, LayerId)> = assignment.entries().to_vec();
1581        by_priority.sort_by_key(|(_, layer_id)| {
1582            ordered
1583                .iter()
1584                .find(|entry| entry.id == *layer_id)
1585                .map_or(i32::MAX, |entry| keep_priority(entry.layer))
1586        });
1587
1588        for (plane_id, _) in by_priority {
1589            if assignment.remove(plane_id).is_none() {
1590                continue; // already dropped as an orphaned multirect child
1591            }
1592            // Dropping a multirect parent orphans its virtual plane; drop that
1593            // too.
1594            let children: Vec<u32> = assignment
1595                .entries()
1596                .iter()
1597                .map(|(id, _)| *id)
1598                .filter(|id| {
1599                    caps_in(planes, *id).is_some_and(|c| c.multirect_parent == Some(plane_id))
1600                })
1601                .collect();
1602            for child in children {
1603                assignment.remove(child);
1604            }
1605            if assignment.is_empty() {
1606                break;
1607            }
1608            if self.try_test(&assignment, &ordered, committer)? {
1609                break;
1610            }
1611            if self.test_commits_this_frame >= self.max_test_commits {
1612                self.budget_exhausted_this_frame = true;
1613                break;
1614            }
1615        }
1616
1617        Ok(assignment)
1618    }
1619
1620    /// Run one test commit, honoring the per-frame budget and recording the
1621    /// verdict.
1622    fn try_test<C: TestCommitter>(
1623        &mut self,
1624        assignment: &PlaneAssignment,
1625        present: &[LayerRef<'_>],
1626        committer: &mut C,
1627    ) -> Result<bool, TestFailure> {
1628        if self.test_commits_this_frame >= self.max_test_commits {
1629            self.budget_exhausted_this_frame = true;
1630            return Ok(false);
1631        }
1632        let pairs = Self::pairs_for(assignment, present);
1633        if pairs.len() != assignment.len() || self.over_stage_limit(pairs.len()) {
1634            return Ok(false);
1635        }
1636
1637        self.test_commits_this_frame += 1;
1638        match committer.test_assignment(&pairs) {
1639            Ok(()) => {
1640                for (plane_id, layer) in &pairs {
1641                    self.failure_cache
1642                        .record(*plane_id, layer.layer.property_hash(), true);
1643                }
1644                Ok(true)
1645            }
1646            Err(TestFailure::NotMaster) => Err(TestFailure::NotMaster),
1647            Err(TestFailure::Rejected) => {
1648                for (plane_id, layer) in &pairs {
1649                    self.failure_cache
1650                        .record(*plane_id, layer.layer.property_hash(), false);
1651                }
1652                Ok(false)
1653            }
1654        }
1655    }
1656
1657    /// Maximum-cardinality preseed over the statically compatible edges.
1658    fn preseed(
1659        &mut self,
1660        ordered: &[LayerRef<'_>],
1661        planes: &[&PlaneCapabilities],
1662        crtc_index: u32,
1663    ) -> PlaneAssignment {
1664        self.matcher.reset(ordered.len(), planes.len());
1665
1666        for (left, entry) in ordered.iter().enumerate() {
1667            for (right, plane) in planes.iter().enumerate() {
1668                if !plane_statically_compatible(plane, entry.layer, crtc_index) {
1669                    continue;
1670                }
1671                if self
1672                    .failure_cache
1673                    .lookup(plane.id, entry.layer.property_hash())
1674                    == Some(false)
1675                {
1676                    continue;
1677                }
1678                if self.probe_rejected(crtc_index, plane.id, entry.layer) {
1679                    continue;
1680                }
1681                let score = self.score(plane, entry);
1682                self.matcher.add_scored_edge(left, right, score);
1683            }
1684        }
1685
1686        self.matcher.solve();
1687
1688        let mut assignment = PlaneAssignment::new();
1689        for (left, entry) in ordered.iter().enumerate() {
1690            if let Some(right) = self.matcher.match_for_left(left) {
1691                assignment.insert(planes[right].id, entry.id);
1692            }
1693        }
1694        assignment
1695    }
1696
1697    /// Every statically compatible `(plane, layer)` pair with its score.
1698    fn rank_candidates<'a>(
1699        &self,
1700        ordered: &[LayerRef<'a>],
1701        planes: &[&PlaneCapabilities],
1702        crtc_index: u32,
1703    ) -> Vec<(u32, LayerRef<'a>, i32)> {
1704        let mut candidates = Vec::new();
1705        for entry in ordered {
1706            for plane in planes {
1707                if plane_statically_compatible(plane, entry.layer, crtc_index) {
1708                    candidates.push((plane.id, *entry, self.score(plane, entry)));
1709                }
1710            }
1711        }
1712        candidates
1713    }
1714
1715    /// Score a candidate, folding in the warm-start bonus and failure history.
1716    fn score(&self, plane: &PlaneCapabilities, entry: &LayerRef<'_>) -> i32 {
1717        let held_last_frame = self.previous_valid && self.previous.get(plane.id) == Some(entry.id);
1718        score_pair(
1719            plane,
1720            entry.layer,
1721            ScoreContext {
1722                held_last_frame,
1723                failure_hits: self
1724                    .failure_cache
1725                    .failure_count(plane.id, entry.layer.property_hash()),
1726            },
1727        )
1728    }
1729
1730    /// Whether a prior single-plane probe proved this layer's
1731    /// `(fourcc, modifier)` cannot scan out here — a lying `IN_FORMATS`.
1732    fn probe_rejected(&self, crtc_index: u32, plane_id: u32, layer: &Layer) -> bool {
1733        let Some(fourcc) = layer.format() else {
1734            return false;
1735        };
1736        self.probe_cache
1737            .lookup(crtc_index, plane_id, fourcc, Modifier(layer.modifier()))
1738            == Verdict::Rejected
1739    }
1740
1741    /// Record that a plane rejected a `(fourcc, modifier)` pair, so a lying
1742    /// `IN_FORMATS` costs one probe rather than a dropped edge re-probed every
1743    /// frame.
1744    pub fn record_probe_rejection(&mut self, crtc_index: u32, plane_id: u32, layer: &Layer) {
1745        if let Some(fourcc) = layer.format() {
1746            self.probe_cache.record(
1747                crtc_index,
1748                plane_id,
1749                fourcc,
1750                Modifier(layer.modifier()),
1751                false,
1752            );
1753        }
1754    }
1755}
1756
1757/// Whether the caller or the scene has sent `layer` to the canvas, whatever
1758/// the planes could take.
1759const fn forced_composited(layer: &Layer) -> bool {
1760    layer.is_force_composited() || layer.is_transient_composited()
1761}
1762
1763/// Whether `layer` on `plane_id` stacks consistently with everything already
1764/// in `assignment` (see [`stacking_consistent`](crate::stacking_consistent)).
1765///
1766/// A plane missing from `planes` cannot be ruled on and is not held against
1767/// the layer, which is what a plane without a zpos property gets as well.
1768fn fits_stacking(
1769    planes: &[&PlaneCapabilities],
1770    assignment: &PlaneAssignment,
1771    present: &[LayerRef<'_>],
1772    plane_id: u32,
1773    layer: &Layer,
1774) -> bool {
1775    let caps = |id: u32| planes.iter().find(|plane| plane.id == id).copied();
1776    let Some(plane) = caps(plane_id) else {
1777        return true;
1778    };
1779    let zpos = layer.property(PropTag::Zpos);
1780    assignment.entries().iter().all(|(other_plane, other_id)| {
1781        if *other_plane == plane_id {
1782            return true;
1783        }
1784        let (Some(other), Some(entry)) = (
1785            caps(*other_plane),
1786            present.iter().find(|entry| entry.id == *other_id),
1787        ) else {
1788            return true;
1789        };
1790        crate::stacking_consistent(plane, zpos, other, entry.layer.property(PropTag::Zpos))
1791    })
1792}
1793
1794/// The capabilities of `plane_id` among `planes`.
1795fn caps_in<'a>(planes: &[&'a PlaneCapabilities], plane_id: u32) -> Option<&'a PlaneCapabilities> {
1796    planes.iter().find(|plane| plane.id == plane_id).copied()
1797}
1798
1799/// Whether `plane_id` may join `assignment`: not a multirect virtual plane, or
1800/// one whose parent is already in it (see
1801/// [`multirect_pairing_ok`](crate::multirect_pairing_ok)).
1802///
1803/// As with stacking, a plane missing from `planes` is not held against it.
1804fn fits_multirect(
1805    planes: &[&PlaneCapabilities],
1806    assignment: &PlaneAssignment,
1807    plane_id: u32,
1808) -> bool {
1809    caps_in(planes, plane_id).is_none_or(|plane| {
1810        crate::multirect_pairing_ok(plane.multirect_parent, |parent| {
1811            assignment.get(parent).is_some()
1812        })
1813    })
1814}
1815
1816/// Whether no multirect virtual plane in `assignment` is without its parent.
1817fn multirect_complete(planes: &[&PlaneCapabilities], assignment: &PlaneAssignment) -> bool {
1818    assignment
1819        .entries()
1820        .iter()
1821        .all(|(plane_id, _)| fits_multirect(planes, assignment, *plane_id))
1822}
1823
1824/// Whether every placement in `assignment` stacks consistently.
1825fn stacking_consistent_all(
1826    planes: &[&PlaneCapabilities],
1827    assignment: &PlaneAssignment,
1828    present: &[LayerRef<'_>],
1829) -> bool {
1830    assignment.entries().iter().all(|(plane_id, layer_id)| {
1831        present
1832            .iter()
1833            .find(|entry| entry.id == *layer_id)
1834            .is_none_or(|entry| fits_stacking(planes, assignment, present, *plane_id, entry.layer))
1835    })
1836}
1837
1838/// Which side of the one composition canvas the placed layers must keep.
1839///
1840/// A placed layer that overlaps a composited one stays on the side of the
1841/// canvas its zpos asks for: `below` is the highest zpos that must stay under
1842/// the canvas, `above` the lowest that must cover it. Port of upstream's
1843/// `Allocator::CanvasBounds` (`07226e4`).
1844#[derive(Debug, Clone, Copy, Default)]
1845struct CanvasBounds {
1846    below: Option<u64>,
1847    above: Option<u64>,
1848}
1849
1850impl CanvasBounds {
1851    /// Whether one canvas zpos satisfies every placed layer: not when one
1852    /// would have to sit on both sides.
1853    fn feasible(self) -> bool {
1854        match (self.below, self.above) {
1855            (Some(below), Some(above)) => below < above,
1856            _ => true,
1857        }
1858    }
1859}
1860
1861/// The layers `assignment` places, with the pinned layers on planes of their
1862/// own, and the layers it leaves to the canvas.
1863fn placed_and_composited<'a>(
1864    assignment: &PlaneAssignment,
1865    layers: &[LayerRef<'a>],
1866) -> (Vec<&'a Layer>, Vec<&'a Layer>) {
1867    let mut placed = Vec::new();
1868    let mut composited = Vec::new();
1869    for entry in layers {
1870        let layer = entry.layer;
1871        if layer.is_composition_layer() || layer.is_externally_bound() {
1872            continue;
1873        }
1874        if assignment.get_plane_of(entry.id).is_some() {
1875            placed.push(layer);
1876        } else if layer.is_pinned() {
1877            if layer
1878                .assigned_plane()
1879                .is_some_and(|plane_id| assignment.get(plane_id).is_none())
1880            {
1881                placed.push(layer);
1882            }
1883        } else {
1884            composited.push(layer);
1885        }
1886    }
1887    (placed, composited)
1888}
1889
1890/// The canvas bounds `assignment` imposes on the layers it leaves composited.
1891fn canvas_bounds(assignment: &PlaneAssignment, layers: &[LayerRef<'_>]) -> CanvasBounds {
1892    let (placed, composited) = placed_and_composited(assignment, layers);
1893    let mut bounds = CanvasBounds::default();
1894    for placed in &placed {
1895        let Some(zp) = placed.property(PropTag::Zpos) else {
1896            continue;
1897        };
1898        for composited in &composited {
1899            let Some(zc) = composited.property(PropTag::Zpos) else {
1900                continue;
1901            };
1902            if zc == zp || !layers_intersect(placed, composited) {
1903                continue;
1904            }
1905            if zc < zp {
1906                bounds.above = Some(bounds.above.map_or(zp, |above| above.min(zp)));
1907            } else {
1908                bounds.below = Some(bounds.below.map_or(zp, |below| below.max(zp)));
1909            }
1910        }
1911    }
1912    bounds
1913}
1914
1915/// Move placed layers into the composition until the canvas bounds are
1916/// feasible: each round, the lowest keep-priority layer caught between them
1917/// that overlaps a composited one, with any multirect virtual plane riding on
1918/// its plane. `false` when nothing placed by the search is caught, which
1919/// leaves the pinned layers.
1920fn resolve_canvas_bounds(
1921    assignment: &mut PlaneAssignment,
1922    layers: &[LayerRef<'_>],
1923    planes: &[&PlaneCapabilities],
1924) -> bool {
1925    loop {
1926        let bounds = canvas_bounds(assignment, layers);
1927        let (Some(above), Some(below)) = (bounds.above, bounds.below) else {
1928            return true;
1929        };
1930        if below < above {
1931            return true;
1932        }
1933        let (_, composited) = placed_and_composited(assignment, layers);
1934        let mut victim: Option<(u32, i32)> = None;
1935        for (plane_id, layer_id) in assignment.entries() {
1936            let Some(entry) = layers.iter().find(|entry| entry.id == *layer_id) else {
1937                continue;
1938            };
1939            let Some(z) = entry.layer.property(PropTag::Zpos) else {
1940                continue;
1941            };
1942            let caught = z >= above
1943                && z <= below
1944                && composited.iter().any(|c| {
1945                    c.property(PropTag::Zpos)
1946                        .is_some_and(|zc| zc != z && layers_intersect(entry.layer, c))
1947                });
1948            if !caught {
1949                continue;
1950            }
1951            let keep = keep_priority(entry.layer);
1952            if victim.is_none_or(|(_, victim_keep)| keep < victim_keep) {
1953                victim = Some((*plane_id, keep));
1954            }
1955        }
1956        let Some((victim, _)) = victim else {
1957            return false;
1958        };
1959        assignment.remove(victim);
1960        let children: Vec<u32> = assignment
1961            .entries()
1962            .iter()
1963            .map(|(id, _)| *id)
1964            .filter(|id| caps_in(planes, *id).is_some_and(|c| c.multirect_parent == Some(victim)))
1965            .collect();
1966        for child in children {
1967            assignment.remove(child);
1968        }
1969    }
1970}