Skip to main content

pebble/assets/
plugin.rs

1use std::collections::HashMap;
2
3use crate::{
4    app::SystemStage,
5    assets::{
6        deps::Dependencies,
7        storage::{Assets, RawAssetHandle},
8        upload::Asset,
9    },
10    ecs::{
11        plugin::Plugin,
12        resources::Resources,
13        system::{Local, Res, ResMut},
14    },
15};
16
17/// Ticks a pending asset or a system blocked on `backend`/`Deps` can retry
18/// before the pipeline escalates from a quiet `debug!`/`trace!` to a
19/// `warn!`. Long enough that a legitimately slow dependency chain doesn't
20/// trip it on every run; short enough that something genuinely stuck doesn't
21/// stay invisible for minutes. Not a hard limit — retries continue past
22/// this, just louder, and repeat every `STUCK_AFTER_TICKS`.
23const STUCK_AFTER_TICKS: u32 = 300;
24
25fn should_warn_stuck(ticks: u32) -> bool {
26    ticks >= STUCK_AFTER_TICKS && ticks.is_multiple_of(STUCK_AFTER_TICKS)
27}
28
29/// Plugin that drives the source → processed conversion pipeline for a
30/// single asset type `T`.
31///
32/// `B` is the *backend* passed to [`Asset::upload`] and is intentionally
33/// generic — it need not be a GPU backend.
34///
35/// Registering `AssetPlugin::<B, T>::new()` will:
36/// - Insert an [`Assets<T>`] resource holding both source and processed data.
37/// - Add a system on [`SystemStage::AssetSync`] that flushes the dirty
38///   queue each tick, calling [`Asset::upload`] for every pending entry.
39///
40/// The sync system waits silently until both `B` and all of `T`'s
41/// [`Dependencies`] are present as resources before processing any uploads.
42pub struct AssetPlugin<B, T: Asset<B>> {
43    _marker: std::marker::PhantomData<(B, T)>,
44}
45
46impl<B, T: Asset<B>> AssetPlugin<B, T> {
47    pub fn new() -> Self {
48        Self { _marker: std::marker::PhantomData }
49    }
50}
51
52impl<B, T> Plugin for AssetPlugin<B, T>
53where
54    B: 'static + Send + Sync,
55    T: Asset<B>,
56{
57    fn build(&self, app: &mut crate::app::App) {
58        app.try_insert_resource(Assets::<T>::new());
59        app.add_system(SystemStage::AssetSync, sync_assets::<B, T>);
60    }
61}
62
63/// Per-tick system: flush the dirty queue and convert pending assets.
64///
65/// Skips processing if `B` or any dependency is not yet available as a
66/// resource. Assets whose [`Asset::upload`] returns `None` are re-queued
67/// for the next tick.
68fn sync_assets<B, T>(
69    mut assets: ResMut<Assets<T>>,
70    backend: Option<Res<B>>,
71    mut blocked_ticks: Local<u32>,
72    mut pending_ticks: Local<HashMap<RawAssetHandle, u32>>,
73    world: &hecs::World,
74    resources: &Resources,
75) where
76    B: 'static + Send + Sync,
77    T: Asset<B>,
78{
79    let Some(backend) = backend else {
80        log_waiting::<B, T>(&assets, "backend", &mut blocked_ticks);
81        return;
82    };
83    let Some(deps) = T::Deps::try_gather(world, resources) else {
84        log_waiting::<B, T>(&assets, "dependencies", &mut blocked_ticks);
85        return;
86    };
87    *blocked_ticks = 0;
88
89    for handle in assets.take_removed() {
90        pending_ticks.remove(&handle);
91    }
92
93    let dirty = assets.take_dirty();
94    let mut still_pending = Vec::new();
95
96    // Phase 1: compute upload results while holding an immutable borrow on
97    // `assets`. Outer `None` = source gone; `Some(None)` = upload pending;
98    // `Some(Some(v))` = ready. Collected into an owned vec so all borrows
99    // are released before phase 2 writes back.
100    let results: Vec<(RawAssetHandle, Option<String>, Option<Option<T::Processed>>)> = dirty
101        .iter()
102        .map(|&handle| match assets.get_source_quiet(handle) {
103            None => (handle, None, None),
104            Some(source) => {
105                let name = assets.name_for_handle(handle).map(String::from);
106                let outcome = source.upload(&backend, &deps);
107                (handle, name, Some(outcome))
108            }
109        })
110        .collect();
111
112    // Phase 2: apply results with mutable borrows on `assets`.
113    for (handle, name, outcome) in results {
114        let label = name.as_deref().map(|n| format!(" ({n})")).unwrap_or_default();
115        match outcome {
116            None => {
117                tracing::debug!(
118                    "{}: handle {:?} was in the dirty queue but the source asset is already \
119                     gone (inserted and removed in the same tick?)",
120                    std::any::type_name::<T>(),
121                    handle
122                );
123                pending_ticks.remove(&handle);
124            }
125            Some(None) => {
126                let ticks = pending_ticks.entry(handle).or_insert(0);
127                *ticks += 1;
128                if should_warn_stuck(*ticks) {
129                    tracing::warn!(
130                        "{}: {:?}{} has not uploaded after {} ticks — upload() may be \
131                         unconditionally returning None, or a Deps resource it needs is never \
132                         actually going to appear. Still retrying every tick.",
133                        std::any::type_name::<T>(),
134                        handle,
135                        label,
136                        *ticks
137                    );
138                } else {
139                    tracing::debug!(
140                        "{}: {:?}{} upload returned None — a required dependency is not yet \
141                         ready, requeued for next tick",
142                        std::any::type_name::<T>(),
143                        handle,
144                        label,
145                    );
146                }
147                still_pending.push(handle);
148            }
149            Some(Some(value)) => {
150                assets.set_processed(handle, value);
151                pending_ticks.remove(&handle);
152                tracing::debug!(
153                    "{}: uploaded {:?}{}",
154                    std::any::type_name::<T>(),
155                    handle,
156                    label
157                );
158            }
159        }
160    }
161
162    if !still_pending.is_empty() {
163        tracing::debug!(
164            "{}: {} handle(s) still pending upload (waiting on dependencies)",
165            std::any::type_name::<T>(),
166            still_pending.len()
167        );
168    }
169
170    assets.requeue(still_pending);
171}
172
173fn log_waiting<B, T>(assets: &Assets<T>, what: &str, blocked_ticks: &mut u32)
174where
175    B: 'static + Send + Sync,
176    T: Asset<B>,
177{
178    if assets.dirty_is_empty() {
179        *blocked_ticks = 0;
180        return;
181    }
182
183    *blocked_ticks += 1;
184    if should_warn_stuck(*blocked_ticks) {
185        tracing::warn!(
186            "{}: {} asset(s) have been queued for {} ticks, still waiting on {what} before \
187             upload can begin — if {what} is never going to appear, this pipeline will wait \
188             forever.",
189            std::any::type_name::<T>(),
190            assets.dirty_len(),
191            *blocked_ticks,
192        );
193    } else {
194        tracing::debug!(
195            "{}: {} asset(s) queued but waiting on {what} before upload can begin",
196            std::any::type_name::<T>(),
197            assets.dirty_len()
198        );
199    }
200}