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 whenever a property value
96 /// changes.
97 cached_hash: Cell<Option<u64>>,
98 force_composited: bool,
99 transient_composited: 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 dirty: true,
127 composition_output: false,
128 externally_bound: false,
129 pinned: false,
130 assigned_plane: None,
131 content_type: ContentType::Generic,
132 update_hz: 0,
133 app_priority: 0,
134 }
135 }
136
137 /// Set a property. The hot path — no string lookup.
138 ///
139 /// Writing a property's existing value is a **no-op**: the layer is not
140 /// marked dirty and the memoized hash is not invalidated. That is not just
141 /// tidiness — the steady-state path rewrites the same geometry every frame,
142 /// so invalidating on a no-op would recompute the hash each time and defeat
143 /// the memoization the FB-only fast path leans on.
144 pub fn set_property(&mut self, tag: PropTag, value: u64) -> &mut Self {
145 let index = tag.index();
146 let was_set = self.set_mask & (1 << index) != 0;
147 let changed = !was_set || self.values[index] != value;
148
149 self.values[index] = value;
150 self.set_mask |= 1 << index;
151
152 if changed {
153 self.cached_hash.set(None);
154 self.dirty = true;
155 }
156 self
157 }
158
159 /// Set a property by canonical KMS name.
160 ///
161 /// Returns `false`, and sets nothing, for an unrecognized name, rather than
162 /// silently ignoring it as the C++ does: the kernel would reject it anyway, and a caller
163 /// that mistyped a name wants to know.
164 pub fn set_property_by_name(&mut self, name: &str, value: u64) -> bool {
165 match PropTag::from_name(name) {
166 Some(tag) => {
167 self.set_property(tag, value);
168 true
169 }
170 None => false,
171 }
172 }
173
174 /// The value of a property, if set.
175 #[must_use]
176 pub const fn property(&self, tag: PropTag) -> Option<u64> {
177 if self.set_mask & (1 << tag.index()) == 0 {
178 None
179 } else {
180 Some(self.values[tag.index()])
181 }
182 }
183
184 /// The value of a property by canonical KMS name, if set.
185 #[must_use]
186 pub fn property_by_name(&self, name: &str) -> Option<u64> {
187 PropTag::from_name(name).and_then(|tag| self.property(tag))
188 }
189
190 /// Every set property, in tag order.
191 pub fn properties(&self) -> impl Iterator<Item = (PropTag, u64)> + '_ {
192 PropTag::ALL
193 .into_iter()
194 .filter_map(|tag| self.property(tag).map(|value| (tag, value)))
195 }
196
197 /// How many properties are set.
198 #[must_use]
199 pub const fn property_count(&self) -> u32 {
200 self.set_mask.count_ones()
201 }
202
203 /// Snapshot the property state for diffing.
204 #[must_use]
205 pub const fn snapshot(&self) -> PropertySnapshot {
206 PropertySnapshot {
207 values: self.values,
208 set_mask: self.set_mask,
209 }
210 }
211
212 /// Disable the layer by detaching its framebuffer.
213 ///
214 /// Sets `FB_ID` to 0 and leaves everything else alone. Geometry, format,
215 /// and stacking stay put, which is what lets a re-enabled layer keep its
216 /// placement — and, because `FB_ID` is a content property, what keeps a
217 /// disable from moving [`property_hash`](Self::property_hash).
218 pub fn disable(&mut self) -> &mut Self {
219 self.set_property(PropTag::FbId, 0)
220 }
221
222 /// Order-independent hash of the layer's **placement** properties.
223 ///
224 /// # Invariant 4 (corollary)
225 ///
226 /// This is what isolates content from placement. Every
227 /// [`PropClass::Placement`] property moves the hash; every
228 /// [`PropClass::Content`] property is skipped, because it changes each
229 /// frame and says nothing about whether the plane can scan the buffer out.
230 ///
231 /// A frame whose hash is unchanged has only new content on already-placed
232 /// layers, so the allocator reuses the cached assignment and skips the
233 /// redundant `TEST_ONLY`. Misclassifying a placement property as content
234 /// would let a real geometry change skip validation — which is why the
235 /// classification lives in [`PropTag::class`]'s exhaustive `match` rather
236 /// than in an `if` here.
237 ///
238 /// Memoized; invalidated whenever a property value changes.
239 #[must_use]
240 pub fn property_hash(&self) -> u64 {
241 if let Some(cached) = self.cached_hash.get() {
242 return cached;
243 }
244
245 // Walking in tag order makes the result independent of the order the
246 // properties were set in.
247 let mut hash = GOLDEN_RATIO;
248 for tag in PropTag::ALL {
249 if tag.class() == PropClass::Content {
250 continue;
251 }
252 let Some(value) = self.property(tag) else {
253 continue;
254 };
255 hash = combine(hash, tag.index() as u64);
256 hash = combine(hash, value);
257 }
258
259 self.cached_hash.set(Some(hash));
260 hash
261 }
262
263 // --- placement flags -----------------------------------------------------
264
265 /// Force this layer through composition rather than a plane.
266 pub fn set_force_composited(&mut self, composited: bool) -> &mut Self {
267 self.force_composited = composited;
268 self
269 }
270
271 /// Whether the caller pinned this layer to composition.
272 #[must_use]
273 pub const fn is_force_composited(&self) -> bool {
274 self.force_composited
275 }
276
277 /// Set the per-commit composition flag.
278 ///
279 /// The allocator treats this exactly like
280 /// [`set_force_composited`](Self::set_force_composited), but the scene sets
281 /// and clears it per commit rather than at layer creation. It lets
282 /// scene-level constraints — an EGL Streams `Exclusive` mixing mode forcing
283 /// FB-backed layers through composition when a stream layer shares the
284 /// CRTC — coexist with a caller's own choice without conflating the two.
285 /// Nothing in this tree's scene sets it yet: the streams tier it exists for
286 /// is not ported.
287 pub fn set_transient_composited(&mut self, composited: bool) -> &mut Self {
288 self.transient_composited = composited;
289 self
290 }
291
292 /// Whether the scene set the per-commit composition flag.
293 #[must_use]
294 pub const fn is_transient_composited(&self) -> bool {
295 self.transient_composited
296 }
297
298 /// Whether the caller or the scene has sent this layer to composition.
299 ///
300 /// Upstream's allocator also sets this on every layer it could not place.
301 /// Here the allocator holds layers by shared reference and reports that
302 /// verdict in [`Allocation::composited`](crate::Allocation::composited)
303 /// instead, which covers these layers too.
304 #[must_use]
305 pub const fn needs_composition(&self) -> bool {
306 self.force_composited || self.transient_composited
307 }
308
309 /// Mark this layer as the output of the composition pass itself.
310 pub fn set_composition_layer(&mut self, is_composition: bool) -> &mut Self {
311 self.composition_output = is_composition;
312 self
313 }
314
315 /// Whether this layer is the composition pass's own output.
316 #[must_use]
317 pub const fn is_composition_layer(&self) -> bool {
318 self.composition_output
319 }
320
321 /// Whether the scene pinned this layer to a plane whose binding a driver
322 /// owns.
323 ///
324 /// The allocator must skip such a layer during assignment, scoring, and
325 /// test commits. EGL stream
326 /// sources establish their plane state out of band, so an allocator test
327 /// commit would reject a stream-bound plane for lacking the kernel-internal
328 /// `FB_ID` those extensions set up.
329 #[must_use]
330 pub const fn is_externally_bound(&self) -> bool {
331 self.externally_bound
332 }
333
334 /// Mark this layer as driver-bound. Set by the scene when a source reports
335 /// `DriverOwnsBinding`.
336 pub fn set_externally_bound(&mut self, externally_bound: bool) -> &mut Self {
337 self.externally_bound = externally_bound;
338 self
339 }
340
341 /// Whether the scene pinned this layer to a caller-chosen plane.
342 ///
343 /// Like [`is_externally_bound`](Self::is_externally_bound), the allocator
344 /// skips it. **Unlike** that flag, the layer keeps its `FB_ID` in the
345 /// property bag — it is a normal source, not a driver-owned one — so this
346 /// must not gate the `FB_ID` write.
347 #[must_use]
348 pub const fn is_pinned(&self) -> bool {
349 self.pinned
350 }
351
352 /// Mark this layer as pinned to a specific plane.
353 pub fn set_pinned(&mut self, pinned: bool) -> &mut Self {
354 self.pinned = pinned;
355 self
356 }
357
358 /// The plane a pinned layer holds, as the scene recorded it.
359 ///
360 /// Upstream's allocator records its placements here too; this one reports
361 /// them in [`Allocation::assignment`](crate::Allocation::assignment).
362 #[must_use]
363 pub const fn assigned_plane(&self) -> Option<u32> {
364 self.assigned_plane
365 }
366
367 /// Record the plane a pinned layer holds.
368 pub fn set_assigned_plane(&mut self, plane_id: Option<u32>) -> &mut Self {
369 self.assigned_plane = plane_id;
370 self
371 }
372
373 // --- hints ---------------------------------------------------------------
374
375 /// What this layer is showing.
376 #[must_use]
377 pub const fn content_type(&self) -> ContentType {
378 self.content_type
379 }
380
381 /// Set what this layer is showing.
382 pub fn set_content_type(&mut self, content_type: ContentType) -> &mut Self {
383 self.content_type = content_type;
384 self
385 }
386
387 /// The layer's expected update rate in Hz, or 0 if unknown.
388 #[must_use]
389 pub const fn update_hz(&self) -> u32 {
390 self.update_hz
391 }
392
393 /// Set the expected update rate.
394 pub fn set_update_hint(&mut self, hz: u32) -> &mut Self {
395 self.update_hz = hz;
396 self
397 }
398
399 /// Application-level placement priority within a content class.
400 #[must_use]
401 pub const fn app_priority(&self) -> u8 {
402 self.app_priority
403 }
404
405 /// Set the application-level placement priority.
406 pub fn set_app_priority(&mut self, priority: u8) -> &mut Self {
407 self.app_priority = priority;
408 self
409 }
410
411 /// Whether the layer changed since the last [`mark_clean`](Self::mark_clean).
412 #[must_use]
413 pub const fn is_dirty(&self) -> bool {
414 self.dirty
415 }
416
417 /// Clear the dirty flag.
418 pub const fn mark_clean(&mut self) {
419 self.dirty = false;
420 }
421
422 // --- derived geometry ----------------------------------------------------
423
424 /// The `FourCC`, if set.
425 ///
426 /// The narrowing casts in this block are the KMS ABI's shape: `CRTC_*`,
427 /// `pixel_format`, and the integer part of a 16.16 `SRC_*` are all 32-bit
428 /// fields carried in the property bag's `u64` slots. A value too wide to
429 /// fit came from a caller writing something the kernel would reject.
430 #[must_use]
431 #[expect(
432 clippy::cast_possible_truncation,
433 reason = "KMS carries these as 32-bit fields in u64 property slots"
434 )]
435 pub const fn format(&self) -> Option<u32> {
436 match self.property(PropTag::PixelFormat) {
437 Some(value) => Some(value as u32),
438 None => None,
439 }
440 }
441
442 /// The layout modifier, defaulting to `LINEAR`'s zero when unset.
443 #[must_use]
444 pub const fn modifier(&self) -> u64 {
445 match self.property(PropTag::FbModifier) {
446 Some(value) => value,
447 None => 0,
448 }
449 }
450
451 /// The rotate/reflect bitmask, or 0.
452 #[must_use]
453 pub const fn rotation(&self) -> u64 {
454 match self.property(PropTag::Rotation) {
455 Some(value) => value,
456 None => 0,
457 }
458 }
459
460 /// Destination width in pixels.
461 #[must_use]
462 #[expect(
463 clippy::cast_possible_truncation,
464 reason = "KMS carries these as 32-bit fields in u64 property slots"
465 )]
466 pub const fn width(&self) -> u32 {
467 match self.property(PropTag::CrtcW) {
468 Some(value) => value as u32,
469 None => 0,
470 }
471 }
472
473 /// Destination height in pixels.
474 #[must_use]
475 #[expect(
476 clippy::cast_possible_truncation,
477 reason = "KMS carries these as 32-bit fields in u64 property slots"
478 )]
479 pub const fn height(&self) -> u32 {
480 match self.property(PropTag::CrtcH) {
481 Some(value) => value as u32,
482 None => 0,
483 }
484 }
485
486 /// The destination rectangle.
487 #[must_use]
488 #[expect(
489 clippy::cast_possible_truncation,
490 reason = "KMS carries CRTC_X/Y as signed 32-bit fields in u64 property slots"
491 )]
492 pub const fn crtc_rect(&self) -> Rect {
493 let x = match self.property(PropTag::CrtcX) {
494 Some(value) => value as i32,
495 None => 0,
496 };
497 let y = match self.property(PropTag::CrtcY) {
498 Some(value) => value as i32,
499 None => 0,
500 };
501 Rect {
502 x,
503 y,
504 w: self.width(),
505 h: self.height(),
506 }
507 }
508
509 /// Whether the plane would have to scale.
510 ///
511 /// `SRC_*` are 16.16 fixed point; `CRTC_*` are whole pixels. A layer
512 /// missing any of the four is treated as unscaled, matching the C++.
513 #[must_use]
514 pub const fn requires_scaling(&self) -> bool {
515 let (Some(src_w), Some(crtc_w), Some(src_h), Some(crtc_h)) = (
516 self.property(PropTag::SrcW),
517 self.property(PropTag::CrtcW),
518 self.property(PropTag::SrcH),
519 self.property(PropTag::CrtcH),
520 ) else {
521 return false;
522 };
523 // SRC_* are 16.16, so compare at that precision. Truncating to whole
524 // pixels, as drm-cxx did before drm-cxx#348, reads 800.25 source pixels
525 // onto 800 as 1:1.
526 src_w != (crtc_w << 16) || src_h != (crtc_h << 16)
527 }
528}
529
530/// Fractional part of the golden ratio, the conventional hash seed.
531const GOLDEN_RATIO: u64 = 0x9e37_79b9_7f4a_7c15;
532
533/// Boost-style `hash_combine`. Order-dependent, which is why
534/// [`Layer::property_hash`] walks tags in a fixed order.
535fn combine(hash: u64, value: u64) -> u64 {
536 let mut hasher = std::collections::hash_map::DefaultHasher::new();
537 value.hash(&mut hasher);
538 hash ^ (hasher
539 .finish()
540 .wrapping_add(GOLDEN_RATIO)
541 .wrapping_add(hash << 6)
542 .wrapping_add(hash >> 2))
543}