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}