Expand description
Traits and derive macros for diffing and patching.
Diffing is the process of comparing a piece of data to some
baseline and generating events to describe the differences.
Patching takes these events and applies them to another
instance of this data. The Diff and Patch traits facilitate fine-grained
event generation, meaning they’ll generate events for
only what’s changed.
In typical usage, Diff will be called in non-realtime contexts
like game logic, whereas Patch will be called directly within
audio processors. Consequently, Patch has been optimized for
maximum performance and realtime predictability.
Diff and Patch are derivable,
and most aggregate types should prefer the derive macros over
manual implementations since the diffing data model is not
yet guaranteed to be stable.
§Examples
Aggregate types like node parameters can derive
Diff and Patch as long as each field also
implements these traits.
use firewheel_core::diff::{Diff, Patch};
#[derive(Diff, Patch)]
struct MyParams {
a: f32,
b: (bool, bool),
}The derived implementation produces fine-grained events, making it easy to keep your audio processors in sync with the rest of your code with minimal overhead.
let mut params = MyParams {
a: 1.0,
b: (false, false),
};
let mut baseline = params.clone();
// A change to any arbitrarily nested parameter
// will produce a single event.
params.b.0 = true;
let mut event_queue = Vec::new();
params.diff(&baseline, PathBuilder::default(), &mut event_queue);
// When we apply this patch to another instance of
// the same type, it will be brought in sync.
baseline.patch_event(&event_queue[0]);
assert_eq!(params, baseline);
Both traits can also be derived on enums.
#[derive(Diff, Patch)]
enum MyParams {
Unit,
Tuple(f32, f32),
Struct { a: f32, b: f32 },
}Changing between tuple or struct fields will incur allocations
in Diff, but changes within a single variant are still fine-grained.
It’s important to note that you can accidentally introduce allocations in audio processors by including types that allocate on clone.
#[derive(Diff, Patch)]
enum MaybeAllocates {
A(Vec<f32>), // Will cause allocations in `Patch`!
B(f32),
}Clone types are permitted because Clone does
not always imply allocation. For example, consider
the type:
use firewheel_core::{collector::ArcGc, sample_resource::SampleResource};
#[derive(Diff, Patch)]
enum SoundSource {
Sample(ArcGc<dyn SampleResource>), // Will _not_ cause allocations in `Patch`.
Frequency(f32),
}This bound may be restricted to Copy in the future.
§Macro attributes
Diff and Patch each accept a single attribute, skip, on
struct fields. Any field annotated with skip will not receive
diffing or patching, which is particularly useful for atomically synchronized
types.
use firewheel_core::{collector::ArcGc, diff::{Diff, Patch}};
use core::sync::atomic::AtomicUsize;
#[derive(Diff, Patch)]
struct MultiParadigm {
normal_field: f32,
#[diff(skip)]
atomic_field: ArcGc<AtomicUsize>,
}§Data model
Diffing events are represented as (data, path) pairs. This approach
provides a few important advantages. For one, the fields within nearly
all Rust types can be uniquely addressed with index paths.
#[derive(Diff, Patch, Default)]
struct MyParams {
a: f32,
b: (bool, bool),
}
let params = MyParams::default();
params.a; // [0]
params.b.0; // [1, 0]
params.b.1; // [1, 1]Since these paths can be arbitrarily long, you can arbitrarily
nest implementors of Diff and Patch.
#[derive(Diff, Patch)]
struct Aggregate {
a: MyParams,
b: MyParams,
// Indexable types work great too!
collection: [MyParams; 8],
}Furthermore, since we build up paths during calls to
Diff, the derive macros and implementations only need
to worry about local indexing. And, since the paths
are built only during Diff, we can traverse them
highly performantly during Patch calls in audio processors.
Firewheel provides a number of primitive types in ParamData
that cover most use-cases for audio parameters. For anything
not covered in the concrete variants, you can insert arbitrary
data into ParamData::Any. Since this only incurs allocations
during Diff, this will still be generally performant.
Structs§
- Memo
- A “memoized” parameters wrapper.
- Path
Builder - A simple builder for
ParamPath.
Enums§
- Param
Path - A path of indices that uniquely describes an arbitrarily nested field.
- Patch
Error - An error encountered when patching a type
from
ParamData.
Traits§
- Diff
- Fine-grained parameter diffing.
- Event
Queue - An event queue for diffing.
- Patch
- Fine-grained parameter patching.