Module diff

Module diff 

Source
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.
PathBuilder
A simple builder for ParamPath.

Enums§

ParamPath
A path of indices that uniquely describes an arbitrarily nested field.
PatchError
An error encountered when patching a type from ParamData.

Traits§

Diff
Fine-grained parameter diffing.
EventQueue
An event queue for diffing.
Patch
Fine-grained parameter patching.
RealtimeClone
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.
RealtimeClone
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.