drmkit_scene/commit.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Turning plane assignments into atomic commits.
5//!
6//! This is the seam between the device-free search in `drmkit-planes` and the
7//! kernel. The allocator decides *what* to try; this knows *how* to ask.
8
9use std::collections::HashMap;
10
11use drmkit_core::{
12 AtomicCommitFlags, CoreError, Device, Mode, ObjectType, PropertyBlob, PropertyStore,
13};
14use drmkit_planes::{
15 ColorEncoding, ColorEncodingValues, ColorRange, ColorRangeValues, LayerRef, PropTag,
16 TestCommitter, TestFailure,
17};
18
19use crate::frame::KernelResult;
20
21/// A plane's `COLOR_ENCODING` and `COLOR_RANGE`: each property's id, and the
22/// driver-assigned integer for every entry it lists.
23///
24/// The two are independent. A plane may expose one without the other, and
25/// each is written where it is writable, as upstream does.
26#[derive(Debug, Clone, Copy, Default)]
27struct ColorProps {
28 encoding: Option<(u32, ColorEncodingValues)>,
29 range: Option<(u32, ColorRangeValues)>,
30}
31
32impl ColorProps {
33 /// The values this plane takes for `want`, one per property it has.
34 fn resolve(&self, want: crate::Ycbcr) -> ResolvedColor {
35 ResolvedColor {
36 encoding: self
37 .encoding
38 .and_then(|(id, values)| Some((id, encoding_value(&values, want.encoding)?))),
39 range: self
40 .range
41 .and_then(|(id, values)| Some((id, range_value(&values, want.range)?))),
42 }
43 }
44}
45
46/// A plane's colorimetry as written: property id and value, for each of the
47/// pair the plane has a usable entry for.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49struct ResolvedColor {
50 encoding: Option<(u32, u64)>,
51 range: Option<(u32, u64)>,
52}
53
54/// The `COLOR_ENCODING` value for `want`, or the nearest the plane has.
55///
56/// A driver that lacks the entry asked for gets BT.709, then BT.601: the
57/// fallback upstream's `arm_layer_plane_color_props` takes. Writing nothing
58/// instead would leave the plane on whatever the previous client chose.
59pub(crate) fn encoding_value(values: &ColorEncodingValues, want: ColorEncoding) -> Option<u64> {
60 let exact = match want {
61 ColorEncoding::Bt601 => values.bt601,
62 ColorEncoding::Bt709 => values.bt709,
63 ColorEncoding::Bt2020 => values.bt2020,
64 };
65 exact.or(values.bt709).or(values.bt601)
66}
67
68/// The `COLOR_RANGE` value for `want`, or limited, or full.
69pub(crate) fn range_value(values: &ColorRangeValues, want: ColorRange) -> Option<u64> {
70 let exact = match want {
71 ColorRange::Limited => values.limited,
72 ColorRange::Full => values.full,
73 };
74 exact.or(values.limited).or(values.full)
75}
76
77/// Resolves a layer's [`PropTag`]s to the DRM property ids of a given plane.
78///
79/// Property ids are per-object and stable for a device's lifetime, so they are
80/// resolved once per plane and cached. Doing it per frame would be an ioctl per
81/// property per layer.
82#[derive(Debug, Default)]
83pub struct PlanePropertyMap {
84 /// `plane_id` to its resolved tag ids.
85 pub(crate) planes: HashMap<u32, HashMap<PropTag, u32>>,
86 /// Properties the kernel marks immutable, which a commit must never write.
87 ///
88 /// Writing one is rejected with `EINVAL` whatever the value, so a single
89 /// stray write poisons the whole commit — including every other layer in
90 /// it.
91 immutable: HashMap<u32, Vec<PropTag>>,
92 /// Colorimetry properties, for the planes that expose them.
93 color: HashMap<u32, ColorProps>,
94 /// `FB_DAMAGE_CLIPS`, for the planes that expose it.
95 ///
96 /// Kept apart from `planes` rather than given a `PropTag`, because its
97 /// value does not exist until emission: it is a blob id, created from the
98 /// rectangles a source reported for *this* frame. A tag would put it in
99 /// the layer's property bag, which is built before any of that is known.
100 damage: HashMap<u32, u32>,
101 /// The inclusive zpos range each plane advertises, where it has one.
102 ///
103 /// zpos is the one plane property drmkit *derives* rather than echoes: the
104 /// composition canvas asks to sit above every layer, and "above" is
105 /// computed from the layer count. Nothing in that arithmetic knows what
106 /// the plane will accept, and a plane whose range is `[1, 17]` rejects 19
107 /// with `EINVAL` -- taking the whole frame down with it, every correctly
108 /// programmed layer included.
109 pub(crate) zpos_range: HashMap<u32, (u64, u64)>,
110 /// The largest `alpha` each plane advertises, where it advertises a
111 /// range. Layers carry 16-bit alpha; see [`drmkit_planes::rescale_alpha`].
112 pub(crate) alpha_max: HashMap<u32, u64>,
113 /// The colorimetry each plane's last real commit carried.
114 ///
115 /// These properties are sticky across clients, so they have to be written
116 /// to displace whatever the previous compositor left -- and then only when
117 /// a layer asks for something else, because restating an unchanged value
118 /// every frame is exactly the per-frame traffic the rest of the emit path
119 /// works to avoid. A frame that [restates](crate::FrameBuild::restates_sticky)
120 /// writes them regardless.
121 color_committed: HashMap<u32, ResolvedColor>,
122}
123
124impl PlanePropertyMap {
125 /// An empty map.
126 #[must_use]
127 pub fn new() -> Self {
128 Self::default()
129 }
130
131 /// Resolve and cache every tag this crate writes for `plane_id`.
132 ///
133 /// Tags the plane does not expose are simply absent: a plane without
134 /// `rotation` or `alpha` is normal, and asking for one is not an error.
135 ///
136 /// # Errors
137 ///
138 /// [`CoreError`] if the plane's properties cannot be read at all.
139 pub fn learn_plane(&mut self, device: &Device, plane_id: u32) -> Result<(), CoreError> {
140 let mut store = PropertyStore::new();
141 store.cache_properties(device, plane_id, ObjectType::Plane)?;
142
143 // Colorimetry is optional -- plenty of planes expose neither property.
144 // An immutable one is left out, since writing it fails the commit
145 // whatever the value, and an entry the driver does not list is `None`
146 // and falls back at emission.
147 let writable = |name: &str| {
148 store
149 .property_id(plane_id, name)
150 .ok()
151 .filter(|_| !store.is_immutable(plane_id, name).unwrap_or(false))
152 };
153 let entry =
154 |name: &str, variant: &str| store.enum_value(device, plane_id, name, variant).ok();
155 let color = ColorProps {
156 encoding: writable("COLOR_ENCODING").map(|id| {
157 (
158 id,
159 ColorEncodingValues {
160 bt601: entry("COLOR_ENCODING", "ITU-R BT.601 YCbCr"),
161 bt709: entry("COLOR_ENCODING", "ITU-R BT.709 YCbCr"),
162 bt2020: entry("COLOR_ENCODING", "ITU-R BT.2020 YCbCr"),
163 },
164 )
165 }),
166 range: writable("COLOR_RANGE").map(|id| {
167 (
168 id,
169 ColorRangeValues {
170 limited: entry("COLOR_RANGE", "YCbCr limited range"),
171 full: entry("COLOR_RANGE", "YCbCr full range"),
172 },
173 )
174 }),
175 };
176 if color.encoding.is_some() || color.range.is_some() {
177 self.color.insert(plane_id, color);
178 }
179
180 if let Ok(Some(range)) = store.range(plane_id, PropTag::Zpos.name()) {
181 self.zpos_range.insert(plane_id, range);
182 }
183 if let Ok(Some((_, max))) = store.range(plane_id, PropTag::Alpha.name()) {
184 self.alpha_max.insert(plane_id, max);
185 }
186
187 if let Ok(id) = store.property_id(plane_id, "FB_DAMAGE_CLIPS") {
188 self.damage.insert(plane_id, id);
189 }
190
191 let mut ids = HashMap::new();
192 let mut immutable = Vec::new();
193 for tag in PropTag::ALL {
194 // `pixel_format` and `FB_MODIFIER` are drmkit's own hints on the
195 // property bag, not KMS plane properties -- the allocator reads
196 // them, the kernel never sees them.
197 if matches!(tag, PropTag::PixelFormat | PropTag::FbModifier) {
198 continue;
199 }
200 if let Ok(id) = store.property_id(plane_id, tag.name()) {
201 if store.is_immutable(plane_id, tag.name()).unwrap_or(false) {
202 immutable.push(tag);
203 continue;
204 }
205 ids.insert(tag, id);
206 }
207 }
208
209 self.planes.insert(plane_id, ids);
210 self.immutable.insert(plane_id, immutable);
211 Ok(())
212 }
213
214 /// Learn every plane the registry knows about.
215 ///
216 /// # Errors
217 ///
218 /// [`CoreError`] if a plane's properties cannot be read.
219 pub fn learn_all(
220 &mut self,
221 device: &Device,
222 registry: &drmkit_planes::PlaneRegistry,
223 ) -> Result<(), CoreError> {
224 for plane in registry.all() {
225 self.learn_plane(device, plane.id)?;
226 }
227 Ok(())
228 }
229
230 /// Record that a real commit carried a plan entry's colorimetry.
231 ///
232 /// Call for every entry of a frame the kernel accepted. A `TEST_ONLY`
233 /// applies nothing, so noting one would suppress the write on the commit
234 /// that actually matters and leave the plane on the previous client's
235 /// settings. An entry that asks for nothing -- the composition canvas --
236 /// leaves the record as it was.
237 pub fn note_color_committed(&mut self, plane_id: u32, color: Option<crate::Ycbcr>) {
238 if let (Some(want), Some(props)) = (color, self.color.get(&plane_id)) {
239 self.color_committed.insert(plane_id, props.resolve(want));
240 }
241 }
242
243 /// The DRM property id for a tag on a plane, if it is writable there.
244 #[must_use]
245 pub fn property_id(&self, plane_id: u32, tag: PropTag) -> Option<u32> {
246 self.planes.get(&plane_id)?.get(&tag).copied()
247 }
248
249 /// Whether the plane marks this property immutable.
250 #[must_use]
251 pub fn is_immutable(&self, plane_id: u32, tag: PropTag) -> bool {
252 self.immutable
253 .get(&plane_id)
254 .is_some_and(|tags| tags.contains(&tag))
255 }
256
257 /// How many planes have been learned.
258 #[must_use]
259 pub fn len(&self) -> usize {
260 self.planes.len()
261 }
262
263 /// Whether nothing has been learned yet.
264 #[must_use]
265 pub fn is_empty(&self) -> bool {
266 self.planes.is_empty()
267 }
268}
269
270/// Emit one layer's properties onto a plane.
271///
272/// Skips tags the plane does not expose or marks immutable, the two hint tags
273/// the kernel never sees, `FB_ID` on an externally bound layer, and values
274/// unchanged from `baseline`. Returns what was written, as [`LayerWrites`].
275///
276/// # Errors
277///
278/// [`CoreError`] if a property write is rejected before the commit — which
279/// only happens for a malformed id.
280pub fn emit_layer(
281 request: &mut drmkit_core::AtomicRequest,
282 map: &PlanePropertyMap,
283 plane_id: u32,
284 layer: &drmkit_planes::Layer,
285 baseline: Option<&drmkit_planes::PropertySnapshot>,
286) -> Result<LayerWrites, CoreError> {
287 let mut written = LayerWrites::default();
288 for (tag, value) in layer.properties() {
289 let Some(property_id) = map.property_id(plane_id, tag) else {
290 continue;
291 };
292 // An externally bound layer's FB_ID is set up by the producer's
293 // extension stack. Writing it from here fights that, so suppress it
294 // even if something has stuffed one into the property bag.
295 if layer.is_externally_bound() && tag == PropTag::FbId {
296 continue;
297 }
298 // The diff compares what the caller asked for, because that is what
299 // the baseline records; the plane's own range is applied to what goes
300 // out. The mapping is fixed per plane, so equal requests always mean
301 // equal writes -- and comparing a rescaled value against a raw
302 // baseline would rewrite alpha on every frame of an 8-bit plane.
303 if !needs_write(tag, value, baseline) {
304 continue;
305 }
306 let value = match tag {
307 PropTag::Zpos => map.clamp_zpos(plane_id, value),
308 PropTag::Alpha => map.rescale_alpha(plane_id, value),
309 _ => value,
310 };
311 request.add_property(plane_id, property_id, value)?;
312 written.properties += 1;
313 if tag == PropTag::FbId {
314 written.framebuffers += 1;
315 }
316 }
317 Ok(written)
318}
319
320/// What emitting one layer put on the wire.
321///
322/// `framebuffers` is a subset of `properties`, kept apart because it answers a
323/// different question: `FB_ID` is written every frame by contract, so it is
324/// the floor a minimal frame is measured against rather than part of what the
325/// diff decided.
326#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
327#[non_exhaustive]
328pub struct LayerWrites {
329 /// Properties written for this layer.
330 pub properties: usize,
331 /// Of those, `FB_ID` attachments.
332 pub framebuffers: usize,
333}
334
335impl PlanePropertyMap {
336 /// A layer's 16-bit alpha, on the scale this plane advertises.
337 ///
338 /// Every write of a layer's properties goes through
339 /// [`emit_layer`], so this one call covers the allocator's tests, the real
340 /// commit and a pinned layer alike -- upstream needed two fixes to reach
341 /// all three.
342 #[must_use]
343 pub fn rescale_alpha(&self, plane_id: u32, value: u64) -> u64 {
344 self.alpha_max
345 .get(&plane_id)
346 .map_or(value, |&max| drmkit_planes::rescale_alpha(value, max))
347 }
348
349 /// Bring a derived zpos inside what the plane will actually take.
350 ///
351 /// Only zpos is clamped, and only against an unsigned range. Every other
352 /// plane property carries a value the caller measured or was handed -- a
353 /// rectangle, a framebuffer id -- where a value outside the range is a
354 /// real error that silently moving would hide. zpos alone is computed, so
355 /// it alone is worth bending to fit; a canvas one slot lower than asked is
356 /// still above every layer, whereas the alternative is no frame at all.
357 #[must_use]
358 pub fn clamp_zpos(&self, plane_id: u32, value: u64) -> u64 {
359 match self.zpos_range.get(&plane_id) {
360 Some((lo, hi)) => value.clamp(*lo, *hi),
361 None => value,
362 }
363 }
364}
365
366/// Whether a property has to be written, given what the kernel last took.
367///
368/// `baseline` of `None` means write everything -- see
369/// [`committed_baseline`](drmkit_planes::Allocator::committed_baseline).
370fn needs_write(
371 tag: PropTag,
372 value: u64,
373 baseline: Option<&drmkit_planes::PropertySnapshot>,
374) -> bool {
375 // Two properties are written every commit no matter what the diff says.
376 //
377 // FB_ID is how KMS is told this is a new frame: the kernel schedules the
378 // page-flip event off the re-attach. A single-buffered source whose pixels
379 // mutate in place presents the same id every frame, so diffing it away
380 // leaves an otherwise-clean scene with an empty request -- which the kernel
381 // accepts and then never queues an event for, wedging the caller's flip
382 // forever.
383 //
384 // IN_FENCE_FD is a one-shot the kernel consumes each commit. An fd number
385 // the diff happens to see unchanged, because the value was recycled, still
386 // has to be re-armed or the plane scans out before its buffer is ready.
387 if matches!(tag, PropTag::FbId | PropTag::InFenceFd) {
388 return true;
389 }
390 baseline.is_none_or(|snapshot| snapshot.get(tag) != Some(value))
391}
392
393/// Emit the writes that detach a plane.
394///
395/// A plane left out of an assignment must be explicitly cleared, or it inherits
396/// whatever the previous commit left on it — an old framebuffer, an old CRTC
397/// binding, an old stacking position. Under a `TEST_ONLY` that shows up as a
398/// spurious rejection when a layer migrates between planes on one CRTC: two
399/// active planes, same stacking slot, same CRTC.
400///
401/// # Errors
402///
403/// [`CoreError`] if a property write is rejected.
404pub fn emit_disable(
405 request: &mut drmkit_core::AtomicRequest,
406 map: &PlanePropertyMap,
407 plane_id: u32,
408) -> Result<usize, CoreError> {
409 let mut written = 0;
410 for tag in [PropTag::FbId, PropTag::CrtcId] {
411 if let Some(property_id) = map.property_id(plane_id, tag) {
412 request.add_property(plane_id, property_id, 0)?;
413 written += 1;
414 }
415 }
416 Ok(written)
417}
418
419/// Turn off what a [`rebind`](crate::LayerScene::rebind) left on the old CRTC.
420///
421/// One commit of its own, before the first frame on the new CRTC: every plane
422/// the scene had lit there is detached, and the old CRTC is switched off
423/// (`ACTIVE = 0`). It has to be separate because the kernel refuses to move a
424/// plane between CRTCs in one commit, and a plane both pipes can use is one
425/// the new frame may want. The CRTC goes off with its planes because some
426/// drivers reject an active CRTC whose primary is disarmed; `ACTIVE = 0` keeps
427/// its mode and connector, so a caller that wants the old output back sets it
428/// active again. Upstream's fix for the same defect (`a335bef`, drm-cxx#340)
429/// leaves the old CRTC active, and retries without the primary instead.
430///
431/// Returns whether there was anything to commit. On success the scene can
432/// build frames again; on failure the detach stays pending and
433/// [`build_frame`](crate::LayerScene::build_frame) goes on refusing.
434///
435/// # Errors
436///
437/// [`CoreError`] if a property cannot be found or the kernel rejects the
438/// commit.
439pub fn commit_detach(device: &Device, scene: &mut crate::LayerScene) -> Result<bool, CoreError> {
440 let Some(detach) = scene.pending_detach() else {
441 return Ok(false);
442 };
443 // Looked up here rather than through the caller's property map: a plane
444 // the map never learned would be skipped silently and stay lit, which is
445 // the defect this exists to fix.
446 let mut store = PropertyStore::new();
447 let mut request = drmkit_core::AtomicRequest::with_capacity(2 * detach.planes.len() + 1);
448 for &plane_id in &detach.planes {
449 store.cache_properties(device, plane_id, ObjectType::Plane)?;
450 request.add_property(plane_id, store.property_id(plane_id, "FB_ID")?, 0)?;
451 request.add_property(plane_id, store.property_id(plane_id, "CRTC_ID")?, 0)?;
452 }
453 store.cache_properties(device, detach.crtc_id, ObjectType::Crtc)?;
454 request.add_property(
455 detach.crtc_id,
456 store.property_id(detach.crtc_id, "ACTIVE")?,
457 0,
458 )?;
459 request.commit(device, AtomicCommitFlags::ALLOW_MODESET)?;
460 scene.detach_committed();
461 Ok(true)
462}
463
464/// `layer` with the stacked zpos for `plane_id`, if the stack has one.
465fn with_stacked_zpos(
466 layer: &drmkit_planes::Layer,
467 plane_id: u32,
468 stacked: &[(u32, u64)],
469) -> drmkit_planes::Layer {
470 let mut layer = layer.clone();
471 if let Some((_, zpos)) = stacked.iter().find(|(id, _)| *id == plane_id) {
472 layer.set_property(PropTag::Zpos, *zpos);
473 }
474 layer
475}
476
477/// A [`TestCommitter`] that issues real `TEST_ONLY` commits.
478///
479/// Builds a fresh request per test that (a) disables every candidate plane not
480/// in the assignment, then (b) applies each assigned layer's properties — the
481/// order matters for the reason [`emit_disable`] describes.
482pub struct DeviceCommitter<'a> {
483 device: &'a Device,
484 map: &'a PlanePropertyMap,
485 /// Planes on this CRTC that a test may need to disable.
486 candidates: Vec<u32>,
487 /// Modeset writes to prepend to every test request, if the CRTC still
488 /// needs bringing up.
489 modeset: Option<&'a Modeset<'a>>,
490 flags: AtomicCommitFlags,
491 /// How many test commits have been issued, for diagnostics.
492 pub commits: usize,
493 /// A plane armed in every test alongside the assignment -- the
494 /// composition canvas. See [`TestCommitter::set_extra_plane`].
495 extra: Option<(u32, drmkit_planes::Layer)>,
496 /// For the zpos each test writes: the same stack the frame will.
497 registry: &'a drmkit_planes::PlaneRegistry,
498}
499
500impl<'a> DeviceCommitter<'a> {
501 /// A committer over the planes this CRTC could use.
502 ///
503 /// The candidate list comes from
504 /// [`force_disable_candidates`](drmkit_planes::PlaneRegistry::force_disable_candidates)
505 /// rather than being supplied, so a test commit and the apply commit that
506 /// follows it clear exactly the same set. Handing them different lists
507 /// would let a configuration pass `TEST_ONLY` and then be applied with a
508 /// plane the test had cleared still live.
509 #[must_use]
510 pub fn new(
511 device: &'a Device,
512 map: &'a PlanePropertyMap,
513 registry: &'a drmkit_planes::PlaneRegistry,
514 crtc_index: u32,
515 flags: AtomicCommitFlags,
516 modeset: Option<&'a Modeset<'a>>,
517 ) -> Self {
518 Self {
519 device,
520 map,
521 candidates: registry
522 .force_disable_candidates(crtc_index)
523 .map(|plane| plane.id)
524 .collect(),
525 flags,
526 modeset,
527 commits: 0,
528 extra: None,
529 registry,
530 }
531 }
532}
533
534impl TestCommitter for DeviceCommitter<'_> {
535 fn lit_planes(&mut self, crtc_id: u32) -> Option<Vec<u32>> {
536 let mut store = drmkit_core::PropertyStore::new();
537 let mut lit = Vec::new();
538 for &plane_id in &self.candidates {
539 store
540 .cache_properties(self.device, plane_id, drmkit_core::ObjectType::Plane)
541 .ok()?;
542 let value = |name| store.property_value(plane_id, name).ok();
543 if value("FB_ID").unwrap_or(0) != 0 && value("CRTC_ID") == Some(u64::from(crtc_id)) {
544 lit.push(plane_id);
545 }
546 }
547 Some(lit)
548 }
549
550 fn test_assignment(&mut self, assignment: &[(u32, LayerRef<'_>)]) -> Result<(), TestFailure> {
551 self.commits += 1;
552 let mut request = drmkit_core::AtomicRequest::new();
553
554 // A test request is plane-only otherwise, and while the CRTC is still
555 // inactive the kernel rejects every plane bound to it with EINVAL. The
556 // search would then find nothing placeable and composite the whole
557 // scene -- on the first frame, on a machine that boots without a DRM
558 // fbdev client to leave the CRTC lit for it.
559 if let Some(modeset) = self.modeset
560 && modeset.emit(&mut request).is_err()
561 {
562 return Err(TestFailure::Rejected);
563 }
564
565 // Disable first: a plane inheriting stale state from the previous
566 // commit is what makes an otherwise-valid migration look invalid.
567 let extra_plane = self.extra.as_ref().map(|(plane_id, _)| *plane_id);
568 for plane_id in &self.candidates {
569 if assignment.iter().all(|(assigned, _)| assigned != plane_id)
570 && extra_plane != Some(*plane_id)
571 && emit_disable(&mut request, self.map, *plane_id).is_err()
572 {
573 return Err(TestFailure::Rejected);
574 }
575 }
576 // Full writes, deliberately. A search commit proposes a configuration
577 // the kernel has not seen; diffing it against what the kernel last
578 // took would test a request that is only valid as a delta from the
579 // current state, while the assignment being tried is not that state.
580 // Upstream splits the same way -- the search path and the apply path
581 // are different functions there for this reason.
582 //
583 // zpos goes out as the frame will write it: stacked over exactly the
584 // planes this test arms, canvas included, so the kernel is asked
585 // about the stack it will be handed.
586 let mut armed: Vec<(u32, Option<u64>)> = assignment
587 .iter()
588 .map(|(plane_id, layer)| (*plane_id, layer.layer.property(PropTag::Zpos)))
589 .collect();
590 if let Some((plane_id, layer)) = &self.extra {
591 armed.push((*plane_id, layer.property(PropTag::Zpos)));
592 }
593 let stacked = drmkit_planes::stacked_zpos(self.registry, &armed);
594 for (plane_id, layer) in assignment {
595 let layer = with_stacked_zpos(layer.layer, *plane_id, &stacked);
596 if emit_layer(&mut request, self.map, *plane_id, &layer, None).is_err() {
597 return Err(TestFailure::Rejected);
598 }
599 }
600 // The canvas, when the caller has told us there will be one. It is
601 // part of the frame the kernel will be asked to take, so it is part of
602 // the frame the kernel is asked about.
603 if let Some((plane_id, layer)) = &self.extra {
604 let layer = with_stacked_zpos(layer, *plane_id, &stacked);
605 if emit_layer(&mut request, self.map, *plane_id, &layer, None).is_err() {
606 return Err(TestFailure::Rejected);
607 }
608 }
609
610 // A request that carries the modeset brings the CRTC up, which the
611 // kernel refuses to validate without ALLOW_MODESET ("requires full
612 // modeset") wherever the CRTC was off or in another mode. Every test
613 // came back EINVAL there and nothing was ever placed (P-41, found on
614 // the SA8155P's DP-4). Upstream tests the first frame under the same
615 // allowance.
616 let flags = if self.modeset.is_some() {
617 self.flags | AtomicCommitFlags::ALLOW_MODESET
618 } else {
619 self.flags
620 };
621 match request.test(self.device, flags) {
622 Ok(()) => Ok(()),
623 Err(CoreError::NotMaster) => Err(TestFailure::NotMaster),
624 Err(_) => Err(TestFailure::Rejected),
625 }
626 }
627
628 fn set_extra_plane(&mut self, plane_id: u32, layer: drmkit_planes::Layer) {
629 self.extra = Some((plane_id, layer));
630 }
631
632 fn clear_extra_plane(&mut self) {
633 self.extra = None;
634 }
635}
636
637/// Classify a real commit's outcome for [`FrameLifecycle`](crate::FrameLifecycle).
638#[must_use]
639pub fn classify(result: &Result<(), CoreError>) -> KernelResult {
640 match result {
641 Ok(()) => KernelResult::Ok,
642 Err(CoreError::NotMaster) => KernelResult::NotMaster,
643 Err(_) => KernelResult::Rejected,
644 }
645}
646
647/// The CRTC and connector writes that bring a display up.
648///
649/// Plane properties alone do not light a screen. Until the CRTC has a mode and
650/// is `ACTIVE`, and some connector routes to it, a commit that binds planes to
651/// that CRTC is rejected -- and the planes have nowhere to scan out to even if
652/// it were not. Upstream folds these writes into the first commit after
653/// `create()` or `rebind()` and ORs in `ALLOW_MODESET` to let them through;
654/// this is the same set.
655///
656/// Easy to leave out and not notice: most desktops run a DRM fbdev client that
657/// re-enables the CRTC as soon as the previous master drops it, so a scene that
658/// never writes these still commits successfully on a developer's machine. It
659/// fails on a system that boots without one.
660pub struct Modeset<'a> {
661 crtc_id: u32,
662 connector_id: u32,
663 crtc_active: u32,
664 crtc_mode_id: u32,
665 connector_crtc_id: u32,
666 mode_blob: PropertyBlob<'a>,
667}
668
669impl<'a> Modeset<'a> {
670 /// Resolve the three property ids and stage `mode` as a blob.
671 ///
672 /// The blob is created once and reused for every commit: it describes the
673 /// mode, which does not change while the scene is bound to this CRTC.
674 ///
675 /// # Errors
676 ///
677 /// [`CoreError`] if a property is missing -- every atomic driver exposes
678 /// all three, so an absent one means this is not an atomic-capable device
679 /// -- or if the kernel refuses the blob.
680 pub fn learn(
681 device: &'a Device,
682 crtc_id: u32,
683 connector_id: u32,
684 mode: &Mode,
685 ) -> Result<Self, CoreError> {
686 let mut store = PropertyStore::new();
687 store.cache_properties(device, crtc_id, ObjectType::Crtc)?;
688 store.cache_properties(device, connector_id, ObjectType::Connector)?;
689
690 let crtc_active = store.property_id(crtc_id, "ACTIVE")?;
691 let crtc_mode_id = store.property_id(crtc_id, "MODE_ID")?;
692 let connector_crtc_id = store.property_id(connector_id, "CRTC_ID")?;
693
694 let mode_blob = device.create_property_blob(mode)?;
695
696 Ok(Self {
697 crtc_id,
698 connector_id,
699 crtc_active,
700 crtc_mode_id,
701 connector_crtc_id,
702 mode_blob,
703 })
704 }
705
706 /// Emit the modeset writes. Returns how many properties were written.
707 ///
708 /// # Errors
709 ///
710 /// [`CoreError`] if a property write is rejected before the commit.
711 pub fn emit(&self, request: &mut drmkit_core::AtomicRequest) -> Result<usize, CoreError> {
712 request.add_property(self.crtc_id, self.crtc_mode_id, self.mode_blob.id())?;
713 request.add_property(self.crtc_id, self.crtc_active, 1)?;
714 request.add_property(
715 self.connector_id,
716 self.connector_crtc_id,
717 u64::from(self.crtc_id),
718 )?;
719 Ok(3)
720 }
721
722 /// The blob id backing `MODE_ID`.
723 #[must_use]
724 pub const fn mode_blob(&self) -> u64 {
725 self.mode_blob.id()
726 }
727}
728
729/// Emit a built frame's apply commit.
730///
731/// Writes, in order: the modeset properties if `modeset` is given, then
732/// a disable for every candidate plane the frame did not use, then each
733/// assigned layer. The disables come before the applies for the reason
734/// [`emit_disable`] gives.
735///
736/// Returns how many properties were written.
737///
738/// # Errors
739///
740/// [`CoreError`] if a property write is rejected before the commit.
741/// # The modeset must outlive the commit, not this call
742///
743/// A [`Modeset`] holds a [`PropertyBlob`] that is
744/// destroyed when it drops, and the request carries only the blob's **id**.
745/// Taking `&Modeset` means it is alive *here*; it says nothing about whether
746/// it is alive at `commit`. A modeset built inside a loop that emits several
747/// scenes into one request, and dropped at the end of its iteration, leaves
748/// the request naming a blob the kernel has already freed — and the commit is
749/// refused with `EINVAL`, naming nothing useful.
750///
751/// Build them all first, into something that outlives the commit:
752///
753/// ```ignore
754/// let modesets: Vec<Modeset<'_>> = slots.iter().map(|s| Modeset::learn(..)).collect::<Result<_, _>>()?;
755/// for (slot, modeset) in slots.iter().zip(&modesets) {
756/// emit_frame(&mut request, &map, &mut build, Some(modeset))?;
757/// }
758/// request.commit(device, flags)?; // every blob still alive
759/// ```
760pub fn emit_frame(
761 request: &mut drmkit_core::AtomicRequest,
762 map: &PlanePropertyMap,
763 build: &mut crate::FrameBuild,
764 modeset: Option<&Modeset<'_>>,
765) -> Result<usize, CoreError> {
766 emit_frame_damaged(request, map, build, modeset, None).map(|(written, _)| written)
767}
768
769/// [`emit_frame`], plus the `FB_DAMAGE_CLIPS` blobs partial repaint needs.
770///
771/// `device` is what creates the blobs, and is separate from everything else
772/// because it is the only part of emission that allocates kernel objects.
773/// Pass `None` to emit no damage at all — every frame then repaints whole,
774/// which is correct, just more bandwidth than the sources asked for.
775///
776/// # The returned blobs must outlive the commit
777///
778/// Each is a kernel object destroyed when it drops, and the request holds only
779/// its id. Dropping them before `commit` leaves the request pointing at blobs
780/// the kernel has already freed. Bind them to a local that lives across the
781/// commit:
782///
783/// ```ignore
784/// let (_, _blobs) = emit_frame_damaged(&mut request, &map, &mut build, None, Some(device))?;
785/// request.commit(device, flags)?;
786/// ```
787///
788/// # Errors
789///
790/// [`CoreError`] if a property write is rejected or a blob cannot be created.
791pub fn emit_frame_damaged<'a>(
792 request: &mut drmkit_core::AtomicRequest,
793 map: &PlanePropertyMap,
794 build: &mut crate::FrameBuild,
795 modeset: Option<&Modeset<'_>>,
796 device: Option<&'a Device>,
797) -> Result<(usize, Vec<drmkit_core::PropertyBlob<'a>>), CoreError> {
798 let mut written = 0;
799 let mut fbs = 0;
800 let mut damaged = 0;
801 let mut blobs = Vec::new();
802 if let Some(modeset) = modeset {
803 written += modeset.emit(request)?;
804 }
805 for plane_id in build.disables() {
806 written += emit_disable(request, map, *plane_id)?;
807 }
808 for entry in build.plan() {
809 let bag = match entry.zpos {
810 Some(zpos) if entry.layer.property(PropTag::Zpos) != Some(zpos) => {
811 let mut bag = entry.layer.clone();
812 bag.set_property(PropTag::Zpos, zpos);
813 std::borrow::Cow::Owned(bag)
814 }
815 _ => std::borrow::Cow::Borrowed(&entry.layer),
816 };
817 let layer = emit_layer(request, map, entry.plane_id, &bag, entry.baseline.as_ref())?;
818 written += layer.properties;
819 fbs += layer.framebuffers;
820 written += emit_color_props(
821 request,
822 map,
823 entry.plane_id,
824 entry.color,
825 build.restates_sticky(),
826 )?;
827
828 if let Some(device) = device
829 && let Some(blob) = emit_damage(request, map, device, entry.plane_id, &entry.damage)?
830 {
831 blobs.push(blob);
832 written += 1;
833 damaged += 1;
834 }
835 }
836 // Recorded rather than only returned: the caller has no way to put these
837 // back into the report, and a discarded return is how `properties_written`
838 // and `fbs_attached` came to be documented counters that were always zero.
839 build.record_emission(written, fbs, damaged);
840 Ok((written, blobs))
841}
842
843/// The most damage rectangles one layer may report before its frame is
844/// treated as whole. Upstream's `k_max_damage_rects`: past this, a damage blob
845/// buys the driver little over a full repaint, and the per-commit union stays
846/// small.
847pub(crate) const MAX_DAMAGE_RECTS: usize = 16;
848
849/// Bound a source's damage to what the kernel will take, as upstream's
850/// `arm_layer_damage_clips` does before it builds a blob.
851///
852/// More than [`MAX_DAMAGE_RECTS`] rectangles is whole-frame. Each rectangle is
853/// clipped to the `width` by `height` buffer, and one with nothing left is
854/// dropped. A source is not trusted to stay inside its own buffer, and a
855/// clip that strays outside it can make the driver refuse the whole commit
856/// rather than the one rectangle. An empty result is whole-frame too, which
857/// is what an empty list already means.
858pub(crate) fn clip_damage(
859 damage: &[crate::DamageRect],
860 width: u32,
861 height: u32,
862) -> Vec<crate::DamageRect> {
863 if damage.len() > MAX_DAMAGE_RECTS {
864 return Vec::new();
865 }
866 damage
867 .iter()
868 .filter_map(|rect| {
869 let x1 = i64::from(rect.x).max(0);
870 let y1 = i64::from(rect.y).max(0);
871 let x2 = (i64::from(rect.x) + i64::from(rect.w)).min(i64::from(width));
872 let y2 = (i64::from(rect.y) + i64::from(rect.h)).min(i64::from(height));
873 if x2 <= x1 || y2 <= y1 {
874 return None;
875 }
876 Some(crate::DamageRect {
877 x: i32::try_from(x1).ok()?,
878 y: i32::try_from(y1).ok()?,
879 w: u32::try_from(x2 - x1).ok()?,
880 h: u32::try_from(y2 - y1).ok()?,
881 })
882 })
883 .collect()
884}
885
886/// Convert damage rectangles into the `struct drm_mode_rect` array the kernel
887/// reads out of an `FB_DAMAGE_CLIPS` blob.
888///
889/// `drm_mode_rect` is four `__s32`: `x1, y1, x2, y2` — **corners, not
890/// origin-and-size**, and the second corner is exclusive. Writing a width
891/// where `x2` belongs describes a rectangle in the wrong place *and* the wrong
892/// size, and the driver repaints that instead — leaving the region that
893/// actually changed stale, which looks exactly like a producer that failed to
894/// draw.
895///
896/// The addition saturates rather than wrapping. A rectangle whose far corner
897/// does not fit an `i32` is nothing a real framebuffer contains, and clamping
898/// over-reports where wrapping would describe a rectangle with its corners the
899/// wrong way round.
900pub(crate) fn damage_rects(damage: &[crate::DamageRect]) -> Vec<[i32; 4]> {
901 damage
902 .iter()
903 .map(|rect| {
904 [
905 rect.x,
906 rect.y,
907 rect.x.saturating_add(rect.w.cast_signed()),
908 rect.y.saturating_add(rect.h.cast_signed()),
909 ]
910 })
911 .collect()
912}
913
914/// Stage one layer's damage as a `FB_DAMAGE_CLIPS` blob and point the plane at
915/// it.
916///
917/// `None` when there is nothing to say: the plane has no such property, or the
918/// layer reported no damage. **An empty list is not an empty blob.** Empty
919/// damage means *the whole frame changed*, and the way to say that is to write
920/// nothing -- a zero-rectangle blob tells the driver nothing changed, and the
921/// frame is never repainted.
922fn emit_damage<'a>(
923 request: &mut drmkit_core::AtomicRequest,
924 map: &PlanePropertyMap,
925 device: &'a Device,
926 plane_id: u32,
927 damage: &[crate::DamageRect],
928) -> Result<Option<drmkit_core::PropertyBlob<'a>>, CoreError> {
929 let Some(property_id) = map.damage.get(&plane_id).copied() else {
930 return Ok(None);
931 };
932 if damage.is_empty() {
933 return Ok(None);
934 }
935
936 let rects = damage_rects(damage);
937 let blob = device.create_property_blob(rects.as_slice())?;
938 request.add_property(plane_id, property_id, blob.id())?;
939 Ok(Some(blob))
940}
941
942/// Emit the colorimetry a plan entry asks of its plane, where the plane has it.
943///
944/// Exempt from the diff that governs everything else in [`emit_layer`]. These
945/// properties are sticky across clients: whatever the last compositor left is
946/// what this one inherits, and inheriting the wrong YCbCr matrix or range
947/// tints every YUV layer -- cyan or magenta, depending which is wrong. So each
948/// is written whenever it differs from what this plane's last real commit
949/// carried, and always when `restate` says the scene does not know what the
950/// kernel holds. `want` is `None` for the composition canvas, which writes
951/// neither (see [`PlanePlan::color`](crate::PlanePlan::color)).
952fn emit_color_props(
953 request: &mut drmkit_core::AtomicRequest,
954 map: &PlanePropertyMap,
955 plane_id: u32,
956 want: Option<crate::Ycbcr>,
957 restate: bool,
958) -> Result<usize, CoreError> {
959 let (Some(want), Some(props)) = (want, map.color.get(&plane_id)) else {
960 return Ok(0);
961 };
962 let resolved = props.resolve(want);
963 let committed = map
964 .color_committed
965 .get(&plane_id)
966 .filter(|_| !restate)
967 .copied()
968 .unwrap_or_default();
969 let mut written = 0;
970 for (now, before) in [
971 (resolved.encoding, committed.encoding),
972 (resolved.range, committed.range),
973 ] {
974 if let Some((property_id, value)) = now
975 && now != before
976 {
977 request.add_property(plane_id, property_id, value)?;
978 written += 1;
979 }
980 }
981 Ok(written)
982}
983
984/// What to do with a layer's acquire fence, given what its plane can take.
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986#[non_exhaustive]
987pub enum FenceAction {
988 /// Hand the descriptor to KMS as `IN_FENCE_FD`.
989 Arm {
990 /// The plane's `IN_FENCE_FD` property.
991 property_id: u32,
992 },
993 /// The plane cannot take one, so the caller has to wait before committing.
994 CpuWait,
995}
996
997/// Decide between the two, from whether the plane exposes a writable
998/// `IN_FENCE_FD`.
999///
1000/// Split out so the decision is testable without a device: the fallback is the
1001/// branch that matters and the one a card that advertises `IN_FENCE_FD` on
1002/// every plane -- vkms among them -- can never reach.
1003#[must_use]
1004pub const fn fence_action(property_id: Option<u32>) -> FenceAction {
1005 match property_id {
1006 Some(property_id) => FenceAction::Arm { property_id },
1007 None => FenceAction::CpuWait,
1008 }
1009}
1010
1011/// Give each fenced layer's fence to its plane, or wait on it here.
1012///
1013/// A buffer's `acquire_fence` says its pixels are not valid yet. Where the
1014/// assigned plane exposes `IN_FENCE_FD` the descriptor goes to KMS and the
1015/// kernel holds scanout until it signals. Where it does not, there is nowhere
1016/// to put it, so the wait happens here instead -- on the CPU, before the
1017/// commit, which is slower but is the difference between a stalled frame and a
1018/// half-rendered one reaching the screen.
1019///
1020/// This runs after allocation on purpose. Until a layer has a plane there is
1021/// nothing to ask about `IN_FENCE_FD`, which is why the fence cannot simply be
1022/// lowered into the property bag with everything else.
1023///
1024/// `real` gates the CPU wait only: a `TEST_ONLY` commit never scans out, so
1025/// blocking on a fence for one would stall the search for no reason. The
1026/// property write is not gated -- a test should carry what the apply will.
1027///
1028/// # Errors
1029///
1030/// [`CoreError`] if a property write is rejected before the commit.
1031pub fn arm_acquire_fences(
1032 build: &mut crate::FrameBuild,
1033 request: &mut drmkit_core::AtomicRequest,
1034 map: &PlanePropertyMap,
1035 real: bool,
1036) -> Result<(), CoreError> {
1037 let mut armed = 0;
1038 let mut waits = 0;
1039
1040 // Driven from the acquisitions rather than the plan, because the fence
1041 // itself lives there. Carrying a bare descriptor on the plan instead would
1042 // mean reconstructing a borrow from an integer, and the whole reason this
1043 // is safe is that the buffer -- and so the fence -- is owned by the frame
1044 // being built.
1045 for acquisition in &build.acquisitions {
1046 let Some(fence) = acquisition.buffer.acquire_fence.as_ref() else {
1047 continue;
1048 };
1049 let Some(fd) = drmkit_sync::SyncFence::as_fd(fence) else {
1050 continue;
1051 };
1052 // A layer with no plane was composited or dropped. There is nothing to
1053 // arm, and the buffer's own lifecycle still owns the fence.
1054 let Some(entry) = build
1055 .plan
1056 .iter()
1057 .find(|entry| entry.layer_id == acquisition.layer)
1058 else {
1059 continue;
1060 };
1061
1062 match fence_action(map.property_id(entry.plane_id, PropTag::InFenceFd)) {
1063 FenceAction::Arm { property_id } => {
1064 let raw = std::os::fd::AsRawFd::as_raw_fd(&fd);
1065 request.add_property(
1066 entry.plane_id,
1067 property_id,
1068 u64::from(raw.cast_unsigned()),
1069 )?;
1070 armed += 1;
1071 }
1072 FenceAction::CpuWait => {
1073 if !real {
1074 continue;
1075 }
1076 waits += 1;
1077 // Reported, not fatal. The producer may never signal this
1078 // fence, and refusing to commit would wedge the display on one
1079 // bad frame rather than showing it.
1080 if let Err(error) = fence.wait(std::time::Duration::from_secs(1)) {
1081 drmkit_log::log_warn!("acquire-fence CPU wait failed: {error}");
1082 }
1083 }
1084 }
1085 }
1086
1087 build.note_fences(armed, waits);
1088 Ok(())
1089}