drmkit_scene/lower.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Lowering a scene layer into the plane property bag the allocator reads.
5
6use drmkit_planes::{Layer as PlaneLayer, PropTag};
7
8use crate::display::{DisplayParams, to_16_16};
9use crate::source::{BindingModel, SourceFormat};
10
11/// Everything `lower_layer` needs about a layer, without borrowing the scene.
12#[derive(Debug, Clone, Copy)]
13#[allow(
14 clippy::exhaustive_structs,
15 reason = "built by callers, with no default to fill a new field from"
16)]
17pub struct LoweringInput {
18 /// How to display the buffer.
19 pub display: DisplayParams,
20 /// What the buffer is.
21 pub format: SourceFormat,
22 /// Who owns the `FB_ID` write.
23 pub binding: BindingModel,
24 /// The framebuffer this frame, ignored for a driver-owned binding.
25 pub fb_id: u32,
26 /// The CRTC this scene drives.
27 pub crtc_id: u32,
28 /// A stacking position to use when the caller set none.
29 ///
30 /// The scene itself passes `None`; with this and the layer's own `zpos`
31 /// both unset, `zpos` is not written.
32 pub default_zpos: Option<u64>,
33}
34
35/// Copy a scene layer's state into the plane property bag.
36///
37/// The allocator and the atomic request read from this bag, so every decision
38/// here is visible downstream as a placement outcome.
39///
40/// # Why some properties are conditional
41///
42/// Three of these were bugs before they were rules, and the C++ records why:
43///
44/// - **`FB_ID` is skipped for a driver-owned binding.** An EGL stream consumer
45/// sets it up through its own extension stack; writing it here races the
46/// consumer's internal state. The layer is also flagged externally bound so
47/// the allocator skips it entirely.
48///
49/// - **`CRTC_ID` is always written.** Without it the kernel rejects the plane
50/// commit — the framebuffer is armed but the plane is still bound to whatever
51/// the previous commit left. Every allocator test commit then fails, and the
52/// scene reports zero layers assigned.
53///
54/// - **`zpos` is written only when asked for.** Emitting 0 unconditionally
55/// would statically disqualify any primary plane with an immutable non-zero
56/// `zpos` — amdgpu pins primary at 2 — leaving a single-layer scene with
57/// nowhere to go.
58///
59/// And one resolution that is easy to get wrong: a zero-sized `src_rect` means
60/// "the buffer's full extent". Callers commonly set only `dst_rect`, because
61/// the scene cannot guess a screen position but *can* read the source extent
62/// from the format. Without this resolution `SRC_W`/`SRC_H` go out as 0, which
63/// the kernel rejects with `EINVAL` — the C++ records this as the root cause of
64/// direct scanout failing on every controller it was tried on, sending every
65/// frame through composition instead.
66pub fn lower_layer(input: &LoweringInput, dst: &mut PlaneLayer) {
67 let display = &input.display;
68 let format = input.format;
69 let driver_owns_binding = input.binding == BindingModel::DriverOwnsBinding;
70
71 dst.set_externally_bound(driver_owns_binding);
72 if !driver_owns_binding {
73 dst.set_property(PropTag::FbId, u64::from(input.fb_id));
74 }
75
76 dst.set_property(PropTag::CrtcId, u64::from(input.crtc_id));
77
78 // Destination is whole signed pixels; source is 16.16 fixed point.
79 dst.set_property(
80 PropTag::CrtcX,
81 u64::from(display.dst_rect.x.cast_unsigned()),
82 );
83 dst.set_property(
84 PropTag::CrtcY,
85 u64::from(display.dst_rect.y.cast_unsigned()),
86 );
87 dst.set_property(PropTag::CrtcW, u64::from(display.dst_rect.w));
88 dst.set_property(PropTag::CrtcH, u64::from(display.dst_rect.h));
89
90 if let Some(fixed) = display.src_rect_fixed {
91 // Already 16.16: written as given, so a sub-pixel crop survives.
92 let extent = |value: u32, full: u32| {
93 if value == 0 {
94 to_16_16(full)
95 } else {
96 u64::from(value)
97 }
98 };
99 dst.set_property(PropTag::SrcX, u64::from(fixed.x));
100 dst.set_property(PropTag::SrcY, u64::from(fixed.y));
101 dst.set_property(PropTag::SrcW, extent(fixed.w, format.width));
102 dst.set_property(PropTag::SrcH, extent(fixed.h, format.height));
103 } else {
104 let src_w = if display.src_rect.w == 0 {
105 format.width
106 } else {
107 display.src_rect.w
108 };
109 let src_h = if display.src_rect.h == 0 {
110 format.height
111 } else {
112 display.src_rect.h
113 };
114 dst.set_property(PropTag::SrcX, to_16_16(display.src_rect.x.cast_unsigned()));
115 dst.set_property(PropTag::SrcY, to_16_16(display.src_rect.y.cast_unsigned()));
116 dst.set_property(PropTag::SrcW, to_16_16(src_w));
117 dst.set_property(PropTag::SrcH, to_16_16(src_h));
118 }
119
120 // Format and modifier let the allocator screen planes statically before any
121 // test commit. These are internal hints on the property bag, not KMS plane
122 // property names.
123 dst.set_property(PropTag::PixelFormat, u64::from(format.fourcc));
124 dst.set_property(PropTag::FbModifier, format.modifier);
125
126 if display.rotation != 0 {
127 dst.set_property(PropTag::Rotation, display.rotation);
128 }
129 if let Some(zpos) = display.zpos.or(input.default_zpos) {
130 dst.set_property(PropTag::Zpos, zpos);
131 }
132 if let Some(alpha) = display.alpha {
133 dst.set_property(PropTag::Alpha, u64::from(alpha));
134 }
135}