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}