drmkit_planes/prop.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The closed set of KMS plane properties a [`Layer`](crate::Layer) carries.
5
6/// Property tags a layer recognizes.
7///
8/// The set is **closed**: every KMS plane property a layer carries lives
9/// here, and an unrecognized name handed to
10/// [`Layer::set_property_by_name`](crate::Layer::set_property_by_name) is
11/// dropped — a kernel commit would reject it with `EINVAL` anyway, so
12/// accepting it would only hide the typo. The scene writes a few per-plane
13/// extras, such as `COLOR_ENCODING` and `FB_DAMAGE_CLIPS`, from plane
14/// capabilities rather than from the layer.
15///
16/// Adding a property means adding a variant here and extending
17/// [`PropTag::name`] and [`PropTag::class`]. Both are exhaustive `match`es, so
18/// the compiler names every place that needs updating.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(u8)]
21#[non_exhaustive]
22pub enum PropTag {
23 /// `FB_ID` — the framebuffer to scan out.
24 FbId,
25 /// `FB_MODIFIER` — the layout of that framebuffer.
26 FbModifier,
27 /// `CRTC_ID` — which CRTC the plane feeds.
28 CrtcId,
29 /// `CRTC_X` — destination x, in pixels.
30 CrtcX,
31 /// `CRTC_Y` — destination y, in pixels.
32 CrtcY,
33 /// `CRTC_W` — destination width, in pixels.
34 CrtcW,
35 /// `CRTC_H` — destination height, in pixels.
36 CrtcH,
37 /// `SRC_X` — source x, 16.16 fixed point.
38 SrcX,
39 /// `SRC_Y` — source y, 16.16 fixed point.
40 SrcY,
41 /// `SRC_W` — source width, 16.16 fixed point.
42 SrcW,
43 /// `SRC_H` — source height, 16.16 fixed point.
44 SrcH,
45 /// `rotation` — rotate/reflect bitmask.
46 Rotation,
47 /// `alpha` — per-plane alpha, u16.
48 Alpha,
49 /// `zpos` — stacking order.
50 Zpos,
51 /// `pixel_format` — the `FourCC`.
52 PixelFormat,
53 /// `IN_FENCE_FD` — a fence the flip waits on.
54 InFenceFd,
55}
56
57/// What a property describes, for the FB-only fast path.
58///
59/// This is the **corollary of invariant 4** the C++ can only state in prose:
60///
61/// > `property_hash` isolates content from placement — every placement or
62/// > format property moves the hash; `FB_ID` and `IN_FENCE_FD` do not. A
63/// > property misclassified as content would let a real change skip the test.
64///
65/// Here it is data, consumed by [`PropTag::class`]'s exhaustive `match`. A new
66/// variant added without a classification does not compile, so the failure the
67/// C++ warns about — a property silently treated as content — cannot reach a
68/// running system.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
70#[non_exhaustive]
71pub enum PropClass {
72 /// Changes every frame and does not affect plane compatibility.
73 ///
74 /// Excluded from [`Layer::property_hash`](crate::Layer::property_hash), so
75 /// a frame that changes only these can reuse the cached plane assignment
76 /// and skip the redundant `TEST_ONLY` commit.
77 Content,
78 /// Describes where or how the plane scans out.
79 ///
80 /// Any change here must move the hash, forcing a re-test. Getting this
81 /// wrong in the permissive direction — calling a placement property
82 /// content — is what would let a real geometry change skip validation.
83 Placement,
84}
85
86/// Number of distinct property tags.
87pub const NUM_PROPS: usize = PropTag::ALL.len();
88
89impl PropTag {
90 /// Every tag, in enum order.
91 ///
92 /// Listing them explicitly rather than deriving a count keeps the
93 /// exhaustiveness of [`PropTag::class`] meaningful: a variant inserted
94 /// without being added here breaks the index-contiguity check in
95 /// `every_prop_tag_is_classified`. One appended at the end is not caught,
96 /// so keep the array's size in step.
97 pub const ALL: [Self; 16] = [
98 Self::FbId,
99 Self::FbModifier,
100 Self::CrtcId,
101 Self::CrtcX,
102 Self::CrtcY,
103 Self::CrtcW,
104 Self::CrtcH,
105 Self::SrcX,
106 Self::SrcY,
107 Self::SrcW,
108 Self::SrcH,
109 Self::Rotation,
110 Self::Alpha,
111 Self::Zpos,
112 Self::PixelFormat,
113 Self::InFenceFd,
114 ];
115
116 /// Index into a layer's property array.
117 #[must_use]
118 pub const fn index(self) -> usize {
119 self as usize
120 }
121
122 /// The canonical KMS property name.
123 #[must_use]
124 pub const fn name(self) -> &'static str {
125 match self {
126 Self::FbId => "FB_ID",
127 Self::FbModifier => "FB_MODIFIER",
128 Self::CrtcId => "CRTC_ID",
129 Self::CrtcX => "CRTC_X",
130 Self::CrtcY => "CRTC_Y",
131 Self::CrtcW => "CRTC_W",
132 Self::CrtcH => "CRTC_H",
133 Self::SrcX => "SRC_X",
134 Self::SrcY => "SRC_Y",
135 Self::SrcW => "SRC_W",
136 Self::SrcH => "SRC_H",
137 Self::Rotation => "rotation",
138 Self::Alpha => "alpha",
139 Self::Zpos => "zpos",
140 Self::PixelFormat => "pixel_format",
141 Self::InFenceFd => "IN_FENCE_FD",
142 }
143 }
144
145 /// Whether this property describes content or placement.
146 ///
147 /// See [`PropClass`]. The `match` is exhaustive by construction: adding a
148 /// variant without classifying it is a compile error, which is the whole
149 /// reason this is a function rather than a lookup table with a default.
150 #[must_use]
151 pub const fn class(self) -> PropClass {
152 match self {
153 // Changes every frame: a new buffer, a new fence descriptor.
154 // Neither says anything about whether the plane can scan it out.
155 Self::FbId | Self::InFenceFd => PropClass::Content,
156
157 // Everything else describes where or how the plane scans out, so
158 // a change must invalidate the cached placement. `FB_MODIFIER`
159 // and `PixelFormat` belong here because a layout or format change
160 // can make a previously valid plane invalid.
161 Self::FbModifier
162 | Self::CrtcId
163 | Self::CrtcX
164 | Self::CrtcY
165 | Self::CrtcW
166 | Self::CrtcH
167 | Self::SrcX
168 | Self::SrcY
169 | Self::SrcW
170 | Self::SrcH
171 | Self::Rotation
172 | Self::Alpha
173 | Self::Zpos
174 | Self::PixelFormat => PropClass::Placement,
175 }
176 }
177
178 /// Look up a tag by its canonical KMS name.
179 ///
180 /// A linear scan over 16 entries: branchless, cache-friendly, and faster
181 /// than a hash map at this size — the same reasoning the C++ records.
182 #[must_use]
183 pub fn from_name(name: &str) -> Option<Self> {
184 Self::ALL.into_iter().find(|tag| tag.name() == name)
185 }
186}