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