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. 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}