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 searched = self
915                .full_search(&placeable, registry, crtc_index, committer)
916                .and_then(|searched| {
917                    self.keep_canvas_bounds(
918                        searched, &placeable, layers, registry, crtc_index, committer,
919                    )
920                });
921            self.arm_canvas(None, committer);
922            searched?
923        };
924        let composited = Self::to_composite(layers, &assignment);
925        if composited.is_empty() {
926            self.canvas_plane = None;
927        }
928
929        self.previous = assignment.clone();
930        self.previous_valid = !assignment.is_empty();
931        self.previous_canvas_plane = self.canvas_plane;
932        self.previous_composited = Self::unplaced(&placeable, &assignment);
933        self.previous_free = self.free_planes(&self.previous, layers, registry, crtc_index);
934
935        Ok(Allocation {
936            assignment,
937            composited,
938            diagnostics: Diagnostics {
939                test_commits_issued: self.test_commits_this_frame,
940                fb_delta_fast_path: false,
941                budget_exhausted: self.budget_exhausted_this_frame,
942            },
943        })
944    }
945
946    /// Re-test the cached assignment, with the canvas armed on `canvas`.
947    /// `None` when the kernel refuses it, which drops the cache for a full
948    /// search.
949    fn warm_start<C: TestCommitter>(
950        &mut self,
951        placeable: &[LayerRef<'_>],
952        layers: &[LayerRef<'_>],
953        canvas: Option<u32>,
954        committer: &mut C,
955    ) -> Result<Option<Allocation>, TestFailure> {
956        let pairs = Self::pairs_for(&self.previous, placeable);
957        if pairs.len() != self.previous.len()
958            || self.stage_limit.is_some_and(|limit| {
959                pairs.len() + usize::from(canvas.is_some() && self.canvas_layer.is_some()) > limit
960            })
961        {
962            return Ok(None);
963        }
964        self.test_commits_this_frame += 1;
965        self.arm_canvas(canvas, committer);
966        let verdict = committer.test_assignment(&pairs);
967        self.arm_canvas(None, committer);
968        match verdict {
969            Ok(()) => {
970                let assignment = self.previous.clone();
971                let composited = Self::to_composite(layers, &assignment);
972                // The plane the test armed is the canvas's now, and stays
973                // paired with the cached assignment.
974                self.canvas_plane = canvas.filter(|_| !composited.is_empty());
975                self.previous_canvas_plane = self.canvas_plane;
976                Ok(Some(Allocation {
977                    assignment,
978                    composited,
979                    diagnostics: Diagnostics {
980                        test_commits_issued: self.test_commits_this_frame,
981                        fb_delta_fast_path: false,
982                        budget_exhausted: false,
983                    },
984                }))
985            }
986            Err(TestFailure::NotMaster) => Err(TestFailure::NotMaster),
987            Err(TestFailure::Rejected) => {
988                self.previous_valid = false;
989                Ok(None)
990            }
991        }
992    }
993
994    /// A full search's assignment, or one the one canvas can stack with.
995    ///
996    /// One canvas stacks at one zpos. When a placed layer would have to sit
997    /// on both sides of it, composite one contiguous zpos run instead;
998    /// failing that, move the caught layers into the composition and keep the
999    /// smaller set if the kernel takes it. Otherwise the search's assignment
1000    /// stands, and the canvas stacks on top. Port of upstream's
1001    /// `full_search` tail (`07226e4`).
1002    fn keep_canvas_bounds<C: TestCommitter>(
1003        &mut self,
1004        searched: PlaneAssignment,
1005        placeable: &[LayerRef<'_>],
1006        layers: &[LayerRef<'_>],
1007        registry: &PlaneRegistry,
1008        crtc_index: u32,
1009        committer: &mut C,
1010    ) -> Result<PlaneAssignment, TestFailure> {
1011        if canvas_bounds(&searched, layers).feasible() {
1012            return Ok(searched);
1013        }
1014        if let Some(run) =
1015            self.place_around_run(searched.len(), layers, registry, crtc_index, committer)?
1016        {
1017            return Ok(run);
1018        }
1019        let planes: Vec<&PlaneCapabilities> = registry.for_crtc(crtc_index).collect();
1020        let mut resolved = searched.clone();
1021        if resolve_canvas_bounds(&mut resolved, layers, &planes)
1022            && (resolved.is_empty() || self.try_test(&resolved, placeable, committer)?)
1023        {
1024            return Ok(resolved);
1025        }
1026        Ok(searched)
1027    }
1028
1029    /// Place every layer outside one contiguous zpos run of at least the
1030    /// layers the search left unplaced: for each run length, smallest first,
1031    /// the run with the lowest total keep-priority, the higher on a tie (a
1032    /// run at the top keeps the canvas on top). Layers the caller or the
1033    /// scene sent to the canvas must be in the run. `None` when no run leaves
1034    /// the canvas bounds feasible within the test budget.
1035    fn place_around_run<C: TestCommitter>(
1036        &mut self,
1037        placed: usize,
1038        layers: &[LayerRef<'_>],
1039        registry: &PlaneRegistry,
1040        crtc_index: u32,
1041        committer: &mut C,
1042    ) -> Result<Option<PlaneAssignment>, TestFailure> {
1043        let mut candidates: Vec<LayerRef<'_>> = layers
1044            .iter()
1045            .filter(|entry| {
1046                !entry.layer.is_composition_layer()
1047                    && !entry.layer.is_externally_bound()
1048                    && !entry.layer.is_pinned()
1049            })
1050            .copied()
1051            .collect();
1052        candidates.sort_by_key(|entry| entry.layer.property(PropTag::Zpos).unwrap_or(0));
1053        let n = candidates.len();
1054        let forced: Vec<usize> = (0..n)
1055            .filter(|&i| forced_composited(candidates[i].layer))
1056            .collect();
1057        let available: Vec<&PlaneCapabilities> = registry
1058            .for_crtc(crtc_index)
1059            .filter(|plane| !self.reserved.contains(&plane.id))
1060            .collect();
1061
1062        for width in (n - placed.min(n)).max(1)..n {
1063            if self.test_commits_this_frame >= self.max_test_commits {
1064                self.budget_exhausted_this_frame = true;
1065                break;
1066            }
1067            let mut best: Option<(usize, i64)> = None;
1068            for start in 0..=n - width {
1069                let covers_forced = forced.iter().all(|&i| (start..start + width).contains(&i));
1070                if !covers_forced {
1071                    continue;
1072                }
1073                let cost: i64 = candidates[start..start + width]
1074                    .iter()
1075                    .map(|entry| i64::from(keep_priority(entry.layer)))
1076                    .sum();
1077                if best.is_none_or(|(_, best_cost)| cost <= best_cost) {
1078                    best = Some((start, cost));
1079                }
1080            }
1081            let Some((start, _)) = best else {
1082                continue;
1083            };
1084            let outside: Vec<LayerRef<'_>> = candidates
1085                .iter()
1086                .enumerate()
1087                .filter(|(i, _)| !(start..start + width).contains(i))
1088                .map(|(_, entry)| *entry)
1089                .collect();
1090            let assignment = self.place_group(&outside, &available, crtc_index, committer)?;
1091            if !assignment.is_empty() && canvas_bounds(&assignment, layers).feasible() {
1092                return Ok(Some(assignment));
1093            }
1094        }
1095        Ok(None)
1096    }
1097
1098    /// Whether a layer present this frame needs a full search to have a
1099    /// fair shot at a plane: the previous frame did not place it, and either
1100    /// did not composite it either, or something the last search decided on
1101    /// has changed in a way that could let it place more.
1102    ///
1103    /// Both cached paths iterate the *previous* assignment to decide what to
1104    /// emit, so neither can place a layer that is not already in it:
1105    /// warm-start would succeed with the old set, the new layer would be
1106    /// composited, and the cached assignment would never grow -- so the same
1107    /// fate would hit every subsequent frame.
1108    ///
1109    /// A layer composited last frame is not new (drm-cxx `6e58d23`): counting
1110    /// it as one forced a full search on every frame composition was active.
1111    /// The cached paths composite it again, on the last search's verdict --
1112    /// unless a composited layer has gone, which can leave the rest few enough
1113    /// to fit, or a plane has come free that the search did not have. Upstream
1114    /// fixed the same trap in drm-cxx#341, searching after any shrink that
1115    /// leaves composition running.
1116    ///
1117    /// A free plane alone is no reason: on a controller that lights fewer
1118    /// planes than it offers (RK3566 VOP2: three eligible, two usable) the
1119    /// search composited those layers with that plane free, and would again.
1120    fn has_new_layer(
1121        &self,
1122        placeable: &[LayerRef<'_>],
1123        layers: &[LayerRef<'_>],
1124        registry: &PlaneRegistry,
1125        crtc_index: u32,
1126    ) -> bool {
1127        let mut unplaced = 0;
1128        for entry in placeable {
1129            if self.previous.get_plane_of(entry.id).is_some() {
1130                continue;
1131            }
1132            if !self.previous_composited.contains(&entry.id) {
1133                return true;
1134            }
1135            unplaced += 1;
1136        }
1137        if unplaced == 0 {
1138            return false;
1139        }
1140        unplaced < self.previous_composited.len()
1141            || self
1142                .free_planes(&self.previous, layers, registry, crtc_index)
1143                .iter()
1144                .any(|plane| !self.previous_free.contains(plane))
1145    }
1146
1147    /// The planes on `crtc_index` a search could still use beside
1148    /// `assignment`: not a cursor, not reserved, not a pinned layer's, not the canvas's.
1149    fn free_planes(
1150        &self,
1151        assignment: &PlaneAssignment,
1152        layers: &[LayerRef<'_>],
1153        registry: &PlaneRegistry,
1154        crtc_index: u32,
1155    ) -> Vec<u32> {
1156        let pinned: Vec<u32> = layers
1157            .iter()
1158            .filter(|entry| entry.layer.is_pinned())
1159            .filter_map(|entry| entry.layer.assigned_plane())
1160            .collect();
1161        registry
1162            .for_crtc(crtc_index)
1163            .filter(|plane| {
1164                plane.plane_type != PlaneType::Cursor
1165                    && !self.reserved.contains(&plane.id)
1166                    && !pinned.contains(&plane.id)
1167                    && assignment.get(plane.id).is_none()
1168                    // The canvas's plane is taken, held back or not.
1169                    && self.previous_canvas_plane != Some(plane.id)
1170            })
1171            .map(|plane| plane.id)
1172            .collect()
1173    }
1174
1175    /// Whether every layer in the cached assignment is still present.
1176    fn previous_still_applies(&self, present: &[LayerRef<'_>]) -> bool {
1177        self.previous
1178            .entries()
1179            .iter()
1180            .all(|(_, id)| present.iter().any(|entry| entry.id == *id))
1181    }
1182
1183    /// Resolve an assignment's layer ids against this frame's layers.
1184    fn pairs_for<'a>(
1185        assignment: &PlaneAssignment,
1186        present: &[LayerRef<'a>],
1187    ) -> Vec<(u32, LayerRef<'a>)> {
1188        assignment
1189            .entries()
1190            .iter()
1191            .filter_map(|(plane_id, layer_id)| {
1192                present
1193                    .iter()
1194                    .find(|entry| entry.id == *layer_id)
1195                    .map(|entry| (*plane_id, *entry))
1196            })
1197            .collect()
1198    }
1199
1200    /// What the caller must composite: every layer without a plane, as
1201    /// upstream marks them after each pass -- transient-composited layers
1202    /// included, which the search never sees -- except the composition
1203    /// layer itself and the layers whose planes the scene owns. In stacking
1204    /// order, ties in the order given.
1205    fn to_composite(layers: &[LayerRef<'_>], assignment: &PlaneAssignment) -> Vec<LayerId> {
1206        let mut unplaced: Vec<&LayerRef<'_>> = layers
1207            .iter()
1208            .filter(|entry| {
1209                !entry.layer.is_composition_layer()
1210                    && !entry.layer.is_externally_bound()
1211                    && !entry.layer.is_pinned()
1212                    && !assignment.entries().iter().any(|(_, id)| *id == entry.id)
1213            })
1214            .collect();
1215        unplaced.sort_by_key(|entry| entry.layer.property(PropTag::Zpos).unwrap_or(0));
1216        unplaced.into_iter().map(|entry| entry.id).collect()
1217    }
1218
1219    /// Searched layers that did not get a plane: the cache's record of what
1220    /// the last search composited, which is what the next frame's
1221    /// [`has_new_layer`](Self::has_new_layer) compares against.
1222    fn unplaced(placeable: &[LayerRef<'_>], assignment: &PlaneAssignment) -> Vec<LayerId> {
1223        placeable
1224            .iter()
1225            .filter(|entry| !assignment.entries().iter().any(|(_, id)| *id == entry.id))
1226            .map(|entry| entry.id)
1227            .collect()
1228    }
1229
1230    /// Every layer that reaches the screen through the allocator, in the one
1231    /// order planes stacked by id can show them: transient-composited layers
1232    /// too, which land on the canvas.
1233    ///
1234    /// Equal zpos asks for no order, so ties go lowest keep-priority first:
1235    /// the composited run has to be contiguous, and this is what lets it take
1236    /// the low-priority layers of a tie rather than whichever the caller added
1237    /// first. Upstream keeps caller order there. Stable, so equal priorities
1238    /// keep caller order.
1239    fn plane_stack<'a>(layers: &[LayerRef<'a>]) -> Vec<LayerRef<'a>> {
1240        let mut stacked: Vec<LayerRef<'a>> = layers
1241            .iter()
1242            .filter(|entry| !entry.layer.is_externally_bound() && !entry.layer.is_pinned())
1243            .copied()
1244            .collect();
1245        stacked.sort_by_key(|entry| {
1246            (
1247                entry.layer.property(PropTag::Zpos).unwrap_or(0),
1248                keep_priority(entry.layer),
1249            )
1250        });
1251        stacked
1252    }
1253
1254    /// Whether the cached assignment and canvas plane still stack `stacked`
1255    /// in zpos order, on a CRTC whose planes stack by id.
1256    ///
1257    /// Without a canvas a layer left off the planes is not on screen at all,
1258    /// so it constrains nothing. Upstream fails the check there, which costs
1259    /// a canvas-less scene with a dropped layer its warm start every frame.
1260    fn plane_order_holds(&self, stacked: &[LayerRef<'_>]) -> bool {
1261        let with_canvas = !self.canvas_hosts.is_empty();
1262        let positions: Vec<Position> = stacked
1263            .iter()
1264            .filter_map(|entry| {
1265                let plane = self.previous.get_plane_of(entry.id);
1266                if plane.is_none() && !with_canvas {
1267                    return None;
1268                }
1269                Some(Position {
1270                    zpos: entry.layer.property(PropTag::Zpos).unwrap_or(0),
1271                    plane: plane.or(self.previous_canvas_plane),
1272                    on_canvas: plane.is_none(),
1273                })
1274            })
1275            .collect();
1276        plane_order_consistent(&positions)
1277    }
1278
1279    /// Place zpos-ordered `stacked` on a CRTC whose planes stack by id.
1280    ///
1281    /// Layers map onto planes in id order, and those left over form one
1282    /// contiguous zpos run on [`canvas_plane`](Self::canvas_plane), between
1283    /// the run's neighbors. Most layers placed wins, then the lowest total
1284    /// keep-priority composited; a refused `TEST_ONLY` retries with one placed
1285    /// layer fewer. The spatial split does not apply -- the canvas spans every
1286    /// group -- and cursor planes are left out: their size and update rules
1287    /// are the cursor path's.
1288    ///
1289    /// Conservative for disjoint layers, which keep plane order too.
1290    fn place_in_plane_order<C: TestCommitter>(
1291        &mut self,
1292        stacked: &[LayerRef<'_>],
1293        layers: &[LayerRef<'_>],
1294        registry: &PlaneRegistry,
1295        crtc_index: u32,
1296        committer: &mut C,
1297    ) -> Result<PlaneAssignment, TestFailure> {
1298        // A pinned layer's plane is the scene's.
1299        let pinned: Vec<u32> = layers
1300            .iter()
1301            .filter(|entry| entry.layer.is_pinned())
1302            .filter_map(|entry| entry.layer.assigned_plane())
1303            .collect();
1304        // Left out rather than modeled: a multirect virtual plane, which needs
1305        // its parent armed alongside.
1306        let mut planes: Vec<&PlaneCapabilities> = registry
1307            .for_crtc(crtc_index)
1308            .filter(|plane| {
1309                plane.plane_type != PlaneType::Cursor
1310                    && plane.multirect_parent.is_none()
1311                    && !self.reserved.contains(&plane.id)
1312                    && !pinned.contains(&plane.id)
1313            })
1314            .collect();
1315        planes.sort_by_key(|plane| plane.id);
1316        if stacked.is_empty() {
1317            return Ok(PlaneAssignment::new());
1318        }
1319
1320        let order = PlaneOrder::new(stacked.len(), planes.len(), |i, j| {
1321            let (plane, layer) = (planes[j], stacked[i].layer);
1322            plane_statically_compatible(plane, layer, crtc_index)
1323                && !self.probe_rejected(crtc_index, plane.id, layer)
1324                && self.failure_cache.lookup(plane.id, layer.property_hash()) != Some(false)
1325        });
1326        let forced = stacked
1327            .iter()
1328            .position(|entry| forced_composited(entry.layer))
1329            .zip(
1330                stacked
1331                    .iter()
1332                    .rposition(|entry| forced_composited(entry.layer)),
1333            )
1334            .map(|(first, last)| first..last + 1);
1335        let host_ids = self.canvas_hosts.clone();
1336        let hosts = |j: usize| host_ids.contains(&planes[j].id);
1337        let hosts_canvas: Option<&dyn Fn(usize) -> bool> = (!host_ids.is_empty()).then_some(&hosts);
1338        let cost = |i: usize| i64::from(keep_priority(stacked[i].layer));
1339
1340        let mut max_placed = stacked.len();
1341        loop {
1342            let Some(choice) = order.choose(max_placed, forced.as_ref(), hosts_canvas, cost) else {
1343                return Ok(PlaneAssignment::new());
1344            };
1345            let mut assignment = PlaneAssignment::new();
1346            for (i, entry) in stacked.iter().enumerate() {
1347                if let Some(j) = order.plane_of(i, &choice) {
1348                    assignment.insert(planes[j].id, entry.id);
1349                }
1350            }
1351            self.canvas_plane = choice.canvas.map(|j| planes[j].id);
1352            if assignment.is_empty() {
1353                return Ok(assignment);
1354            }
1355            self.arm_canvas(self.canvas_plane, committer);
1356            let passed = self.try_test(&assignment, stacked, committer);
1357            self.arm_canvas(None, committer);
1358            if passed? {
1359                return Ok(assignment);
1360            }
1361            if self.test_commits_this_frame >= self.max_test_commits {
1362                // Out of tests: composite everything rather than arm an
1363                // untested stack.
1364                self.budget_exhausted_this_frame = true;
1365                self.canvas_plane = order
1366                    .choose(0, forced.as_ref(), hosts_canvas, cost)
1367                    .and_then(|all| all.canvas)
1368                    .map(|j| planes[j].id);
1369                return Ok(PlaneAssignment::new());
1370            }
1371            max_placed = assignment.len() - 1;
1372        }
1373    }
1374
1375    /// Arm the canvas on `plane` in the tests that follow, or disarm it.
1376    fn arm_canvas<C: TestCommitter>(&mut self, plane: Option<u32>, committer: &mut C) {
1377        if let (Some(plane_id), Some(layer)) = (plane, &self.canvas_layer) {
1378            committer.set_extra_plane(plane_id, layer.clone());
1379            self.canvas_armed = Some(plane_id);
1380        } else {
1381            committer.clear_extra_plane();
1382            self.canvas_armed = None;
1383        }
1384    }
1385
1386    /// Whether `planes` placed planes, with the canvas when it is armed, are
1387    /// more than the CRTC can stack. Such an assignment is refused without
1388    /// asking the kernel: the answer is known, and on the SA8155P each such
1389    /// test came back `EINVAL` and spent the search's budget getting there
1390    /// (P-46).
1391    fn over_stage_limit(&self, planes: usize) -> bool {
1392        self.stage_limit
1393            .is_some_and(|limit| planes + usize::from(self.canvas_armed.is_some()) > limit)
1394    }
1395
1396    /// Full search: split into independent groups, place each from a shared
1397    /// plane pool.
1398    fn full_search<C: TestCommitter>(
1399        &mut self,
1400        placeable: &[LayerRef<'_>],
1401        registry: &PlaneRegistry,
1402        crtc_index: u32,
1403        committer: &mut C,
1404    ) -> Result<PlaneAssignment, TestFailure> {
1405        let mut available: Vec<&PlaneCapabilities> = registry
1406            .for_crtc(crtc_index)
1407            .filter(|plane| !self.reserved.contains(&plane.id))
1408            .collect();
1409        let layer_refs: Vec<&Layer> = placeable.iter().map(|entry| entry.layer).collect();
1410        let groups = split_independent_groups(&layer_refs);
1411
1412        let mut result = PlaneAssignment::new();
1413        for group in groups {
1414            let members: Vec<LayerRef<'_>> = group.iter().map(|&i| placeable[i]).collect();
1415            let placed = self.place_group(&members, &available, crtc_index, committer)?;
1416
1417            for (plane_id, layer_id) in placed.entries() {
1418                result.insert(*plane_id, *layer_id);
1419            }
1420            // Planes this group took are gone for the next one.
1421            available.retain(|plane| placed.get(plane.id).is_none());
1422        }
1423
1424        Ok(result)
1425    }
1426
1427    /// Place one spatially independent group: preseed, greedy, then backtrack.
1428    fn place_group<C: TestCommitter>(
1429        &mut self,
1430        members: &[LayerRef<'_>],
1431        planes: &[&PlaneCapabilities],
1432        crtc_index: u32,
1433        committer: &mut C,
1434    ) -> Result<PlaneAssignment, TestFailure> {
1435        // Highest keep-priority first, so when planes are scarce the matching
1436        // and the greedy pass keep the higher-priority layers. The matching is
1437        // weight-agnostic about *which* layer it drops, so input order is what
1438        // decides who survives. Stable, so equal priorities keep caller order.
1439        let mut ordered: Vec<LayerRef<'_>> = members.to_vec();
1440        ordered.sort_by(|a, b| keep_priority(b.layer).cmp(&keep_priority(a.layer)));
1441
1442        // --- bipartite preseed --------------------------------------------
1443        let mut assignment = self.preseed(&ordered, planes, crtc_index);
1444        if assignment.is_empty() {
1445            return Ok(assignment);
1446        }
1447        // The matching knows nothing about stacking, so an inverted preseed
1448        // goes straight to the greedy pass rather than to a TEST that would
1449        // accept it. Likewise a multirect virtual plane matched without its
1450        // parent, which the TEST would refuse.
1451        if stacking_consistent_all(planes, &assignment, &ordered)
1452            && multirect_complete(planes, &assignment)
1453            && self.try_test(&assignment, &ordered, committer)?
1454        {
1455            return Ok(assignment);
1456        }
1457
1458        // --- greedy from ranked candidates ---------------------------------
1459        assignment.clear();
1460        let mut candidates = self.rank_candidates(&ordered, planes, crtc_index);
1461        candidates.sort_by(|a, b| b.2.cmp(&a.2));
1462
1463        // Ordinary planes first, then multirect virtual planes, each only
1464        // once the first pass has taken its parent.
1465        let (ordinary, virtual_planes): (Vec<_>, Vec<_>) =
1466            candidates.into_iter().partition(|(plane_id, _, _)| {
1467                caps_in(planes, *plane_id).is_none_or(|plane| plane.multirect_parent.is_none())
1468            });
1469        let mut used_planes: Vec<u32> = Vec::new();
1470        let mut used_layers: Vec<LayerId> = Vec::new();
1471        for (plane_id, layer, _) in ordinary.into_iter().chain(virtual_planes) {
1472            if used_planes.contains(&plane_id) || used_layers.contains(&layer.id) {
1473                continue;
1474            }
1475            if self
1476                .failure_cache
1477                .lookup(plane_id, layer.layer.property_hash())
1478                == Some(false)
1479            {
1480                continue;
1481            }
1482            if self.probe_rejected(crtc_index, plane_id, layer.layer) {
1483                continue;
1484            }
1485            // The kernel accepts an inverted stack, so no TEST would catch
1486            // one: a layer is placed only where it stacks in the order its
1487            // zpos asks relative to what is already placed.
1488            if !fits_stacking(planes, &assignment, &ordered, plane_id, layer.layer)
1489                || !fits_multirect(planes, &assignment, plane_id)
1490            {
1491                continue;
1492            }
1493            assignment.insert(plane_id, layer.id);
1494            used_planes.push(plane_id);
1495            used_layers.push(layer.id);
1496        }
1497
1498        if assignment.is_empty() {
1499            return Ok(assignment);
1500        }
1501        if self.try_test(&assignment, &ordered, committer)? {
1502            return Ok(assignment);
1503        }
1504
1505        // --- backtrack: drop lowest keep-priority first ---------------------
1506        let mut by_priority: Vec<(u32, LayerId)> = assignment.entries().to_vec();
1507        by_priority.sort_by_key(|(_, layer_id)| {
1508            ordered
1509                .iter()
1510                .find(|entry| entry.id == *layer_id)
1511                .map_or(i32::MAX, |entry| keep_priority(entry.layer))
1512        });
1513
1514        for (plane_id, _) in by_priority {
1515            if assignment.remove(plane_id).is_none() {
1516                continue; // already dropped as an orphaned multirect child
1517            }
1518            // Dropping a multirect parent orphans its virtual plane; drop that
1519            // too.
1520            let children: Vec<u32> = assignment
1521                .entries()
1522                .iter()
1523                .map(|(id, _)| *id)
1524                .filter(|id| {
1525                    caps_in(planes, *id).is_some_and(|c| c.multirect_parent == Some(plane_id))
1526                })
1527                .collect();
1528            for child in children {
1529                assignment.remove(child);
1530            }
1531            if assignment.is_empty() {
1532                break;
1533            }
1534            if self.try_test(&assignment, &ordered, committer)? {
1535                break;
1536            }
1537            if self.test_commits_this_frame >= self.max_test_commits {
1538                self.budget_exhausted_this_frame = true;
1539                break;
1540            }
1541        }
1542
1543        Ok(assignment)
1544    }
1545
1546    /// Run one test commit, honoring the per-frame budget and recording the
1547    /// verdict.
1548    fn try_test<C: TestCommitter>(
1549        &mut self,
1550        assignment: &PlaneAssignment,
1551        present: &[LayerRef<'_>],
1552        committer: &mut C,
1553    ) -> Result<bool, TestFailure> {
1554        if self.test_commits_this_frame >= self.max_test_commits {
1555            self.budget_exhausted_this_frame = true;
1556            return Ok(false);
1557        }
1558        let pairs = Self::pairs_for(assignment, present);
1559        if pairs.len() != assignment.len() || self.over_stage_limit(pairs.len()) {
1560            return Ok(false);
1561        }
1562
1563        self.test_commits_this_frame += 1;
1564        match committer.test_assignment(&pairs) {
1565            Ok(()) => {
1566                for (plane_id, layer) in &pairs {
1567                    self.failure_cache
1568                        .record(*plane_id, layer.layer.property_hash(), true);
1569                }
1570                Ok(true)
1571            }
1572            Err(TestFailure::NotMaster) => Err(TestFailure::NotMaster),
1573            Err(TestFailure::Rejected) => {
1574                for (plane_id, layer) in &pairs {
1575                    self.failure_cache
1576                        .record(*plane_id, layer.layer.property_hash(), false);
1577                }
1578                Ok(false)
1579            }
1580        }
1581    }
1582
1583    /// Maximum-cardinality preseed over the statically compatible edges.
1584    fn preseed(
1585        &mut self,
1586        ordered: &[LayerRef<'_>],
1587        planes: &[&PlaneCapabilities],
1588        crtc_index: u32,
1589    ) -> PlaneAssignment {
1590        self.matcher.reset(ordered.len(), planes.len());
1591
1592        for (left, entry) in ordered.iter().enumerate() {
1593            for (right, plane) in planes.iter().enumerate() {
1594                if !plane_statically_compatible(plane, entry.layer, crtc_index) {
1595                    continue;
1596                }
1597                if self
1598                    .failure_cache
1599                    .lookup(plane.id, entry.layer.property_hash())
1600                    == Some(false)
1601                {
1602                    continue;
1603                }
1604                if self.probe_rejected(crtc_index, plane.id, entry.layer) {
1605                    continue;
1606                }
1607                let score = self.score(plane, entry);
1608                self.matcher.add_scored_edge(left, right, score);
1609            }
1610        }
1611
1612        self.matcher.solve();
1613
1614        let mut assignment = PlaneAssignment::new();
1615        for (left, entry) in ordered.iter().enumerate() {
1616            if let Some(right) = self.matcher.match_for_left(left) {
1617                assignment.insert(planes[right].id, entry.id);
1618            }
1619        }
1620        assignment
1621    }
1622
1623    /// Every statically compatible `(plane, layer)` pair with its score.
1624    fn rank_candidates<'a>(
1625        &self,
1626        ordered: &[LayerRef<'a>],
1627        planes: &[&PlaneCapabilities],
1628        crtc_index: u32,
1629    ) -> Vec<(u32, LayerRef<'a>, i32)> {
1630        let mut candidates = Vec::new();
1631        for entry in ordered {
1632            for plane in planes {
1633                if plane_statically_compatible(plane, entry.layer, crtc_index) {
1634                    candidates.push((plane.id, *entry, self.score(plane, entry)));
1635                }
1636            }
1637        }
1638        candidates
1639    }
1640
1641    /// Score a candidate, folding in the warm-start bonus and failure history.
1642    fn score(&self, plane: &PlaneCapabilities, entry: &LayerRef<'_>) -> i32 {
1643        let held_last_frame = self.previous_valid && self.previous.get(plane.id) == Some(entry.id);
1644        score_pair(
1645            plane,
1646            entry.layer,
1647            ScoreContext {
1648                held_last_frame,
1649                failure_hits: self
1650                    .failure_cache
1651                    .failure_count(plane.id, entry.layer.property_hash()),
1652            },
1653        )
1654    }
1655
1656    /// Whether a prior single-plane probe proved this layer's
1657    /// `(fourcc, modifier)` cannot scan out here — a lying `IN_FORMATS`.
1658    fn probe_rejected(&self, crtc_index: u32, plane_id: u32, layer: &Layer) -> bool {
1659        let Some(fourcc) = layer.format() else {
1660            return false;
1661        };
1662        self.probe_cache
1663            .lookup(crtc_index, plane_id, fourcc, Modifier(layer.modifier()))
1664            == Verdict::Rejected
1665    }
1666
1667    /// Record that a plane rejected a `(fourcc, modifier)` pair, so a lying
1668    /// `IN_FORMATS` costs one probe rather than a dropped edge re-probed every
1669    /// frame.
1670    pub fn record_probe_rejection(&mut self, crtc_index: u32, plane_id: u32, layer: &Layer) {
1671        if let Some(fourcc) = layer.format() {
1672            self.probe_cache.record(
1673                crtc_index,
1674                plane_id,
1675                fourcc,
1676                Modifier(layer.modifier()),
1677                false,
1678            );
1679        }
1680    }
1681}
1682
1683/// Whether the caller or the scene has sent `layer` to the canvas, whatever
1684/// the planes could take.
1685const fn forced_composited(layer: &Layer) -> bool {
1686    layer.is_force_composited() || layer.is_transient_composited()
1687}
1688
1689/// Whether `layer` on `plane_id` stacks consistently with everything already
1690/// in `assignment` (see [`stacking_consistent`](crate::stacking_consistent)).
1691///
1692/// A plane missing from `planes` cannot be ruled on and is not held against
1693/// the layer, which is what a plane without a zpos property gets as well.
1694fn fits_stacking(
1695    planes: &[&PlaneCapabilities],
1696    assignment: &PlaneAssignment,
1697    present: &[LayerRef<'_>],
1698    plane_id: u32,
1699    layer: &Layer,
1700) -> bool {
1701    let caps = |id: u32| planes.iter().find(|plane| plane.id == id).copied();
1702    let Some(plane) = caps(plane_id) else {
1703        return true;
1704    };
1705    let zpos = layer.property(PropTag::Zpos);
1706    assignment.entries().iter().all(|(other_plane, other_id)| {
1707        if *other_plane == plane_id {
1708            return true;
1709        }
1710        let (Some(other), Some(entry)) = (
1711            caps(*other_plane),
1712            present.iter().find(|entry| entry.id == *other_id),
1713        ) else {
1714            return true;
1715        };
1716        crate::stacking_consistent(plane, zpos, other, entry.layer.property(PropTag::Zpos))
1717    })
1718}
1719
1720/// The capabilities of `plane_id` among `planes`.
1721fn caps_in<'a>(planes: &[&'a PlaneCapabilities], plane_id: u32) -> Option<&'a PlaneCapabilities> {
1722    planes.iter().find(|plane| plane.id == plane_id).copied()
1723}
1724
1725/// Whether `plane_id` may join `assignment`: not a multirect virtual plane, or
1726/// one whose parent is already in it (see
1727/// [`multirect_pairing_ok`](crate::multirect_pairing_ok)).
1728///
1729/// As with stacking, a plane missing from `planes` is not held against it.
1730fn fits_multirect(
1731    planes: &[&PlaneCapabilities],
1732    assignment: &PlaneAssignment,
1733    plane_id: u32,
1734) -> bool {
1735    caps_in(planes, plane_id).is_none_or(|plane| {
1736        crate::multirect_pairing_ok(plane.multirect_parent, |parent| {
1737            assignment.get(parent).is_some()
1738        })
1739    })
1740}
1741
1742/// Whether no multirect virtual plane in `assignment` is without its parent.
1743fn multirect_complete(planes: &[&PlaneCapabilities], assignment: &PlaneAssignment) -> bool {
1744    assignment
1745        .entries()
1746        .iter()
1747        .all(|(plane_id, _)| fits_multirect(planes, assignment, *plane_id))
1748}
1749
1750/// Whether every placement in `assignment` stacks consistently.
1751fn stacking_consistent_all(
1752    planes: &[&PlaneCapabilities],
1753    assignment: &PlaneAssignment,
1754    present: &[LayerRef<'_>],
1755) -> bool {
1756    assignment.entries().iter().all(|(plane_id, layer_id)| {
1757        present
1758            .iter()
1759            .find(|entry| entry.id == *layer_id)
1760            .is_none_or(|entry| fits_stacking(planes, assignment, present, *plane_id, entry.layer))
1761    })
1762}
1763
1764/// Which side of the one composition canvas the placed layers must keep.
1765///
1766/// A placed layer that overlaps a composited one stays on the side of the
1767/// canvas its zpos asks for: `below` is the highest zpos that must stay under
1768/// the canvas, `above` the lowest that must cover it. Port of upstream's
1769/// `Allocator::CanvasBounds` (`07226e4`).
1770#[derive(Debug, Clone, Copy, Default)]
1771struct CanvasBounds {
1772    below: Option<u64>,
1773    above: Option<u64>,
1774}
1775
1776impl CanvasBounds {
1777    /// Whether one canvas zpos satisfies every placed layer: not when one
1778    /// would have to sit on both sides.
1779    fn feasible(self) -> bool {
1780        match (self.below, self.above) {
1781            (Some(below), Some(above)) => below < above,
1782            _ => true,
1783        }
1784    }
1785}
1786
1787/// The layers `assignment` places, with the pinned layers on planes of their
1788/// own, and the layers it leaves to the canvas.
1789fn placed_and_composited<'a>(
1790    assignment: &PlaneAssignment,
1791    layers: &[LayerRef<'a>],
1792) -> (Vec<&'a Layer>, Vec<&'a Layer>) {
1793    let mut placed = Vec::new();
1794    let mut composited = Vec::new();
1795    for entry in layers {
1796        let layer = entry.layer;
1797        if layer.is_composition_layer() || layer.is_externally_bound() {
1798            continue;
1799        }
1800        if assignment.get_plane_of(entry.id).is_some() {
1801            placed.push(layer);
1802        } else if layer.is_pinned() {
1803            if layer
1804                .assigned_plane()
1805                .is_some_and(|plane_id| assignment.get(plane_id).is_none())
1806            {
1807                placed.push(layer);
1808            }
1809        } else {
1810            composited.push(layer);
1811        }
1812    }
1813    (placed, composited)
1814}
1815
1816/// The canvas bounds `assignment` imposes on the layers it leaves composited.
1817fn canvas_bounds(assignment: &PlaneAssignment, layers: &[LayerRef<'_>]) -> CanvasBounds {
1818    let (placed, composited) = placed_and_composited(assignment, layers);
1819    let mut bounds = CanvasBounds::default();
1820    for placed in &placed {
1821        let Some(zp) = placed.property(PropTag::Zpos) else {
1822            continue;
1823        };
1824        for composited in &composited {
1825            let Some(zc) = composited.property(PropTag::Zpos) else {
1826                continue;
1827            };
1828            if zc == zp || !layers_intersect(placed, composited) {
1829                continue;
1830            }
1831            if zc < zp {
1832                bounds.above = Some(bounds.above.map_or(zp, |above| above.min(zp)));
1833            } else {
1834                bounds.below = Some(bounds.below.map_or(zp, |below| below.max(zp)));
1835            }
1836        }
1837    }
1838    bounds
1839}
1840
1841/// Move placed layers into the composition until the canvas bounds are
1842/// feasible: each round, the lowest keep-priority layer caught between them
1843/// that overlaps a composited one, with any multirect virtual plane riding on
1844/// its plane. `false` when nothing placed by the search is caught, which
1845/// leaves the pinned layers.
1846fn resolve_canvas_bounds(
1847    assignment: &mut PlaneAssignment,
1848    layers: &[LayerRef<'_>],
1849    planes: &[&PlaneCapabilities],
1850) -> bool {
1851    loop {
1852        let bounds = canvas_bounds(assignment, layers);
1853        let (Some(above), Some(below)) = (bounds.above, bounds.below) else {
1854            return true;
1855        };
1856        if below < above {
1857            return true;
1858        }
1859        let (_, composited) = placed_and_composited(assignment, layers);
1860        let mut victim: Option<(u32, i32)> = None;
1861        for (plane_id, layer_id) in assignment.entries() {
1862            let Some(entry) = layers.iter().find(|entry| entry.id == *layer_id) else {
1863                continue;
1864            };
1865            let Some(z) = entry.layer.property(PropTag::Zpos) else {
1866                continue;
1867            };
1868            let caught = z >= above
1869                && z <= below
1870                && composited.iter().any(|c| {
1871                    c.property(PropTag::Zpos)
1872                        .is_some_and(|zc| zc != z && layers_intersect(entry.layer, c))
1873                });
1874            if !caught {
1875                continue;
1876            }
1877            let keep = keep_priority(entry.layer);
1878            if victim.is_none_or(|(_, victim_keep)| keep < victim_keep) {
1879                victim = Some((*plane_id, keep));
1880            }
1881        }
1882        let Some((victim, _)) = victim else {
1883            return false;
1884        };
1885        assignment.remove(victim);
1886        let children: Vec<u32> = assignment
1887            .entries()
1888            .iter()
1889            .map(|(id, _)| *id)
1890            .filter(|id| caps_in(planes, *id).is_some_and(|c| c.multirect_parent == Some(victim)))
1891            .collect();
1892        for child in children {
1893            assignment.remove(child);
1894        }
1895    }
1896}