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