From 79fc6fa3822967cb5ca06c32d663f1cb5a4fe35f Mon Sep 17 00:00:00 2001 From: Austin Hicks Date: Sat, 21 Dec 2024 22:40:23 -0800 Subject: [PATCH] Add a BoxedSignal and support boxing signals, to simplify types and bring build times down --- crates/synthizer/examples/sin_chords.rs | 9 +- crates/synthizer/src/chain.rs | 18 ++ crates/synthizer/src/core_traits.rs | 6 +- crates/synthizer/src/signals/boxed.rs | 270 ++++++++++++++++++ crates/synthizer/src/signals/consume_input.rs | 2 + crates/synthizer/src/signals/conversion.rs | 2 + crates/synthizer/src/signals/map.rs | 2 +- crates/synthizer/src/signals/mod.rs | 2 + 8 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 crates/synthizer/src/signals/boxed.rs diff --git a/crates/synthizer/examples/sin_chords.rs b/crates/synthizer/examples/sin_chords.rs index fcdf4e4..7591eac 100644 --- a/crates/synthizer/examples/sin_chords.rs +++ b/crates/synthizer/examples/sin_chords.rs @@ -25,17 +25,20 @@ fn main() -> Result<()> { .divide_by_sr() .periodic_sum(1.0f64, 0.0f64) .inline_mul(Chain::new(pi2)) - .sin(); + .sin() + .boxed(); let note2 = read_slot(&freq2, E_FREQ) .divide_by_sr() .periodic_sum(1.0f64, 0.0) .inline_mul(Chain::new(pi2)) - .sin(); + .sin() + .boxed(); let note3 = read_slot(&freq3, G_FREQ) .divide_by_sr() .periodic_sum(1.0f64, 0.0) .inline_mul(Chain::new(pi2)) - .sin(); + .sin() + .boxed(); let added = note1 + note2 + note3; let ready = added * Chain::new(0.1f64); diff --git a/crates/synthizer/src/chain.rs b/crates/synthizer/src/chain.rs index 1e6fcdc..3a75d20 100644 --- a/crates/synthizer/src/chain.rs +++ b/crates/synthizer/src/chain.rs @@ -117,6 +117,7 @@ impl Chain { where for<'a> IntoSignalInput<'a, S>: Default, S::Signal: 'static, + NewInputType: 'static, { Chain { inner: sigs::ConsumeInputSignalConfig::<_, NewInputType>::new(self.inner), @@ -158,6 +159,7 @@ impl Chain { where for<'a> T: From>, for<'a> IntoSignalOutput<'a, S>: Clone, + T: 'static, { Chain { inner: sigs::ConvertOutputConfig::::new(self.inner), @@ -207,4 +209,20 @@ impl Chain { { self * other } + + /// Box this signal. + /// + /// This simplifies the type, at a performance cost. If you do not put this boxed signal in a recursive path, the + /// performance cost is minimal. + pub fn boxed(self) -> Chain> + where + I: Copy + Send + Sync + 'static, + O: Copy + Send + Sync + 'static, + S: Send + Sync + 'static, + for<'il, 'ol> S::Signal: Signal = I, Output<'ol> = O>, + { + Chain { + inner: sigs::BoxedSignalConfig::new(self.inner), + } + } } diff --git a/crates/synthizer/src/core_traits.rs b/crates/synthizer/src/core_traits.rs index d03757e..3ad50aa 100644 --- a/crates/synthizer/src/core_traits.rs +++ b/crates/synthizer/src/core_traits.rs @@ -15,11 +15,11 @@ pub(crate) mod sealed { /// This trait is unsafe because the library relies on it to uphold the contracts documented with the method. This /// lets us get performance out, especially in debug builds where things like immediate unwrapping of options will /// not be optimized away. - pub unsafe trait Signal: Sized + Send + Sync { + pub unsafe trait Signal: Sized + Send + Sync + 'static { type Input<'il>: Sized; type Output<'ol>: Sized; - type State: Sized + Send + Sync; - type Parameters: Sized + Send + Sync; + type State: Sized + Send + Sync + 'static; + type Parameters: Sized + Send + Sync + 'static; /// Tick this signal. /// diff --git a/crates/synthizer/src/signals/boxed.rs b/crates/synthizer/src/signals/boxed.rs new file mode 100644 index 0000000..fb955fa --- /dev/null +++ b/crates/synthizer/src/signals/boxed.rs @@ -0,0 +1,270 @@ +use std::any::Any; +use std::marker::PhantomData as PD; +use std::mem::MaybeUninit; +use std::sync::Arc; + +use crate::context::*; +use crate::core_traits::*; +use crate::error::Result; +use crate::unique_id::UniqueId; + +/// A signal behind a box, whose input is `I` and output is `O`. +/// +/// This is almost as efficient as the non-boxed version, save in recursive structures. In practice, this value is not +/// allocating itself, as signals are zero-sized. The primary use of this is compile times and error messages: it can +/// be used to simplify the types to something readable and manageable, both for humans and the compiler. +/// +/// Unfortunately, however, it is required that the input and output be `'static` and `Copy`. This is due to the +/// inability to pass owned arrays of varying type to the underlying signal. +pub struct BoxedSignalConfig { + signal: Box>, +} + +pub struct BoxedSignal { + _phantom: PD<(I, O)>, +} + +pub struct BoxedSignalState { + state: Box, +} + +pub struct BoxedSignalParams { + signal: Box>, + underlying_params: Box, +} + +trait ErasedSignal +where + Self: 'static + Send + Sync, + I: 'static + Copy, + O: 'static + Copy, +{ + fn on_block_start_erased( + &self, + ctx: &SignalExecutionContext<'_, '_>, + params: &dyn Any, + state: &mut dyn Any, + ); + + fn trace_slots_erased( + &self, + state: &dyn Any, + params: &dyn Any, + tracer: &mut dyn FnMut(UniqueId, Arc), + ); + + fn tick_erased( + &self, + ctx: &SignalExecutionContext<'_, '_>, + input: &[I], + params: &dyn Any, + state: &mut dyn Any, + output: &mut dyn FnMut(O), + ); +} + +trait ErasedIntoSignal +where + Self: 'static + Send + Sync, + I: 'static + Copy, + O: 'static + Copy, +{ + #[allow(clippy::type_complexity)] + fn erased_into( + &mut self, + ) -> Result, BoxedSignalState, BoxedSignalParams>>; +} + +impl ErasedIntoSignal for Option +where + I: 'static + Copy, + O: 'static + Copy, + T: IntoSignal + Send + Sync + 'static, + for<'il, 'ol> T::Signal: Signal = I, Output<'ol> = O> + 'static, +{ + fn erased_into( + &mut self, + ) -> Result, BoxedSignalState, BoxedSignalParams>> { + let underlying = self + .take() + .expect("This should never be called twice; we are using `Option` to do a move at runtime") + .into_signal()?; + Ok(ReadySignal { + signal: BoxedSignal { _phantom: PD }, + parameters: BoxedSignalParams { + signal: Box::new(underlying.signal), + underlying_params: Box::new(underlying.parameters), + }, + state: BoxedSignalState { + state: Box::new(underlying.state), + }, + }) + } +} + +impl ErasedSignal for T +where + for<'il, 'ol> T: Signal = I, Output<'ol> = O>, + I: 'static + Copy, + O: 'static + Copy, +{ + fn trace_slots_erased( + &self, + state: &dyn Any, + params: &dyn Any, + mut tracer: &mut dyn FnMut(UniqueId, Arc), + ) { + let params = params.downcast_ref::>().unwrap(); + let state = state.downcast_ref::().unwrap(); + let underlying_params = params + .underlying_params + .downcast_ref::() + .unwrap(); + let underlying_state = state.state.downcast_ref::().unwrap(); + T::trace_slots(underlying_state, underlying_params, &mut tracer); + } + + fn on_block_start_erased( + &self, + ctx: &SignalExecutionContext<'_, '_>, + params: &dyn Any, + state: &mut dyn Any, + ) { + let params = params.downcast_ref::>().unwrap(); + let state = state.downcast_mut::().unwrap(); + let underlying_params = params + .underlying_params + .downcast_ref::() + .unwrap(); + let underlying_state = state.state.downcast_mut::().unwrap(); + T::on_block_start(ctx, underlying_params, underlying_state); + } + + fn tick_erased( + &self, + ctx: &SignalExecutionContext<'_, '_>, + mut input: &[I], + params: &dyn Any, + state: &mut dyn Any, + mut output: &mut dyn FnMut(O), + ) { + let params = params.downcast_ref::>().unwrap(); + let state = state.downcast_mut::().unwrap(); + let underlying_params = params + .underlying_params + .downcast_ref::() + .unwrap(); + let underlying_state = state.state.downcast_mut::().unwrap(); + + macro_rules! do_one { + ($num: expr) => { + while let Some(this_input) = input.first_chunk::<$num>().copied() { + T::tick::<_, $num>( + ctx, + this_input, + underlying_params, + underlying_state, + |x: [O; $num]| { + x.into_iter().for_each(&mut output); + }, + ); + input = &input[$num..]; + } + }; + } + + do_one!(8); + do_one!(4); + do_one!(1); + } +} +// Rust is not happy about casting the references we have into `&dyn Any` without a reborrow. It's kind of unclear why: +// the references we get have longer lifetimes than where they're going. I'm guessing this is just a type inference +// weakness. +#[allow(clippy::borrow_ref_deref)] +unsafe impl Signal for BoxedSignal +where + I: Copy + Send + Sync + 'static, + O: Copy + Send + Sync + 'static, +{ + type Input<'il> = I; + type Output<'ol> = O; + type Parameters = BoxedSignalParams; + type State = BoxedSignalState; + + fn on_block_start( + ctx: &SignalExecutionContext<'_, '_>, + params: &Self::Parameters, + state: &mut Self::State, + ) { + params + .signal + .on_block_start_erased(ctx, &*params, &mut *state); + } + + fn tick<'il, 'ol, D, const N: usize>( + ctx: &'_ SignalExecutionContext<'_, '_>, + input: [Self::Input<'il>; N], + params: &Self::Parameters, + state: &mut Self::State, + destination: D, + ) where + D: SignalDestination, N>, + Self::Input<'il>: 'ol, + 'il: 'ol, + { + let mut dest: [MaybeUninit; N] = [const { MaybeUninit::uninit() }; N]; + let mut i = 0; + + params + .signal + .tick_erased(ctx, &input, &*params, &mut *state, &mut |o| { + dest[i].write(o); + i += 1; + }); + + assert_eq!(i, N); + + unsafe { destination.send(dest.map(|x| x.assume_init())) }; + } + + fn trace_slots)>( + state: &Self::State, + parameters: &Self::Parameters, + inserter: &mut F, + ) { + parameters + .signal + .trace_slots_erased(&*state, &*parameters, inserter); + } +} + +impl BoxedSignalConfig +where + I: Copy + 'static, + O: Copy + 'static, +{ + pub(crate) fn new(underlying: S) -> Self + where + S: IntoSignal + Send + Sync + 'static, + for<'il, 'ol> S::Signal: Signal = I, Output<'ol> = O>, + { + Self { + signal: Box::new(Some(underlying)), + } + } +} + +impl IntoSignal for BoxedSignalConfig +where + I: Copy + Send + Sync + 'static, + O: Copy + Send + Sync + 'static, +{ + type Signal = BoxedSignal; + + fn into_signal( + mut self, + ) -> Result, IntoSignalParameters>> { + self.signal.erased_into() + } +} diff --git a/crates/synthizer/src/signals/consume_input.rs b/crates/synthizer/src/signals/consume_input.rs index eccd3fb..09c0b35 100644 --- a/crates/synthizer/src/signals/consume_input.rs +++ b/crates/synthizer/src/signals/consume_input.rs @@ -26,6 +26,7 @@ unsafe impl Signal for ConsumeInputSignal where S: Signal + 'static, for<'a> S::Input<'a>: Default, + I: 'static, { type Input<'il> = I; type Output<'ol> = S::Output<'ol>; @@ -74,6 +75,7 @@ where S: IntoSignal, for<'a> IntoSignalInput<'a, S>: Default, S::Signal: 'static, + DiscardingInputType: 'static, { type Signal = ConsumeInputSignal; diff --git a/crates/synthizer/src/signals/conversion.rs b/crates/synthizer/src/signals/conversion.rs index 237faa2..527274e 100644 --- a/crates/synthizer/src/signals/conversion.rs +++ b/crates/synthizer/src/signals/conversion.rs @@ -26,6 +26,7 @@ unsafe impl Signal for ConvertOutput where Sig: Signal, for<'a> Sig::Output<'a>: Into + Clone, + DType: 'static, { type Output<'ol> = DType; type Input<'il> = Sig::Input<'il>; @@ -79,6 +80,7 @@ impl IntoSignal for ConvertOutputConfig where Sig: IntoSignal, for<'a> IntoSignalOutput<'a, Sig>: Into + Clone, + DType: 'static, { type Signal = ConvertOutput; diff --git a/crates/synthizer/src/signals/map.rs b/crates/synthizer/src/signals/map.rs index 51d5f88..7384594 100644 --- a/crates/synthizer/src/signals/map.rs +++ b/crates/synthizer/src/signals/map.rs @@ -21,7 +21,7 @@ unsafe impl Signal for MapSignal where ParSig: Signal, F: FnMut(SignalOutput) -> O + Send + Sync + 'static, - O: Send, + O: Send + 'static, { type Input<'il> = SignalInput<'il, ParSig>; type Output<'ol> = O; diff --git a/crates/synthizer/src/signals/mod.rs b/crates/synthizer/src/signals/mod.rs index 936cc1e..1600b40 100644 --- a/crates/synthizer/src/signals/mod.rs +++ b/crates/synthizer/src/signals/mod.rs @@ -1,5 +1,6 @@ mod and_then; mod audio_io; +mod boxed; mod consume_input; mod conversion; mod map; @@ -11,6 +12,7 @@ mod trig; pub use and_then::*; pub use audio_io::*; +pub use boxed::*; pub(crate) use consume_input::*; pub use conversion::*; pub use map::*;