Skip to main content

drmkit_planes/
registry.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! What the hardware's display planes can do.
5
6use drmkit_fmt::{BandwidthClass, FormatTable, Modifier, classify};
7
8/// The kernel's plane type.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
10#[non_exhaustive]
11pub enum PlaneType {
12    /// The CRTC's primary plane.
13    Primary,
14    /// An overlay plane.
15    #[default]
16    Overlay,
17    /// The cursor plane.
18    Cursor,
19}
20
21/// Plane `COLOR_ENCODING`: the YCbCr-to-RGB matrix applied when scanning out a
22/// YUV format. Ignored by the kernel for RGB.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[allow(
25    clippy::exhaustive_enums,
26    reason = "drmkit-scene resolves it with an exhaustive match, so a new encoding fails to compile until it is mapped"
27)]
28pub enum ColorEncoding {
29    /// BT.601.
30    Bt601,
31    /// BT.709.
32    Bt709,
33    /// BT.2020.
34    Bt2020,
35}
36
37/// Plane `COLOR_RANGE`: whether 8-bit YCbCr is full-range (0..255) or
38/// limited-range (16..235 luma, 16..240 chroma).
39///
40/// Most camera and broadcast YCbCr is limited-range; PC-generated YCbCr is
41/// sometimes full.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43#[allow(
44    clippy::exhaustive_enums,
45    reason = "a closed set: there is no further variant to add"
46)]
47pub enum ColorRange {
48    /// Limited range.
49    Limited,
50    /// Full range.
51    Full,
52}
53
54/// Driver-assigned enum integers for a property's named values.
55///
56/// The kernel hands back enum values in the order the driver registered them,
57/// so a caller must look up by name and cache the integer — which is what this
58/// holds. `None` means the plane does not advertise that value at all.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60#[allow(
61    clippy::exhaustive_structs,
62    reason = "built by callers with `..Default::default()`, which a new field does not break"
63)]
64pub struct BlendModeValues {
65    /// `"Pre-multiplied"`. Prefer this: it matches the canvas's premultiplied
66    /// output convention.
67    pub premultiplied: Option<u64>,
68    /// `"Coverage"` — straight-alpha `SRC_OVER`.
69    pub coverage: Option<u64>,
70    /// `"None"` — pixel replace, no blending.
71    pub none: Option<u64>,
72}
73
74/// Driver-assigned enum integers for `COLOR_ENCODING`.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76#[allow(
77    clippy::exhaustive_structs,
78    reason = "built by callers with `..Default::default()`, which a new field does not break"
79)]
80pub struct ColorEncodingValues {
81    /// BT.601, if advertised.
82    pub bt601: Option<u64>,
83    /// BT.709, if advertised.
84    pub bt709: Option<u64>,
85    /// BT.2020, if advertised. Older drivers often omit it.
86    pub bt2020: Option<u64>,
87}
88
89/// Driver-assigned enum integers for `COLOR_RANGE`.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91#[allow(
92    clippy::exhaustive_structs,
93    reason = "built by callers with `..Default::default()`, which a new field does not break"
94)]
95pub struct ColorRangeValues {
96    /// Limited range, if advertised.
97    pub limited: Option<u64>,
98    /// Full range, if advertised.
99    pub full: Option<u64>,
100}
101
102/// Per-plane color pipeline support (pre-blend CMS).
103///
104/// When all three are present the plane can apply per-layer EOTF inversion,
105/// gamut conversion, and an SDR-luminance multiplier *before* the kernel blends
106/// in linear light — the only correct way to mix HDR and SDR layers on one
107/// CRTC. Hardware without them (RDNA2, vkms, older i915) leaves them false and
108/// the scene falls back to the CRTC pipeline or to pre-composition.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110#[allow(
111    clippy::exhaustive_structs,
112    reason = "built by callers with `..Default::default()`, which a new field does not break"
113)]
114pub struct ColorPipeline {
115    /// `PLANE_DEGAMMA_LUT` is present.
116    pub has_degamma_lut: bool,
117    /// `PLANE_CTM` is present.
118    pub has_ctm: bool,
119    /// `PLANE_HDR_MULT` is present — a 16.16 fixed-point scalar applied to
120    /// post-degamma linear values, used to lift SDR luminance into an HDR
121    /// blend space.
122    pub has_hdr_mult: bool,
123    /// How many `drm_color_lut` entries `PLANE_DEGAMMA_LUT` expects. Zero when
124    /// the property is absent.
125    pub degamma_lut_size: u32,
126}
127
128/// Everything the allocator needs to know about one plane.
129///
130/// The `has_*` flags mirror the kernel's own property presence one-for-one, so
131/// they stay independent booleans rather than a bitfield: a caller building a
132/// fixture writes exactly the properties the hardware it is modeling exposes,
133/// and packing them would make that unreadable.
134#[expect(
135    clippy::struct_excessive_bools,
136    reason = "one flag per kernel property; see the type documentation"
137)]
138#[derive(Debug, Clone, Default)]
139#[allow(
140    clippy::exhaustive_structs,
141    reason = "built by callers with `..Default::default()`, which a new field does not break"
142)]
143pub struct PlaneCapabilities {
144    /// The plane's KMS object id.
145    pub id: u32,
146    /// Bitmask of CRTC indices this plane can feed.
147    pub possible_crtcs: u32,
148    /// Primary, overlay, or cursor.
149    pub plane_type: PlaneType,
150    /// Advertised `FourCC`s, from the bare `formats` list.
151    pub formats: Vec<u32>,
152    /// The queryable view of `IN_FORMATS`.
153    ///
154    /// Built from the blob at enumeration. When the driver exposes no
155    /// `IN_FORMATS`, [`build_format_metadata`](Self::build_format_metadata)
156    /// synthesizes `LINEAR` for each advertised format. The allocator's edge
157    /// predicate queries this through `drmkit_fmt::layer_fits_plane`.
158    pub format_table: FormatTable,
159    /// Minimum `zpos`, if the plane exposes the property.
160    pub zpos_min: Option<u64>,
161    /// Maximum `zpos`, if the plane exposes the property.
162    pub zpos_max: Option<u64>,
163    /// For a multirect "virtual" plane -- the second rectangle of a hardware
164    /// pipe, which some controllers publish as its own plane -- the id of the
165    /// parent plane it is only valid alongside. `None` for ordinary planes.
166    /// Read from the driver's read-only `capabilities` blob
167    /// (`primary_smart_plane_id=`); see [`parse_multirect_parent`](crate::parse_multirect_parent).
168    pub multirect_parent: Option<u32>,
169    /// Supported rotate/reflect angles as a `DRM_MODE_ROTATE_*` /
170    /// `DRM_MODE_REFLECT_*` mask; 0 when the plane exposes no rotation.
171    ///
172    /// A plane can have [`supports_rotation`](Self::supports_rotation) true and
173    /// still omit 90/270 here — 0/180- or reflect-only hardware — which is why
174    /// the allocator gates on these bits rather than the flag. Not
175    /// hypothetical: measured on a Raspberry Pi 5, every one of vc4's 56
176    /// planes reports `0x35` — 0, 180, and both reflects, with no 90 or 270.
177    /// vkms reports `0x3f` and amdgpu `0xf` on the same probe.
178    pub rotation_bits: u64,
179    /// Whether the plane exposes a rotation property at all.
180    pub supports_rotation: bool,
181    /// Whether the plane may be asked to scale.
182    ///
183    /// Not a measured capability, and the name is the most optimistic reading
184    /// of what KMS offers. `PlaneRegistry::probe` sets it from the presence
185    /// of `SRC_W`, which every atomic plane has -- so on a real device it is
186    /// true everywhere, vkms and its absent scaler included. There is no
187    /// property that answers the question; the `TEST_ONLY` commit is what
188    /// finds out, and the allocator's search is built around that.
189    ///
190    /// It stays because a fixture, a replay, or a caller that knows its
191    /// hardware can set it false and have the matcher skip those planes
192    /// before spending a test commit on them. See
193    /// [drm-cxx#252](https://github.com/jwinarske/drm-cxx/issues/252).
194    pub supports_scaling: bool,
195    /// Whether the driver exposed `IN_FORMATS`.
196    pub has_format_modifiers: bool,
197    /// Whether the plane exposes `"pixel blend mode"`.
198    ///
199    /// Absence means scanout blending is hardwired by the driver — typically
200    /// opaque pixel-replace, so a transparent source pixel paints opaque black
201    /// over whatever is beneath it.
202    pub has_pixel_blend_mode: bool,
203    /// Whether the plane exposes `FB_DAMAGE_CLIPS`.
204    ///
205    /// Per plane, not per device, because it is not all-or-nothing: amdgpu
206    /// attaches it to six of its ten planes. A scene that reports partial
207    /// damage on a plane without it gets full-frame repaints there and
208    /// partial ones elsewhere, which is correct but worth being able to see.
209    ///
210    /// Narrower than it looks. `drm_plane_enable_fb_damage_clips` is opt-in
211    /// per driver: amdgpu calls it, vkms and vc4 do not.
212    pub has_fb_damage_clips: bool,
213
214    /// Whether the plane exposes the `"alpha"` per-plane alpha property.
215    /// Independent of [`has_pixel_blend_mode`](Self::has_pixel_blend_mode) —
216    /// some hardware has one without the other.
217    pub has_per_plane_alpha: bool,
218    /// Cached blend-mode enum integers.
219    pub blend_modes: BlendModeValues,
220    /// Whether the plane exposes `COLOR_ENCODING`. Only YCbCr-capable planes
221    /// do; the scene skips the write for the rest.
222    pub has_color_encoding: bool,
223    /// Cached `COLOR_ENCODING` enum integers.
224    pub color_encodings: ColorEncodingValues,
225    /// Whether the plane exposes `COLOR_RANGE`.
226    pub has_color_range: bool,
227    /// Cached `COLOR_RANGE` enum integers.
228    pub color_ranges: ColorRangeValues,
229    /// Per-plane color pipeline support.
230    pub color_pipeline: ColorPipeline,
231    /// The device's `DRM_CAP_CURSOR_WIDTH`, on a cursor plane. Zero on any
232    /// other plane, and where the driver reports none; zero leaves the
233    /// allocator's cursor-size guard off.
234    pub cursor_max_w: u32,
235    /// The device's `DRM_CAP_CURSOR_HEIGHT`, on a cursor plane, as
236    /// [`cursor_max_w`](Self::cursor_max_w).
237    pub cursor_max_h: u32,
238    /// One bandwidth class per distinct advertised modifier, sorted by
239    /// modifier, so the allocator hot path never re-decodes modifier bits.
240    ///
241    /// Filled by [`build_format_metadata`](Self::build_format_metadata); query
242    /// through [`bandwidth_class`](Self::bandwidth_class).
243    ///
244    /// Public because a caller building a fixture constructs
245    /// `PlaneCapabilities` with struct-update syntax, which a private field
246    /// would block — the same reason the C++ leaves it a public member. It is
247    /// derived state: leave it empty and let
248    /// [`PlaneRegistry::from_capabilities`] fill it.
249    pub modifier_classes: Vec<(u64, BandwidthClass)>,
250}
251
252impl PlaneCapabilities {
253    /// Whether the bare `formats` list advertises a `FourCC`.
254    ///
255    /// This is the pre-`IN_FORMATS` check and ignores modifiers entirely, so
256    /// it does not say whether a given buffer will scan out: a plane can list
257    /// a format it takes only compressed. Ask [`scans_out`](Self::scans_out)
258    /// that.
259    #[must_use]
260    pub fn supports_format(&self, fourcc: u32) -> bool {
261        self.formats.contains(&fourcc)
262    }
263
264    /// Whether the plane scans out `fourcc` in the `modifier` layout.
265    ///
266    /// By the plane's `IN_FORMATS` table, with `layer_fits_plane`'s
267    /// equivalences, or, on a plane without one, by the bare format list as
268    /// linear only, as the reference's does: such a plane says nothing about
269    /// any other layout. The RK3566's Cluster windows are why the list alone
270    /// is not enough: they list `ARGB8888` and take it only as AFBC, and
271    /// refuse a linear buffer at check time (P-48).
272    #[must_use]
273    pub fn scans_out(&self, fourcc: u32, modifier: Modifier) -> bool {
274        if self.has_format_modifiers {
275            drmkit_fmt::layer_fits_plane(&self.format_table, fourcc, modifier)
276        } else {
277            (modifier.is_linear() || modifier.is_invalid()) && self.supports_format(fourcc)
278        }
279    }
280
281    /// Whether the plane scans out a linear buffer of `fourcc`: what a dumb
282    /// buffer is.
283    #[must_use]
284    pub fn scans_out_linear(&self, fourcc: u32) -> bool {
285        self.scans_out(fourcc, Modifier::LINEAR)
286    }
287
288    /// Whether this plane can feed the CRTC at `crtc_index`.
289    #[must_use]
290    pub const fn compatible_with_crtc(&self, crtc_index: u32) -> bool {
291        if crtc_index >= u32::BITS {
292            return false;
293        }
294        self.possible_crtcs & (1 << crtc_index) != 0
295    }
296
297    /// The precomputed bandwidth class for a modifier.
298    ///
299    /// Falls back to a live `classify` for a modifier outside the advertised
300    /// set — decoding it rather than guessing.
301    #[must_use]
302    pub fn bandwidth_class(&self, modifier: u64) -> BandwidthClass {
303        self.modifier_classes
304            .binary_search_by_key(&modifier, |(m, _)| *m)
305            .map_or_else(
306                |_| classify(Modifier(modifier)),
307                |index| self.modifier_classes[index].1,
308            )
309    }
310
311    /// Populate the format table and bandwidth-class index.
312    ///
313    /// Called once per plane at registry construction, and safe to re-run. A
314    /// plane exposing no `IN_FORMATS` gets `LINEAR` synthesized for each
315    /// advertised format, so the allocator can still place linear buffers on it.
316    pub fn build_format_metadata(&mut self) {
317        if self.format_table.is_empty() && !self.formats.is_empty() {
318            self.format_table = FormatTable::from_pairs(
319                self.formats
320                    .iter()
321                    .map(|&fourcc| (fourcc, Modifier::LINEAR)),
322            );
323        }
324
325        self.modifier_classes.clear();
326        self.modifier_classes.extend(
327            self.format_table
328                .all()
329                .iter()
330                .map(|fm| (fm.modifier.0, classify(fm.modifier))),
331        );
332        self.modifier_classes.sort_unstable_by_key(|(m, _)| *m);
333        self.modifier_classes.dedup_by_key(|(m, _)| *m);
334    }
335
336    /// How many distinct modifiers have a precomputed class.
337    #[must_use]
338    pub fn distinct_modifier_count(&self) -> usize {
339        self.modifier_classes.len()
340    }
341}
342
343/// Every display plane the device exposes.
344///
345/// # Deviation: no interior-mutability cache
346///
347/// The C++ memoizes `for_crtc` in a `mutable` map of pointers into its own
348/// vector, and deletes the copy constructor so those pointers cannot dangle.
349/// Here [`for_crtc`](Self::for_crtc) returns a borrowed iterator computed on
350/// demand: the filter is a bitmask test over a handful of planes, so the cache
351/// bought little, and dropping it removes both the interior mutability and the
352/// reason copying had to be forbidden.
353#[derive(Debug, Clone, Default)]
354pub struct PlaneRegistry {
355    planes: Vec<PlaneCapabilities>,
356    /// How many planes each CRTC can stack, by CRTC index, where the driver
357    /// says. See [`blend_stage_limit`](Self::blend_stage_limit).
358    stage_limits: Vec<Option<usize>>,
359}
360
361impl PlaneRegistry {
362    /// An empty registry.
363    #[must_use]
364    pub const fn new() -> Self {
365        Self {
366            planes: Vec::new(),
367            stage_limits: Vec::new(),
368        }
369    }
370
371    /// Build a registry from a caller-supplied capability list.
372    ///
373    /// The linchpin for testing: it lets the whole allocator run against
374    /// synthetic fixtures with no device, which is how the ported allocator
375    /// suite reaches its coverage. Also used by replay and snapshot tools that
376    /// hold a serialized capability set.
377    ///
378    /// The result behaves identically to an enumerated registry — the format
379    /// table and bandwidth-class metadata are derived the same way.
380    #[must_use]
381    pub fn from_capabilities(mut capabilities: Vec<PlaneCapabilities>) -> Self {
382        for plane in &mut capabilities {
383            plane.build_format_metadata();
384        }
385        Self {
386            planes: capabilities,
387            stage_limits: Vec::new(),
388        }
389    }
390
391    /// How many planes the CRTC at `crtc_index` can stack at once, where the
392    /// driver publishes it; `None` for no limit known.
393    ///
394    /// Not the plane count, and not the width of the advertised zpos range:
395    /// the SA8155P offers more planes than it has blend stages, and
396    /// advertises zpos up to 10 while only 0 to 8 are usable. The allocator
397    /// never asks the kernel about more armed planes than this. Read by
398    /// `probe` from the CRTC's `capabilities` blob
399    /// ([`parse_blend_stage_limit`](crate::parse_blend_stage_limit)).
400    #[must_use]
401    pub fn blend_stage_limit(&self, crtc_index: u32) -> Option<usize> {
402        self.stage_limits
403            .get(usize::try_from(crtc_index).ok()?)
404            .copied()
405            .flatten()
406    }
407
408    /// Set [`blend_stage_limit`](Self::blend_stage_limit) for one CRTC, for
409    /// a registry built with
410    /// [`from_capabilities`](Self::from_capabilities).
411    pub fn set_blend_stage_limit(&mut self, crtc_index: u32, limit: Option<usize>) {
412        let Ok(index) = usize::try_from(crtc_index) else {
413            return;
414        };
415        if self.stage_limits.len() <= index {
416            self.stage_limits.resize(index + 1, None);
417        }
418        self.stage_limits[index] = limit;
419    }
420
421    /// Every plane.
422    #[must_use]
423    pub fn all(&self) -> &[PlaneCapabilities] {
424        &self.planes
425    }
426
427    /// Planes compatible with a CRTC.
428    pub fn for_crtc(&self, crtc_index: u32) -> impl Iterator<Item = &PlaneCapabilities> + '_ {
429        self.planes
430            .iter()
431            .filter(move |plane| plane.compatible_with_crtc(crtc_index))
432    }
433
434    /// Every modifier any non-cursor plane on `crtc_index` can scan `fourcc`
435    /// out with, deduplicated and in ascending order.
436    ///
437    /// The union across all of them, not the primary's `IN_FORMATS` alone.
438    /// On a split `SoC` the primary can advertise only a compressed layout while
439    /// the producer exports `LINEAR`: intersecting against the primary would
440    /// come back empty and miss the `LINEAR`-capable overlay the allocator was
441    /// always going to place the layer on. The `TEST_ONLY` commit during
442    /// placement remains the real arbiter of which plane takes it.
443    ///
444    /// Cursor planes are excluded for the same reason they are excluded from
445    /// [`force_disable_candidates`](Self::force_disable_candidates): the cursor
446    /// path owns them, and a scanout layer is never placed there.
447    #[must_use]
448    pub fn candidate_modifiers(&self, crtc_index: u32, fourcc: u32) -> Vec<u64> {
449        let mut modifiers: Vec<u64> = self
450            .for_crtc(crtc_index)
451            .filter(|plane| plane.plane_type != PlaneType::Cursor)
452            .flat_map(|plane| plane.format_table.modifiers_for(fourcc))
453            .map(|modifier| modifier.0)
454            .collect();
455        modifiers.sort_unstable();
456        modifiers.dedup();
457        modifiers
458    }
459
460    /// Planes on `crtc_index` that a commit may force-disable.
461    ///
462    /// [`for_crtc`](Self::for_crtc) minus the cursor planes. A cursor plane is
463    /// owned by the cursor path, not the scene: clearing it here would erase
464    /// the cursor on every frame the scene commits. Upstream carves out the
465    /// same exception in `disable_unused_planes`.
466    pub fn force_disable_candidates(
467        &self,
468        crtc_index: u32,
469    ) -> impl Iterator<Item = &PlaneCapabilities> + '_ {
470        self.for_crtc(crtc_index)
471            .filter(|plane| plane.plane_type != PlaneType::Cursor)
472    }
473
474    /// A plane by its KMS object id.
475    #[must_use]
476    pub fn by_id(&self, plane_id: u32) -> Option<&PlaneCapabilities> {
477        self.planes.iter().find(|plane| plane.id == plane_id)
478    }
479
480    /// How many planes the registry holds.
481    #[must_use]
482    pub fn len(&self) -> usize {
483        self.planes.len()
484    }
485
486    /// Whether the registry holds no planes.
487    #[must_use]
488    pub fn is_empty(&self) -> bool {
489        self.planes.is_empty()
490    }
491}
492
493/// Map a 16-bit layer alpha onto the range a plane advertises.
494///
495/// Layers carry a 16-bit alpha because that is what the DRM documentation
496/// describes, but the range is the driver's to declare: vendor drivers ship
497/// 8-bit alpha advertising `[0, 255]`, and writing `0xFFFF` to one of those
498/// fails the whole atomic commit with `EINVAL` -- which a `TEST_ONLY` reports
499/// only as "this assignment does not fit", so it surfaces as a dropped layer
500/// that mentions nothing about alpha (drm-cxx#265).
501///
502/// Rescaled rather than clamped: alpha is a fraction of full opacity, so half
503/// alpha must become half of `alpha_max`. Clamping would turn every partially
504/// transparent layer opaque on an 8-bit plane, and would agree with this on
505/// exactly one value, full opacity, which is the one value most testing uses.
506/// Rounds to nearest, so full opacity lands exactly on `alpha_max`; the
507/// identity for a plane advertising the full 16 bits; saturates an oversized
508/// input rather than scaling past the maximum.
509#[must_use]
510pub const fn rescale_alpha(value: u64, alpha_max: u64) -> u64 {
511    const FULL: u64 = 0xFFFF;
512    let clamped = if value < FULL { value } else { FULL };
513    if alpha_max >= FULL {
514        return clamped;
515    }
516    (clamped * alpha_max + FULL / 2) / FULL
517}