Skip to main content

cvkg_render_native/
asset_manager.rs

1type AssetCacheMap =
2    std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>;
3
4/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
5///
6/// The cache is read on every render frame (lock-free via `ArcSwap::load()`) but written
7/// at most once per URL after disk I/O completes. `rcu()` atomically inserts the result
8/// without blocking concurrent render-loop readers.
9pub struct NativeAssetManager {
10    cache: std::sync::Arc<arc_swap::ArcSwap<AssetCacheMap>>,
11}
12
13impl Default for NativeAssetManager {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl NativeAssetManager {
20    /// Create a new, empty NativeAssetManager.
21    pub fn new() -> Self {
22        Self {
23            cache: std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
24                std::collections::HashMap::new(),
25            )),
26        }
27    }
28}
29
30impl cvkg_core::AssetManager for NativeAssetManager {
31    /// Return the cached asset state for `url`.
32    ///
33    /// Fast path: lock-free snapshot read via `ArcSwap::load()`.
34    /// Slow path (cache miss): atomically insert a Loading sentinel via `rcu()`,
35    /// then spawn a background thread for I/O. The `rcu()` closure may execute
36    /// more than once under contention, so `already_tracked` is determined by
37    /// whether the closure actually inserted the Loading entry (detected by checking
38    /// the returned map). This prevents duplicate I/O threads for the same URL.
39    ///
40    /// FIX #5: The previous implementation set `already_tracked` inside the `rcu`
41    /// closure body, which is incorrect because `rcu` retries the closure on
42    /// contention -- the bool would reflect only the last execution. The fix uses
43    /// the fast-path check result plus the atomic `rcu` insertion to determine
44    /// whether a thread must be spawned, making the logic correct under concurrency.
45    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
46        if let Some(state) = self.cache.load().get(url) {
47            return state.clone();
48        }
49
50        let cache = self.cache.clone();
51        let key = url.to_string();
52
53        let mut we_inserted = false;
54        self.cache.rcu(|map| {
55            if map.contains_key(&key) {
56                (**map).clone()
57            } else {
58                we_inserted = true;
59                let mut m = (**map).clone();
60                m.insert(key.clone(), cvkg_core::AssetState::Loading);
61                m
62            }
63        });
64
65        if we_inserted {
66            let cache_inner = cache.clone();
67            let key_inner = key.clone();
68
69            std::thread::spawn(move || {
70                tracing::debug!("[Native] Asynchronously loading asset: {}", key_inner);
71                let result = match std::fs::read(&key_inner) {
72                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
73                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
74                };
75
76                cache_inner.rcu(move |map| {
77                    let mut m = (**map).clone();
78                    m.insert(key_inner.clone(), result.clone());
79                    m
80                });
81            });
82        }
83
84        cvkg_core::AssetState::Loading
85    }
86
87    /// Trigger a background load of `url` without waiting for the result.
88    ///
89    /// FIX #6: The previous implementation had a bare fast-path check followed
90    /// by an unconditional thread spawn, allowing two concurrent calls for the
91    /// same URL to both spawn I/O threads. Now uses the same rcu-based insertion
92    /// guard as `load_image` to ensure exactly one thread is spawned per URL.
93    fn preload_image(&self, url: &str) {
94        if self.cache.load().contains_key(url) {
95            return;
96        }
97
98        let cache = self.cache.clone();
99        let key = url.to_string();
100
101        let mut we_inserted = false;
102        self.cache.rcu(|map| {
103            if map.contains_key(&key) {
104                (**map).clone()
105            } else {
106                we_inserted = true;
107                let mut m = (**map).clone();
108                m.insert(key.clone(), cvkg_core::AssetState::Loading);
109                m
110            }
111        });
112
113        if we_inserted {
114            std::thread::spawn(move || {
115                tracing::debug!("[Native] Preloading asset: {}", key);
116                let result = match std::fs::read(&key) {
117                    Ok(data) => cvkg_core::AssetState::Ready(std::sync::Arc::new(data)),
118                    Err(e) => cvkg_core::AssetState::Error(e.to_string()),
119                };
120
121                cache.rcu(move |map| {
122                    let mut m = (**map).clone();
123                    m.insert(key.clone(), result.clone());
124                    m
125                });
126            });
127        }
128    }
129}