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}