drmkit_planes/layer.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The layer model: what the scene asks the allocator to place.
5
6use std::cell::Cell;
7use std::hash::{Hash as _, Hasher as _};
8
9use crate::prop::{NUM_PROPS, PropClass, PropTag};
10
11/// What a layer is showing, for placement priority.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
13#[non_exhaustive]
14pub enum ContentType {
15 /// Unclassified.
16 #[default]
17 Generic,
18 /// Video, which benefits most from a dedicated plane.
19 Video,
20 /// Interface chrome.
21 Ui,
22 /// A cursor.
23 Cursor,
24}
25
26/// An axis-aligned rectangle in CRTC pixels.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[allow(
29 clippy::exhaustive_structs,
30 reason = "a value defined by its fields; there is nothing to add to it"
31)]
32pub struct Rect {
33 /// Left edge.
34 pub x: i32,
35 /// Top edge.
36 pub y: i32,
37 /// Width.
38 pub w: u32,
39 /// Height.
40 pub h: u32,
41}
42
43/// A snapshot of a layer's property state, for diffing against the last commit.
44///
45/// Trivially copyable, as in the C++: copying is a memcpy.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct PropertySnapshot {
48 values: [u64; NUM_PROPS],
49 set_mask: u16,
50}
51
52impl PropertySnapshot {
53 /// The value of a property, if it was set when the snapshot was taken.
54 #[must_use]
55 pub const fn get(&self, tag: PropTag) -> Option<u64> {
56 if self.set_mask & (1 << tag.index()) == 0 {
57 None
58 } else {
59 Some(self.values[tag.index()])
60 }
61 }
62
63 /// This snapshot with one property replaced.
64 ///
65 /// For a value the commit wrote in place of the layer's own: the stacked
66 /// zpos, which the next frame's diff has to compare against what went out
67 /// rather than what was asked for.
68 #[must_use]
69 pub const fn with(mut self, tag: PropTag, value: u64) -> Self {
70 self.values[tag.index()] = value;
71 self.set_mask |= 1 << tag.index();
72 self
73 }
74}
75
76/// One layer the scene wants on screen.
77///
78/// A layer is a property bag plus the flags the allocator consults when
79/// deciding whether — and where — to place it.
80///
81/// The flags stay separate `bool`s rather than a bitfield: they are set and
82/// read independently by different owners — the caller, the scene, and the
83/// allocator — and the C++ keeps them separate for the same reason. Packing
84/// them would save a few bytes on a type allocated a handful of times per
85/// frame and cost the ability to name each one.
86#[expect(
87 clippy::struct_excessive_bools,
88 reason = "independently-owned placement flags; see the type documentation"
89)]
90#[derive(Debug, Clone)]
91pub struct Layer {
92 values: [u64; NUM_PROPS],
93 /// One bit per [`PropTag`]; 16 tags fit a `u16` exactly.
94 set_mask: u16,
95 /// Memoized [`Layer::property_hash`], invalidated on every mutation.
96 cached_hash: Cell<Option<u64>>,
97 force_composited: bool,
98 transient_composited: bool,
99 needs_composition: bool,
100 dirty: bool,
101 composition_output: bool,
102 externally_bound: bool,
103 pinned: bool,
104 assigned_plane: Option<u32>,
105 content_type: ContentType,
106 update_hz: u32,
107 app_priority: u8,
108}
109
110impl Default for Layer {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl Layer {
117 /// A layer with no properties set.
118 #[must_use]
119 pub const fn new() -> Self {
120 Self {
121 values: [0; NUM_PROPS],
122 set_mask: 0,
123 cached_hash: Cell::new(None),
124 force_composited: false,
125 transient_composited: false,
126 needs_composition: false,
127 dirty: true,
128 composition_output: false,
129 externally_bound: false,
130 pinned: false,
131 assigned_plane: None,
132 content_type: ContentType::Generic,
133 update_hz: 0,
134 app_priority: 0,
135 }
136 }
137
138 /// Set a property. The hot path — no string lookup.
139 ///
140 /// Writing a property's existing value is a **no-op**: the layer is not
141 /// marked dirty and the memoized hash is not invalidated. That is not just
142 /// tidiness — the steady-state path rewrites the same geometry every frame,
143 /// so invalidating on a no-op would recompute the hash each time and defeat
144 /// the memoization the FB-only fast path leans on.
145 pub fn set_property(&mut self, tag: PropTag, value: u64) -> &mut Self {
146 let index = tag.index();
147 let was_set = self.set_mask & (1 << index) != 0;
148 let changed = !was_set || self.values[index] != value;
149
150 self.values[index] = value;
151 self.set_mask |= 1 << index;
152
153 if changed {
154 self.cached_hash.set(None);
155 self.dirty = true;
156 }
157 self
158 }
159
160 /// Set a property by canonical KMS name.
161 ///
162 /// An unrecognized name is dropped and reported, rather than silently
163 /// ignored as in the C++: the kernel would reject it anyway, and a caller
164 /// that mistyped a name wants to know.
165 pub fn set_property_by_name(&mut self, name: &str, value: u64) -> bool {
166 match PropTag::from_name(name) {
167 Some(tag) => {
168 self.set_property(tag, value);
169 true
170 }
171 None => false,
172 }
173 }
174
175 /// The value of a property, if set.
176 #[must_use]
177 pub const fn property(&self, tag: PropTag) -> Option<u64> {
178 if self.set_mask & (1 << tag.index()) == 0 {
179 None
180 } else {
181 Some(self.values[tag.index()])
182 }
183 }
184
185 /// The value of a property by canonical KMS name, if set.
186 #[must_use]
187 pub fn property_by_name(&self, name: &str) -> Option<u64> {
188 PropTag::from_name(name).and_then(|tag| self.property(tag))
189 }
190
191 /// Every set property, in tag order.
192 pub fn properties(&self) -> impl Iterator<Item = (PropTag, u64)> + '_ {
193 PropTag::ALL
194 .into_iter()
195 .filter_map(|tag| self.property(tag).map(|value| (tag, value)))
196 }
197
198 /// How many properties are set.
199 #[must_use]
200 pub const fn property_count(&self) -> u32 {
201 self.set_mask.count_ones()
202 }
203
204 /// Snapshot the property state for diffing.
205 #[must_use]
206 pub const fn snapshot(&self) -> PropertySnapshot {
207 PropertySnapshot {
208 values: self.values,
209 set_mask: self.set_mask,
210 }
211 }
212
213 /// Disable the layer by detaching its framebuffer.
214 ///
215 /// Sets `FB_ID` to 0 and leaves everything else alone. Geometry, format,
216 /// and stacking stay put, which is what lets a re-enabled layer keep its
217 /// placement — and, because `FB_ID` is a content property, what keeps a
218 /// disable from moving [`property_hash`](Self::property_hash).
219 pub fn disable(&mut self) -> &mut Self {
220 self.set_property(PropTag::FbId, 0)
221 }
222
223 /// Order-independent hash of the layer's **placement** properties.
224 ///
225 /// # Invariant 4 (corollary)
226 ///
227 /// This is what isolates content from placement. Every
228 /// [`PropClass::Placement`] property moves the hash; every
229 /// [`PropClass::Content`] property is skipped, because it changes each
230 /// frame and says nothing about whether the plane can scan the buffer out.
231 ///
232 /// A frame whose hash is unchanged has only new content on already-placed
233 /// layers, so the allocator reuses the cached assignment and skips the
234 /// redundant `TEST_ONLY`. Misclassifying a placement property as content
235 /// would let a real geometry change skip validation — which is why the
236 /// classification lives in [`PropTag::class`]'s exhaustive `match` rather
237 /// than in an `if` here.
238 ///
239 /// Memoized; invalidated by every mutation.
240 #[must_use]
241 pub fn property_hash(&self) -> u64 {
242 if let Some(cached) = self.cached_hash.get() {
243 return cached;
244 }
245
246 // Walking in tag order makes the result independent of the order the
247 // properties were set in.
248 let mut hash = GOLDEN_RATIO;
249 for tag in PropTag::ALL {
250 if tag.class() == PropClass::Content {
251 continue;
252 }
253 let Some(value) = self.property(tag) else {
254 continue;
255 };
256 hash = combine(hash, tag.index() as u64);
257 hash = combine(hash, value);
258 }
259
260 self.cached_hash.set(Some(hash));
261 hash
262 }
263
264 // --- placement flags -----------------------------------------------------
265
266 /// Force this layer through composition rather than a plane.
267 pub fn set_force_composited(&mut self, composited: bool) -> &mut Self {
268 self.force_composited = composited;
269 self
270 }
271
272 /// Whether the caller pinned this layer to composition.
273 #[must_use]
274 pub const fn is_force_composited(&self) -> bool {
275 self.force_composited
276 }
277
278 /// Set the per-commit composition flag.
279 ///
280 /// The allocator treats this exactly like
281 /// [`set_force_composited`](Self::set_force_composited), but the scene sets
282 /// and clears it per commit rather than at layer creation. It lets
283 /// scene-level constraints — an EGL Streams `Exclusive` mixing mode forcing
284 /// FB-backed layers through composition when a stream layer shares the
285 /// CRTC — coexist with a caller's own choice without conflating the two.
286 pub fn set_transient_composited(&mut self, composited: bool) -> &mut Self {
287 self.transient_composited = composited;
288 self
289 }
290
291 /// Whether the scene set the per-commit composition flag.
292 #[must_use]
293 pub const fn is_transient_composited(&self) -> bool {
294 self.transient_composited
295 }
296
297 /// Whether this layer must go through composition for any reason.
298 #[must_use]
299 pub const fn needs_composition(&self) -> bool {
300 self.needs_composition || self.force_composited || self.transient_composited
301 }
302
303 #[expect(
304 dead_code,
305 reason = "set by the allocator, which lands in the next commit of this \
306 phase; expect rather than allow so the attribute has to be \
307 removed once it is"
308 )]
309 pub(crate) const fn set_needs_composition(&mut self, needs: bool) {
310 self.needs_composition = needs;
311 }
312
313 /// Mark this layer as the output of the composition pass itself.
314 pub fn set_composition_layer(&mut self, is_composition: bool) -> &mut Self {
315 self.composition_output = is_composition;
316 self
317 }
318
319 /// Whether this layer is the composition pass's own output.
320 #[must_use]
321 pub const fn is_composition_layer(&self) -> bool {
322 self.composition_output
323 }
324
325 /// Whether the scene pinned this layer to a plane whose binding a driver
326 /// owns.
327 ///
328 /// The allocator must skip such a layer during assignment, scoring, and
329 /// test commits, exactly as it skips a composition layer. EGL stream
330 /// sources establish their plane state out of band, so an allocator test
331 /// commit would reject a stream-bound plane for lacking the kernel-internal
332 /// `FB_ID` those extensions set up.
333 #[must_use]
334 pub const fn is_externally_bound(&self) -> bool {
335 self.externally_bound
336 }
337
338 /// Mark this layer as driver-bound. Set by the scene when a source reports
339 /// `DriverOwnsBinding`.
340 pub fn set_externally_bound(&mut self, externally_bound: bool) -> &mut Self {
341 self.externally_bound = externally_bound;
342 self
343 }
344
345 /// Whether the scene pinned this layer to a caller-chosen plane.
346 ///
347 /// Like [`is_externally_bound`](Self::is_externally_bound), the allocator
348 /// skips it. **Unlike** that flag, the layer keeps its `FB_ID` in the
349 /// property bag — it is a normal source, not a driver-owned one — so this
350 /// must not gate the `FB_ID` write.
351 #[must_use]
352 pub const fn is_pinned(&self) -> bool {
353 self.pinned
354 }
355
356 /// Mark this layer as pinned to a specific plane.
357 pub fn set_pinned(&mut self, pinned: bool) -> &mut Self {
358 self.pinned = pinned;
359 self
360 }
361
362 /// The plane this layer was placed on.
363 #[must_use]
364 pub const fn assigned_plane(&self) -> Option<u32> {
365 self.assigned_plane
366 }
367
368 /// Record the plane this layer was placed on.
369 pub fn set_assigned_plane(&mut self, plane_id: Option<u32>) -> &mut Self {
370 self.assigned_plane = plane_id;
371 self
372 }
373
374 // --- hints ---------------------------------------------------------------
375
376 /// What this layer is showing.
377 #[must_use]
378 pub const fn content_type(&self) -> ContentType {
379 self.content_type
380 }
381
382 /// Set what this layer is showing.
383 pub fn set_content_type(&mut self, content_type: ContentType) -> &mut Self {
384 self.content_type = content_type;
385 self
386 }
387
388 /// The layer's expected update rate in Hz, or 0 if unknown.
389 #[must_use]
390 pub const fn update_hz(&self) -> u32 {
391 self.update_hz
392 }
393
394 /// Set the expected update rate.
395 pub fn set_update_hint(&mut self, hz: u32) -> &mut Self {
396 self.update_hz = hz;
397 self
398 }
399
400 /// Application-level placement priority within a content class.
401 #[must_use]
402 pub const fn app_priority(&self) -> u8 {
403 self.app_priority
404 }
405
406 /// Set the application-level placement priority.
407 pub fn set_app_priority(&mut self, priority: u8) -> &mut Self {
408 self.app_priority = priority;
409 self
410 }
411
412 /// Whether the layer changed since the last [`mark_clean`](Self::mark_clean).
413 #[must_use]
414 pub const fn is_dirty(&self) -> bool {
415 self.dirty
416 }
417
418 /// Clear the dirty flag.
419 pub const fn mark_clean(&mut self) {
420 self.dirty = false;
421 }
422
423 // --- derived geometry ----------------------------------------------------
424
425 /// The `FourCC`, if set.
426 ///
427 /// The narrowing casts in this block are the KMS ABI's shape: `CRTC_*`,
428 /// `pixel_format`, and the integer part of a 16.16 `SRC_*` are all 32-bit
429 /// fields carried in the property bag's `u64` slots. A value too wide to
430 /// fit came from a caller writing something the kernel would reject.
431 #[must_use]
432 #[expect(
433 clippy::cast_possible_truncation,
434 reason = "KMS carries these as 32-bit fields in u64 property slots"
435 )]
436 pub const fn format(&self) -> Option<u32> {
437 match self.property(PropTag::PixelFormat) {
438 Some(value) => Some(value as u32),
439 None => None,
440 }
441 }
442
443 /// The layout modifier, defaulting to `LINEAR`'s zero when unset.
444 #[must_use]
445 pub const fn modifier(&self) -> u64 {
446 match self.property(PropTag::FbModifier) {
447 Some(value) => value,
448 None => 0,
449 }
450 }
451
452 /// The rotate/reflect bitmask, or 0.
453 #[must_use]
454 pub const fn rotation(&self) -> u64 {
455 match self.property(PropTag::Rotation) {
456 Some(value) => value,
457 None => 0,
458 }
459 }
460
461 /// Destination width in pixels.
462 #[must_use]
463 #[expect(
464 clippy::cast_possible_truncation,
465 reason = "KMS carries these as 32-bit fields in u64 property slots"
466 )]
467 pub const fn width(&self) -> u32 {
468 match self.property(PropTag::CrtcW) {
469 Some(value) => value as u32,
470 None => 0,
471 }
472 }
473
474 /// Destination height in pixels.
475 #[must_use]
476 #[expect(
477 clippy::cast_possible_truncation,
478 reason = "KMS carries these as 32-bit fields in u64 property slots"
479 )]
480 pub const fn height(&self) -> u32 {
481 match self.property(PropTag::CrtcH) {
482 Some(value) => value as u32,
483 None => 0,
484 }
485 }
486
487 /// The destination rectangle.
488 #[must_use]
489 #[expect(
490 clippy::cast_possible_truncation,
491 reason = "KMS carries CRTC_X/Y as signed 32-bit fields in u64 property slots"
492 )]
493 pub const fn crtc_rect(&self) -> Rect {
494 let x = match self.property(PropTag::CrtcX) {
495 Some(value) => value as i32,
496 None => 0,
497 };
498 let y = match self.property(PropTag::CrtcY) {
499 Some(value) => value as i32,
500 None => 0,
501 };
502 Rect {
503 x,
504 y,
505 w: self.width(),
506 h: self.height(),
507 }
508 }
509
510 /// Whether the plane would have to scale.
511 ///
512 /// `SRC_*` are 16.16 fixed point; `CRTC_*` are whole pixels. A layer
513 /// missing any of the four is treated as unscaled, matching the C++.
514 #[must_use]
515 pub const fn requires_scaling(&self) -> bool {
516 let (Some(src_w), Some(crtc_w), Some(src_h), Some(crtc_h)) = (
517 self.property(PropTag::SrcW),
518 self.property(PropTag::CrtcW),
519 self.property(PropTag::SrcH),
520 self.property(PropTag::CrtcH),
521 ) else {
522 return false;
523 };
524 // SRC_* are 16.16, so compare at that precision. Truncating to whole
525 // pixels, as drm-cxx does, reads 800.25 source pixels onto 800 as 1:1
526 // (drm-cxx#348).
527 src_w != (crtc_w << 16) || src_h != (crtc_h << 16)
528 }
529}
530
531/// Fractional part of the golden ratio, the conventional hash seed.
532const GOLDEN_RATIO: u64 = 0x9e37_79b9_7f4a_7c15;
533
534/// Boost-style `hash_combine`. Order-dependent, which is why
535/// [`Layer::property_hash`] walks tags in a fixed order.
536fn combine(hash: u64, value: u64) -> u64 {
537 let mut hasher = std::collections::hash_map::DefaultHasher::new();
538 value.hash(&mut hasher);
539 hash ^ (hasher
540 .finish()
541 .wrapping_add(GOLDEN_RATIO)
542 .wrapping_add(hash << 6)
543 .wrapping_add(hash >> 2))
544}