1use std::collections::HashMap;
2
3use crate::{
4 app::SystemStage,
5 assets::{
6 deps::Dependencies,
7 storage::{Assets, ProcessedAssets, RawAssetHandle},
8 upload::Asset,
9 },
10 ecs::{
11 plugin::Plugin,
12 resources::Resources,
13 system::{Local, Res, ResMut},
14 },
15};
16
17const STUCK_AFTER_TICKS: u32 = 300;
28
29fn should_warn_stuck(ticks: u32) -> bool {
34 ticks >= STUCK_AFTER_TICKS && ticks.is_multiple_of(STUCK_AFTER_TICKS)
35}
36
37pub struct AssetPlugin<B, T: Asset<B>> {
54 _marker: std::marker::PhantomData<(B, T)>,
55}
56
57impl<B, T: Asset<B>> AssetPlugin<B, T> {
58 pub fn new() -> Self {
60 Self {
61 _marker: std::marker::PhantomData,
62 }
63 }
64}
65
66impl<B, T> Plugin for AssetPlugin<B, T>
67where
68 B: 'static + Send + Sync,
69 T: Asset<B>,
70{
71 fn build(&self, app: &mut crate::app::App) {
72 app.try_insert_resource(Assets::<T::Source>::new());
73 app.try_insert_resource(ProcessedAssets::<T>::new());
74 app.add_system(SystemStage::AssetSync, sync_assets::<B, T>);
75 app.provides::<ProcessedAssets<T>>();
76 }
77}
78
79fn sync_assets<B, T>(
85 mut cpu: ResMut<Assets<T::Source>>,
86 mut processed: ResMut<ProcessedAssets<T>>,
87 backend: Option<Res<B>>,
88 mut blocked_ticks: Local<u32>,
89 mut pending_ticks: Local<HashMap<RawAssetHandle, u32>>,
90 world: &hecs::World,
91 resources: &Resources,
92) where
93 B: 'static + Send + Sync,
94 T: Asset<B>,
95{
96 let Some(backend) = backend else {
97 log_waiting::<B, T>(&cpu, "backend", &mut blocked_ticks);
98 return;
99 };
100 let Some(deps) = T::Deps::try_gather(world, resources) else {
101 log_waiting::<B, T>(&cpu, "dependencies", &mut blocked_ticks);
102 return;
103 };
104 *blocked_ticks = 0;
105
106 for handle in cpu.take_removed() {
107 processed.remove(handle);
108 pending_ticks.remove(&handle);
109 }
110
111 let mut still_pending = Vec::new();
112
113 for handle in cpu.take_dirty() {
114 let Some(source) = cpu.get_quiet(handle) else {
115 tracing::debug!(
117 "{}: handle {:?} was in the dirty queue but the source asset is already gone \
118 (inserted and removed in the same tick?)",
119 std::any::type_name::<T>(),
120 handle
121 );
122 pending_ticks.remove(&handle);
123 continue;
124 };
125 match T::upload(source, &backend, &deps) {
126 Some(value) => {
127 if let Some(name) = cpu.name_for_handle(handle) {
128 processed.names.insert(name.to_string(), handle);
129 }
130 tracing::debug!(
131 "{}: uploaded {:?}{}",
132 std::any::type_name::<T>(),
133 handle,
134 cpu.name_for_handle(handle)
135 .map(|n| format!(" ({n})"))
136 .unwrap_or_default()
137 );
138 processed.insert(handle, value);
139 pending_ticks.remove(&handle);
140 }
141 None => {
142 let ticks = pending_ticks.entry(handle).or_insert(0);
143 *ticks += 1;
144 if should_warn_stuck(*ticks) {
145 tracing::warn!(
146 "{}: {:?}{} has not uploaded after {} ticks — upload() may be \
147 unconditionally returning None, or a Deps resource it needs is never \
148 actually going to appear. Still retrying every tick.",
149 std::any::type_name::<T>(),
150 handle,
151 cpu.name_for_handle(handle)
152 .map(|n| format!(" ({n})"))
153 .unwrap_or_default(),
154 *ticks
155 );
156 } else {
157 tracing::debug!(
158 "{}: {:?} upload returned None — a required dependency is not yet ready, \
159 requeued for next tick",
160 std::any::type_name::<T>(),
161 handle
162 );
163 }
164 still_pending.push(handle);
165 }
166 }
167 }
168
169 if !still_pending.is_empty() {
170 tracing::debug!(
171 "{}: {} handle(s) still pending upload (waiting on dependencies)",
172 std::any::type_name::<T>(),
173 still_pending.len()
174 );
175 }
176
177 cpu.requeue(still_pending);
178}
179
180fn log_waiting<D, T>(cpu: &Assets<T::Source>, what: &str, blocked_ticks: &mut u32)
181where
182 D: 'static + Send + Sync,
183 T: Asset<D>,
184{
185 if cpu.dirty_is_empty() {
186 *blocked_ticks = 0;
187 return;
188 }
189
190 *blocked_ticks += 1;
191 if should_warn_stuck(*blocked_ticks) {
192 tracing::warn!(
193 "{}: {} asset(s) have been queued for {} ticks, still waiting on {what} before \
194 upload can begin — if {what} is never going to appear, this pipeline will wait \
195 forever.",
196 std::any::type_name::<T>(),
197 cpu.dirty_len(),
198 *blocked_ticks,
199 );
200 } else {
201 tracing::debug!(
202 "{}: {} asset(s) queued but waiting on {what} before upload can begin",
203 std::any::type_name::<T>(),
204 cpu.dirty_len()
205 );
206 }
207}