StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::sync::{Arc, Weak}; |
| 2 | |
| 3 | use crate::image_cache::StaticImage; |
| 4 | |
| 5 | /// An opaque identifier for a texture from which we can render an image. |
| 6 | /// |
| 7 | /// This *MUST* only be used within a single frame, and is not safe to use |
| 8 | /// across frames. |
| 9 | #[derive(Copy, Clone)] |
| 10 | pub struct TextureCacheIndex(usize); |
| 11 | |
| 12 | pub struct TextureInfo<T> { |
| 13 | /// The actual information about the texture. |
| 14 | inner: T, |
| 15 | |
| 16 | /// The backing asset for the texture. |
| 17 | asset: Weak<StaticImage>, |
| 18 | |
| 19 | /// The index of the last frame on which this texture was accessed. This |
| 20 | /// is used to know when a texture has gone "stale" and can be dropped from |
| 21 | /// the cache. |
| 22 | last_accessed_frame: usize, |
| 23 | } |
| 24 | |
| 25 | /// A simple cache for textures from which we can render images. |
| 26 | pub struct TextureCache<T> { |
| 27 | textures: Vec<TextureInfo<T>>, |
| 28 | |
| 29 | /// The index of the last frame that was rendered. |
| 30 | frame_index: usize, |
| 31 | } |
| 32 | |
| 33 | impl<T> TextureCache<T> { |
| 34 | /// The maximum number of frames that a texture can go unused before it |
| 35 | /// gets dropped from the cache. |
| 36 | const MAX_UNUSED_FRAMES: usize = 10; |
| 37 | |
| 38 | pub fn new() -> Self { |
| 39 | Self { |
| 40 | textures: Default::default(), |
| 41 | frame_index: 0, |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | pub fn get(&self, texture_id: TextureCacheIndex) -> Option<&T> { |
| 46 | self.textures.get(texture_id.0).map(|info| &info.inner) |
| 47 | } |
| 48 | |
| 49 | pub fn get_or_insert_by_asset( |
| 50 | &mut self, |
| 51 | asset: &Arc<StaticImage>, |
| 52 | texinfo_func: impl FnOnce(&Arc<StaticImage>) -> T, |
| 53 | ) -> (TextureCacheIndex, &T) { |
| 54 | let mut found = None; |
| 55 | let weak_asset = Arc::downgrade(asset); |
| 56 | for (index, texture) in self.textures.iter().enumerate() { |
| 57 | if texture.asset.ptr_eq(&weak_asset) { |
| 58 | found = Some(index); |
| 59 | } |
| 60 | } |
| 61 | let index = match found { |
| 62 | Some(index) => index, |
| 63 | None => { |
| 64 | self.textures.push(TextureInfo { |
| 65 | inner: texinfo_func(asset), |
| 66 | asset: weak_asset.clone(), |
| 67 | last_accessed_frame: self.frame_index, |
| 68 | }); |
| 69 | self.textures.len() - 1 |
| 70 | } |
| 71 | }; |
| 72 | |
| 73 | // This array lookup is safe, as we either found the texture in the |
| 74 | // cache or we inserted a new one and returned its index. |
| 75 | self.textures[index].last_accessed_frame = self.frame_index; |
| 76 | |
| 77 | (TextureCacheIndex(index), &self.textures[index].inner) |
| 78 | } |
| 79 | |
| 80 | /// Updates the texture cache at the end of a frame. |
| 81 | /// |
| 82 | /// This should be called at the end of every frame to ensure that stale |
| 83 | /// texture resources get cleaned up. |
| 84 | pub fn end_frame(&mut self) { |
| 85 | // Drop any textures which are no longer referenced by the asset cache |
| 86 | // or have not been rendered in the last MAX_UNUSED_FRAMES frames. |
| 87 | self.textures.retain(|texture| { |
| 88 | texture.asset.strong_count() > 0 |
| 89 | && self.frame_index - texture.last_accessed_frame < Self::MAX_UNUSED_FRAMES |
| 90 | }); |
| 91 | |
| 92 | self.frame_index += 1; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | impl<T> Default for TextureCache<T> { |
| 97 | fn default() -> Self { |
| 98 | Self::new() |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | #[cfg(test)] |
| 103 | #[path = "texture_cache_tests.rs"] |
| 104 | mod tests; |
| 105 |