Skip to main content

drmkit_core/
property.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Cached DRM object properties.
5//!
6//! Port of `src/core/property_store.{hpp,cpp}`.
7
8use std::collections::HashMap;
9use std::os::fd::AsFd as _;
10
11use drm::control::Device as ControlDevice;
12
13use crate::device::Device;
14use crate::error::{CoreError, Result};
15
16/// A DRM object kind, for property queries.
17///
18/// The C++ passes the raw `DRM_MODE_OBJECT_*` constant. Here the kind is an
19/// enum so the `drm` crate's typed handle can be reconstructed, which is what
20/// keeps `get_properties` type-safe on the way out.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[non_exhaustive]
23pub enum ObjectType {
24    /// A CRTC.
25    Crtc,
26    /// A connector.
27    Connector,
28    /// An encoder.
29    Encoder,
30    /// A plane.
31    Plane,
32    /// A framebuffer.
33    Framebuffer,
34}
35
36/// One cached property.
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct PropertyInfo {
40    /// The property id, for use in an atomic request.
41    pub id: u32,
42    /// The property name as the kernel reports it.
43    pub name: String,
44    /// The value at the time the object was cached.
45    pub value: u64,
46    /// Whether the kernel marks this property `DRM_MODE_PROP_IMMUTABLE`.
47    ///
48    /// The C++ keeps the whole raw `flags` word so callers can distinguish
49    /// immutable, range, enum, and blob properties without re-querying. Only
50    /// the immutable bit is actually consulted anywhere in the tree, and the
51    /// `drm` crate surfaces the rest through typed accessors, so this carries
52    /// the one bit that has a caller and leaves the rest to
53    /// [`get_property`](drm::control::Device::get_property).
54    pub immutable: bool,
55    /// Inclusive `(min, max)` for an **unsigned** range property, `None` for
56    /// every other kind -- signed ranges included.
57    ///
58    /// The kernel rejects a range write outside these bounds with `EINVAL`,
59    /// and an atomic commit is all-or-nothing: one out-of-range value fails
60    /// the whole frame, including every layer that was fine. A caller that
61    /// derives a value rather than echoing one back -- a zpos computed from a
62    /// layer count, say -- has no other way to know what the plane will take.
63    ///
64    /// Signed ranges are deliberately not reported. The atomic ioctl carries
65    /// every value as a `u64`, so a signed range arrives as two's complement:
66    /// `CRTC_X`'s bounds are `(-2^31, 2^31 - 1)`, which as `u64` is
67    /// `(18446744071562067968, 2147483647)` -- a pair where the minimum
68    /// compares *greater* than the maximum. Anything doing unsigned arithmetic
69    /// on that would turn a small negative offset into `INT32_MAX`, so the
70    /// safe thing is to hand back nothing rather than a bound that is wrong in
71    /// a direction the caller cannot see.
72    pub range: Option<(u64, u64)>,
73}
74
75/// Per-object property cache.
76///
77/// Building an atomic request means naming properties by id, and resolving a
78/// name to an id costs an ioctl per property. Caching once per object turns
79/// per-frame property lookup into a hash-map hit.
80#[derive(Debug, Clone, Default)]
81pub struct PropertyStore {
82    store: HashMap<u32, Vec<PropertyInfo>>,
83}
84
85impl PropertyStore {
86    /// An empty store.
87    #[must_use]
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Query and cache every property of one object, replacing any previous
93    /// entry for it.
94    ///
95    /// A property whose `get_property` lookup fails is skipped rather than
96    /// failing the whole object, matching the C++ `continue`: one unreadable
97    /// property should not make a plane unusable.
98    ///
99    /// # Errors
100    ///
101    /// [`CoreError::Io`] if the object's property set cannot be read at all.
102    pub fn cache_properties(
103        &mut self,
104        device: &Device,
105        object_id: u32,
106        object_type: ObjectType,
107    ) -> Result<()> {
108        let values = get_properties_raw(device, object_id, object_type)?;
109        let (handles, raw_values) = values.as_props_and_values();
110
111        let mut entries = Vec::with_capacity(handles.len());
112        for (handle, value) in handles.iter().zip(raw_values.iter()) {
113            let Ok(info) = device.get_property(*handle) else {
114                // One unreadable property must not disqualify the object.
115                continue;
116            };
117            let range = match info.value_type() {
118                drm::control::property::ValueType::UnsignedRange(lo, hi) => Some((*lo, *hi)),
119                _ => None,
120            };
121            entries.push(PropertyInfo {
122                id: (*handle).into(),
123                name: info.name().to_string_lossy().into_owned(),
124                value: *value,
125                immutable: !info.mutable(),
126                range,
127            });
128        }
129
130        self.store.insert(object_id, entries);
131        Ok(())
132    }
133
134    /// Every cached property of an object, or `None` if it was never cached.
135    #[must_use]
136    pub fn properties(&self, object_id: u32) -> Option<&[PropertyInfo]> {
137        self.store.get(&object_id).map(Vec::as_slice)
138    }
139
140    /// Look up one property by name.
141    fn find(&self, object_id: u32, name: &str) -> Result<&PropertyInfo> {
142        let entries = self
143            .store
144            .get(&object_id)
145            .ok_or(CoreError::NoSuchObject { object_id })?;
146
147        entries
148            .iter()
149            .find(|p| p.name == name)
150            .ok_or_else(|| CoreError::NoSuchProperty {
151                object_id,
152                name: name.to_owned(),
153            })
154    }
155
156    /// The id of a named property, for building an atomic request.
157    ///
158    /// # Errors
159    ///
160    /// [`CoreError::NoSuchObject`] if the object was never cached, or
161    /// [`CoreError::NoSuchProperty`] if it has no such property. The C++
162    /// reports `no_such_file_or_directory` for both; splitting them says which
163    /// mistake was made.
164    pub fn property_id(&self, object_id: u32, name: &str) -> Result<u32> {
165        self.find(object_id, name).map(|p| p.id)
166    }
167
168    /// The cached value of a named property.
169    ///
170    /// # Errors
171    ///
172    /// As [`PropertyStore::property_id`].
173    pub fn property_value(&self, object_id: u32, name: &str) -> Result<u64> {
174        self.find(object_id, name).map(|p| p.value)
175    }
176
177    /// The raw value of an enum property's named variant.
178    ///
179    /// Enum properties are named by string in the KMS ABI but written as
180    /// integers, and the integer a driver assigns to a given name is its own
181    /// business. Hardcoding one that happened to be right on the card in front
182    /// of you silently selects a different variant elsewhere -- for
183    /// `COLOR_ENCODING` that is a wrong YCbCr matrix, which is a visible tint
184    /// rather than an error.
185    ///
186    /// # Errors
187    ///
188    /// [`CoreError::NoSuchProperty`] if the object has no such property, if it
189    /// is not an enum, or if it has no variant by that name.
190    pub fn enum_value(
191        &self,
192        device: &Device,
193        object_id: u32,
194        property: &str,
195        variant: &str,
196    ) -> Result<u64> {
197        let id = self.property_id(object_id, property)?;
198        let missing = || CoreError::NoSuchProperty {
199            object_id,
200            name: property.to_owned(),
201        };
202        // `RawResourceHandle` is a `NonZeroU32`; a zero id is not a property.
203        let handle = drm::control::RawResourceHandle::new(id).ok_or_else(missing)?;
204        let info =
205            drm::control::Device::get_property(device, handle.into()).map_err(|_| missing())?;
206        let drm::control::property::ValueType::Enum(values) = info.value_type() else {
207            return Err(missing());
208        };
209        let (raw, enums) = values.values();
210        enums
211            .iter()
212            .zip(raw.iter())
213            .find(|(entry, _)| entry.name().to_string_lossy() == variant)
214            .map(|(_, value)| *value)
215            .ok_or_else(missing)
216    }
217
218    /// The union of the bits a bitmask property advertises.
219    ///
220    /// `rotation` is the one that matters: it is a bitmask whose enum entries
221    /// name the `DRM_MODE_ROTATE_*` and `DRM_MODE_REFLECT_*` bits the plane
222    /// actually implements, and a plane can expose the property while
223    /// implementing only 0/180 or a reflect. A caller that treats the
224    /// property's presence as "can rotate" and asks for 90 degrees gets a
225    /// driver that silently drops the request, so the layer comes out the
226    /// wrong way round with every ioctl succeeding.
227    ///
228    /// The `drm` crate reports a bitmask property as
229    /// `ValueType::Bitmask` with no payload -- it parses the enum table only
230    /// for `ValueType::Enum` -- so this reads the property directly.
231    ///
232    /// # Errors
233    ///
234    /// [`CoreError::NoSuchProperty`] if the object has no such property or it
235    /// is not a bitmask.
236    pub fn bitmask_bits(&self, device: &Device, object_id: u32, property: &str) -> Result<u64> {
237        let id = self.property_id(object_id, property)?;
238        let missing = || CoreError::NoSuchProperty {
239            object_id,
240            name: property.to_owned(),
241        };
242        // Both buffers, even though only the enums are wanted: the kernel
243        // copies the values array into whatever pointer it is given, and a
244        // null one with a non-zero count fails the whole ioctl with EFAULT.
245        let mut values = Vec::new();
246        let mut enums = Vec::new();
247        let info =
248            drm_ffi::mode::get_property(device.as_fd(), id, Some(&mut values), Some(&mut enums))
249                .map_err(|_| missing())?;
250        if info.flags & drm_ffi::DRM_MODE_PROP_BITMASK == 0 {
251            return Err(missing());
252        }
253        // For a bitmask property each entry's value is the *bit index*, not
254        // the bit -- so 90 degrees is entry value 1, meaning bit 1.
255        Ok(enums
256            .iter()
257            .filter(|entry| entry.value < u64::BITS.into())
258            .fold(0_u64, |bits, entry| bits | (1_u64 << entry.value)))
259    }
260
261    /// Whether a named property is `DRM_MODE_PROP_IMMUTABLE`.
262    ///
263    /// Writing an immutable property in an atomic commit is rejected with
264    /// `EINVAL` whatever the value, so a request builder must skip it.
265    ///
266    /// # Errors
267    ///
268    /// As [`PropertyStore::property_id`].
269    pub fn is_immutable(&self, object_id: u32, name: &str) -> Result<bool> {
270        self.find(object_id, name).map(|p| p.immutable)
271    }
272
273    /// The inclusive `(min, max)` of a named unsigned range property.
274    ///
275    /// `None` when the property is not an unsigned range -- see
276    /// [`PropertyInfo::range`] -- which callers treat as "no bound to enforce"
277    /// rather than an error.
278    ///
279    /// # Errors
280    ///
281    /// As [`PropertyStore::property_id`].
282    pub fn range(&self, object_id: u32, name: &str) -> Result<Option<(u64, u64)>> {
283        self.find(object_id, name).map(|p| p.range)
284    }
285
286    /// Drop everything cached.
287    pub fn clear(&mut self) {
288        self.store.clear();
289    }
290
291    /// Drop the cache for one object, for instance after a hotplug.
292    pub fn forget(&mut self, object_id: u32) {
293        self.store.remove(&object_id);
294    }
295
296    /// How many objects are cached.
297    #[must_use]
298    pub fn len(&self) -> usize {
299        self.store.len()
300    }
301
302    /// Whether nothing is cached.
303    #[must_use]
304    pub fn is_empty(&self) -> bool {
305        self.store.is_empty()
306    }
307}
308
309/// Fetch an object's property set, reconstructing the `drm` crate's typed
310/// handle from the raw id and kind.
311fn get_properties_raw(
312    device: &Device,
313    object_id: u32,
314    object_type: ObjectType,
315) -> Result<drm::control::PropertyValueSet> {
316    use drm::control::{connector, crtc, encoder, framebuffer, plane};
317
318    let raw: drm::control::RawResourceHandle = object_id
319        .try_into()
320        .map_err(|_| CoreError::NoSuchObject { object_id })?;
321
322    let result = match object_type {
323        ObjectType::Crtc => device.get_properties(crtc::Handle::from(raw)),
324        ObjectType::Connector => device.get_properties(connector::Handle::from(raw)),
325        ObjectType::Encoder => device.get_properties(encoder::Handle::from(raw)),
326        ObjectType::Plane => device.get_properties(plane::Handle::from(raw)),
327        ObjectType::Framebuffer => device.get_properties(framebuffer::Handle::from(raw)),
328    };
329
330    result.map_err(|e| CoreError::from_io(&e))
331}