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}