Skip to main content

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}