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 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}