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}