Skip to main content

pebble/assets/
plugin.rs

1use crate::{
2    app::SystemStage,
3    assets::{
4        deps::Dependencies,
5        storage::{Assets, ProcessedAssets},
6        upload::Asset,
7    },
8    ecs::{
9        plugin::Plugin,
10        resources::Resources,
11        system::{Res, ResMut},
12    },
13};
14
15/// Plugin that drives the source → processed conversion pipeline for a single
16/// asset type `T`.
17///
18/// `B` is the *backend* passed to [`Asset::upload`] and is intentionally
19/// generic — it need not be a GPU backend:
20/// - **GPU assets**: `B` = your graphics backend (e.g. wgpu `Device`).
21/// - **CPU-only / audio / other**: `B = ()` or any other service type.
22///
23/// Registering `AssetPlugin::<B, T>::new()` will:
24/// - Insert an [`Assets<T::Source>`] resource for raw source assets.
25/// - Insert a [`ProcessedAssets<T>`] resource for the converted results.
26/// - Add a system on [`SystemStage::AssetSync`] that flushes the dirty queue
27///   each tick, calling [`Asset::upload`] for every pending entry.
28///
29/// The sync system waits silently until both `B` and all of `T`'s
30/// [`Dependencies`] are present as resources before processing any uploads.
31pub struct AssetPlugin<B, T: Asset<B>> {
32    _marker: std::marker::PhantomData<(B, T)>,
33}
34
35impl<B, T: Asset<B>> AssetPlugin<B, T> {
36    pub fn new() -> Self {
37        Self {
38            _marker: std::marker::PhantomData,
39        }
40    }
41}
42
43impl<B, T> Plugin for AssetPlugin<B, T>
44where
45    B: 'static + Send + Sync,
46    T: Asset<B>,
47{
48    fn build(&self, app: &mut crate::app::App) {
49        app.try_insert_resource(Assets::<T::Source>::new());
50        app.try_insert_resource(ProcessedAssets::<T>::new());
51        app.add_system(SystemStage::AssetSync, sync_assets::<B, T>);
52        app.required.provides::<ProcessedAssets<T>>();
53    }
54}
55
56/// Per-tick system: flush the dirty queue and convert pending assets.
57///
58/// Skips processing if `B` or any dependency is not yet available as a
59/// resource. Assets whose [`Asset::upload`] returns `None` are re-queued for
60/// the next tick.
61fn sync_assets<B, T>(
62    mut cpu: ResMut<Assets<T::Source>>,
63    mut processed: ResMut<ProcessedAssets<T>>,
64    backend: Option<Res<B>>,
65    world: &hecs::World,
66    resources: &Resources,
67) where
68    B: 'static + Send + Sync,
69    T: Asset<B>,
70{
71    let Some(backend) = backend else {
72        log_waiting::<B, T>(&cpu, "backend");
73        return;
74    };
75    let Some(deps) = T::Deps::try_gather(world, resources) else {
76        log_waiting::<B, T>(&cpu, "dependencies");
77        return;
78    };
79
80    for handle in cpu.take_removed() {
81        processed.remove(handle);
82    }
83
84    let mut still_pending = Vec::new();
85
86    for handle in cpu.take_dirty() {
87        let Some(source) = cpu.get(handle) else {
88            // Asset was inserted then removed before sync ran — nothing to upload.
89            tracing::debug!(
90                "{}: handle {:?} was in the dirty queue but the source asset is already gone \
91                 (inserted and removed in the same tick?)",
92                std::any::type_name::<T>(),
93                handle
94            );
95            continue;
96        };
97        match T::upload(source, &backend, &deps) {
98            Some(value) => {
99                if let Some(name) = cpu.name_for_handle(handle) {
100                    processed.names.insert(name.to_string(), handle);
101                }
102                tracing::debug!(
103                    "{}: uploaded {:?}{}",
104                    std::any::type_name::<T>(),
105                    handle,
106                    cpu.name_for_handle(handle)
107                        .map(|n| format!(" ({n})"))
108                        .unwrap_or_default()
109                );
110                processed.insert(handle, value);
111            }
112            None => {
113                tracing::debug!(
114                    "{}: {:?} upload returned None — a required dependency is not yet ready, \
115                     requeued for next tick",
116                    std::any::type_name::<T>(),
117                    handle
118                );
119                still_pending.push(handle);
120            }
121        }
122    }
123
124    if !still_pending.is_empty() {
125        tracing::debug!(
126            "{}: {} handle(s) still pending upload (waiting on dependencies)",
127            std::any::type_name::<T>(),
128            still_pending.len()
129        );
130    }
131
132    cpu.requeue(still_pending);
133}
134
135fn log_waiting<D, T>(cpu: &Assets<T::Source>, what: &str)
136where
137    D: 'static + Send + Sync,
138    T: Asset<D>,
139{
140    if !cpu.dirty_is_empty() {
141        tracing::debug!(
142            "{}: {} asset(s) queued but waiting on {what} before upload can begin",
143            std::any::type_name::<T>(),
144            cpu.dirty_len()
145        );
146    }
147}