Skip to content

Commit

Permalink
Add a BoxedSignal and support boxing signals, to simplify types and b…
Browse files Browse the repository at this point in the history
…ring build times down
  • Loading branch information
ahicks92 committed Dec 22, 2024
1 parent 6a65835 commit 79fc6fa
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 7 deletions.
9 changes: 6 additions & 3 deletions crates/synthizer/examples/sin_chords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions crates/synthizer/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl<S: IntoSignal> Chain<S> {
where
for<'a> IntoSignalInput<'a, S>: Default,
S::Signal: 'static,
NewInputType: 'static,
{
Chain {
inner: sigs::ConsumeInputSignalConfig::<_, NewInputType>::new(self.inner),
Expand Down Expand Up @@ -158,6 +159,7 @@ impl<S: IntoSignal> Chain<S> {
where
for<'a> T: From<IntoSignalOutput<'a, S>>,
for<'a> IntoSignalOutput<'a, S>: Clone,
T: 'static,
{
Chain {
inner: sigs::ConvertOutputConfig::<S, T>::new(self.inner),
Expand Down Expand Up @@ -207,4 +209,20 @@ impl<S: IntoSignal> Chain<S> {
{
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<I, O>(self) -> Chain<sigs::BoxedSignalConfig<I, O>>
where
I: Copy + Send + Sync + 'static,
O: Copy + Send + Sync + 'static,
S: Send + Sync + 'static,
for<'il, 'ol> S::Signal: Signal<Input<'il> = I, Output<'ol> = O>,
{
Chain {
inner: sigs::BoxedSignalConfig::new(self.inner),
}
}
}
6 changes: 3 additions & 3 deletions crates/synthizer/src/core_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
270 changes: 270 additions & 0 deletions crates/synthizer/src/signals/boxed.rs
Original file line number Diff line number Diff line change
@@ -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<I, O> {
signal: Box<dyn ErasedIntoSignal<I, O>>,
}

pub struct BoxedSignal<I, O> {
_phantom: PD<(I, O)>,
}

pub struct BoxedSignalState {
state: Box<dyn Any + Send + Sync + 'static>,
}

pub struct BoxedSignalParams<I, O> {
signal: Box<dyn ErasedSignal<I, O>>,
underlying_params: Box<dyn Any + Send + Sync + 'static>,
}

trait ErasedSignal<I, O>
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<dyn Any + Send + Sync + 'static>),
);

fn tick_erased(
&self,
ctx: &SignalExecutionContext<'_, '_>,
input: &[I],
params: &dyn Any,
state: &mut dyn Any,
output: &mut dyn FnMut(O),
);
}

trait ErasedIntoSignal<I, O>
where
Self: 'static + Send + Sync,
I: 'static + Copy,
O: 'static + Copy,
{
#[allow(clippy::type_complexity)]
fn erased_into(
&mut self,
) -> Result<ReadySignal<BoxedSignal<I, O>, BoxedSignalState, BoxedSignalParams<I, O>>>;
}

impl<T, I, O> ErasedIntoSignal<I, O> for Option<T>
where
I: 'static + Copy,
O: 'static + Copy,
T: IntoSignal + Send + Sync + 'static,
for<'il, 'ol> T::Signal: Signal<Input<'il> = I, Output<'ol> = O> + 'static,
{
fn erased_into(
&mut self,
) -> Result<ReadySignal<BoxedSignal<I, O>, BoxedSignalState, BoxedSignalParams<I, O>>> {
let underlying = self
.take()
.expect("This should never be called twice; we are using `Option<T>` 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<I, O, T> ErasedSignal<I, O> for T
where
for<'il, 'ol> T: Signal<Input<'il> = 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<dyn Any + Send + Sync + 'static>),
) {
let params = params.downcast_ref::<BoxedSignalParams<I, O>>().unwrap();
let state = state.downcast_ref::<BoxedSignalState>().unwrap();
let underlying_params = params
.underlying_params
.downcast_ref::<T::Parameters>()
.unwrap();
let underlying_state = state.state.downcast_ref::<T::State>().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::<BoxedSignalParams<I, O>>().unwrap();
let state = state.downcast_mut::<BoxedSignalState>().unwrap();
let underlying_params = params
.underlying_params
.downcast_ref::<T::Parameters>()
.unwrap();
let underlying_state = state.state.downcast_mut::<T::State>().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::<BoxedSignalParams<I, O>>().unwrap();
let state = state.downcast_mut::<BoxedSignalState>().unwrap();
let underlying_params = params
.underlying_params
.downcast_ref::<T::Parameters>()
.unwrap();
let underlying_state = state.state.downcast_mut::<T::State>().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<I, O> Signal for BoxedSignal<I, O>
where
I: Copy + Send + Sync + 'static,
O: Copy + Send + Sync + 'static,
{
type Input<'il> = I;
type Output<'ol> = O;
type Parameters = BoxedSignalParams<I, O>;
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<Self::Output<'ol>, N>,
Self::Input<'il>: 'ol,
'il: 'ol,
{
let mut dest: [MaybeUninit<O>; 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<F: FnMut(UniqueId, Arc<dyn Any + Send + Sync + 'static>)>(
state: &Self::State,
parameters: &Self::Parameters,
inserter: &mut F,
) {
parameters
.signal
.trace_slots_erased(&*state, &*parameters, inserter);
}
}

impl<I, O> BoxedSignalConfig<I, O>
where
I: Copy + 'static,
O: Copy + 'static,
{
pub(crate) fn new<S>(underlying: S) -> Self
where
S: IntoSignal + Send + Sync + 'static,
for<'il, 'ol> S::Signal: Signal<Input<'il> = I, Output<'ol> = O>,
{
Self {
signal: Box::new(Some(underlying)),
}
}
}

impl<I, O> IntoSignal for BoxedSignalConfig<I, O>
where
I: Copy + Send + Sync + 'static,
O: Copy + Send + Sync + 'static,
{
type Signal = BoxedSignal<I, O>;

fn into_signal(
mut self,
) -> Result<ReadySignal<Self::Signal, IntoSignalState<Self>, IntoSignalParameters<Self>>> {
self.signal.erased_into()
}
}
2 changes: 2 additions & 0 deletions crates/synthizer/src/signals/consume_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ unsafe impl<S, I> Signal for ConsumeInputSignal<S, I>
where
S: Signal + 'static,
for<'a> S::Input<'a>: Default,
I: 'static,
{
type Input<'il> = I;
type Output<'ol> = S::Output<'ol>;
Expand Down Expand Up @@ -74,6 +75,7 @@ where
S: IntoSignal,
for<'a> IntoSignalInput<'a, S>: Default,
S::Signal: 'static,
DiscardingInputType: 'static,
{
type Signal = ConsumeInputSignal<S::Signal, DiscardingInputType>;

Expand Down
2 changes: 2 additions & 0 deletions crates/synthizer/src/signals/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ unsafe impl<Sig, DType> Signal for ConvertOutput<Sig, DType>
where
Sig: Signal,
for<'a> Sig::Output<'a>: Into<DType> + Clone,
DType: 'static,
{
type Output<'ol> = DType;
type Input<'il> = Sig::Input<'il>;
Expand Down Expand Up @@ -79,6 +80,7 @@ impl<Sig, DType> IntoSignal for ConvertOutputConfig<Sig, DType>
where
Sig: IntoSignal,
for<'a> IntoSignalOutput<'a, Sig>: Into<DType> + Clone,
DType: 'static,
{
type Signal = ConvertOutput<Sig::Signal, DType>;

Expand Down
2 changes: 1 addition & 1 deletion crates/synthizer/src/signals/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ unsafe impl<ParSig, F, O> Signal for MapSignal<ParSig, F, O>
where
ParSig: Signal,
F: FnMut(SignalOutput<ParSig>) -> O + Send + Sync + 'static,
O: Send,
O: Send + 'static,
{
type Input<'il> = SignalInput<'il, ParSig>;
type Output<'ol> = O;
Expand Down
Loading

0 comments on commit 79fc6fa

Please sign in to comment.