Patch types#37
Conversation
|
After creating this PR, I realized that we can make a slightly more ergonomic method for iterating over patches. Something like: events.for_each_patch::<FilterNode>(|p| match p {
FilterNodePatch::CutoffHz(cutoff) => {
self.cutoff_hz.set_value(cutoff.clamp(20.0, 20_000.0));
}
FilterNodePatch::Volume(volume) => {
self.gain.set_value(volume.amp_clamped(DEFAULT_AMP_EPSILON));
}
FilterNodePatch::Enabled(enabled) => {
self.enable_declicker
.fade_to_enabled(enabled, proc_info.declick_values);
}
});Let me know if this seems worth adding! We could replace the events.for_each_patch::<FilterNode>(|p| self.params.apply(p)); |
817d02a to
b2f9beb
Compare
|
After rebasing on main, I've integrated the new derive macros with the sampler node. This should have the same performance characteristics as before (the same number of allocations are created), but now we can patch Detecting sequence completionWhen integrating playback control with This is largely due to the fact that a My solution was to add a specific This solution is not perfect, however. If the sample pool is at max capacity and we choose to play a new sample with a sampler that's currently running, there could be a condition where we clear the flag, set the new sequence in the ECS, and before the event arrives at the sampler node, it finishes its old sequence and sets the flag. The ECS will then interpret this as the new sequence being completed, even thought it hasn't played. This error case could be mitigated with another mechanism. Sequences could have an ID, acquired in the same way that |
|
This PR is a little large, so I could separate out the sampler node changes into a new PR. I've kept it all in one PR because it's easier for me to integrate and verify the new behavior in |
Note: I've made this a draft PR because there's a little bit more documentation work that needs to be done if we adopt this approach. I also need to carefully review each node to make sure I haven't changed their behavior.
This PR introduces explicit patch types, which live as associated types on the
Patchtrait. To update a struct from a Firewheel event,Patchimplementations must now construct their associated type and then apply it.This might seem a little odd at first, but this approach has some major upsides.
First, audio processors can return to reacting to individual field updates just like earlier version of Firewheel. Only this time, users can work with a much easier, type-safe enum:
Second, you can still keep a
Nodetype around and apply patches to it, but you're free to intercept the patches and enforce invariants without disrupting diffing and patching in the main thread.Additionally, with types like
Notify, you can react to events like events. In other words, you can reliably trigger behavior like moving a sample playhead without additional effort or synchronization.Finally, if your type doesn't need any of this, you can just ignore all the new features and use it like before.
Overall, this seems like a huge win-win.
At first glance, you might think this would perform a bit worse than applying mutations directly from the events. However, the benchmarking revealed no changes between the previous patch trait and this one -- even for deeply nested types. It appears that LLVM has an easy time optimizing out the construction of patch types.
Footnote: Diffing and patching A with B should make A == B
If we want to reliably send the minimum amount of data to synchronize normal code and audio code, we need a way to make two pieces of data equal after diffing. Previously,
bevy_seedlingcloned after diffing. This is simple, but may not be efficient depending on what's being cloned.Since publishing, however,
bevy_seedlingnow relies on diffing and patching to rectify two pieces of data. We built patching to be highly efficient and fine-grained, so it is generally much more efficient to apply patches to a struct than to clone it. However, this means that patching must make two structs equal. Enforcing invariants inPatchbreaks this relationship.Luckily, we no longer need this half-measure. Since it's now much easier to work with a patch before it's applied, invariants can be upheld in audio processors directly. Consequently, the only time users will need to implement
Patchmanually is when they're defining leaf types. This should be very rare, since we've already defined most of these (e.g.f32,i32, etc.).