Skip to main content

motion_core/asset/
handle.rs

1//! Shared lazy handles, lifecycle and sticky decoding results.
2
3use super::*;
4
5/// Observable shared lifecycle; loading starts only at the first get call.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum AssetStatus {
8    /// No read or decode has begun.
9    Unloaded,
10    /// One caller is reading/decoding; others join the same result.
11    Loading,
12    /// An immutable decoded resource is available.
13    Ready,
14    /// A sticky diagnostic is available; failures are not retried implicitly.
15    Failed,
16}
17
18pub(super) struct Entry<A: Asset> {
19    pub(super) pool: ResourcePool,
20    pub(super) source: Source,
21    pub(super) context: LoadContext,
22    pub(super) result: OnceLock<Result<Arc<A>, Arc<crate::Error>>>,
23    pub(super) loading: AtomicBool,
24    pub(super) charged: AtomicUsize,
25}
26
27impl<A: Asset> Drop for Entry<A> {
28    fn drop(&mut self) {
29        self.pool
30            .0
31            .bytes
32            .fetch_sub(self.charged.load(Ordering::Relaxed), Ordering::Relaxed);
33        let key = (
34            TypeId::of::<A>(),
35            self.context.root.as_ref().clone(),
36            self.context
37                .fonts
38                .as_ref()
39                .map_or(0, crate::asset::environment::FontEnvironment::snapshot_id),
40            self.context.namespace,
41            self.source.clone(),
42        );
43        let mut cache = self
44            .pool
45            .0
46            .entries
47            .lock()
48            .unwrap_or_else(std::sync::PoisonError::into_inner);
49        if cache
50            .get(&key)
51            .is_some_and(|entry| entry.strong_count() == 0)
52        {
53            cache.remove(&key);
54        }
55    }
56}
57
58/// Cheap shared reference to a lazy resource; clones share success or failure.
59pub struct Handle<A: Asset> {
60    pub(super) entry: Arc<Entry<A>>,
61    pub(super) diagnostics: Option<RequestDiagnostics>,
62}
63
64impl<A: Asset> Clone for Handle<A> {
65    fn clone(&self) -> Self {
66        Self {
67            entry: self.entry.clone(),
68            diagnostics: self.diagnostics.clone(),
69        }
70    }
71}
72
73impl<A: Asset> std::fmt::Debug for Handle<A> {
74    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        formatter
76            .debug_struct("Handle")
77            .field("kind", &A::KIND)
78            .field("source", &self.entry.source)
79            .field("status", &self.status())
80            .finish()
81    }
82}
83
84impl<A: Asset> Handle<A> {
85    /// Current state without triggering a file read or decode.
86    pub fn status(&self) -> AssetStatus {
87        match self.entry.result.get() {
88            Some(Ok(_)) => AssetStatus::Ready,
89            Some(Err(_)) => AssetStatus::Failed,
90            None if self.entry.loading.load(Ordering::Acquire) => AssetStatus::Loading,
91            None => AssetStatus::Unloaded,
92        }
93    }
94
95    /// Sticky original diagnostic, without triggering loading.
96    pub fn error(&self) -> Option<Arc<crate::Error>> {
97        self.entry
98            .result
99            .get()
100            .and_then(|result| result.as_ref().err().cloned())
101    }
102
103    /// Resolves this handle synchronously, joining one concurrent decode.
104    /// User decoder panics become sticky asset errors. Decoders must not
105    /// recursively resolve the same handle. Returned resources are immutable.
106    pub fn get(&self) -> crate::Result<Arc<A>> {
107        match &self.diagnostics {
108            Some(diagnostics) => self.get_with(diagnostics),
109            None => self.get_with(&RequestDiagnostics::default()),
110        }
111    }
112
113    /// Resolves into an explicit request sink, including when called on another thread.
114    pub fn get_with(&self, diagnostics: &RequestDiagnostics) -> crate::Result<Arc<A>> {
115        let _guard = if self.entry.result.get().is_none() {
116            let identity = Arc::as_ptr(&self.entry) as usize;
117            let accepted = RESOLVING.with(|entries| {
118                let mut entries = entries.borrow_mut();
119                if entries.len() >= 64 || entries.contains(&identity) {
120                    false
121                } else {
122                    entries.push(identity);
123                    true
124                }
125            });
126            if !accepted {
127                return Err(crate::Error::asset(
128                    A::KIND,
129                    self.source_name(),
130                    io::Error::other("Recursive resource resolution or load depth exceeds 64"),
131                ));
132            }
133            Some(ResolveGuard)
134        } else {
135            None
136        };
137        let result = self.entry.result.get_or_init(|| {
138            self.entry.loading.store(true, Ordering::Release);
139            let decoded =
140                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> crate::Result<A> {
141                    let bytes: Arc<[u8]> = match &self.entry.source {
142                        Source::Bytes { data, .. } => data.clone(),
143                        Source::File(path) => {
144                            let metadata = std::fs::metadata(path)?;
145                            if metadata.len() > MAX_ENCODED as u64 {
146                                return Err(io::Error::new(
147                                    io::ErrorKind::InvalidData,
148                                    "Asset source exceeds 64 MiB",
149                                )
150                                .into());
151                            }
152                            if !metadata.is_file() {
153                                return Err(io::Error::new(
154                                    io::ErrorKind::InvalidInput,
155                                    "Asset source must be a regular file",
156                                )
157                                .into());
158                            }
159                            let file = std::fs::File::open(path)?;
160                            if !file.metadata()?.is_file() {
161                                return Err(io::Error::new(
162                                    io::ErrorKind::InvalidInput,
163                                    "Asset source must be a regular file",
164                                )
165                                .into());
166                            }
167                            let mut bytes = Vec::new();
168                            file.take(MAX_ENCODED as u64 + 1).read_to_end(&mut bytes)?;
169                            bytes.into()
170                        }
171                    };
172                    if bytes.len() > MAX_ENCODED {
173                        return Err(io::Error::new(
174                            io::ErrorKind::InvalidData,
175                            "Asset source exceeds 64 MiB",
176                        )
177                        .into());
178                    }
179                    let asset = A::decode(&bytes, &self.entry.context)?;
180                    let charge = asset
181                        .retained_bytes()
182                        .checked_add(if matches!(self.entry.source, Source::Bytes { .. }) {
183                            bytes.len()
184                        } else {
185                            0
186                        })
187                        .ok_or_else(|| {
188                            io::Error::new(
189                                io::ErrorKind::OutOfMemory,
190                                "Asset payload accounting overflow",
191                            )
192                        })?
193                        .max(bytes.len());
194                    let additional = charge - self.entry.charged.load(Ordering::Acquire);
195                    self.entry
196                        .pool
197                        .0
198                        .bytes
199                        .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
200                            current
201                                .checked_add(additional)
202                                .filter(|total| *total <= self.entry.pool.0.max_bytes)
203                        })
204                        .map_err(|_| {
205                            io::Error::new(
206                                io::ErrorKind::OutOfMemory,
207                                "Shared asset-cache logical payload exceeds its allowance",
208                            )
209                        })?;
210                    self.entry.charged.store(charge, Ordering::Release);
211                    Ok(asset)
212                }));
213            let decoded = decoded.unwrap_or_else(|payload| {
214                let message = payload
215                    .downcast_ref::<String>()
216                    .cloned()
217                    .or_else(|| {
218                        payload
219                            .downcast_ref::<&str>()
220                            .map(|message| (*message).to_owned())
221                    })
222                    .unwrap_or_else(|| "non-string decoder panic".into());
223                Err(io::Error::other(format!("Asset decoder panicked: {message}")).into())
224            });
225
226            decoded
227                .map(Arc::new)
228                .map_err(|error| Arc::new(crate::Error::asset(A::KIND, self.source_name(), error)))
229        });
230        if let Err(error) = result {
231            diagnostics.record(error.clone());
232        }
233        result
234            .as_ref()
235            .map(Arc::clone)
236            .map_err(|error| crate::Error::asset(A::KIND, self.source_name(), error.clone()))
237    }
238
239    /// Resource path or embedded key, suitable for diagnostic display.
240    pub fn source_name(&self) -> String {
241        match &self.entry.source {
242            Source::File(path) => path.display().to_string(),
243            Source::Bytes { key, .. } => key.to_string(),
244        }
245    }
246}