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}