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