Skip to main content

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}