drmkit_display/hdr_cache.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Per-CRTC cache of the `HDR_OUTPUT_METADATA` blob.
5//!
6//! Holds the blob most recently handed to each CRTC and dedups by content
7//! hash, so metadata that has not changed does not rebuild a kernel blob every
8//! frame.
9//!
10//! Replacing a blob does not destroy the old one straight away. Between
11//! creating the replacement and the atomic commit that repoints the connector
12//! property, the kernel is still holding the old id -- so the outgoing blob
13//! goes on a queue and [`HdrMetadataCache::acknowledge_committed`] drains it
14//! once the commit has landed, as the reference does. The display itself is
15//! safe either way -- the kernel's committed state holds its own reference to
16//! the blob a property names -- but a blob destroyed before the commit leaves
17//! nothing to fall back to if that commit is refused.
18
19use std::collections::HashMap;
20
21use drmkit_core::{Device, Result as CoreResult};
22
23use crate::hdr::{HdrBlob, HdrMetadataBlob, HdrSourceMetadata, hdr_metadata_hash};
24
25/// A cached blob and the hash of what produced it.
26#[derive(Debug)]
27struct Entry<B> {
28 blob: B,
29 hash: u64,
30}
31
32/// Per-CRTC `HDR_OUTPUT_METADATA` blobs, deduped by content.
33///
34/// Generic over the blob so the caching logic can be tested without a DRM
35/// device -- see [`HdrBlob`].
36#[derive(Debug)]
37pub struct HdrMetadataCache<B> {
38 active: HashMap<u32, Entry<B>>,
39 pending_destruction: Vec<B>,
40}
41
42impl<B> Default for HdrMetadataCache<B> {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl<B> HdrMetadataCache<B> {
49 /// An empty cache.
50 #[must_use]
51 pub fn new() -> Self {
52 Self {
53 active: HashMap::new(),
54 pending_destruction: Vec::new(),
55 }
56 }
57
58 /// Blobs waiting for a commit to land before they can be destroyed.
59 #[must_use]
60 pub fn pending_destruction_count(&self) -> usize {
61 self.pending_destruction.len()
62 }
63
64 /// Destroy everything queued for destruction.
65 ///
66 /// Call this *after* the atomic commit that references the new blob has
67 /// landed. By then the connector property has moved off every older id, so
68 /// letting them go is safe.
69 pub fn acknowledge_committed(&mut self) {
70 self.pending_destruction.clear();
71 }
72}
73
74impl<B: HdrBlob> HdrMetadataCache<B> {
75 /// The blob id currently cached for `crtc_id`, or `0` if there is none.
76 #[must_use]
77 pub fn current_blob_id(&self, crtc_id: u32) -> u32 {
78 self.active
79 .get(&crtc_id)
80 .map_or(0, |entry| entry.blob.blob_id())
81 }
82
83 /// The content hash currently cached for `crtc_id`, or `0` if there is
84 /// none.
85 ///
86 /// A real hash of exactly zero is possible and vanishingly unlikely; use
87 /// [`HdrMetadataCache::current_blob_id`] to ask whether anything is
88 /// cached.
89 #[must_use]
90 pub fn current_content_hash(&self, crtc_id: u32) -> u64 {
91 self.active.get(&crtc_id).map_or(0, |entry| entry.hash)
92 }
93
94 /// Set or clear the metadata for `crtc_id`, returning the blob id to write
95 /// to the connector's `HDR_OUTPUT_METADATA` property.
96 ///
97 /// `None` clears: returns `0` and queues any cached blob. `Some` whose
98 /// hash matches what is cached is a no-op that returns the cached id
99 /// without touching the kernel. Otherwise `factory` builds a replacement
100 /// and the outgoing blob is queued.
101 ///
102 /// The factory runs only when a new blob is actually needed, so a caller
103 /// can put the expensive part inside it.
104 ///
105 /// # Errors
106 ///
107 /// Whatever `factory` returns. A failing factory leaves the cache exactly
108 /// as it was -- the previous blob stays live and stays cached, because a
109 /// caller that ignores the error and commits anyway should show the old
110 /// metadata rather than a freed blob or none at all.
111 pub fn set_with<E, F>(
112 &mut self,
113 crtc_id: u32,
114 src: Option<&HdrSourceMetadata>,
115 factory: F,
116 ) -> Result<u32, E>
117 where
118 F: FnOnce(&HdrSourceMetadata) -> Result<B, E>,
119 {
120 let Some(src) = src else {
121 if let Some(entry) = self.active.remove(&crtc_id) {
122 self.pending_destruction.push(entry.blob);
123 }
124 return Ok(0);
125 };
126
127 let hash = hdr_metadata_hash(src);
128 if let Some(entry) = self.active.get(&crtc_id)
129 && entry.hash == hash
130 {
131 return Ok(entry.blob.blob_id());
132 }
133
134 // Built before the old entry is disturbed, so a failure here is a
135 // no-op rather than a cache that has already dropped what it had.
136 let blob = factory(src)?;
137 let blob_id = blob.blob_id();
138 if let Some(previous) = self.active.insert(crtc_id, Entry { blob, hash }) {
139 self.pending_destruction.push(previous.blob);
140 }
141 Ok(blob_id)
142 }
143
144 /// Forget every blob without asking the kernel to destroy any of them.
145 ///
146 /// For session loss -- a libseat pause revokes the descriptor, and the
147 /// kernel reclaimed every blob on it at that moment. Destroying them
148 /// through the replacement descriptor would at best fail and at worst free
149 /// an unrelated blob that was given the same id there.
150 pub fn clear_for_session_loss(&mut self) {
151 for (_, entry) in self.active.drain() {
152 entry.blob.forget();
153 }
154 for blob in self.pending_destruction.drain(..) {
155 blob.forget();
156 }
157 }
158
159 /// Destroy every blob, cached and queued alike.
160 ///
161 /// For rebinding to a different CRTC or connector on the *same* descriptor,
162 /// where nothing references the old blobs any more and holding them until
163 /// the descriptor closes would leak. Unlike
164 /// [`HdrMetadataCache::clear_for_session_loss`], this really does destroy
165 /// them, so the descriptor has to still be live.
166 pub fn flush(&mut self) {
167 self.active.clear();
168 self.pending_destruction.clear();
169 }
170}
171
172impl<'a> HdrMetadataCache<HdrMetadataBlob<'a>> {
173 /// Set or clear the metadata for `crtc_id`, building blobs on `device`.
174 ///
175 /// [`HdrMetadataCache::set_with`] with the obvious factory.
176 ///
177 /// # Errors
178 ///
179 /// [`drmkit_core::CoreError`] if the kernel refuses to create the blob.
180 pub fn set(
181 &mut self,
182 device: &'a Device,
183 crtc_id: u32,
184 src: Option<&HdrSourceMetadata>,
185 ) -> CoreResult<u32> {
186 self.set_with(crtc_id, src, |s| HdrMetadataBlob::create(device, s))
187 }
188}