diff --git a/Cargo.toml b/Cargo.toml index bb8e6d1..091cdc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,8 +122,8 @@ bevy_reflect = [ "firewheel-core/bevy_reflect", "firewheel-graph/bevy_reflect", ] -# Enables the wasm-bindgen feature for the CPAL backend -wasm-bindgen = ["firewheel-cpal/wasm-bindgen"] +# Enables wasm-bindgen for dependencies +wasm-bindgen = ["firewheel-cpal/wasm-bindgen", "firewheel-graph/wasm-bindgen"] # Enables `glam::Vec2` and `glam::Vec3` parameter derives for glam 0.29. glam-29 = ["firewheel-core/glam-29"] # Enables `glam::Vec2` and `glam::Vec3` parameter derives for glam 0.30. diff --git a/crates/firewheel-graph/Cargo.toml b/crates/firewheel-graph/Cargo.toml index 87a08b3..1d14dab 100644 --- a/crates/firewheel-graph/Cargo.toml +++ b/crates/firewheel-graph/Cargo.toml @@ -61,6 +61,9 @@ node_profiling = [] # For an explanation on why denormal numbers are a problem, see: # https://mu.krj.st/denormal/ unsafe_flush_denormals_to_zero = [] +# Provides wasm-bindgen, allowing for non-panicking `Instant::now` +# calls in audio worklet contexts. +wasm-bindgen = ["dep:wasm-bindgen"] [dependencies] firewheel-core = { path = "../firewheel-core", version = "0.10.0", default-features = false } @@ -78,6 +81,7 @@ num-traits.workspace = true audioadapter.workspace = true serde = { workspace = true, optional = true } bevy_reflect = { workspace = true, optional = true } +wasm-bindgen = { version = "0.2", default-features = false, optional = true } [dev-dependencies] audioadapter-buffers = { workspace = true, features = ["alloc"] } diff --git a/crates/firewheel-graph/src/lib.rs b/crates/firewheel-graph/src/lib.rs index 6cfb7b4..b96a02d 100644 --- a/crates/firewheel-graph/src/lib.rs +++ b/crates/firewheel-graph/src/lib.rs @@ -5,6 +5,7 @@ mod context; pub mod error; pub mod graph; pub mod processor; +mod time; #[cfg(feature = "unsafe_flush_denormals_to_zero")] mod ftz; diff --git a/crates/firewheel-graph/src/processor/profiling.rs b/crates/firewheel-graph/src/processor/profiling.rs index fcae5b0..095b70b 100644 --- a/crates/firewheel-graph/src/processor/profiling.rs +++ b/crates/firewheel-graph/src/processor/profiling.rs @@ -182,8 +182,10 @@ impl ProfilerTx { } pub fn begin_new_bookkeeping_part(&mut self) { - if self.is_profiling_bookkeeping { - self.bookkeeping_start_instant = Instant::now(); + if self.is_profiling_bookkeeping + && let Some(now) = crate::time::now() + { + self.bookkeeping_start_instant = now; } } @@ -199,16 +201,18 @@ impl ProfilerTx { pub fn begin_node_profiling(&mut self) { self.node_schedule_index = 0; - if self.is_profiling_nodes { - self.node_profile_start_instant = Instant::now(); + if self.is_profiling_nodes + && let Some(now) = crate::time::now() + { + self.node_profile_start_instant = now; } } #[cfg(feature = "node_profiling")] pub fn node_completed(&mut self) { - if self.is_profiling_nodes { - let new_profile_instant = Instant::now(); - + if self.is_profiling_nodes + && let Some(new_profile_instant) = crate::time::now() + { let node_cpu_usage = new_profile_instant .duration_since(self.node_profile_start_instant) .as_secs_f64() @@ -221,6 +225,10 @@ impl ProfilerTx { } pub fn process_loop_completed(&mut self) { + let Some(now) = crate::time::now() else { + return; + }; + #[cfg(feature = "node_profiling")] if self.is_profiling_nodes { for (node, &sum) in self.nodes.iter_mut().zip(self.node_cpu_sums.iter()) { @@ -228,8 +236,6 @@ impl ProfilerTx { } } - let now = Instant::now(); - let overall_cpu_usage = now.duration_since(self.proc_start_instant).as_secs_f64() * self.total_cpu_seconds_recip; self.overall_cpu_usage = self.overall_cpu_usage.max(overall_cpu_usage); diff --git a/crates/firewheel-graph/src/time.rs b/crates/firewheel-graph/src/time.rs new file mode 100644 index 0000000..f558a73 --- /dev/null +++ b/crates/firewheel-graph/src/time.rs @@ -0,0 +1,57 @@ +use bevy_platform::time::Instant; + +/// Return [`Instant::now`] in contexts where it's available. +/// +/// In a Wasm-with-JS context, [`Instant::now`] will panic in +/// an audio worklet. Rather than panicking, this function returns `None`. +pub fn now() -> Option { + #[cfg(all( + feature = "wasm-bindgen", + target_family = "wasm", + target_feature = "atomics" + ))] + return is_not_worklet().then(|| bevy_platform::time::Instant::now()); + + #[cfg(not(all( + feature = "wasm-bindgen", + target_arch = "wasm32", + target_feature = "atomics" + )))] + return Some(bevy_platform::time::Instant::now()); +} + +#[cfg(all( + feature = "wasm-bindgen", + target_family = "wasm", + target_feature = "atomics" +))] +#[wasm_bindgen::prelude::wasm_bindgen(inline_js = " + export function is_audio_worklet() { + return typeof sampleRate !== 'undefined'; + } +")] +extern "C" { + fn is_audio_worklet() -> bool; +} + +/// Determines if this execution context is an audio worklet. +#[cfg(all( + feature = "wasm-bindgen", + target_family = "wasm", + target_feature = "atomics" +))] +fn is_not_worklet() -> bool { + #[cfg(feature = "std")] + { + // A thread local allows us to limit calls into JS to once per + // execution context. + thread_local! { + static IS_NOT_WORKLET: bool = !is_audio_worklet(); + } + + return IS_NOT_WORKLET.with(|w| *w); + } + + #[cfg(not(feature = "std"))] + return !is_audio_worklet(); +}