motion_core/asset/
handle.rs1use super::*;
4use crate::sync::MutexExt;
5
6pub enum AssetState<A> {
8 Unloaded,
10 Loading,
12 Ready(Arc<A>),
14 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 pub fn is_unloaded(&self) -> bool {
43 matches!(self, Self::Unloaded)
44 }
45
46 pub fn is_loading(&self) -> bool {
48 matches!(self, Self::Loading)
49 }
50
51 pub fn is_ready(&self) -> bool {
53 matches!(self, Self::Ready(_))
54 }
55
56 pub fn is_failed(&self) -> bool {
58 matches!(self, Self::Failed(_))
59 }
60
61 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
105pub 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 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 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 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 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}