Skip to main content

pfx_load/
cache.rs

1use crate::font::Font;
2use crate::image::{ColorSpace, Image};
3use crate::mesh::Mesh;
4use crate::sky::Sky;
5use crate::source::Source;
6use sha2::{Digest, Sha256};
7use std::collections::BTreeMap;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct Handle(u32);
11
12#[derive(Clone, Debug)]
13pub enum Asset {
14    Mesh(Mesh),
15    Image(Image),
16    Sky(Sky),
17    Font(Font),
18}
19
20#[derive(Clone, Copy, PartialEq, Eq)]
21enum Kind {
22    Mesh,
23    Png,
24    Rgba16 {
25        width: u32,
26        height: u32,
27        space: ColorSpace,
28    },
29    Sky,
30    Font,
31}
32
33struct Entry {
34    hash: [u8; 32],
35    kind: Kind,
36    asset: Asset,
37}
38
39pub struct Cache {
40    index: BTreeMap<[u8; 32], u32>,
41    entries: Vec<Entry>,
42}
43
44impl Cache {
45    pub fn new() -> Self {
46        Cache {
47            index: BTreeMap::new(),
48            entries: Vec::new(),
49        }
50    }
51
52    pub fn len(&self) -> usize {
53        self.entries.len()
54    }
55
56    pub fn is_empty(&self) -> bool {
57        self.entries.is_empty()
58    }
59
60    pub fn get(&self, handle: Handle) -> Option<&Asset> {
61        self.entries
62            .get(handle.0 as usize)
63            .map(|entry| &entry.asset)
64    }
65
66    pub fn digest(&self, handle: Handle) -> Option<[u8; 32]> {
67        self.entries.get(handle.0 as usize).map(|entry| entry.hash)
68    }
69
70    pub fn load_mesh(&mut self, bytes: &[u8]) -> Result<Handle, String> {
71        self.insert(bytes, Kind::Mesh, || Ok(Asset::Mesh(Mesh::parse(bytes)?)))
72    }
73
74    pub fn load_mesh_from(&mut self, source: &Source, name: &str) -> Result<Handle, String> {
75        let bytes = source.read(name)?;
76        self.insert(&bytes, Kind::Mesh, || {
77            Ok(Asset::Mesh(Mesh::parse_with(&bytes, |uri| {
78                source.read_beside(name, uri)
79            })?))
80        })
81    }
82
83    pub fn load_png(&mut self, bytes: &[u8]) -> Result<Handle, String> {
84        self.insert(bytes, Kind::Png, || Ok(Asset::Image(Image::png(bytes)?)))
85    }
86
87    pub fn load_rgba16(
88        &mut self,
89        bytes: &[u8],
90        width: u32,
91        height: u32,
92        space: ColorSpace,
93    ) -> Result<Handle, String> {
94        self.insert(
95            bytes,
96            Kind::Rgba16 {
97                width,
98                height,
99                space,
100            },
101            || Ok(Asset::Image(Image::rgba16(bytes, width, height, space)?)),
102        )
103    }
104
105    pub fn load_sky(&mut self, bytes: &[u8]) -> Result<Handle, String> {
106        self.insert(bytes, Kind::Sky, || Ok(Asset::Sky(Sky::parse(bytes)?)))
107    }
108
109    pub fn load_font(&mut self, bytes: &[u8]) -> Result<Handle, String> {
110        self.insert(bytes, Kind::Font, || Ok(Asset::Font(Font::parse(bytes)?)))
111    }
112
113    fn insert(
114        &mut self,
115        bytes: &[u8],
116        kind: Kind,
117        build: impl FnOnce() -> Result<Asset, String>,
118    ) -> Result<Handle, String> {
119        let hash = sha256(bytes);
120        if let Some(&index) = self.index.get(&hash) {
121            return if self.entries[index as usize].kind == kind {
122                Ok(Handle(index))
123            } else {
124                Err("the same bytes are already cached as a different asset".into())
125            };
126        }
127        let asset = build()?;
128        let index =
129            u32::try_from(self.entries.len()).map_err(|_| "the cache is full".to_string())?;
130        self.index.insert(hash, index);
131        self.entries.push(Entry { hash, kind, asset });
132        Ok(Handle(index))
133    }
134}
135
136impl Default for Cache {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142pub fn sha256(bytes: &[u8]) -> [u8; 32] {
143    let digest = Sha256::digest(bytes);
144    let mut hash = [0u8; 32];
145    hash.copy_from_slice(&digest);
146    hash
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::Asset;
153    use std::path::Path;
154
155    const FILES: &[(&str, &[u8])] = &[
156        ("triangle.glb", include_bytes!("../tests/data/triangle.glb")),
157        (
158            "triangle.gltf",
159            include_bytes!("../tests/data/triangle.gltf"),
160        ),
161        ("swatch8.png", include_bytes!("../tests/data/swatch8.png")),
162        ("sky.hdr", include_bytes!("../tests/data/sky.hdr")),
163        ("family.ttf", include_bytes!("../tests/data/family.ttf")),
164    ];
165
166    #[test]
167    fn the_same_bytes_return_the_same_handle() {
168        let glb = include_bytes!("../tests/data/triangle.glb");
169        let mut cache = Cache::new();
170        assert!(cache.load_mesh(b"nope").is_err());
171        assert!(cache.is_empty());
172        let first = cache.load_mesh(glb).unwrap();
173        let second = cache.load_mesh(glb).unwrap();
174        assert_eq!(first, second);
175        assert_eq!(cache.len(), 1);
176        assert_eq!(cache.digest(first), Some(sha256(glb)));
177        match cache.get(first) {
178            Some(Asset::Mesh(mesh)) => assert_eq!(mesh.materials[0].name, "ink"),
179            other => panic!("mesh, got {other:?}"),
180        }
181
182        let png = include_bytes!("../tests/data/swatch8.png");
183        let image = cache.load_png(png).unwrap();
184        assert_ne!(image, first);
185        assert!(cache.load_mesh(png).is_err());
186        assert_eq!(cache.len(), 2);
187
188        let embedded = Source::embedded(FILES);
189        let folder = Source::folder(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data"));
190        let from_binary = cache.load_mesh_from(&embedded, "triangle.glb").unwrap();
191        let from_folder = cache.load_mesh_from(&folder, "triangle.glb").unwrap();
192        assert_eq!(from_binary, first);
193        assert_eq!(from_folder, first);
194
195        let sky = cache
196            .load_sky(include_bytes!("../tests/data/sky.hdr"))
197            .unwrap();
198        let font = cache
199            .load_font(include_bytes!("../tests/data/family.ttf"))
200            .unwrap();
201        let raw = cache
202            .load_rgba16(
203                include_bytes!("../tests/data/swatch.rgba16"),
204                2,
205                2,
206                ColorSpace::Linear,
207            )
208            .unwrap();
209        assert_eq!(
210            cache
211                .load_sky(include_bytes!("../tests/data/sky.hdr"))
212                .unwrap(),
213            sky
214        );
215        assert_eq!(
216            cache
217                .load_font(include_bytes!("../tests/data/family.ttf"))
218                .unwrap(),
219            font
220        );
221        assert_eq!(
222            cache
223                .load_rgba16(
224                    include_bytes!("../tests/data/swatch.rgba16"),
225                    2,
226                    2,
227                    ColorSpace::Linear
228                )
229                .unwrap(),
230            raw
231        );
232        assert!(
233            cache
234                .load_rgba16(
235                    include_bytes!("../tests/data/swatch.rgba16"),
236                    4,
237                    1,
238                    ColorSpace::Linear
239                )
240                .is_err()
241        );
242        let sidecar = cache.load_mesh_from(&folder, "triangle.gltf").unwrap();
243        match cache.get(sidecar) {
244            Some(Asset::Mesh(mesh)) => assert_eq!(mesh.groups[0].name, "sheet"),
245            other => panic!("mesh, got {other:?}"),
246        }
247    }
248}