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