Skip to main content

drmkit_scene/
scene.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The scene: layers, their sources, and the two-phase commit.
5//!
6//! Ports the layer table and frame flow from `src/scene/layer_scene.cpp`.
7
8use drmkit_planes::{Allocator, LayerRef};
9use drmkit_planes::{
10    Layer as PlaneLayer, LayerId, PlaneRegistry, PropTag, Rect, TestCommitter, TestFailure,
11};
12use std::collections::HashMap;
13
14use drmkit_core::Device;
15use drmkit_sync::SyncFence;
16
17use crate::canvas::{CompositeCanvas, CompositeRect, CompositeSrc};
18use crate::display::DisplayParams;
19use crate::frame::{AcquireTally, CommitKind, FrameLifecycle, FrameOutcome, KernelResult};
20use crate::lower::{LoweringInput, lower_layer};
21use crate::release::{Acquisition, Released};
22use crate::report::{CommitReport, LayerPlacement, Placement};
23use crate::source::{BindingModel, LayerBufferSource, SourceError, SourceFormat};
24
25/// Opaque, generation-tagged identity for a scene layer.
26///
27/// The scene recycles table slots, so a bare index would let a handle to a
28/// removed layer address whatever took its place. The generation counter makes
29/// that impossible: a stale handle no longer matches, and
30/// [`layer`](LayerScene::layer) returns `None`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
32pub struct LayerHandle {
33    /// 1-based slot index; 0 is the invalid sentinel.
34    id: u32,
35    /// Bumped each time the slot is reused.
36    generation: u32,
37}
38
39impl LayerHandle {
40    /// Whether this is not the default sentinel.
41    ///
42    /// Says nothing about liveness — a valid handle can still be stale. Resolve
43    /// with [`LayerScene::layer`].
44    #[must_use]
45    pub const fn is_valid(self) -> bool {
46        self.id != 0
47    }
48
49    /// A stable identity for the allocator.
50    ///
51    /// Packs slot and generation, so a recycled slot yields a **different**
52    /// id. That is what the allocator's warm-start state needs: a new layer
53    /// must never be able to masquerade as the removed one whose place it took.
54    #[must_use]
55    pub const fn layer_id(self) -> LayerId {
56        LayerId(((self.id as u64) << 32) | self.generation as u64)
57    }
58}
59
60impl From<LayerHandle> for LayerId {
61    /// The same packing [`LayerHandle::layer_id`] does.
62    ///
63    /// Here as well so anything taking a layer's identity can accept either,
64    /// which is what lets a report be queried with a handle by a caller and
65    /// with a bare id by a test that has no scene to get a handle from.
66    fn from(handle: LayerHandle) -> Self {
67        handle.layer_id()
68    }
69}
70
71/// A layer's live scene state.
72pub struct SceneLayer {
73    source: Box<dyn LayerBufferSource>,
74    display: DisplayParams,
75    content_type: drmkit_planes::ContentType,
76    update_hint_hz: u32,
77    app_priority: u8,
78    /// Set by the hint setters: these change plane *scoring*, not just the
79    /// values written to a chosen plane, so the scene drops the allocator's
80    /// warm start for that frame and lets the layer move.
81    hints_dirty: bool,
82    /// A plane the caller insists this layer scans out on.
83    ///
84    /// A *request*, not a guarantee: the plane has to be on this CRTC, take
85    /// the layer's format, and not already be spoken for. When it is not, the
86    /// layer falls back to normal allocation and the frame reports it through
87    /// [`CommitReport::pin_requests_unhonored`] rather than dropping it — a
88    /// caller's deterministic-plane assumption being violated is worth saying
89    /// out loud, and worth saying more than it is worth blanking a layer over.
90    pinned_plane: Option<u32>,
91    /// Whether the caller insists this layer is composited rather than
92    /// placed on a plane of its own.
93    ///
94    /// The allocator reads it through
95    /// [`Layer::is_force_composited`](drmkit_planes::Layer::is_force_composited)
96    /// and scores the layer out of plane candidacy. For a caller that knows
97    /// something the allocator does not — content that must blend with what
98    /// is under it, or a layer it would rather spend no plane on so the
99    /// others have more to choose from.
100    force_composited: bool,
101    /// A caller-chosen identity, stable across a rebind.
102    ///
103    /// A [`LayerHandle`] identifies a layer *within one scene's lifetime*.
104    /// This identifies what the layer **is** to the caller — a window, a
105    /// video stream, a cursor — so a caller holding its own state can find its
106    /// layer again without keeping a parallel map keyed by handle.
107    ///
108    /// Upstream uses a `const void*`, which works there because the caller
109    /// already has a stable object to point at. A `u64` says the same thing
110    /// without inviting a raw pointer into a type the scene stores.
111    identity_tag: Option<u64>,
112    /// Whether the geometry changed since the last commit.
113    ///
114    /// Separate from `hints_dirty`, which drops the warm start: geometry only
115    /// changes what is written to the plane a layer already has. This exists
116    /// so [`LayerScene::content_changed`] can tell a moved layer from an idle
117    /// one, since a source with nothing new to say would otherwise make a
118    /// repositioned layer look like no change at all.
119    display_dirty: bool,
120    /// The framebuffer this layer last put on screen.
121    ///
122    /// Kept so a frame where the source has nothing ready can re-attach it:
123    /// the layer goes on showing what it already had, rather than being
124    /// dropped from the commit and switched off.
125    last_fb_id: Option<u32>,
126}
127
128impl std::fmt::Debug for SceneLayer {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("SceneLayer")
131            .field("display", &self.display)
132            .field("content_type", &self.content_type)
133            .field("update_hint_hz", &self.update_hint_hz)
134            .field("app_priority", &self.app_priority)
135            .finish_non_exhaustive()
136    }
137}
138
139impl SceneLayer {
140    /// The buffer source. The scene keeps ownership.
141    #[must_use]
142    pub fn source(&self) -> &dyn LayerBufferSource {
143        self.source.as_ref()
144    }
145
146    /// The buffer source, mutably — for painting into it, not for replacing it.
147    pub fn source_mut(&mut self) -> &mut dyn LayerBufferSource {
148        self.source.as_mut()
149    }
150
151    /// How this layer is displayed.
152    #[must_use]
153    pub const fn display(&self) -> &DisplayParams {
154        &self.display
155    }
156
157    /// Change how this layer is displayed.
158    ///
159    /// Geometry only affects the values written to whatever plane the layer
160    /// already has, so this does **not** drop the warm start.
161    pub const fn set_display(&mut self, display: DisplayParams) {
162        self.display = display;
163        self.display_dirty = true;
164    }
165
166    /// Whether this layer is forced through composition.
167    #[must_use]
168    pub const fn is_force_composited(&self) -> bool {
169        self.force_composited
170    }
171
172    /// Force this layer through composition rather than onto its own plane.
173    ///
174    /// Flags for re-allocation: it changes what the allocator will consider,
175    /// and therefore what is left for every other layer.
176    pub const fn set_force_composited(&mut self, force: bool) {
177        self.force_composited = force;
178        self.hints_dirty = true;
179    }
180
181    /// The caller's identity for this layer, if it set one.
182    #[must_use]
183    pub const fn identity_tag(&self) -> Option<u64> {
184        self.identity_tag
185    }
186
187    /// Label this layer with a caller-chosen identity.
188    ///
189    /// Purely for the caller's own lookups — see
190    /// [`identity_tag`](Self::identity_tag). The scene never interprets it,
191    /// and does not require it to be unique; a duplicate simply means
192    /// [`find_by_identity_tag`](LayerScene::find_by_identity_tag) answers with
193    /// the first match.
194    pub const fn set_identity_tag(&mut self, tag: Option<u64>) {
195        self.identity_tag = tag;
196    }
197
198    /// The plane this layer is pinned to, if any.
199    #[must_use]
200    pub const fn pinned_plane(&self) -> Option<u32> {
201        self.pinned_plane
202    }
203
204    /// Pin this layer to a plane, or clear the pin with `None`.
205    ///
206    /// For a caller that needs a specific plane rather than whichever one the
207    /// allocator picks: a driver-bound producer, a plane with hardware the
208    /// others lack, or a layout the caller has already validated. Honoured
209    /// where it can be, reported where it cannot — see
210    /// [`pinned_plane`](Self::pinned_plane).
211    ///
212    /// Flags for re-allocation, because it changes where this layer goes and
213    /// therefore what is left for everything else.
214    pub const fn set_pinned_plane(&mut self, plane_id: Option<u32>) {
215        self.pinned_plane = plane_id;
216        self.hints_dirty = true;
217    }
218
219    /// The allocator's content-type hint.
220    #[must_use]
221    pub const fn content_type(&self) -> drmkit_planes::ContentType {
222        self.content_type
223    }
224
225    /// Change the content-type hint.
226    ///
227    /// Unlike the display setters this changes plane **scoring**, so it also
228    /// flags the layer for re-allocation: the scene drops the warm start that
229    /// frame and lets the layer move to a plane its new hint prefers.
230    pub const fn set_content_type(&mut self, content_type: drmkit_planes::ContentType) {
231        self.content_type = content_type;
232        self.hints_dirty = true;
233    }
234
235    /// The producer's expected refresh rate, or 0.
236    #[must_use]
237    pub const fn update_hint_hz(&self) -> u32 {
238        self.update_hint_hz
239    }
240
241    /// Change the refresh-rate hint. Flags for re-allocation, as above.
242    pub const fn set_update_hint(&mut self, hz: u32) {
243        self.update_hint_hz = hz;
244        self.hints_dirty = true;
245    }
246
247    /// The application's placement priority within a content class.
248    #[must_use]
249    pub const fn app_priority(&self) -> u8 {
250        self.app_priority
251    }
252
253    /// Change the placement priority. Flags for re-allocation, as above.
254    pub const fn set_app_priority(&mut self, priority: u8) {
255        self.app_priority = priority;
256        self.hints_dirty = true;
257    }
258
259    /// Whether a hint changed since the last commit.
260    #[must_use]
261    pub const fn hints_dirty(&self) -> bool {
262        self.hints_dirty
263    }
264}
265
266/// A table slot: either occupied or free, with a generation either way.
267#[derive(Debug)]
268enum Slot {
269    Occupied(Box<SceneLayer>),
270    Free,
271}
272
273/// What a [`rebind`](LayerScene::rebind) found that will not fit.
274#[derive(Debug, Clone, Default, PartialEq, Eq)]
275#[non_exhaustive]
276pub struct RebindReport {
277    /// Layers the new output cannot display as they stand.
278    pub incompatibilities: Vec<LayerIncompatibility>,
279}
280
281impl RebindReport {
282    /// Whether every layer fits the new output.
283    #[must_use]
284    pub fn is_empty(&self) -> bool {
285        self.incompatibilities.is_empty()
286    }
287}
288
289/// One layer the new output cannot display as it stands.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291#[non_exhaustive]
292pub struct LayerIncompatibility {
293    /// Which layer. Still valid: a rebind does not invalidate handles.
294    pub handle: LayerHandle,
295    /// Why it does not fit.
296    pub reason: IncompatibilityReason,
297}
298
299/// Why a layer does not fit its new output.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301#[non_exhaustive]
302pub enum IncompatibilityReason {
303    /// The destination rectangle falls outside the new mode.
304    ///
305    /// Reported rather than clamped. A layer at the old mode's bottom-right
306    /// is not *wrong*, it is somewhere the caller now has to decide about, and
307    /// silently moving it would put a window somewhere the caller never asked
308    /// for and cannot detect.
309    DstRectOffScreen,
310}
311
312/// One plane's share of a built frame.
313#[derive(Debug, Clone)]
314#[non_exhaustive]
315pub struct PlanePlan {
316    /// The plane this layer landed on.
317    pub plane_id: u32,
318    /// Which layer it is, for the allocator's committed baseline.
319    pub layer_id: LayerId,
320    /// The lowered property bag to write.
321    pub layer: drmkit_planes::Layer,
322    /// What the kernel last took on this plane, or `None` to write it all.
323    ///
324    /// Captured at build time on purpose: the baseline is replaced when the
325    /// frame is finalized, so reading it afterwards would diff this frame
326    /// against itself and suppress every property.
327    pub baseline: Option<drmkit_planes::PropertySnapshot>,
328    /// What this frame's source said changed, in destination pixels.
329    ///
330    /// Empty means whole-frame, which is what an absent `FB_DAMAGE_CLIPS`
331    /// already says -- so an empty list writes nothing rather than an empty
332    /// blob, which the kernel reads as *nothing changed* and would leave the
333    /// frame unrepainted.
334    pub damage: Vec<crate::DamageRect>,
335    /// The zpos to write in place of the layer's own: the frame's stack,
336    /// numbered densely over the planes it arms (see
337    /// [`stacked_zpos`](drmkit_planes::stacked_zpos)). `None` where the plane
338    /// is not ranked, and the layer's own value goes out.
339    pub zpos: Option<u64>,
340    /// The `COLOR_ENCODING` and `COLOR_RANGE` the layer asks of its plane.
341    ///
342    /// `None` for the composition canvas, which is no one layer's and is
343    /// RGB, where the kernel applies neither: its plane is left with what it
344    /// has, as upstream leaves it.
345    pub color: Option<crate::Ycbcr>,
346}
347
348/// One pass over the sources: what they gave, and what it cost.
349struct Acquired {
350    /// Layers holding their previous frame because the source had none.
351    starved: Vec<LayerId>,
352    /// Buffers taken this frame, owed back after the commit.
353    acquisitions: Vec<Acquisition>,
354    /// Each layer lowered into the property bag the allocator reads.
355    plane_layers: Vec<(LayerId, PlaneLayer)>,
356    /// What each source said changed, for `FB_DAMAGE_CLIPS`.
357    frame_damage: HashMap<LayerId, Vec<crate::DamageRect>>,
358    /// Pin requests the hardware could not honour.
359    pins_unhonored: usize,
360    /// Pins that were honoured, layer to plane.
361    pinned: Vec<(LayerId, u32)>,
362}
363
364/// A frame that has been built and is awaiting the kernel's answer.
365///
366/// Holding one means holding acquisitions. **Dropping it without finalizing
367/// leaks them** — the same contract the C++ states, and the reason this type
368/// warns on drop.
369#[derive(Debug)]
370pub struct FrameBuild {
371    pub(crate) acquisitions: Vec<Acquisition>,
372    pub(crate) plan: Vec<PlanePlan>,
373    disables: Vec<u32>,
374    report: CommitReport,
375    kind: CommitKind,
376    finalized: bool,
377    /// Whether this frame was built not knowing what the kernel holds: the
378    /// first frame, or the first after a resume or a rebind. Another client
379    /// may have changed the planes' sticky properties since they were last
380    /// written, so they are restated rather than diffed.
381    restate_sticky: bool,
382}
383
384impl FrameBuild {
385    /// The report so far, complete except for what the kernel's answer decides.
386    #[must_use]
387    pub const fn report(&self) -> &CommitReport {
388        &self.report
389    }
390
391    /// Record what this frame's emission wrote.
392    ///
393    /// Called by [`emit_frame`](crate::emit_frame), which is the only thing
394    /// that knows: the counts are decided by the diff against what the kernel
395    /// last took, and that happens at emission rather than at build time.
396    pub(crate) const fn record_emission(
397        &mut self,
398        properties: usize,
399        framebuffers: usize,
400        damaged: usize,
401    ) {
402        self.report.properties_written = properties;
403        self.report.fbs_attached = framebuffers;
404        self.report.damaged_layers = damaged;
405    }
406
407    /// How many buffers this frame is holding.
408    #[must_use]
409    pub fn held(&self) -> usize {
410        self.acquisitions.len()
411    }
412
413    /// Which layer landed on which plane, as lowered property bags.
414    ///
415    /// This is what an apply commit writes. The search that produced it ran
416    /// against `TEST_ONLY` commits, so by the time a caller sees this the
417    /// kernel has already accepted this exact set -- emitting it is the cheap
418    /// part.
419    #[must_use]
420    pub fn plan(&self) -> &[PlanePlan] {
421        &self.plan
422    }
423
424    /// Whether emission restates every plane's sticky properties --
425    /// `COLOR_ENCODING` and `COLOR_RANGE` -- rather than only the ones that
426    /// changed since the kernel last took them.
427    ///
428    /// True until the scene knows what the kernel holds: on the first frame,
429    /// and the first after [`resume`](crate::LayerScene::resume) or
430    /// [`rebind`](crate::LayerScene::rebind), when whoever held the planes
431    /// meanwhile may have left them on their own settings.
432    #[must_use]
433    pub const fn restates_sticky(&self) -> bool {
434        self.restate_sticky
435    }
436
437    /// Record what the fence arming pass did, for the commit report.
438    pub(crate) fn note_fences(&mut self, armed: usize, cpu_waits: usize) {
439        self.report.in_fences_armed = armed;
440        self.report.in_fence_cpu_waits = cpu_waits;
441    }
442
443    /// Planes this frame has to switch off.
444    ///
445    /// Candidate planes the frame did not use **and** that the kernel does not
446    /// already have off. Re-disabling an already-off plane is two property
447    /// writes per plane per frame that change nothing.
448    ///
449    /// Computed at build time for the same reason as
450    /// [`PlanePlan::baseline`]: finalizing the frame replaces the state this
451    /// is derived from.
452    #[must_use]
453    pub fn disables(&self) -> &[u32] {
454        &self.disables
455    }
456}
457
458impl Drop for FrameBuild {
459    fn drop(&mut self) {
460        if !self.finalized && !self.acquisitions.is_empty() && !std::thread::panicking() {
461            // Same hazard as invariant 5's, one level up: these buffers never
462            // go back to their sources, so the producer's ring starves. There
463            // is nothing safe to do about it here -- the sources live in the
464            // scene, which is not reachable from a drop -- so say so loudly.
465            //
466            // Not while already unwinding, though: a second panic during
467            // cleanup aborts the process, which replaces the failure the
468            // caller needs to read with this one.
469            debug_assert!(
470                false,
471                "FrameBuild dropped without finalize_frame: {} acquisitions leaked",
472                self.acquisitions.len()
473            );
474        }
475    }
476}
477
478/// Why a frame could not be built.
479#[derive(Debug, thiserror::Error)]
480#[non_exhaustive]
481pub enum SceneError {
482    /// The scene is suspended after losing DRM master, and short-circuits until
483    /// resumed.
484    #[error("scene is suspended")]
485    Suspended,
486
487    /// A source failed for a real reason. Distinguished from a source that
488    /// merely had no frame, which is not an error at all.
489    #[error("layer source failed: {0}")]
490    Source(#[from] SourceError),
491
492    /// The allocator could not run because DRM master was lost mid-search.
493    #[error("lost DRM master during allocation")]
494    NotMaster,
495
496    /// A [`rebind`](LayerScene::rebind) to another CRTC left planes lit on the
497    /// old one, and they have not been turned off yet. Commit
498    /// [`commit_detach`](crate::commit_detach) first.
499    #[error("planes from the previous CRTC are still lit; commit the detach first")]
500    DetachPending,
501}
502
503/// One CRTC's layers and the commit machinery around them.
504///
505/// Layers are added once and kept: [`add_layer`](Self::add_layer) returns a
506/// [`LayerHandle`] that stays valid until the layer is removed, and a stale
507/// one resolves to nothing rather than to whatever now occupies the slot. Per
508/// frame, the caller changes what it wants — geometry through
509/// [`set_display`](SceneLayer::set_display), content through the source — and
510/// asks for a [`build_frame`](Self::build_frame).
511///
512/// The scene is deliberately not a commit loop. It builds a plan and reads the
513/// kernel's answer; issuing the commit, attaching a modeset, and dispatching
514/// the page-flip event belong to the caller, because a compositor has its own
515/// ideas about all three. See the crate documentation for the shape of a
516/// frame.
517///
518/// Four of the six pinned invariants are the scene's: buffers released a flip
519/// late rather than an ioctl early (1), a failed commit releasing immediately
520/// because no flip will gate it (2), the FB-only fast path (4), and teardown
521/// draining an outstanding flip before letting go of what it scans out (5).
522/// None of them is something a caller opts into; they are what the type does.
523pub struct LayerScene {
524    crtc_id: u32,
525    slots: Vec<Slot>,
526    generations: Vec<u32>,
527    free: Vec<u32>,
528    allocator: Allocator,
529    canvas: Option<CompositeCanvas>,
530    lifecycle: FrameLifecycle,
531    /// Sources whose layers were removed while their buffers were still in
532    /// flight.
533    ///
534    /// Dropping such a source immediately would strand the buffers: the release
535    /// would have nowhere to go, and any handles it owns would be freed while
536    /// the display engine may still be scanning them. They are kept until every
537    /// buffer has come back.
538    retiring: Vec<(LayerId, Box<dyn LayerBufferSource>)>,
539    /// Whether a layer was added or removed since the last commit.
540    topology_dirty: bool,
541    /// Whether any real commit has succeeded yet.
542    ///
543    /// Until one has, the scanout contents are undefined and *nothing changed*
544    /// describes nothing -- so the first frame is never skipped, however
545    /// quiet the sources are.
546    committed_once: bool,
547    /// Whether the kernel may hold planes on this CRTC that the scene did not
548    /// light: until a real commit lands, and again after a rebind or a
549    /// resume.
550    ///
551    /// Another client -- the session the scene took over from, a splash, a
552    /// compositor that held the CRTC while the scene was suspended -- can
553    /// leave a plane armed, and the allocator's baseline, knowing nothing of
554    /// it, reports it off. Every `TEST_ONLY` disables it, so the frame the
555    /// kernel accepts is one without it; without this the real commit leaves
556    /// it lit, on screen over or under the scene and counted against a
557    /// controller's plane limit.
558    planes_unknown: bool,
559    /// The plane the canvas was armed on last frame, if composition ran: the
560    /// canvas's first preference this frame (drm-cxx `6d787f9`). Forgotten on
561    /// a frame that composites nothing, and on rebind and resume.
562    last_canvas_plane: Option<u32>,
563    /// What the kernel said about each pin it was asked about, so a pin is
564    /// tested once per layer, plane and geometry (drm-cxx `c766e97`).
565    /// Forgotten on rebind and resume.
566    pin_verdicts: Vec<PinVerdict>,
567    /// What a rebind to another CRTC left lit on the old one.
568    detach: Option<PendingDetach>,
569}
570
571/// Planes a scene left lit on the CRTC it moved away from.
572///
573/// The kernel will not move a plane from one CRTC to another in one commit
574/// ("switching CRTC directly"), so one both pipes can use has to be turned off
575/// in a commit of its own before the new CRTC can have it. And nothing else
576/// would turn them off: the baseline that recorded them describes the old
577/// output, and goes with it.
578#[derive(Debug, Clone, PartialEq, Eq)]
579#[non_exhaustive]
580pub struct PendingDetach {
581    /// The CRTC the scene left.
582    pub crtc_id: u32,
583    /// The planes it had lit there.
584    pub planes: Vec<u32>,
585}
586
587impl std::fmt::Debug for LayerScene {
588    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589        f.debug_struct("LayerScene")
590            .field("crtc_id", &self.crtc_id)
591            .field("layers", &self.len())
592            .field("suspended", &self.lifecycle.is_suspended())
593            .field("retiring_sources", &self.retiring.len())
594            .finish_non_exhaustive()
595    }
596}
597
598impl LayerScene {
599    /// A scene driving `crtc_id`, with no layers.
600    #[must_use]
601    pub fn new(crtc_id: u32) -> Self {
602        Self {
603            crtc_id,
604            slots: Vec::new(),
605            generations: Vec::new(),
606            free: Vec::new(),
607            allocator: Allocator::new(),
608            canvas: None,
609            lifecycle: FrameLifecycle::new(),
610            retiring: Vec::new(),
611            topology_dirty: true,
612            committed_once: false,
613            planes_unknown: true,
614            last_canvas_plane: None,
615            pin_verdicts: Vec::new(),
616            detach: None,
617        }
618    }
619
620    /// The CRTC this scene drives.
621    #[must_use]
622    pub const fn crtc_id(&self) -> u32 {
623        self.crtc_id
624    }
625
626    /// How many layers the scene holds.
627    #[must_use]
628    pub fn len(&self) -> usize {
629        self.slots
630            .iter()
631            .filter(|slot| matches!(slot, Slot::Occupied(_)))
632            .count()
633    }
634
635    /// Whether the scene holds no layers.
636    #[must_use]
637    pub fn is_empty(&self) -> bool {
638        self.len() == 0
639    }
640
641    /// Whether the scene is suspended after losing DRM master.
642    #[must_use]
643    pub const fn is_suspended(&self) -> bool {
644        self.lifecycle.is_suspended()
645    }
646
647    /// Whether a page-flip event is armed and undispatched.
648    ///
649    /// **Invariant 5.** Dropping the scene while this is true tears down a
650    /// framebuffer the flip still references.
651    #[must_use]
652    pub const fn has_pending_flip(&self) -> bool {
653        self.lifecycle.has_pending_flip()
654    }
655
656    /// Record that the armed page-flip event was dispatched.
657    pub const fn flip_landed(&mut self) {
658        self.lifecycle.flip_landed();
659    }
660
661    /// Lift a suspension after the session resumes.
662    ///
663    /// Whoever held the CRTC meanwhile may have left planes lit, so the next
664    /// frame turns off every plane it does not use.
665    pub fn resume(&mut self) {
666        self.lifecycle.resume();
667        self.planes_unknown = true;
668        self.last_canvas_plane = None;
669        self.pin_verdicts.clear();
670    }
671
672    /// Add a layer backed by `source`.
673    pub fn add_layer(&mut self, source: Box<dyn LayerBufferSource>) -> LayerHandle {
674        self.topology_dirty = true;
675        let layer = Box::new(SceneLayer {
676            source,
677            display: DisplayParams::default(),
678            content_type: drmkit_planes::ContentType::Generic,
679            update_hint_hz: 0,
680            app_priority: 0,
681            hints_dirty: false,
682            pinned_plane: None,
683            force_composited: false,
684            identity_tag: None,
685            display_dirty: true,
686            last_fb_id: None,
687        });
688
689        if let Some(slot_index) = self.free.pop() {
690            let index = slot_index as usize - 1;
691            self.slots[index] = Slot::Occupied(layer);
692            return LayerHandle {
693                id: slot_index,
694                generation: self.generations[index],
695            };
696        }
697
698        self.slots.push(Slot::Occupied(layer));
699        self.generations.push(1);
700        let id = u32::try_from(self.slots.len()).unwrap_or(u32::MAX);
701        LayerHandle { id, generation: 1 }
702    }
703
704    /// Remove a layer. A stale handle is a no-op.
705    ///
706    /// If the layer's buffers are still in flight, its source is kept alive
707    /// until they come back — releasing to a dropped source would strand them.
708    /// The source hears [`LayerBufferSource::on_retired`] first.
709    pub fn remove_layer(&mut self, handle: LayerHandle) -> bool {
710        let Some(index) = self.resolve(handle) else {
711            return false;
712        };
713
714        let Slot::Occupied(mut layer) = std::mem::replace(&mut self.slots[index], Slot::Free)
715        else {
716            return false;
717        };
718
719        // Bump first, so any handle to this slot is stale from here on.
720        self.generations[index] = self.generations[index].wrapping_add(1);
721        self.free.push(handle.id);
722
723        // Before its buffers drain, so the one on screen is handed back with
724        // them rather than held for a newer frame that will never come.
725        layer.source.on_retired();
726        if self.lifecycle.buffers_in_flight() > 0 {
727            self.topology_dirty = true;
728            self.retiring.push((handle.layer_id(), layer.source));
729        }
730
731        // Prune just this layer rather than dropping the whole warm-start
732        // cache. Removing a layer only frees resources, so the assignment the
733        // kernel already accepted stays valid without it -- invalidating would
734        // force a full search, and a full search is several test commits.
735        self.allocator.forget_layer(handle.layer_id());
736        true
737    }
738
739    /// Borrow a layer, or `None` if the handle is stale.
740    #[must_use]
741    pub fn layer(&self, handle: LayerHandle) -> Option<&SceneLayer> {
742        let index = self.resolve(handle)?;
743        match &self.slots[index] {
744            Slot::Occupied(layer) => Some(layer),
745            Slot::Free => None,
746        }
747    }
748
749    /// Borrow a layer mutably, or `None` if the handle is stale.
750    pub fn layer_mut(&mut self, handle: LayerHandle) -> Option<&mut SceneLayer> {
751        let index = self.resolve(handle)?;
752        match &mut self.slots[index] {
753            Slot::Occupied(layer) => Some(layer),
754            Slot::Free => None,
755        }
756    }
757
758    /// Every live handle, in slot order.
759    pub fn handles(&self) -> impl Iterator<Item = LayerHandle> + '_ {
760        self.slots
761            .iter()
762            .enumerate()
763            .filter_map(|(index, slot)| match slot {
764                Slot::Occupied(_) => Some(LayerHandle {
765                    id: u32::try_from(index + 1).unwrap_or(u32::MAX),
766                    generation: self.generations[index],
767                }),
768                Slot::Free => None,
769            })
770    }
771
772    /// Resolve a handle to a slot index, checking the generation.
773    fn resolve(&self, handle: LayerHandle) -> Option<usize> {
774        if !handle.is_valid() {
775            return None;
776        }
777        let index = handle.id as usize - 1;
778        if self.generations.get(index).copied()? != handle.generation {
779            return None;
780        }
781        Some(index)
782    }
783
784    /// Acquire every layer's buffer and lower it into a property bag.
785    ///
786    /// Split out of [`build_frame`](Self::build_frame) because it is the one
787    /// pass that touches the sources, and because everything after it works
788    /// only from what this returns.
789    fn acquire_every_layer(
790        &mut self,
791        registry: &PlaneRegistry,
792        crtc_index: u32,
793        tally: &mut AcquireTally,
794    ) -> Result<Acquired, SceneError> {
795        let mut starved: Vec<LayerId> = Vec::new();
796        let mut acquisitions = Vec::new();
797        let mut plane_layers: Vec<(LayerId, PlaneLayer)> = Vec::new();
798        let mut frame_damage: HashMap<LayerId, Vec<crate::DamageRect>> = HashMap::new();
799        let mut pins_unhonored = 0usize;
800        // Honoured pins, layer to plane. The allocator skips these layers by
801        // design -- the scene owns their planes -- so the plan and the report
802        // have to carry them, or a pinned layer is programmed by nobody and
803        // reported by nobody while still being counted as considered.
804        let mut pinned: Vec<(LayerId, u32)> = Vec::new();
805
806        for handle in self.handles().collect::<Vec<_>>() {
807            let layer_id = handle.layer_id();
808            let Some(index) = self.resolve(handle) else {
809                continue;
810            };
811            let Slot::Occupied(layer) = &mut self.slots[index] else {
812                continue;
813            };
814
815            let acquired = match layer.source.acquire() {
816                Ok(buffer) => buffer,
817                Err(SourceError::WouldBlock) => {
818                    // Flow control, not failure: the source has no frame this
819                    // vblank. The layer keeps its plane and its last
820                    // framebuffer, so it goes on showing what it already had.
821                    //
822                    // Dropping it from the frame instead would take its plane
823                    // out of the assignment and the commit would disable it --
824                    // a layer whose source hiccups would blink off, which is
825                    // the visible failure this path exists to avoid.
826                    tally.record(false);
827                    let Some(fb_id) = layer.last_fb_id else {
828                        // Starved before it ever produced anything. There is
829                        // no previous frame to hold, so there is nothing to
830                        // program and the layer really is absent.
831                        continue;
832                    };
833                    starved.push(layer_id);
834                    let (mut lowered, pin) =
835                        lower(layer, fb_id, self.crtc_id, registry, crtc_index);
836                    let pin = revoke_refused_pin(&self.pin_verdicts, layer_id, &mut lowered, pin);
837                    match pin {
838                        PinOutcome::Refused => pins_unhonored += 1,
839                        PinOutcome::Honoured(plane_id) => pinned.push((layer_id, plane_id)),
840                        PinOutcome::NotRequested => {}
841                    }
842                    plane_layers.push((layer_id, lowered));
843                    continue;
844                }
845                Err(other) => {
846                    // Hand back whatever this pass already took: the commit is
847                    // not happening, so holding them would stall those rings.
848                    self.release_all(acquisitions);
849                    return Err(SceneError::Source(other));
850                }
851            };
852            tally.record(true);
853
854            let (mut plane_layer, pin) =
855                lower(layer, acquired.fb_id, self.crtc_id, registry, crtc_index);
856            let pin = revoke_refused_pin(&self.pin_verdicts, layer_id, &mut plane_layer, pin);
857            match pin {
858                PinOutcome::Refused => pins_unhonored += 1,
859                PinOutcome::Honoured(plane_id) => pinned.push((layer_id, plane_id)),
860                PinOutcome::NotRequested => {}
861            }
862
863            layer.last_fb_id = Some(acquired.fb_id);
864            plane_layers.push((layer_id, plane_layer));
865            // Taken before the acquisition owns it: the damage describes this
866            // frame and is consumed at emission, whereas the acquisition
867            // outlives the commit by two generations.
868            if !acquired.damage.is_empty() {
869                frame_damage.insert(layer_id, acquired.damage.clone());
870            }
871            acquisitions.push(Acquisition::new(layer_id, acquired));
872        }
873
874        Ok(Acquired {
875            starved,
876            acquisitions,
877            plane_layers,
878            frame_damage,
879            pins_unhonored,
880            pinned,
881        })
882    }
883
884    /// Acquire from every layer, lower it, and run the allocator.
885    ///
886    /// A source with nothing to contribute is **skipped and counted**, never
887    /// treated as a failure — see [`SourceError::WouldBlock`].
888    ///
889    /// # Errors
890    ///
891    /// - [`SceneError::Suspended`] while the scene is suspended.
892    /// - [`SceneError::Source`] if a source failed for a real reason.
893    /// - [`SceneError::NotMaster`] if the allocator lost DRM master.
894    pub fn build_frame<C: TestCommitter>(
895        &mut self,
896        registry: &PlaneRegistry,
897        crtc_index: u32,
898        kind: CommitKind,
899        committer: &mut C,
900    ) -> Result<FrameBuild, SceneError> {
901        if self.lifecycle.is_suspended() {
902            return Err(SceneError::Suspended);
903        }
904        if self.detach.is_some() {
905            return Err(SceneError::DetachPending);
906        }
907
908        // A changed hint affects plane scoring, so the warm start must go or
909        // the layer can never move to the plane it now prefers.
910        if self.handles().any(|h| {
911            self.layer(h)
912                .is_some_and(super::scene::SceneLayer::hints_dirty)
913        }) {
914            self.allocator.invalidate_allocation();
915        }
916
917        let mut tally = AcquireTally::default();
918        // Layers holding their previous frame. They are programmed but not
919        // acquired, so the report must not count them as assigned -- the
920        // identity is assigned + composited + unassigned + skipped.
921        let Acquired {
922            starved,
923            acquisitions,
924            plane_layers,
925            frame_damage,
926            pins_unhonored,
927            mut pinned,
928        } = self.acquire_every_layer(registry, crtc_index, &mut tally)?;
929        let mut plane_layers = plane_layers;
930        let (allocation, refused) = match self.allocate_frame(
931            &mut plane_layers,
932            &mut pinned,
933            registry,
934            crtc_index,
935            committer,
936        ) {
937            Ok(allocated) => allocated,
938            Err(error) => {
939                self.release_all(acquisitions);
940                return Err(error);
941            }
942        };
943        let pins_unhonored = pins_unhonored + refused;
944
945        // Rescue what the allocator dropped, before the plan is built: the
946        // canvas takes a plane, and a plane carrying the canvas must not also
947        // appear in the disable pass.
948        let canvas_plane = self.compose_unassigned(&allocation, registry, crtc_index);
949        self.last_canvas_plane = canvas_plane.as_ref().map(|c| c.plane_id);
950        // What actually landed in the canvas, not what the allocator dropped.
951        // A layer whose source the CPU cannot read, or whose format the blend
952        // does not handle, is dropped *and* unrescued -- counting it composited
953        // would report a layer as on screen when it is not. The identity holds
954        // either way, so only the split tells them apart.
955        let composited = canvas_plane.as_ref().map_or(0, |c| c.blended);
956
957        let report = build_report(
958            &tally,
959            &allocation,
960            registry,
961            &starved,
962            composited,
963            &pinned,
964            pins_unhonored,
965        );
966
967        // Clear the change flags now the allocation has seen them.
968        for handle in self.handles().collect::<Vec<_>>() {
969            if let Some(layer) = self.layer_mut(handle) {
970                layer.hints_dirty = false;
971                layer.display_dirty = false;
972            }
973        }
974        self.topology_dirty = false;
975        self.committed_once = true;
976
977        let (mut plan, mut disables) = build_plan(
978            &self.allocator,
979            &allocation,
980            &plane_layers,
981            registry,
982            crtc_index,
983            &frame_damage,
984            &pinned,
985        );
986        self.disable_foreign(&mut disables, &plan, registry, crtc_index, committer);
987        self.request_colors(&mut plan);
988        // A scene with no layers keeps its planes, and the last frame stays
989        // up: an active CRTC's only primary cannot be disabled on some
990        // controllers (i.MX LCDIF), so turning it off would get the commit
991        // refused (drm-cxx `d895c8d`). A scene whose layers were replaced
992        // still turns off the planes they left.
993        if self.is_empty() {
994            disables.clear();
995        }
996        let canvas_id = canvas_plane
997            .as_ref()
998            .map(|composition| composition.plane_id);
999        let disables = if let Some(composition) = canvas_plane {
1000            let plane_id = composition.plane_id;
1001            plan.push(PlanePlan {
1002                plane_id,
1003                // The canvas is the scene's own surface, not any one layer's,
1004                // so it carries no layer identity and no committed baseline:
1005                // every property is written every frame it is armed.
1006                layer_id: LayerId(0),
1007                layer: composition.layer,
1008                baseline: None,
1009                // The canvas is composited fresh each frame it is armed, and
1010                // its content is the union of what it rescued -- there is no
1011                // one source's damage to report, so it repaints whole.
1012                damage: Vec::new(),
1013                zpos: None,
1014                color: None,
1015            });
1016            disables.into_iter().filter(|id| *id != plane_id).collect()
1017        } else {
1018            disables
1019        };
1020
1021        self.stack_plan(&mut plan, canvas_id, registry);
1022
1023        Ok(FrameBuild {
1024            acquisitions,
1025            plan,
1026            disables,
1027            report,
1028            kind,
1029            finalized: false,
1030            restate_sticky: self.planes_unknown,
1031        })
1032    }
1033
1034    /// Give each layer's plan entry the YCbCr interpretation the layer asks
1035    /// for. Called before the canvas joins the plan: it is no layer's, and
1036    /// asks for nothing.
1037    fn request_colors(&self, plan: &mut [PlanePlan]) {
1038        for entry in plan {
1039            entry.color = self
1040                .handles()
1041                .find(|handle| handle.layer_id() == entry.layer_id)
1042                .and_then(|handle| self.layer(handle))
1043                .map(|layer| layer.display().ycbcr());
1044        }
1045    }
1046
1047    /// Turn off the planes another client left lit, while the scene does not
1048    /// yet know what the kernel holds (see `planes_unknown`).
1049    ///
1050    /// Asks the committer what is lit on this CRTC; one that cannot tell gets
1051    /// every plane the frame does not use turned off, as every test does. Not
1052    /// for an empty frame: that keeps whatever is there, since an active
1053    /// CRTC's only primary cannot be disabled on some controllers (i.MX
1054    /// LCDIF), the rule upstream's `d895c8d` keeps too.
1055    fn disable_foreign<C: TestCommitter>(
1056        &self,
1057        disables: &mut Vec<u32>,
1058        plan: &[PlanePlan],
1059        registry: &PlaneRegistry,
1060        crtc_index: u32,
1061        committer: &mut C,
1062    ) {
1063        if !self.planes_unknown || plan.is_empty() {
1064            return;
1065        }
1066        let candidates: Vec<u32> = registry
1067            .force_disable_candidates(crtc_index)
1068            .map(|plane| plane.id)
1069            .collect();
1070        let lit = committer
1071            .lit_planes(self.crtc_id)
1072            .unwrap_or_else(|| candidates.clone());
1073        for plane_id in lit {
1074            if candidates.contains(&plane_id)
1075                && !disables.contains(&plane_id)
1076                && !plan.iter().any(|entry| entry.plane_id == plane_id)
1077            {
1078                disables.push(plane_id);
1079            }
1080        }
1081    }
1082
1083    /// Give the scene a composition canvas.
1084    ///
1085    /// Layers the allocator cannot place are blended into this surface and it
1086    /// goes on a plane of its own. Without one they are reported unassigned
1087    /// and simply do not reach the screen.
1088    ///
1089    /// Separate from [`new`](Self::new) because the search itself is
1090    /// device-free and this is not: the canvas owns two dumb buffers.
1091    ///
1092    /// The canvas is `ARGB8888`, which every plane that matters takes.
1093    /// [`enable_composition_on`](Self::enable_composition_on) is for the ones
1094    /// that do not.
1095    ///
1096    /// # Errors
1097    ///
1098    /// `drmkit_dumb::DumbError` if either buffer cannot be allocated.
1099    pub fn enable_composition(
1100        &mut self,
1101        device: &Device,
1102        width: u32,
1103        height: u32,
1104    ) -> Result<(), drmkit_dumb::DumbError> {
1105        self.canvas = Some(CompositeCanvas::create(device, width, height)?);
1106        Ok(())
1107    }
1108
1109    /// Give the scene a canvas in a format `plane` can scan out.
1110    ///
1111    /// On a controller with one plane and no `ARGB8888` -- tilcdc on a
1112    /// `BeagleBone`, i.MX LCDIF, the small SPI panels -- an `ARGB8888` canvas
1113    /// cannot be armed at all, so every layer that overflows the plane count
1114    /// is dropped rather than composited. This negotiates instead, and blends
1115    /// in `ARGB8888` regardless: the conversion happens once at flush rather
1116    /// than quantizing at every layer.
1117    ///
1118    /// # Errors
1119    ///
1120    /// `drmkit_dumb::DumbError::InvalidConfig` if the plane advertises nothing
1121    /// the canvas
1122    /// can write -- a YUV-only overlay, say -- and otherwise as
1123    /// [`enable_composition`](Self::enable_composition).
1124    pub fn enable_composition_on(
1125        &mut self,
1126        device: &Device,
1127        width: u32,
1128        height: u32,
1129        plane: &drmkit_planes::PlaneCapabilities,
1130    ) -> Result<(), drmkit_dumb::DumbError> {
1131        let fourcc = crate::canvas::canvas_format_for_plane(plane).ok_or(
1132            drmkit_dumb::DumbError::InvalidConfig {
1133                reason: "this plane scans out nothing the canvas can write",
1134            },
1135        )?;
1136        self.canvas = Some(CompositeCanvas::create_in(device, width, height, fourcc)?);
1137        Ok(())
1138    }
1139
1140    /// What the composition canvas scans out as, if there is one.
1141    #[must_use]
1142    pub fn canvas_fourcc(&self) -> Option<u32> {
1143        self.canvas.as_ref().map(CompositeCanvas::fourcc)
1144    }
1145
1146    /// Blend the layers the allocator dropped, and say which plane to put them
1147    /// on.
1148    ///
1149    /// Returns the canvas plane and its lowered properties, or `None` when
1150    /// nothing needs compositing or the frame cannot rescue them.
1151    ///
1152    /// The rescue is best-effort and silent by design, matching upstream: no
1153    /// canvas, no free plane, or a source that cannot be read leaves those
1154    /// layers off the screen for this frame rather than failing the commit.
1155    /// That asymmetry is deliberate — the canvas's properties go into the same
1156    /// atomic request as everything else, so once it is armed a kernel
1157    /// rejection takes the whole frame down, including every layer that *was*
1158    /// placed. Dropping one layer beats dropping all of them.
1159    /// Allocate planes, holding one back for the composition canvas when the
1160    /// frame turns out to need one.
1161    ///
1162    /// The canvas takes a plane, and until this ran nothing had validated a
1163    /// frame that included it: the allocator tests the layer assignment, and
1164    /// `compose_unassigned` adds the canvas afterwards. On hardware that can
1165    /// light fewer planes than it advertises the difference is fatal --
1166    /// rockchip's VOP2 on an RK3566 offers three planes and accepts two, so a
1167    /// validated two-plane assignment plus a canvas is a frame the kernel
1168    /// refuses, and a refused frame puts nothing on screen at all.
1169    ///
1170    /// [`canvas_reservation`] alone cannot see this. It reserves when the
1171    /// layer count exceeds the *candidate* count, a proxy for "the canvas will
1172    /// be needed" that says no here: two layers, two candidates, and a
1173    /// hardware limit of two including the canvas. So when composition turns
1174    /// out to be needed and nothing was reserved up front, hold a plane back
1175    /// and allocate again. The second pass runs only on frames that composite.
1176    ///
1177    /// This narrows the problem rather than closing it. Where the hardware
1178    /// limit is at or below the candidate count, `assigned` is already the
1179    /// most the kernel would take, so `assigned` plus a canvas overshoots
1180    /// whatever is held back. Closing it means putting the canvas plane into
1181    /// the assignments the allocator tests -- see P-18 and drm-cxx#244.
1182    fn allocate_with_canvas<C: TestCommitter>(
1183        &mut self,
1184        refs: &[LayerRef<'_>],
1185        registry: &PlaneRegistry,
1186        crtc_index: u32,
1187        committer: &mut C,
1188    ) -> Result<drmkit_planes::Allocation, SceneError> {
1189        let reserved = canvas_reservation(
1190            self.canvas.is_some(),
1191            refs,
1192            registry,
1193            crtc_index,
1194            self.last_canvas_plane,
1195        );
1196        self.allocator.set_reserved_planes(&reserved);
1197        // A plane reserved up front is the canvas's, so the search arms the
1198        // canvas there in its tests. Without that the tests left it out, and
1199        // where the kernel lights fewer planes than it offers the frame
1200        // committed one plane over what it took (the SA8155P, P-18).
1201        self.allocator.hold_canvas_plane(reserved.first().copied());
1202        // Where planes stack by id the allocator picks the canvas plane
1203        // itself, so it has to know which ones can carry it.
1204        let hosts: Vec<u32> = self.canvas.as_ref().map_or_else(Vec::new, |canvas| {
1205            registry
1206                .force_disable_candidates(crtc_index)
1207                .filter(|plane| plane.scans_out_linear(canvas.fourcc()))
1208                .map(|plane| plane.id)
1209                .collect()
1210        });
1211        let canvas_layer = self
1212            .canvas
1213            .as_ref()
1214            .and_then(|canvas| lower_canvas(canvas, self.crtc_id, self.canvas_zpos()));
1215        self.allocator.set_canvas(&hosts, canvas_layer.as_ref());
1216
1217        let mut allocation = match self
1218            .allocator
1219            .allocate(refs, registry, crtc_index, committer)
1220        {
1221            Ok(allocation) => allocation,
1222            Err(TestFailure::NotMaster) => return Err(SceneError::NotMaster),
1223            // The allocator handles ordinary rejections internally, so this is
1224            // unreachable in practice; treating it as "nothing placed" keeps
1225            // the frame honest rather than panicking.
1226            Err(TestFailure::Rejected) => drmkit_planes::Allocation::default(),
1227        };
1228
1229        // The canvas already has a plane it was tested on: the one held up
1230        // front, the one a cached assignment kept, or, where planes stack by
1231        // id, the one between the run's neighbors. A spare held back there
1232        // would sit wherever its id puts it.
1233        if self.canvas.is_none()
1234            || allocation.composited.is_empty()
1235            || self.allocator.canvas_plane().is_some()
1236            || drmkit_planes::stacks_by_plane_id(registry, crtc_index)
1237        {
1238            return Ok(allocation);
1239        }
1240        // Not a multirect virtual plane: held, it is armed in every test of
1241        // the second pass, and the search may give its parent away.
1242        let Some(spare) = canvas_plane_order(&hosts, registry, crtc_index, self.last_canvas_plane)
1243            .find(|id| {
1244                allocation.assignment.get(*id).is_none() && !is_multirect_virtual(registry, *id)
1245            })
1246        else {
1247            return Ok(allocation);
1248        };
1249
1250        let first_pass_tests = allocation.diagnostics.test_commits_issued;
1251        let first_pass_budget = allocation.diagnostics.budget_exhausted;
1252        // Hold the spare for the canvas, which arms it in every test of the
1253        // second pass. This is what makes the search account for it: the
1254        // allocator stops when the kernel accepts, and now what the kernel is
1255        // accepting is the whole frame rather than the frame minus a plane.
1256        // The first pass's assignment does not use the spare, so its warm
1257        // start re-tests that assignment with the canvas armed, and searches
1258        // only if the kernel refuses the pair.
1259        self.allocator.set_reserved_planes(&[spare]);
1260        self.allocator.hold_canvas_plane(Some(spare));
1261        let outcome = self
1262            .allocator
1263            .allocate(refs, registry, crtc_index, committer);
1264        match outcome {
1265            Ok(mut second) => {
1266                // `allocate` zeroes its own per-frame counter, so without this
1267                // the report would show the second pass's cost and hide the
1268                // first.
1269                second.diagnostics.test_commits_issued += first_pass_tests;
1270                // Nor its verdict: the second pass re-tests what the first
1271                // settled on, so a budget that bound the first decided what
1272                // is composited, and the report has to say so.
1273                second.diagnostics.budget_exhausted |= first_pass_budget;
1274                allocation = second;
1275            }
1276            Err(TestFailure::NotMaster) => return Err(SceneError::NotMaster),
1277            // Keep the first pass rather than dropping the frame: a refused
1278            // second attempt is worse than an unvalidated canvas, which at
1279            // least usually works.
1280            Err(TestFailure::Rejected) => {}
1281        }
1282        Ok(allocation)
1283    }
1284
1285    /// Allocate planes, then test the pins: the allocation, and how many
1286    /// pins the kernel refused.
1287    fn allocate_frame<C: TestCommitter>(
1288        &mut self,
1289        plane_layers: &mut [(LayerId, PlaneLayer)],
1290        pinned: &mut Vec<(LayerId, u32)>,
1291        registry: &PlaneRegistry,
1292        crtc_index: u32,
1293        committer: &mut C,
1294    ) -> Result<(drmkit_planes::Allocation, usize), SceneError> {
1295        lower_bottom_layer_to_primary_slot(plane_layers, registry, crtc_index);
1296        let refs: Vec<LayerRef<'_>> = plane_layers
1297            .iter()
1298            .map(|(id, layer)| LayerRef { id: *id, layer })
1299            .collect();
1300        let mut allocation = self.allocate_with_canvas(&refs, registry, crtc_index, committer)?;
1301        drop(refs);
1302        let refused = self.test_pins(&mut allocation, plane_layers, pinned, committer)?;
1303        Ok((allocation, refused))
1304    }
1305
1306    /// Test each pin the kernel has not been asked about, once per layer,
1307    /// plane and geometry, and say how many it refused (drm-cxx `c766e97`).
1308    ///
1309    /// A pin the static checks accept can still be one the driver will not
1310    /// drive -- a plane it refuses on this CRTC -- and the allocator's tests
1311    /// leave pinned planes off, so nothing else would find out before the real
1312    /// commit failed whole. Each pin is tested with the assignment and the
1313    /// pins already proven. A refused one is composited this frame, and from
1314    /// the next is not honoured, so the layer takes normal allocation.
1315    fn test_pins<C: TestCommitter>(
1316        &mut self,
1317        allocation: &mut drmkit_planes::Allocation,
1318        plane_layers: &mut [(LayerId, PlaneLayer)],
1319        pinned: &mut Vec<(LayerId, u32)>,
1320        committer: &mut C,
1321    ) -> Result<usize, SceneError> {
1322        let (refused, tests) = self.ask_about_pins(allocation, plane_layers, pinned, committer)?;
1323        allocation.diagnostics.test_commits_issued += tests;
1324        if refused.is_empty() {
1325            return Ok(0);
1326        }
1327        // Composited this frame; from the next the pin is not honoured and the
1328        // layer takes normal allocation.
1329        for (layer_id, plane_layer) in plane_layers.iter_mut() {
1330            if refused.contains(layer_id) {
1331                plane_layer.set_pinned(false).set_assigned_plane(None);
1332            }
1333        }
1334        pinned.retain(|(layer_id, _)| !refused.contains(layer_id));
1335        allocation.composited.extend(refused.iter().copied());
1336        self.allocator.invalidate_allocation();
1337        Ok(refused.len())
1338    }
1339
1340    /// The pins `test_pins` refused this frame, and how many tests it took.
1341    fn ask_about_pins<C: TestCommitter>(
1342        &mut self,
1343        allocation: &drmkit_planes::Allocation,
1344        plane_layers: &[(LayerId, PlaneLayer)],
1345        pinned: &[(LayerId, u32)],
1346        committer: &mut C,
1347    ) -> Result<(Vec<LayerId>, usize), SceneError> {
1348        let layer_of = |id: LayerId| {
1349            plane_layers
1350                .iter()
1351                .find(|(layer_id, _)| *layer_id == id)
1352                .map(|(_, layer)| layer)
1353        };
1354        let mut pairs: Vec<(u32, LayerRef<'_>)> = allocation
1355            .assignment
1356            .entries()
1357            .iter()
1358            .filter_map(|(plane_id, id)| {
1359                layer_of(*id).map(|layer| (*plane_id, LayerRef { id: *id, layer }))
1360            })
1361            .collect();
1362        let mut untested = Vec::new();
1363        for (id, plane_id) in pinned {
1364            let Some(layer) = layer_of(*id) else {
1365                continue;
1366            };
1367            match pin_verdict(&self.pin_verdicts, *id, *plane_id, layer.property_hash()) {
1368                Some(true) => pairs.push((*plane_id, LayerRef { id: *id, layer })),
1369                // Revoked when lowered; not here.
1370                Some(false) => {}
1371                None => untested.push((*id, *plane_id, layer)),
1372            }
1373        }
1374
1375        let mut refused = Vec::new();
1376        let mut tests = 0;
1377        for (id, plane_id, layer) in untested {
1378            pairs.push((plane_id, LayerRef { id, layer }));
1379            tests += 1;
1380            let accepted = match committer.test_assignment(&pairs) {
1381                Ok(()) => true,
1382                Err(TestFailure::NotMaster) => return Err(SceneError::NotMaster),
1383                Err(TestFailure::Rejected) => false,
1384            };
1385            remember_pin(
1386                &mut self.pin_verdicts,
1387                PinVerdict {
1388                    layer: id,
1389                    plane: plane_id,
1390                    hash: layer.property_hash(),
1391                    accepted,
1392                },
1393            );
1394            if !accepted {
1395                pairs.pop();
1396                refused.push(id);
1397            }
1398        }
1399        Ok((refused, tests))
1400    }
1401
1402    /// Number the zpos of everything `plan` arms in one stack -- allocated,
1403    /// pinned and the canvas on `canvas`.
1404    ///
1405    /// Numbering only the armed planes is what keeps composited layers from
1406    /// using up the range (drm-cxx `1384526`). The canvas asks for the
1407    /// allocator's slot and stacks under the placed layer that shares it;
1408    /// where the slot leaves no room in the range it goes on top, as upstream
1409    /// drops it (`07226e4`).
1410    fn stack_plan(&self, plan: &mut [PlanePlan], canvas: Option<u32>, registry: &PlaneRegistry) {
1411        let armed = |plan: &[PlanePlan]| -> Vec<(u32, Option<u64>)> {
1412            plan.iter()
1413                .map(|entry| (entry.plane_id, entry.layer.property(PropTag::Zpos)))
1414                .collect()
1415        };
1416        let slot = self.allocator.canvas_zpos();
1417        let slotted = canvas
1418            .filter(|_| slot.is_some())
1419            .and_then(|canvas| drmkit_planes::stacked_zpos_beneath(registry, &armed(plan), canvas));
1420        let stack = slotted.unwrap_or_else(|| {
1421            if let Some(canvas) = canvas
1422                && slot.is_some()
1423                && let Some(entry) = plan.iter_mut().find(|entry| entry.plane_id == canvas)
1424            {
1425                entry.layer.set_property(PropTag::Zpos, self.canvas_zpos());
1426            }
1427            drmkit_planes::stacked_zpos(registry, &armed(plan))
1428        });
1429        for (plane_id, zpos) in stack {
1430            if let Some(entry) = plan.iter_mut().find(|entry| entry.plane_id == plane_id) {
1431                entry.zpos = Some(zpos);
1432            }
1433        }
1434    }
1435
1436    /// Where the canvas sits in the allocator's tests: above every layer.
1437    ///
1438    /// The tests run before anything is composited, so they cannot know the
1439    /// run's own zpos, which is where the built frame stacks the canvas (see
1440    /// `compose_unassigned`).
1441    fn canvas_zpos(&self) -> u64 {
1442        let mut zpos = 0u64;
1443        for handle in self.handles().collect::<Vec<_>>() {
1444            if let Some(layer) = self.layer(handle) {
1445                zpos = zpos.max(layer.display().zpos.unwrap_or(0));
1446            }
1447        }
1448        zpos.saturating_add(1)
1449    }
1450
1451    fn compose_unassigned(
1452        &mut self,
1453        allocation: &drmkit_planes::Allocation,
1454        registry: &PlaneRegistry,
1455        crtc_index: u32,
1456    ) -> Option<Composition> {
1457        if allocation.composited.is_empty() {
1458            return None;
1459        }
1460
1461        // A plane the assignment did not take. Cursor planes are excluded for
1462        // the same reason they are never force-disabled: they belong to the
1463        // cursor path.
1464        //
1465        // Where planes stack by id, the allocator's pick is the only plane
1466        // that stacks the canvas between its neighbors; without one, the
1467        // topmost free plane is the closest to "above every layer".
1468        // A multirect virtual plane can carry the canvas only alongside its
1469        // parent, so only when the assignment armed the parent.
1470        let free = |id: &u32| {
1471            allocation.assignment.get(*id).is_none()
1472                && registry.by_id(*id).is_none_or(|plane| {
1473                    drmkit_planes::multirect_pairing_ok(plane.multirect_parent, |parent| {
1474                        allocation.assignment.get(parent).is_some()
1475                    })
1476                })
1477        };
1478        let plane_id = if drmkit_planes::stacks_by_plane_id(registry, crtc_index) {
1479            self.allocator.canvas_plane().filter(free).or_else(|| {
1480                registry
1481                    .force_disable_candidates(crtc_index)
1482                    .map(|plane| plane.id)
1483                    .filter(free)
1484                    .max()
1485            })?
1486        } else {
1487            // The plane its tests armed it on, else upstream's order.
1488            let hosts: Vec<u32> = registry
1489                .force_disable_candidates(crtc_index)
1490                .filter(|plane| {
1491                    self.canvas
1492                        .as_ref()
1493                        .is_some_and(|canvas| plane.scans_out_linear(canvas.fourcc()))
1494                })
1495                .map(|plane| plane.id)
1496                .collect();
1497            self.allocator.canvas_plane().filter(free).or_else(|| {
1498                canvas_plane_order(&hosts, registry, crtc_index, self.last_canvas_plane)
1499                    .find(|id| free(id))
1500            })?
1501        };
1502
1503        // Resolve every composited layer to its slot before touching the
1504        // canvas: `LayerId` packs a handle and a generation, and undoing that
1505        // inside the blend loop would duplicate `resolve`'s staleness rules.
1506        let mut targets: Vec<(u64, usize)> = Vec::new();
1507        for handle in self.handles().collect::<Vec<_>>() {
1508            let id = handle.layer_id();
1509            if !allocation.composited.contains(&id) {
1510                continue;
1511            }
1512            let Some(index) = self.resolve(handle) else {
1513                continue;
1514            };
1515            let order = self
1516                .layer(handle)
1517                .and_then(|l| l.display().zpos)
1518                .unwrap_or(0);
1519            targets.push((order, index));
1520        }
1521        if targets.is_empty() {
1522            return None;
1523        }
1524        // Bottom-up, so stacking inside the canvas matches what the layers
1525        // asked for. `composited` comes back in allocation order, which is not
1526        // it.
1527        targets.sort_unstable();
1528
1529        // Just under the lowest placed layer that overlaps a composited one
1530        // and must cover it, the slot the allocator found (drm-cxx `07226e4`,
1531        // P-44, drm-cxx#343); above every layer when no placed layer must
1532        // cover the canvas. Above every layer regardless, as this once was,
1533        // the canvas covered any placed layer stacked over what it carries,
1534        // which overlay-first placement makes the common case: on a Pi 5 the
1535        // plane-limit scene's top tile went under a canvas carrying the
1536        // background. The allocator keeps a placed layer from having to sit on
1537        // both sides of the canvas, so where planes take a zpos the slot
1538        // exists; `build_frame` stacks the canvas under the layer whose zpos
1539        // the slot shares.
1540        //
1541        // The tests armed the canvas above every layer (see `canvas_zpos`),
1542        // since which layers are composited is not known before the search;
1543        // the planes and buffers are the ones tested, only the order differs.
1544        let zpos = self
1545            .allocator
1546            .canvas_zpos()
1547            .unwrap_or_else(|| self.canvas_zpos());
1548
1549        // Disjoint field borrows: the sources live in `slots`, the canvas does
1550        // not, and the blend needs both at once.
1551        let Self {
1552            slots,
1553            canvas,
1554            crtc_id,
1555            ..
1556        } = self;
1557        let canvas = canvas.as_mut()?;
1558
1559        canvas.begin_frame();
1560        canvas.clear();
1561
1562        let blended = blend_targets(canvas, slots, &targets);
1563
1564        if blended == 0 {
1565            return None;
1566        }
1567        canvas.flush();
1568
1569        Some(Composition {
1570            plane_id,
1571            layer: lower_canvas(canvas, *crtc_id, zpos)?,
1572            blended,
1573        })
1574    }
1575
1576    /// Reconcile scene state with the kernel's answer, releasing buffers per
1577    /// invariants 1, 2 and 5.
1578    pub fn finalize_frame(&mut self, build: FrameBuild, result: KernelResult) -> CommitReport {
1579        self.finalize_frame_with_fence(build, result, None)
1580    }
1581
1582    /// Finalize a frame, handing over the commit's `OUT_FENCE`.
1583    ///
1584    /// `release_fence` signals once this commit's buffers are on screen —
1585    /// which is the moment the buffers it *displaced* are off screen and safe
1586    /// to render into again. Sources that opted in through
1587    /// [`wants_release_fence`](LayerBufferSource::wants_release_fence) get a
1588    /// duplicate of it alongside each released buffer, so a GPU producer waits
1589    /// on it GPU-side instead of blocking until a later release edge.
1590    ///
1591    /// Ask [`wants_release_fence`](Self::wants_release_fence) whether to arm
1592    /// `OUT_FENCE_PTR` at all: with no source listening, the fence costs a
1593    /// descriptor per commit and nothing reads it.
1594    ///
1595    /// Passing `None` is [`finalize_frame`](Self::finalize_frame), and is
1596    /// correct whenever the CRTC has no `OUT_FENCE_PTR` or the commit did not
1597    /// ask for one.
1598    pub fn finalize_frame_with_fence(
1599        &mut self,
1600        mut build: FrameBuild,
1601        result: KernelResult,
1602        release_fence: Option<&SyncFence>,
1603    ) -> CommitReport {
1604        build.finalized = true;
1605        let acquisitions = std::mem::take(&mut build.acquisitions);
1606        let report = std::mem::take(&mut build.report);
1607        let kind = build.kind;
1608        let plan = std::mem::take(&mut build.plan);
1609
1610        // Record what the kernel actually took, so the next frame's FB-only
1611        // fast path has a baseline to diff against (invariant 4).
1612        //
1613        // Only after a real commit that succeeded. A `TEST_ONLY` applies
1614        // nothing, so recording one would let a later commit diff against
1615        // state the kernel never held and suppress properties it still needs.
1616        //
1617        // Without this the baseline stays empty forever, `is_fb_only_frame`
1618        // can never be true, and the fast path is unreachable outside the
1619        // unit tests that populate the baseline by hand -- which is exactly
1620        // how it went unnoticed until the parity harness counted the test
1621        // commits the reference did not issue.
1622        if matches!(kind, CommitKind::Real { .. }) && matches!(result, KernelResult::Ok) {
1623            let applied: Vec<(u32, drmkit_planes::LayerRef<'_>)> = plan
1624                .iter()
1625                .map(|entry| {
1626                    (
1627                        entry.plane_id,
1628                        drmkit_planes::LayerRef {
1629                            id: entry.layer_id,
1630                            layer: &entry.layer,
1631                        },
1632                    )
1633                })
1634                .collect();
1635            let stacked: Vec<(u32, u64)> = plan
1636                .iter()
1637                .filter_map(|entry| entry.zpos.map(|zpos| (entry.plane_id, zpos)))
1638                .collect();
1639            self.allocator.record_commit_stacked(&applied, &stacked);
1640            // An empty frame swept nothing, so it settles nothing.
1641            if !plan.is_empty() {
1642                self.planes_unknown = false;
1643            }
1644        }
1645
1646        // Which layers asked for the release fence, decided before the
1647        // lifecycle borrow: the answer lives in the sources, and the closure
1648        // cannot reach them while `finalize` holds `self` mutably.
1649        let wanting: Vec<LayerId> = if release_fence.is_some() {
1650            self.handles()
1651                .filter(|handle| {
1652                    self.layer(*handle)
1653                        .is_some_and(|layer| layer.source().wants_release_fence())
1654                })
1655                .map(LayerHandle::layer_id)
1656                .collect()
1657        } else {
1658            Vec::new()
1659        };
1660        let wants_fence = |layer: LayerId| wanting.contains(&layer);
1661        let outcome: FrameOutcome = self.lifecycle.finalize(
1662            kind,
1663            result,
1664            acquisitions,
1665            report,
1666            release_fence,
1667            wants_fence,
1668        );
1669
1670        if outcome.invalidate_allocation {
1671            self.allocator.invalidate_allocation();
1672        }
1673
1674        self.deliver(outcome.released);
1675        outcome.report
1676    }
1677
1678    /// Re-emit every property on every commit, for a driver that mishandles a
1679    /// partial write.
1680    ///
1681    /// Off by default. A quirk escape hatch, not a tuning knob: it defeats
1682    /// invariant 4's minimal-write path and multiplies per-frame property
1683    /// traffic, and the only reason to reach for it is a driver that gets the
1684    /// minimal set wrong. Exposed here because the allocator it configures is
1685    /// the scene's, and a caller stuck on such a driver otherwise has no way
1686    /// in.
1687    pub const fn set_force_full_property_writes(&mut self, force: bool) {
1688        self.allocator.set_force_full_property_writes(force);
1689    }
1690
1691    /// Whether full property writes are being forced.
1692    #[must_use]
1693    pub const fn force_full_property_writes(&self) -> bool {
1694        self.allocator.force_full_property_writes()
1695    }
1696
1697    /// Whether this frame is worth committing at all.
1698    ///
1699    /// **The whole-commit skip.** When every live source says it has nothing
1700    /// new and no layer has moved, the frame can be dropped without issuing an
1701    /// atomic commit — the display goes on scanning out what is already there.
1702    /// That is a power win on any device and the thing that lets a
1703    /// self-refresh panel stay in self-refresh, which it cannot do if a
1704    /// commit arrives every vblank saying nothing changed.
1705    ///
1706    /// Answers `true` for anything it cannot rule out. A source that cannot
1707    /// tell whether its buffer changed reports "changed" by default, so a CPU
1708    /// producer painting into a dumb buffer never gets skipped; the layer set
1709    /// changing counts, geometry changing counts, and the first frame always
1710    /// counts, because nothing is on screen yet for "unchanged" to describe.
1711    #[must_use]
1712    pub fn content_changed(&self) -> bool {
1713        if !self.committed_once || self.topology_dirty {
1714            return true;
1715        }
1716        self.handles().any(|handle| {
1717            self.layer(handle).is_some_and(|layer| {
1718                layer.hints_dirty() || layer.display_dirty || layer.source().has_fresh_content()
1719            })
1720        })
1721    }
1722
1723    /// The first layer carrying `tag`, if any.
1724    ///
1725    /// The lookup that makes [`SceneLayer::set_identity_tag`] worth setting:
1726    /// a caller that survived a rebind, a session resume, or its own restart
1727    /// can find its layers again from its own identifiers rather than from
1728    /// handles it may no longer hold.
1729    #[must_use]
1730    pub fn find_by_identity_tag(&self, tag: u64) -> Option<LayerHandle> {
1731        self.handles().find(|handle| {
1732            self.layer(*handle)
1733                .is_some_and(|layer| layer.identity_tag() == Some(tag))
1734        })
1735    }
1736
1737    /// What a rebind left lit on the CRTC the scene moved away from, until it
1738    /// is committed.
1739    #[must_use]
1740    pub const fn pending_detach(&self) -> Option<&PendingDetach> {
1741        self.detach.as_ref()
1742    }
1743
1744    /// Record that the pending detach reached the kernel.
1745    ///
1746    /// [`commit_detach`](crate::commit_detach) calls this after a successful
1747    /// commit; a caller committing the detach some other way must too.
1748    pub fn detach_committed(&mut self) {
1749        self.detach = None;
1750    }
1751
1752    /// Move this scene to another output, keeping its layers.
1753    ///
1754    /// For an output that changed underneath the caller: a mode set, a
1755    /// different CRTC after a session resume, a display swapped for another.
1756    /// **Layer handles survive** — that is the point, since a caller that had
1757    /// to rebuild its layers would have to rebuild everything it knows about
1758    /// them too.
1759    ///
1760    /// Everything cached about the previous output is dropped -- the
1761    /// assignment, the committed baseline, the failure cache -- and the next
1762    /// commit is a full emit, with which the caller must set the new mode.
1763    ///
1764    /// **Moving to another CRTC leaves the old one's planes lit**, and the
1765    /// scene cannot turn them off itself: it does not commit. They are queued
1766    /// as a [`PendingDetach`], and until the caller commits it through
1767    /// [`commit_detach`](crate::commit_detach), [`build_frame`](Self::build_frame)
1768    /// refuses with [`SceneError::DetachPending`]. Two reasons it cannot be
1769    /// skipped: the kernel will not move a plane from one CRTC to another in a
1770    /// single commit, so a plane both pipes can use is rejected outright on the
1771    /// new one; and a plane the new pipe cannot use would go on showing this
1772    /// scene's last frame on the old output indefinitely. A rebind to the same
1773    /// CRTC queues nothing. Upstream fixed the same defect in `a335bef`
1774    /// (drm-cxx#340), from inside its `rebind`, which owns a device.
1775    ///
1776    /// Returns what will not fit. A layer whose destination lies outside the
1777    /// new mode is **not** dropped — the caller may be about to move it, and
1778    /// deciding on its behalf would be worse than saying so.
1779    pub fn rebind(&mut self, crtc_id: u32, width: u32, height: u32) -> RebindReport {
1780        if crtc_id != self.crtc_id {
1781            // Read before the baseline that records them is forgotten. A
1782            // second rebind before the first detach landed keeps the first:
1783            // those planes are still lit on that CRTC, not on this one.
1784            let planes = self.allocator.lit_planes();
1785            if self.detach.is_none() && !planes.is_empty() {
1786                self.detach = Some(PendingDetach {
1787                    crtc_id: self.crtc_id,
1788                    planes,
1789                });
1790            }
1791        }
1792        self.crtc_id = crtc_id;
1793        self.allocator.forget_output();
1794        // Everything has to be re-emitted against the new pipe, and the layer
1795        // set is effectively new to it.
1796        self.topology_dirty = true;
1797        self.committed_once = false;
1798        self.planes_unknown = true;
1799        self.last_canvas_plane = None;
1800        self.pin_verdicts.clear();
1801        for handle in self.handles().collect::<Vec<_>>() {
1802            if let Some(layer) = self.layer_mut(handle) {
1803                layer.hints_dirty = true;
1804                layer.display_dirty = true;
1805                layer.last_fb_id = None;
1806            }
1807        }
1808
1809        let incompatibilities = self
1810            .handles()
1811            .filter_map(|handle| {
1812                let layer = self.layer(handle)?;
1813                let dst = layer.display().dst_rect;
1814                let off_screen = dst.x < 0
1815                    || dst.y < 0
1816                    || dst.x.saturating_add(dst.w.cast_signed()) > width.cast_signed()
1817                    || dst.y.saturating_add(dst.h.cast_signed()) > height.cast_signed();
1818                off_screen.then_some(LayerIncompatibility {
1819                    handle,
1820                    reason: IncompatibilityReason::DstRectOffScreen,
1821                })
1822            })
1823            .collect();
1824
1825        RebindReport { incompatibilities }
1826    }
1827
1828    /// Whether any live source wants this commit's `OUT_FENCE`.
1829    ///
1830    /// The commit path asks before arming `OUT_FENCE_PTR`: the fence costs a
1831    /// descriptor per commit that has to be closed, and with nothing listening
1832    /// it is a descriptor leak dressed as a feature.
1833    #[must_use]
1834    pub fn wants_release_fence(&self) -> bool {
1835        self.handles().any(|handle| {
1836            self.layer(handle)
1837                .is_some_and(|layer| layer.source().wants_release_fence())
1838        })
1839    }
1840
1841    /// Hand back now the buffers that the page flip of real commit
1842    /// `commit_seq` (its [`CommitReport::commit_seq`]) took off screen,
1843    /// instead of at the next real commit.
1844    ///
1845    /// A buffer leaves the screen when the flip of the commit after the one
1846    /// that showed it completes, and the scene hands it back at the commit
1847    /// after that (invariant 1) -- right for a caller that commits ahead of its
1848    /// flips. A caller that waits for each flip before committing again,
1849    /// feeding producers that wait for a free buffer before rendering, may
1850    /// never make that next commit: a producer with two buffers then waits for
1851    /// good, and one with three gets each buffer a frame late. Call this once
1852    /// the flip completes -- after dispatching its event, or at the start of
1853    /// the next frame -- to hand those buffers back then.
1854    ///
1855    /// A no-op unless `commit_seq` names the most recent real commit, so a
1856    /// late or repeated call cannot release a buffer still on screen. Port of
1857    /// upstream's `release_displaced` (`ee90026`).
1858    pub fn release_displaced(&mut self, commit_seq: u64) {
1859        let released = self.lifecycle.release_displaced(commit_seq);
1860        self.deliver(released);
1861    }
1862
1863    /// Hand every held buffer back, without waiting on the kernel.
1864    ///
1865    /// **Invariant 5.** For teardown, session pause, and rebind. Landing an
1866    /// armed flip first is the caller's job — see
1867    /// [`has_pending_flip`](Self::has_pending_flip).
1868    pub fn drain(&mut self) {
1869        let released = self.lifecycle.drain();
1870        self.deliver(released);
1871    }
1872
1873    /// Route released buffers to the sources that produced them.
1874    ///
1875    /// A buffer whose layer was removed goes to the retired source that is
1876    /// being kept alive for exactly this. Once a retired source has no buffers
1877    /// left in flight it is dropped.
1878    fn deliver(&mut self, released: Released) {
1879        for acquisition in released.acquisitions {
1880            let layer_id = acquisition.layer;
1881            let fence = acquisition.release_fence;
1882
1883            let live = self.handles().find(|h| h.layer_id() == layer_id);
1884            if let Some(handle) = live
1885                && let Some(layer) = self.layer_mut(handle)
1886            {
1887                layer.source.release_with_fence(acquisition.buffer, fence);
1888                continue;
1889            }
1890
1891            if let Some(entry) = self.retiring.iter_mut().find(|(id, _)| *id == layer_id) {
1892                entry.1.release_with_fence(acquisition.buffer, fence);
1893            }
1894            // Otherwise the source is already gone, which can only happen if a
1895            // caller dropped the scene mid-flight; the buffer drops with it.
1896        }
1897
1898        if self.lifecycle.buffers_in_flight() == 0 {
1899            // Every retired source has had its buffers back.
1900            self.retiring.clear();
1901        }
1902    }
1903
1904    /// Release a set of acquisitions immediately, for a build that aborted.
1905    fn release_all(&mut self, acquisitions: Vec<Acquisition>) {
1906        self.deliver(Released {
1907            acquisitions,
1908            reason: crate::release::ReleaseReason::CommitFailed,
1909        });
1910    }
1911}
1912
1913/// Assemble the report for a built frame.
1914///
1915/// Split out of `build_frame` because it is pure over the tally, the
1916/// allocation, and the registry — nothing here touches scene state, so it reads
1917/// and reviews on its own.
1918fn build_report(
1919    tally: &AcquireTally,
1920    allocation: &drmkit_planes::Allocation,
1921    registry: &PlaneRegistry,
1922    starved: &[LayerId],
1923    composited: usize,
1924    pinned: &[(LayerId, u32)],
1925    pins_unhonored: usize,
1926) -> CommitReport {
1927    // A starved layer holds its plane so it keeps showing its last frame, so
1928    // it is in the assignment -- but it is also counted as skipped, and the
1929    // report's identity is
1930    //
1931    //     total = assigned + composited + unassigned + skipped
1932    //
1933    // so counting it in both would make every starved frame report one layer
1934    // too many. It is skipped, not assigned: nothing new was put on screen.
1935    let starved_assigned = starved
1936        .iter()
1937        .filter(|layer| allocation.assignment.get_plane_of(**layer).is_some())
1938        .count();
1939    let placements = allocation
1940        .assignment
1941        .entries()
1942        .iter()
1943        .map(|(plane_id, layer)| LayerPlacement {
1944            layer: *layer,
1945            placement: Placement::AssignedToPlane,
1946            plane_id: Some(*plane_id),
1947            plane_rotation_bits: registry
1948                .by_id(*plane_id)
1949                .map_or(0, |plane| plane.rotation_bits),
1950        })
1951        .chain(allocation.composited.iter().map(|layer| LayerPlacement {
1952            layer: *layer,
1953            placement: Placement::Unassigned,
1954            plane_id: None,
1955            plane_rotation_bits: 0,
1956        }))
1957        // A pinned layer never entered the allocation, so it has to be
1958        // reported from the pin itself. It reached a plane exactly as surely
1959        // as an allocated one did.
1960        .chain(pinned.iter().map(|(layer, plane_id)| {
1961            LayerPlacement {
1962                layer: *layer,
1963                placement: Placement::AssignedToPlane,
1964                plane_id: Some(*plane_id),
1965                plane_rotation_bits: registry
1966                    .by_id(*plane_id)
1967                    .map_or(0, |plane| plane.rotation_bits),
1968            }
1969        }))
1970        .collect();
1971
1972    CommitReport {
1973        layers_total: tally.considered(),
1974        // Pinned layers are assigned too, and counting them keeps the
1975        // accounting identity -- they are in `considered` either way.
1976        layers_assigned: allocation.assignment.len() - starved_assigned + pinned.len(),
1977        pin_requests_unhonored: pins_unhonored,
1978        // A layer the allocator dropped is only *unassigned* if composition
1979        // did not rescue it. One that reached the canvas reached hardware, so
1980        // reporting it unassigned would tell a caller a frame was lost when it
1981        // was not.
1982        layers_composited: composited,
1983        layers_unassigned: allocation.composited.len() - composited,
1984        layers_skipped_no_frame: tally.skipped_no_frame,
1985        test_commits_issued: allocation.diagnostics.test_commits_issued,
1986        fb_delta_fast_path: allocation.diagnostics.fb_delta_fast_path,
1987        budget_exhausted: allocation.diagnostics.budget_exhausted,
1988        placements,
1989        ..CommitReport::default()
1990    }
1991}
1992
1993/// What a frame's composition pass produced.
1994struct Composition {
1995    /// The plane the canvas is armed on.
1996    plane_id: u32,
1997    /// The canvas's lowered properties.
1998    layer: PlaneLayer,
1999    /// How many layers actually reached the canvas.
2000    ///
2001    /// Not the same as how many the allocator dropped: a source the CPU cannot
2002    /// read has no route in and stays off the screen.
2003    blended: usize,
2004}
2005
2006/// Which planes to hold back from the search, if any.
2007///
2008/// Two reasons to reserve, and they are not the same reason.
2009///
2010/// **Overflow.** The canvas is armed after the search, onto a plane the search
2011/// did not take -- so with more layers than planes there would be none left,
2012/// and the overflow the canvas exists to rescue would be dropped instead.
2013///
2014/// Which plane follows [`canvas_plane_order`]: last frame's canvas plane, else
2015/// the first overlay, else a primary, so the primary stays free for the
2016/// bottom layer (drm-cxx `6d787f9`). Only planes that can carry the canvas
2017/// count, toward the overflow as well as for the pick. This used to take the
2018/// *last* candidate, so the search kept the lower-indexed planes for the
2019/// layers that stack below the canvas -- which matters on hardware where plane
2020/// index is the stacking order and nothing can reorder it. Such hardware now
2021/// takes the plane-order path, which reserves nothing.
2022///
2023/// **Primary anchor.** A primary whose zpos is pinned, with no live layer
2024/// eligible to land on it. Nothing then assigns the primary, so the disable
2025/// pass writes `FB_ID = 0` and `CRTC_ID = 0` to it in **every** test -- and a
2026/// driver that enforces "an active CRTC has an armed primary" refuses each
2027/// one. Every test fails, the allocator places nothing, and the whole scene
2028/// falls through to software composition. The frame is still correct and
2029/// still arrives; it simply stops using the hardware, and no counter says so.
2030///
2031/// amdgpu pins its primary at zpos 2 and rejects the disable; ARM Mali display
2032/// cores pin similarly. The reference documents both the behaviour and the
2033/// remedy, and this is the remedy: reserving the primary takes it out of the
2034/// disable pass, so tests validate against whatever it was already showing --
2035/// the fbcon framebuffer on a cold start, the previous canvas when warm.
2036///
2037/// A layer counts as eligible when its zpos matches the pin, or when it has
2038/// none at all: an unpinned layer is free to land there and the scoring bonus
2039/// in `plane_score` will put it there. So does the unique lowest layer when
2040/// every other one sits above a fixed slot -- see
2041/// [`lower_bottom_layer_to_primary_slot`], which is what then lands it there.
2042///
2043/// Not reachable on vkms, which exposes no zpos property at all, so nothing
2044/// in CI can show this. See P-26.
2045pub(crate) fn canvas_reservation(
2046    has_canvas: bool,
2047    refs: &[LayerRef<'_>],
2048    registry: &PlaneRegistry,
2049    crtc_index: u32,
2050    previous: Option<u32>,
2051) -> Vec<u32> {
2052    // Planes stacked by id: the allocator places the canvas itself, between
2053    // the layers it carries.
2054    if !has_canvas || drmkit_planes::stacks_by_plane_id(registry, crtc_index) {
2055        return Vec::new();
2056    }
2057    let candidates: Vec<&drmkit_planes::PlaneCapabilities> =
2058        registry.force_disable_candidates(crtc_index).collect();
2059    if candidates.is_empty() {
2060        return Vec::new();
2061    }
2062
2063    // The anchor comes first: it is about a test commit failing outright,
2064    // where overflow is only about a layer being composited that need not be.
2065    if !refs.is_empty()
2066        && let Some(primary) = candidates
2067            .iter()
2068            .find(|plane| plane.plane_type == drmkit_planes::PlaneType::Primary)
2069        && let Some(pin) = primary.zpos_min
2070        && crate::canvas_format_for_plane(primary).is_some()
2071    {
2072        let zposes: Vec<Option<u64>> = refs
2073            .iter()
2074            .map(|layer| layer.layer.property(drmkit_planes::PropTag::Zpos))
2075            .collect();
2076        let anyone_eligible = zposes
2077            .iter()
2078            .any(|zpos| zpos.is_none_or(|zpos| zpos == pin))
2079            || (drmkit_planes::zpos_fixed(primary) && bottom_slot_layer(&zposes, pin).is_some());
2080        if !anyone_eligible {
2081            return vec![primary.id];
2082        }
2083    }
2084
2085    let hosts: Vec<u32> = candidates
2086        .iter()
2087        .filter(|plane| crate::canvas_format_for_plane(plane).is_some())
2088        .map(|plane| plane.id)
2089        .collect();
2090    // Not a multirect virtual plane: reserved, it stays armed through the
2091    // allocator's tests while the allocator gives its parent away, so every
2092    // test fails (drm-cxx `4b366b5`). Left free, the canvas is re-picked each
2093    // frame.
2094    if refs.len() > hosts.len() {
2095        canvas_plane_order(&hosts, registry, crtc_index, previous)
2096            .find(|id| !is_multirect_virtual(registry, *id))
2097            .into_iter()
2098            .collect()
2099    } else {
2100        Vec::new()
2101    }
2102}
2103
2104/// Whether `plane_id` is a multirect virtual plane, valid only alongside its
2105/// parent.
2106fn is_multirect_virtual(registry: &PlaneRegistry, plane_id: u32) -> bool {
2107    registry
2108        .by_id(plane_id)
2109        .is_some_and(|plane| plane.multirect_parent.is_some())
2110}
2111
2112/// The planes the canvas prefers, best first: `previous`, the plane it had
2113/// last frame, then overlays, then primaries, each in registry order. Only
2114/// `hosts`, the planes that can carry it.
2115///
2116/// Upstream's order (`6d787f9`, closing P-24): a primary is a preference for
2117/// the bottom layer, not a contract for the canvas, so on a CRTC with two
2118/// primaries and one taken a free overlay comes before the other primary.
2119/// The sticky first choice keeps the canvas from moving between planes frame
2120/// to frame.
2121pub(crate) fn canvas_plane_order<'a>(
2122    hosts: &'a [u32],
2123    registry: &'a PlaneRegistry,
2124    crtc_index: u32,
2125    previous: Option<u32>,
2126) -> impl Iterator<Item = u32> + 'a {
2127    let of_type = move |plane_type: drmkit_planes::PlaneType| {
2128        registry
2129            .for_crtc(crtc_index)
2130            .filter(move |plane| {
2131                plane.plane_type == plane_type
2132                    && hosts.contains(&plane.id)
2133                    && previous != Some(plane.id)
2134            })
2135            .map(|plane| plane.id)
2136    };
2137    previous
2138        .filter(|id| hosts.contains(id))
2139        .into_iter()
2140        .chain(of_type(drmkit_planes::PlaneType::Overlay))
2141        .chain(of_type(drmkit_planes::PlaneType::Primary))
2142}
2143
2144/// A 16.16 value rounded to the nearest whole pixel.
2145pub(crate) const fn round_16_16(value: u32) -> u32 {
2146    // Half rounds up: the bit below the binary point.
2147    (value >> 16) + ((value >> 15) & 1)
2148}
2149
2150/// Blend each target's source into the canvas, and say how many landed.
2151///
2152/// A source that cannot be CPU-read, or whose format the blend does not
2153/// support, is skipped rather than failing the frame — it simply stays off the
2154/// screen. Returns zero when none could be read, which the caller treats as
2155/// "no canvas this frame".
2156fn blend_targets(
2157    canvas: &mut CompositeCanvas,
2158    slots: &mut [Slot],
2159    targets: &[(u64, usize)],
2160) -> usize {
2161    let mut blended = 0;
2162    for (_, index) in targets {
2163        let Slot::Occupied(layer) = &mut slots[*index] else {
2164            continue;
2165        };
2166        let display = layer.display;
2167        let format = layer.source.format();
2168        if !crate::format_supported(format.fourcc) {
2169            continue;
2170        }
2171        let Ok(mapping) = layer.source.map(drmkit_dumb::MapAccess::Read) else {
2172            // A source whose pixels never reach the CPU cannot be rescued this
2173            // way. It stays dropped for the frame.
2174            continue;
2175        };
2176        let src = CompositeSrc {
2177            pixels: mapping.pixels(),
2178            src_stride_bytes: mapping.stride(),
2179            src_width: mapping.width(),
2180            src_height: mapping.height(),
2181            drm_fourcc: format.fourcc,
2182            plane_alpha: display.alpha.unwrap_or(u16::MAX),
2183        };
2184        // The canvas samples whole pixels, so a sub-pixel crop rounds to the
2185        // nearest one.
2186        let src_rect = display
2187            .src_rect_fixed
2188            .map_or(display.src_rect, |fixed| Rect {
2189                x: round_16_16(fixed.x).cast_signed(),
2190                y: round_16_16(fixed.y).cast_signed(),
2191                w: round_16_16(fixed.w),
2192                h: round_16_16(fixed.h),
2193            });
2194        canvas.blend(
2195            &src,
2196            CompositeRect {
2197                x: src_rect.x,
2198                y: src_rect.y,
2199                w: if src_rect.w == 0 {
2200                    format.width
2201                } else {
2202                    src_rect.w
2203                },
2204                h: if src_rect.h == 0 {
2205                    format.height
2206                } else {
2207                    src_rect.h
2208                },
2209            },
2210            CompositeRect {
2211                x: display.dst_rect.x,
2212                y: display.dst_rect.y,
2213                w: display.dst_rect.w,
2214                h: display.dst_rect.h,
2215            },
2216        );
2217        blended += 1;
2218    }
2219    blended
2220}
2221
2222/// The layer that can take a fixed primary slot `pin` though it asks for
2223/// another zpos: the unique lowest, with every other layer asking strictly
2224/// above the slot.
2225///
2226/// Port of upstream's `bottom_slot_layer` (drm-cxx `8bf20e6`). On it, the stack
2227/// comes out as asked whichever plane each layer lands on. `None` when any
2228/// layer leaves zpos unset -- the existing primary hint covers that -- when the
2229/// lowest is tied, or when another layer asks at or below the slot (amdgpu
2230/// layers at zpos 2 or lower keep the existing behavior).
2231pub(crate) fn bottom_slot_layer(zposes: &[Option<u64>], pin: u64) -> Option<usize> {
2232    let all: Option<Vec<u64>> = zposes.iter().copied().collect();
2233    let all = all?;
2234    let lowest = *all.iter().min()?;
2235    let mut at_lowest = all.iter().enumerate().filter(|(_, z)| **z == lowest);
2236    let (index, _) = at_lowest.next()?;
2237    if at_lowest.next().is_some() {
2238        return None;
2239    }
2240    all.iter()
2241        .enumerate()
2242        .all(|(i, z)| i == index || *z > pin)
2243        .then_some(index)
2244}
2245
2246/// Lower the bottom layer at the primary's fixed slot, when it can take it.
2247///
2248/// On a single-primary controller (i.MX LCDIF, tilcdc: slot 0) a lone layer
2249/// at zpos 1 or above otherwise never reaches the only plane, and on one that
2250/// enforces an armed primary (amdgpu, slot 2) the disable every test would
2251/// carry refuses them all. Asking at the slot keeps the requested stacking --
2252/// every other layer asks above it -- and is what the primary's scoring bonus
2253/// keys on.
2254fn lower_bottom_layer_to_primary_slot(
2255    plane_layers: &mut [(LayerId, PlaneLayer)],
2256    registry: &PlaneRegistry,
2257    crtc_index: u32,
2258) {
2259    let Some(primary) = registry.for_crtc(crtc_index).find(|plane| {
2260        plane.plane_type == drmkit_planes::PlaneType::Primary && drmkit_planes::zpos_fixed(plane)
2261    }) else {
2262        return;
2263    };
2264    let Some(pin) = primary.zpos_min else {
2265        return;
2266    };
2267    let zposes: Vec<Option<u64>> = plane_layers
2268        .iter()
2269        .map(|(_, layer)| layer.property(PropTag::Zpos))
2270        .collect();
2271    if let Some(index) = bottom_slot_layer(&zposes, pin) {
2272        plane_layers[index].1.set_property(PropTag::Zpos, pin);
2273    }
2274}
2275
2276/// Lower the canvas itself into the property bag its plane is programmed from.
2277fn lower_canvas(canvas: &CompositeCanvas, crtc_id: u32, zpos: u64) -> Option<PlaneLayer> {
2278    let fb_id = canvas.fb_id()?;
2279    let (w, h) = (canvas.width(), canvas.height());
2280    let mut plane_layer = PlaneLayer::new();
2281    lower_layer(
2282        &LoweringInput {
2283            display: DisplayParams {
2284                src_rect: Rect { x: 0, y: 0, w, h },
2285                dst_rect: Rect { x: 0, y: 0, w, h },
2286                zpos: Some(zpos),
2287                ..DisplayParams::default()
2288            },
2289            format: SourceFormat {
2290                fourcc: drmkit_fmt::fourcc::ARGB8888,
2291                modifier: 0,
2292                width: w,
2293                height: h,
2294            },
2295            binding: BindingModel::SceneSubmitsFbId,
2296            fb_id,
2297            crtc_id,
2298            default_zpos: None,
2299        },
2300        &mut plane_layer,
2301    );
2302    Some(plane_layer)
2303}
2304
2305/// Lower one scene layer into the property bag a plane is programmed from.
2306///
2307/// Shared by the ordinary path and the starved path, which differ only in
2308/// which framebuffer they name: a fresh acquisition, or the one the layer
2309/// already has on screen.
2310fn lower(
2311    layer: &SceneLayer,
2312    fb_id: u32,
2313    crtc_id: u32,
2314    registry: &PlaneRegistry,
2315    crtc_index: u32,
2316) -> (drmkit_planes::Layer, PinOutcome) {
2317    let mut plane_layer = PlaneLayer::new();
2318    lower_layer(
2319        &LoweringInput {
2320            display: layer.display,
2321            format: layer.source.format(),
2322            binding: layer.source.binding_model(),
2323            fb_id,
2324            crtc_id,
2325            default_zpos: None,
2326        },
2327        &mut plane_layer,
2328    );
2329    plane_layer.set_content_type(layer.content_type);
2330    plane_layer.set_update_hint(layer.update_hint_hz);
2331    plane_layer.set_app_priority(layer.app_priority);
2332    plane_layer.set_force_composited(layer.force_composited);
2333
2334    // The pin, if it can be honoured. `set_pinned` makes the allocator skip
2335    // this layer entirely -- the scene owns the plane from here -- so an
2336    // unhonourable pin must not set it, or the layer would be skipped by the
2337    // allocator *and* placed on nothing.
2338    let pin = match layer.pinned_plane {
2339        None => PinOutcome::NotRequested,
2340        Some(plane_id) if pin_is_honourable(&plane_layer, plane_id, registry, crtc_index) => {
2341            plane_layer.set_pinned(true);
2342            plane_layer.set_assigned_plane(Some(plane_id));
2343            PinOutcome::Honoured(plane_id)
2344        }
2345        Some(_) => PinOutcome::Refused,
2346    };
2347    (plane_layer, pin)
2348}
2349
2350/// What became of a layer's pin request.
2351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2352enum PinOutcome {
2353    /// The caller asked for nothing.
2354    NotRequested,
2355    /// The layer owns this plane; the allocator will not offer it elsewhere.
2356    Honoured(u32),
2357    /// The plane cannot take this layer, so it goes through normal allocation.
2358    Refused,
2359}
2360
2361/// The kernel's answer to one pin: this layer on this plane with this
2362/// geometry (`Layer::property_hash`, which leaves content out).
2363#[derive(Debug, Clone, Copy)]
2364struct PinVerdict {
2365    layer: LayerId,
2366    plane: u32,
2367    hash: u64,
2368    accepted: bool,
2369}
2370
2371/// What the kernel said about this pin, if it was asked.
2372fn pin_verdict(verdicts: &[PinVerdict], layer: LayerId, plane: u32, hash: u64) -> Option<bool> {
2373    verdicts
2374        .iter()
2375        .find(|v| v.layer == layer && v.plane == plane && v.hash == hash)
2376        .map(|v| v.accepted)
2377}
2378
2379/// Keep `verdict`. Bounded: a pinned layer whose geometry keeps changing adds
2380/// one entry per geometry, so past the bound the cache starts over.
2381fn remember_pin(verdicts: &mut Vec<PinVerdict>, verdict: PinVerdict) {
2382    const MAX_PIN_VERDICTS: usize = 64;
2383    if verdicts.len() >= MAX_PIN_VERDICTS {
2384        verdicts.clear();
2385    }
2386    verdicts.push(verdict);
2387}
2388
2389/// A pin the kernel refused is not honoured again: the layer is unpinned and
2390/// takes normal allocation, and the refusal is counted.
2391fn revoke_refused_pin(
2392    verdicts: &[PinVerdict],
2393    layer_id: LayerId,
2394    plane_layer: &mut PlaneLayer,
2395    pin: PinOutcome,
2396) -> PinOutcome {
2397    match pin {
2398        PinOutcome::Honoured(plane_id)
2399            if pin_verdict(verdicts, layer_id, plane_id, plane_layer.property_hash())
2400                == Some(false) =>
2401        {
2402            plane_layer.set_pinned(false).set_assigned_plane(None);
2403            PinOutcome::Refused
2404        }
2405        other => other,
2406    }
2407}
2408
2409/// Whether a plane can actually take the layer pinned to it.
2410///
2411/// Three ways a pin fails, and all three are the caller describing hardware
2412/// that is not there: a plane on another CRTC, a plane that cannot scan the
2413/// layer's format out in its layout, or a plane the scene has already
2414/// reserved for something else. Committing an impossible pin would fail the
2415/// `TEST_ONLY` and take the whole frame down -- every correctly placed layer
2416/// with it -- so the check happens here, and a refused pin costs the caller
2417/// determinism rather than a frame.
2418fn pin_is_honourable(
2419    layer: &PlaneLayer,
2420    plane_id: u32,
2421    registry: &PlaneRegistry,
2422    crtc_index: u32,
2423) -> bool {
2424    let Some(plane) = registry.by_id(plane_id) else {
2425        return false;
2426    };
2427    if !plane.compatible_with_crtc(crtc_index) {
2428        return false;
2429    }
2430    // The cursor path owns cursor planes, the same carve-out
2431    // `force_disable_candidates` makes.
2432    if plane.plane_type == drmkit_planes::PlaneType::Cursor {
2433        return false;
2434    }
2435    layer
2436        .property(PropTag::PixelFormat)
2437        .and_then(|fourcc| u32::try_from(fourcc).ok())
2438        .is_none_or(|fourcc| plane.scans_out(fourcc, drmkit_fmt::Modifier(layer.modifier())))
2439}
2440
2441/// Work out what this frame writes: which layer goes on which plane, with what
2442/// baseline to diff against, and which planes have to be switched off.
2443///
2444/// Both halves read the allocator's committed baseline, so both have to be
2445/// computed **before** the frame is finalized -- finalizing replaces that
2446/// baseline with this frame's own, and a plan derived from it afterwards would
2447/// diff the frame against itself and write nothing at all.
2448fn build_plan(
2449    allocator: &drmkit_planes::Allocator,
2450    allocation: &drmkit_planes::Allocation,
2451    plane_layers: &[(LayerId, drmkit_planes::Layer)],
2452    registry: &PlaneRegistry,
2453    crtc_index: u32,
2454    damage: &HashMap<LayerId, Vec<crate::DamageRect>>,
2455    pinned: &[(LayerId, u32)],
2456) -> (Vec<PlanePlan>, Vec<u32>) {
2457    let plan: Vec<PlanePlan> = plane_layers
2458        .iter()
2459        .filter_map(|(id, layer)| {
2460            // A pinned layer never entered the allocation -- the allocator
2461            // skips it by design -- so its plane comes from the pin. Without
2462            // this it is reported as assigned and programmed by nobody, and
2463            // the plane it claimed stays dark.
2464            allocation
2465                .assignment
2466                .get_plane_of(*id)
2467                .or_else(|| {
2468                    pinned
2469                        .iter()
2470                        .find(|(layer_id, _)| layer_id == id)
2471                        .map(|(_, plane_id)| *plane_id)
2472                })
2473                .map(|plane_id| PlanePlan {
2474                    plane_id,
2475                    layer_id: *id,
2476                    layer: layer.clone(),
2477                    baseline: allocator.committed_baseline(plane_id, *id).copied(),
2478                    damage: damage.get(id).cloned().unwrap_or_default(),
2479                    zpos: None,
2480                    color: None,
2481                })
2482        })
2483        .collect();
2484
2485    let disables = registry
2486        .force_disable_candidates(crtc_index)
2487        .map(|plane| plane.id)
2488        .filter(|plane_id| {
2489            !plan.iter().any(|entry| entry.plane_id == *plane_id)
2490                && !allocator.plane_is_off(*plane_id)
2491        })
2492        .collect();
2493
2494    (plan, disables)
2495}
2496
2497impl Drop for LayerScene {
2498    fn drop(&mut self) {
2499        if self.lifecycle.has_pending_flip() && !std::thread::panicking() {
2500            // Invariant 5. The flip still references a framebuffer this drop is
2501            // about to tear down. Nothing can be done here -- waiting is
2502            // exactly what a drop must not do -- so make it loud in
2503            // development rather than a rare tear in production.
2504            //
2505            // Except while unwinding: a panicking caller drops the scene on
2506            // its way out, and asserting here aborts the process and hides
2507            // the panic that actually started it.
2508            debug_assert!(
2509                false,
2510                "LayerScene dropped with a page-flip event still armed: call \
2511                 drain() or dispatch the event first (invariant 5)"
2512            );
2513        }
2514    }
2515}