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.apply(MyParams::patch_event(&event_queue[0]).unwrap());
assert_eq!(params, baseline);
Both traits can also be derived on enums.
#[derive(Diff, Patch, Clone, PartialEq)]
enum MyParams {
Unit,
Tuple(f32, f32),
Struct { a: f32, b: f32 },
}However, note that enums will only perform coarse diffing. If a single field in a variant changes, the entire variant will still be sent. As a result, you can accidentally introduce allocations in audio processors by including types that allocate on clone.
#[derive(Diff, Patch, Clone, PartialEq)]
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, Clone, PartialEq)]
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 may be useful for atomically synchronized
types.
use firewheel_core::{collector::ArcGc, diff::{Diff, Patch}};
use bevy_platform::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.
§Preserving invariants
Firewheel’s Patch derive macro cannot make assurances about
your type’s invariants. If two types A and B have similar structures:
struct A {
pub field_one: f32,
pub field_two: f32,
}
struct B {
special_field_one: f32,
special_field_two: f32,
}Then events produced for A are also valid for B.
Receiving events produced by the wrong type is unlikely. Most
types will not need special handling to preserve invariants.
However, if your invariants are safety-critical, you must
implement Patch manually.
Structs§
- Memo
- A “memoized” parameters wrapper.
- Notify
- A lightweight wrapper that guarantees an event will be generated every time the inner value is accessed mutably, even if the value doesn’t change.
- 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.
- Realtime
Clone - A trait which signifies that a struct implements
Clone, cloning does not allocate or deallocate data, and the data will not be dropped on the audio thread if the struct is dropped.
Derive Macros§
- Diff
- Derive macros for diffing and patching.
- Patch
- Derive macros for diffing and patching.
- Realtime
Clone - Derive macros for diffing and patching.
Derive this to signify that a struct implements
Clone, cloning does not allocate or deallocate data, and the data will not be dropped on the audio thread if the struct is dropped.