Skip to main content

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}