Skip to main content

concinnity_core/blob/
cache.rs

1// The cache segment's metadata: an index of regenerable artifacts, addressed by
2// producer and key rather than by filename. A segment is one container holding
3// every entry, so the index is what keeps two producers -- and two adapters of
4// one producer -- out of each other's bytes.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9use serde::{Deserialize, Serialize};
10
11use crate::blob::kind::BlobKind;
12
13/// The four magic bytes a cache segment starts with, the [`BlobKind`] magic of
14/// [`CacheMeta`].
15pub const CACHE_MAGIC: [u8; 4] = *b"CNC\0";
16
17/// The validity token a cache segment's header carries: the layout version of
18/// [`CacheMeta`] and of the payload addressing its entries use. postcard cannot
19/// tell a layout change from valid bytes, so a segment stamped with another
20/// value is regenerated whole rather than decoded.
21pub const CACHE_SEGMENT_VERSION: u32 = 2;
22
23/// Which producer an entry belongs to.
24///
25/// The discriminant is part of the segment bytes and
26/// [`CACHE_SEGMENT_VERSION`] guards them, so a new producer takes the next
27/// value rather than reshaping the index.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum CacheEntryKind {
30    /// A driver pipeline blob: a serialized `VkPipelineCache` or a D3D12
31    /// pipeline library, machine code for the one adapter its key names.
32    Pipeline,
33    /// A compiled shader binary (backend IR: DXBC, SPIR-V, a metallib), keyed
34    /// by a digest of everything the compile was a function of.
35    Shader,
36    /// A compiled asset payload, keyed by a digest of the args and source
37    /// files the compile read.
38    Payload,
39    /// The asset entries a scene import expands to, keyed by a digest of the
40    /// source file and the import options.
41    Expansion,
42    /// A baked asset preview: a PNG keyed by a digest of what it depicts, plus
43    /// the one entry holding the asset-name-to-key map over the whole set.
44    Thumbnail,
45}
46
47/// One cached artifact: which producer owns it, what it is valid for, and where
48/// its bytes sit in the payload section.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CacheEntry {
51    /// The producer that wrote the entry.
52    pub kind: CacheEntryKind,
53    /// The producer's key, which doubles as the entry's validity: a lookup
54    /// naming another key misses. A pipeline blob keys on the adapter it is
55    /// machine code for; a producer whose artifacts are valid anywhere keys on
56    /// content alone.
57    pub key: String,
58    /// Byte offset of the entry's bytes within the payload section.
59    pub offset: u64,
60    /// Byte length of the entry's bytes.
61    pub len: u64,
62}
63
64/// A cache segment's metadata block: the index of everything its payload
65/// section holds.
66#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
67pub struct CacheMeta {
68    /// Names the host shader toolchain that produced the segment's entries,
69    /// empty when nothing stamped it. An entry is a function of its source
70    /// rather than of what compiled it, so an external compiler upgrade moves
71    /// no key: a segment naming another toolchain is discarded instead.
72    pub toolchain: String,
73    /// The segment's entries, in payload order.
74    pub entries: Vec<CacheEntry>,
75}
76
77impl CacheMeta {
78    /// The entry `kind` stored under `key`, if the segment holds one.
79    pub fn find(&self, kind: CacheEntryKind, key: &str) -> Option<&CacheEntry> {
80        self.entries.iter().find(|e| e.kind == kind && e.key == key)
81    }
82}
83
84impl BlobKind for CacheMeta {
85    const MAGIC: [u8; 4] = CACHE_MAGIC;
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::blob::schema::BlobMeta;
92    use crate::blob::{BlobError, encode_cnb, parse_cnb};
93    use alloc::vec;
94
95    fn entry(kind: CacheEntryKind, key: &str, offset: u64, len: u64) -> CacheEntry {
96        CacheEntry {
97            kind,
98            key: String::from(key),
99            offset,
100            len,
101        }
102    }
103
104    #[test]
105    fn a_segment_round_trips_its_index_and_payload() {
106        let meta = CacheMeta {
107            toolchain: String::from("slang 2026.1"),
108            entries: vec![
109                entry(CacheEntryKind::Pipeline, "vk-aa", 0, 3),
110                entry(CacheEntryKind::Shader, "deadbeef", 3, 2),
111            ],
112        };
113        let image = encode_cnb(CACHE_SEGMENT_VERSION, &meta, &[1, 2, 3, 4, 5]).unwrap();
114        let (got, payload_start) =
115            parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image).expect("parse");
116        assert_eq!(got, meta);
117        assert_eq!(&image[payload_start..], &[1, 2, 3, 4, 5]);
118    }
119
120    // Two kinds of container must never be read as each other, which is the
121    // whole reason the magic hangs off the meta type.
122    #[test]
123    fn a_world_blob_is_not_a_cache_segment() {
124        assert_ne!(CacheMeta::MAGIC, BlobMeta::MAGIC);
125        let world = encode_cnb(CACHE_SEGMENT_VERSION, &BlobMeta::default(), &[]).unwrap();
126        assert_eq!(
127            parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &world),
128            Err(BlobError::BadMagic)
129        );
130    }
131
132    // A segment written under another index layout is not decoded into a
133    // plausible-looking index; it is rejected so the caller regenerates it.
134    #[test]
135    fn a_segment_of_another_version_is_rejected() {
136        let image = encode_cnb(CACHE_SEGMENT_VERSION - 1, &CacheMeta::default(), &[]).unwrap();
137        assert_eq!(
138            parse_cnb::<CacheMeta>(CACHE_SEGMENT_VERSION, &image),
139            Err(BlobError::ValidityMismatch(CACHE_SEGMENT_VERSION - 1))
140        );
141    }
142
143    // The build segment holds its own kinds in the same index, which is what
144    // keeps a payload and an expansion that hash alike out of each other's
145    // bytes now that no source hash separates their key spaces.
146    #[test]
147    fn a_kind_separates_two_entries_sharing_one_key() {
148        let meta = CacheMeta {
149            toolchain: String::new(),
150            entries: vec![
151                entry(CacheEntryKind::Payload, "cafe", 0, 1),
152                entry(CacheEntryKind::Expansion, "cafe", 1, 1),
153            ],
154        };
155        assert_eq!(
156            meta.find(CacheEntryKind::Payload, "cafe"),
157            Some(&meta.entries[0])
158        );
159        assert_eq!(
160            meta.find(CacheEntryKind::Expansion, "cafe"),
161            Some(&meta.entries[1])
162        );
163    }
164
165    #[test]
166    fn lookup_matches_on_both_kind_and_key() {
167        let meta = CacheMeta {
168            toolchain: String::new(),
169            entries: vec![entry(CacheEntryKind::Pipeline, "vk-aa", 0, 3)],
170        };
171        assert_eq!(
172            meta.find(CacheEntryKind::Pipeline, "vk-aa"),
173            Some(&meta.entries[0])
174        );
175        assert_eq!(meta.find(CacheEntryKind::Pipeline, "vk-bb"), None);
176        // Two producers share one index, so a key must not match across kinds.
177        assert_eq!(meta.find(CacheEntryKind::Shader, "vk-aa"), None);
178    }
179}