Skip to main content

motion_core/asset/
handle.rs

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