Skip to main content

ferrum_wgpu/assets/
store.rs

1use {
2    crate::{
3        assets::{self, Asset, Model, ModelDesc, TypeModel},
4        config::config::FerrumConfig,
5    },
6    std::{
7        collections::HashMap,
8        marker::PhantomData,
9        sync::{
10            Arc,
11            atomic::{AtomicUsize, Ordering},
12            mpsc::{self, Receiver, Sender},
13        },
14    },
15    wgpu::{BindGroupLayout, Device, Queue},
16};
17
18/// Handle to a model spawned in the [`ModelStore`]; the forge metaphor: the
19/// caller keeps the ingot's id while the bead melts in the background.
20pub struct Ingot<T> {
21    pub id: usize,
22    _marker: PhantomData<T>,
23}
24
25/// Loading state of a model: requested (`Burning`), ready (`Molten`) or
26/// discarded (`Ash`).
27pub(crate) enum Bead<T> {
28    Burning,
29    Molten(T),
30    #[allow(dead_code)]
31    Ash,
32}
33
34/// Owns every model of the scene and the channel through which asynchronously
35/// loaded models arrive.
36pub struct ModelStore {
37    static_models: HashMap<usize, Bead<Model>>,
38    light_models: HashMap<usize, Bead<Model>>,
39    next_id: AtomicUsize,
40    sender: Sender<(usize, Model)>,
41    receiver: Receiver<(usize, Model)>,
42}
43
44impl ModelStore {
45    pub(crate) fn new() -> Self {
46        let (sender, receiver) = mpsc::channel::<(usize, Model)>();
47        Self {
48            static_models: HashMap::new(),
49            light_models: HashMap::new(),
50            next_id: AtomicUsize::new(0),
51            sender,
52            receiver,
53        }
54    }
55
56    /// Registers the model as `Burning` and launches its load. The loading is
57    /// asynchronous, and the model arrives via the channel when it finishes
58    /// (`Bead::Burning` until then). In native: thread + block_on; in wasm
59    /// there are no threads or block_on, so it's queued in the browser's event
60    /// loop with spawn_local (asset fetching is already async).
61    pub(crate) fn spawn(
62        &mut self,
63        device: &Arc<Device>,
64        queue: &Arc<Queue>,
65        layout: &Arc<BindGroupLayout>,
66        model_desc: ModelDesc,
67        config: &FerrumConfig,
68    ) -> Ingot<Model> {
69        let id: usize = self.next_id.fetch_add(1, Ordering::SeqCst);
70
71        match model_desc.kind {
72            TypeModel::StaticObj => self.static_models.insert(id, Bead::Burning),
73            TypeModel::PointOfLight => self.light_models.insert(id, Bead::Burning),
74        };
75
76        let device: Arc<Device> = Arc::clone(device);
77        let queue: Arc<Queue> = Arc::clone(queue);
78        let layout: Arc<BindGroupLayout> = Arc::clone(layout);
79        let sender: Sender<(usize, Model)> = self.sender.clone();
80        let instances: Vec<assets::Instance> = model_desc.instances;
81        let kind: TypeModel = model_desc.kind;
82        let file_name: String = model_desc.file_name.to_string();
83        let asset: Asset = config.asset.clone();
84
85        #[cfg(not(target_arch = "wasm32"))]
86        std::thread::spawn(move || {
87            let result: Result<Model, anyhow::Error> = pollster::block_on(assets::load_model(
88                &asset, &file_name, &device, &queue, &layout, instances, kind,
89            ));
90            match result {
91                Ok(model) => {
92                    let _ = sender.send((id, model));
93                }
94                Err(e) => log::error!("Failed to load model '{file_name}': {e:?}"),
95            }
96        });
97
98        #[cfg(target_arch = "wasm32")]
99        wasm_bindgen_futures::spawn_local(async move {
100            let result: Result<Model, anyhow::Error> = assets::load_model(
101                &asset, &file_name, &device, &queue, &layout, instances, kind,
102            )
103            .await;
104            match result {
105                Ok(model) => {
106                    let _ = sender.send((id, model));
107                }
108                Err(e) => log::error!("Failed to load model '{file_name}': {e:?}"),
109            }
110        });
111
112        Ingot {
113            id,
114            _marker: PhantomData,
115        }
116    }
117
118    /// Drains the channel and turns every finished load into `Molten`.
119    pub(crate) fn collect_loaded(&mut self) {
120        while let Ok((id, model)) = self.receiver.try_recv() {
121            match model.type_model {
122                TypeModel::StaticObj => self.static_models.insert(id, Bead::Molten(model)),
123                TypeModel::PointOfLight => self.light_models.insert(id, Bead::Molten(model)),
124            };
125        }
126    }
127
128    pub(crate) fn static_loaded(&self) -> impl Iterator<Item = &Model> {
129        self.static_models.values().filter_map(|bead| match bead {
130            Bead::Molten(model) => Some(model),
131            _ => None,
132        })
133    }
134
135    pub(crate) fn light_loaded(&self) -> impl Iterator<Item = &Model> {
136        self.light_models.values().filter_map(|bead| match bead {
137            Bead::Molten(model) => Some(model),
138            _ => None,
139        })
140    }
141
142    pub(crate) fn light_model_mut(&mut self, id: &usize) -> Option<&mut Model> {
143        match self.light_models.get_mut(id) {
144            Some(Bead::Molten(model)) => Some(model),
145            _ => None,
146        }
147    }
148}