all_is_cubes_gpu/common/
reloadable.rs

1//! “Hot-reloadable” data sources such as shaders.
2//!
3//! This module builds on top of the `resource` library to add change notification
4//! via a background thread and all-is-cubes's `listen::Cell` mechanism.
5
6use std::sync::{Arc, LazyLock, Mutex, PoisonError};
7use std::time::Duration;
8
9use resource::Resource;
10
11use all_is_cubes::listen;
12
13#[derive(Clone)]
14pub(crate) struct Reloadable(Arc<ReloadableInner>);
15
16pub(crate) struct ReloadableInner {
17    resource: Mutex<Resource<str>>,
18    cell: listen::Cell<Arc<str>>,
19}
20
21/// `file_path` is relative to the Cargo package root.
22macro_rules! reloadable_str {
23    ($file_path:literal) => {
24        $crate::reloadable::Reloadable::new(::resource::resource_str!($file_path))
25    };
26}
27pub(crate) use reloadable_str;
28
29impl Reloadable {
30    pub fn new(resource: Resource<str>) -> Self {
31        let this = Reloadable(Arc::new(ReloadableInner {
32            cell: listen::Cell::new(res_arc_str(&resource)),
33            resource: Mutex::new(resource),
34        }));
35
36        // TODO: make this optional if we ever care
37        if let Ok(mut reloadables) = POLLED_RELOADABLES.lock() {
38            reloadables.push(this.clone());
39        }
40
41        this
42    }
43
44    pub fn poll(&self) -> bool {
45        match self.0.resource.lock().as_deref_mut() {
46            Ok(resource) => {
47                let changed = resource.reload_if_changed();
48                if changed {
49                    // unfortunately listen::Cell wants an Arc with Sized contents
50                    self.0.cell.set(res_arc_str(resource));
51                }
52                changed
53            }
54            Err(PoisonError { .. }) => false,
55        }
56    }
57
58    pub fn as_source(&self) -> listen::DynSource<Arc<str>> {
59        self.0.cell.as_source()
60    }
61}
62
63fn res_arc_str(r: &Resource<str>) -> Arc<str> {
64    Arc::from(<Resource<str> as AsRef<str>>::as_ref(r))
65}
66
67static POLLED_RELOADABLES: LazyLock<Mutex<Vec<Reloadable>>> = LazyLock::new(|| {
68    // Spawn a thread to poll for reloading, as soon as anyone cares,
69    // but only if it's possible and useful.
70    if cfg!(all(not(target_family = "wasm"), debug_assertions)) {
71        std::thread::spawn(poll_reloadables_loop);
72    }
73
74    Mutex::new(Vec::new())
75});
76
77fn poll_reloadables_loop() -> ! {
78    loop {
79        std::thread::sleep(Duration::from_secs(1));
80
81        // If this expect fails it'll just kill the thread
82        let v = POLLED_RELOADABLES
83            .lock()
84            .expect("could not lock POLLED_RELOADABLES from polling thread");
85        let mut any = false;
86        for reloadable in v.iter() {
87            any |= reloadable.poll();
88        }
89        if any {
90            log::debug!("Reloadable polled and found changed");
91        }
92    }
93}