Migrations
Upgrade guides for truce's breaking major releases. Each section is self-contained: follow the steps for the version you're jumping to. For the full release notes behind these changes, see the changelog.
#Upgrading to 3.0 (from 2.x)
3.0 refactored editor into an associated function: it takes the
parameter store as an argument (Arc<Self::Params>) instead of borrowing
the plugin (&self). The editor is now, by construction, a function of
its parameters, so building it can't take the plugin lock or reach into
DSP state. For almost every plugin this is a one-line signature change.
Moving editor also shifted the hot-reload ABI, so a --shell shell and
its logic dylib must be rebuilt and reinstalled together (GUI edits still
hot-reload on the next editor close and reopen; static shipped builds need
nothing).
-
Update each editor. Add
type Params, takeparamsinstead of&self, and use it in the body. For a typical plugin (the editor only readsself.params) that's the whole migration:impl PluginLogic for MyPlugin { + type Params = MyParams; + // reset, process, ... - fn editor(&self) -> Box<dyn Editor> { - default_editor(self.params.clone(), layout()) + fn editor(params: Arc<MyParams>) -> Box<dyn Editor> { + default_editor(params, layout()) } }The same
self.params->paramsswap covers the other backends:.into_editor(¶ms),EguiEditor::new(params, ...),IcedEditor::new(params, ...), and so on. -
Only if an editor read live DSP state at construction (an analyzer handing its spectrum to the GUI, say): it no longer has
self. Route the shared handle through the params struct as a#[skip]field (a non-parameter), fill it innew(), and read it back ineditor:#[derive(Params)] pub struct MyParams { #[param(name = "Gain", /* ... */)] pub gain: FloatParam, + #[skip] + spectrum: Arc<OnceLock<Arc<Spectrum>>>, } fn new(params: Arc<MyParams>) -> Self { let spectrum = Arc::new(Spectrum::new()); + let _ = params.spectrum.set(spectrum.clone()); Self { params, spectrum, /* ... */ } } - fn editor(&self) -> Box<dyn Editor> { - MyEditor::new(self.params.clone(), self.spectrum.clone()) + fn editor(params: Arc<MyParams>) -> Box<dyn Editor> { + let spectrum = params.spectrum.get().expect("set in new()").clone(); + MyEditor::new(params.clone(), spectrum) } -
Rebuild
--shellpairs together. A 3.0 shell won't pair with a 2.x logic dylib; rebuild and reinstall both in one pass. Ordinary (non---shell) builds need nothing beyond step 1.
#Upgrading to 2.0 (from 1.x)
2.0 was a MIDI overhaul: MIDI 2.0 / UMP and multiple MIDI ports, opt-in per plugin. Existing MIDI-1.0, single-port plugins are unchanged at runtime; the API breaks below are what a source upgrade has to touch.
Eventgained aportfield - the MIDI port an event arrived on or should go out on (0for single-port plugins).- Every plugin identity derives from
bundle_id.clap_id/vst3_id/ the state-envelope hash no longer follow the display name, so renaming a plugin no longer changes its identity. EventList::sortis removed. It was a std stable sort, which allocates - unusable on the audio thread.
Steps:
-
Pin your identity first. 2.0 derives
clap_id/vst3_id/ the state hash frombundle_id, not the display name. If a 1.x build ever shipped, setbundle_idto the old name-derived slug (the display name lowercased with spaces stripped: "Truce Envelope" ->truceenvelope); otherwise hosts treat the plugin as new and old sessions won't find it. Unshipped plugins pick any valid id. -
Fix
Eventconstruction. Struct literals need the newportfield; prefer the constructors:- Event { sample_offset, body } + Event::new(sample_offset, body) // port 0, the common case + Event::on_port(sample_offset, port, body) // explicit portReads (
event.port) are unaffected. -
Replace
EventList::sort()withensure_sorted_by_offset(), the allocation-free equivalent with the same stable-by-offset semantics. -
Rebuild everything together. The hot-reload canary and the AU v3 framework/appex handshake are version-checked: a 2.x shell refuses a 1.x logic dylib and the appex refuses a 1.x framework, so rebuild and reinstall all artifacts in one pass instead of mixing.
-
Loads fail honestly now. State the plugin doesn't recognize no longer resets to defaults - implement
migrate_state(plus[plugin.legacy_state]keys for keyed formats) if you need to accept pre-truce or re-identified blobs; otherwise nothing to do. See the state guide. -
MIDI stays opt-in. Single-port MIDI 1.0 plugins behave exactly as on 1.x; add
midi2/midi_input_ports/midi_output_portsonly when you want the new capabilities. See the MIDI guide.