truce_core/custom_state.rs
1//! Custom state serialization for plugin-specific persistent data.
2//!
3//! Use `#[derive(State)]` on a struct to auto-generate binary serialization:
4//!
5//! ```ignore
6//! #[derive(State, Default)]
7//! pub struct MyState {
8//! pub instance_name: String,
9//! pub view_mode: u8,
10//! pub selected_ids: Vec<u32>,
11//! }
12//! ```
13//!
14//! Then persist it through your plugin's `snapshot_into`/`load_state` -
15//! the real-time-safe custom-state path. `snapshot_into` publishes into a
16//! lock-free slot the host reads without stalling audio, and CLAP / VST3
17//! / AU persist from it:
18//!
19//! ```ignore
20//! fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
21//! buf.extend_from_slice(&state.persistent.serialize());
22//! true
23//! }
24//! fn load_state(state: &mut Self::DspState, data: &[u8]) -> Result<(), StateLoadError> {
25//! match MyState::deserialize(data) {
26//! Some(s) => { state.persistent = s; Ok(()) }
27//! None => Err(StateLoadError::Malformed("MyState")),
28//! }
29//! }
30//! ```
31//!
32//! For MB-scale state (a sampler's audio, big wavetables), publish it off
33//! the audio thread via `InitContext::snapshot_publisher()` instead. The
34//! older `save_state` still works, but on CLAP / VST3 / AU it runs on the
35//! audio thread, so prefer `snapshot_into`.
36//!
37//! ## Schema evolution
38//!
39//! The codec is **keyed by field name** (like `#[persist]`), so evolving
40//! the struct is safe: adding, removing, *and* reordering fields all keep
41//! every surviving field's value. A field absent from an older blob loads
42//! as its `Default`; a field present in the blob but no longer declared is
43//! ignored. Two fields whose names collide under the key hash are rejected
44//! at compile time. Pre-keyed blobs (saved by an older truce) still load
45//! through a legacy positional path, so existing sessions are unaffected.
46
47/// Re-exported so plugin authors name the lock-free `#[persist]` cell
48/// as `AtomicCell` through the prelude, without depending on crossbeam.
49pub use crossbeam_utils::atomic::AtomicCell;
50
51/// Cursor for reading binary state data.
52pub struct StateCursor<'a> {
53 data: &'a [u8],
54 pos: usize,
55}
56
57impl<'a> StateCursor<'a> {
58 #[must_use]
59 pub fn new(data: &'a [u8]) -> Self {
60 Self { data, pos: 0 }
61 }
62
63 #[must_use]
64 pub fn remaining(&self) -> usize {
65 self.data.len().saturating_sub(self.pos)
66 }
67
68 pub fn read_bytes(&mut self, n: usize) -> Option<&'a [u8]> {
69 if self.pos + n > self.data.len() {
70 return None;
71 }
72 let slice = &self.data[self.pos..self.pos + n];
73 self.pos += n;
74 Some(slice)
75 }
76
77 /// Skip the next field (reads its encoded size and advances past it).
78 /// Returns false if the data is malformed.
79 ///
80 /// # Panics
81 ///
82 /// Does not panic - the `expect` inside is unreachable because
83 /// `read_bytes(4)` only returns `Some` when the slice is exactly
84 /// 4 bytes long.
85 pub fn skip_field(&mut self) -> bool {
86 // Fields are prefixed with a u32 byte length by the derive macro.
87 if let Some(bytes) = self.read_bytes(4) {
88 // `read_bytes(4)` returns `Some(slice of length 4)` or
89 // `None`, so the `try_into::<[u8; 4]>()` here cannot fail.
90 // The `expect` documents that invariant for readers.
91 let len = u32::from_le_bytes(
92 bytes
93 .try_into()
94 .expect("read_bytes(4) returned a slice of unexpected length"),
95 ) as usize;
96 if self.pos + len <= self.data.len() {
97 self.pos += len;
98 return true;
99 }
100 }
101 false
102 }
103}
104
105/// Trait for types that can be serialized as a single state field.
106///
107/// Implemented for primitives, `String`, `Vec<T>`, and `Option<T>`.
108pub trait StateField: Sized {
109 fn write_field(&self, buf: &mut Vec<u8>);
110 fn read_field(cursor: &mut StateCursor) -> Option<Self>;
111}
112
113/// Trait for custom plugin state structs.
114///
115/// Derive with `#[derive(State)]`. The struct must also implement `Default`
116/// so a field absent from an older (or reordered / trimmed) blob loads as
117/// its default - the codec matches fields by name, so schema evolution is
118/// safe in every direction (see the module docs).
119pub trait State: Sized + Default {
120 /// Serialize into `buf`. Clears `buf` first, then reuses its
121 /// capacity - calling this repeatedly with the same buffer is
122 /// allocation-free once warmed, so it is the form to use on the
123 /// audio thread (e.g. `PluginLogic::snapshot_into`).
124 fn serialize_into(&self, buf: &mut Vec<u8>);
125
126 /// Serialize to a fresh `Vec`. Convenience wrapper over
127 /// [`Self::serialize_into`]; allocates, so prefer `serialize_into`
128 /// on the real-time path.
129 #[must_use]
130 fn serialize(&self) -> Vec<u8> {
131 let mut buf = Vec::new();
132 self.serialize_into(&mut buf);
133 buf
134 }
135
136 fn deserialize(data: &[u8]) -> Option<Self>;
137}
138
139/// A `#[persist]` field on a `#[derive(Params)]` struct: an
140/// interior-mutable, `Sync` wrapper around a [`StateField`] value that
141/// the host saves alongside the parameter values (session / preset) and
142/// restores on load. Implemented for `RwLock<T>` / `Mutex<T>` where
143/// `T: StateField` (a primitive, `String`, `Vec`, `Option`, or a
144/// `#[derive(State)]` struct - the derive also emits a `StateField`
145/// impl). Interior mutability is required because a load reaches the
146/// field through `&Params` (the store is shared via `Arc<Params>`).
147pub trait PersistField {
148 /// Append the current value's bytes.
149 fn persist_write(&self, buf: &mut Vec<u8>);
150 /// Read a value from `cursor` and store it in place. A short or
151 /// malformed read leaves the current value untouched.
152 fn persist_read(&self, cursor: &mut StateCursor);
153}
154
155impl<T: StateField> PersistField for std::sync::RwLock<T> {
156 fn persist_write(&self, buf: &mut Vec<u8>) {
157 if let Ok(guard) = self.read() {
158 guard.write_field(buf);
159 }
160 }
161 fn persist_read(&self, cursor: &mut StateCursor) {
162 if let Some(value) = T::read_field(cursor)
163 && let Ok(mut guard) = self.write()
164 {
165 *guard = value;
166 }
167 }
168}
169
170impl<T: StateField> PersistField for std::sync::Mutex<T> {
171 fn persist_write(&self, buf: &mut Vec<u8>) {
172 if let Ok(guard) = self.lock() {
173 guard.write_field(buf);
174 }
175 }
176 fn persist_read(&self, cursor: &mut StateCursor) {
177 if let Some(value) = T::read_field(cursor)
178 && let Ok(mut guard) = self.lock()
179 {
180 *guard = value;
181 }
182 }
183}
184
185/// Lock-free-friendly cell for `Copy` config: a persisted `f32`, small
186/// enum, or index reads as `AtomicCell<T>` instead of sitting behind a
187/// `Mutex`. `AtomicCell` is genuinely atomic for word-sized types and
188/// falls back to an internal lock for larger ones.
189impl<T: StateField + Copy> PersistField for AtomicCell<T> {
190 fn persist_write(&self, buf: &mut Vec<u8>) {
191 self.load().write_field(buf);
192 }
193 fn persist_read(&self, cursor: &mut StateCursor) {
194 if let Some(value) = T::read_field(cursor) {
195 self.store(value);
196 }
197 }
198}
199
200// ---------------------------------------------------------------------------
201// StateField implementations for primitives
202// ---------------------------------------------------------------------------
203
204macro_rules! impl_state_field_int {
205 ($($ty:ty),*) => {
206 $(
207 impl StateField for $ty {
208 fn write_field(&self, buf: &mut Vec<u8>) {
209 buf.extend_from_slice(&self.to_le_bytes());
210 }
211 fn read_field(cursor: &mut StateCursor) -> Option<Self> {
212 let bytes = cursor.read_bytes(std::mem::size_of::<Self>())?;
213 Some(Self::from_le_bytes(bytes.try_into().ok()?))
214 }
215 }
216 )*
217 };
218}
219
220impl_state_field_int!(u8, u16, u32, u64, i8, i16, i32, i64, f32, f64);
221
222impl StateField for bool {
223 fn write_field(&self, buf: &mut Vec<u8>) {
224 buf.push(u8::from(*self));
225 }
226 fn read_field(cursor: &mut StateCursor) -> Option<Self> {
227 let b = cursor.read_bytes(1)?;
228 Some(b[0] != 0)
229 }
230}
231
232impl StateField for String {
233 fn write_field(&self, buf: &mut Vec<u8>) {
234 let bytes = self.as_bytes();
235 crate::cast::len_u32(bytes.len()).write_field(buf);
236 buf.extend_from_slice(bytes);
237 }
238 fn read_field(cursor: &mut StateCursor) -> Option<Self> {
239 let len = u32::read_field(cursor)? as usize;
240 let bytes = cursor.read_bytes(len)?;
241 String::from_utf8(bytes.to_vec()).ok()
242 }
243}
244
245impl<T: StateField> StateField for Vec<T> {
246 fn write_field(&self, buf: &mut Vec<u8>) {
247 crate::cast::len_u32(self.len()).write_field(buf);
248 for item in self {
249 item.write_field(buf);
250 }
251 }
252 fn read_field(cursor: &mut StateCursor) -> Option<Self> {
253 let len = u32::read_field(cursor)? as usize;
254 let mut vec = Vec::with_capacity(len.min(1024));
255 for _ in 0..len {
256 vec.push(T::read_field(cursor)?);
257 }
258 Some(vec)
259 }
260}
261
262impl<T: StateField> StateField for Option<T> {
263 fn write_field(&self, buf: &mut Vec<u8>) {
264 match self {
265 Some(val) => {
266 1u8.write_field(buf);
267 val.write_field(buf);
268 }
269 None => {
270 0u8.write_field(buf);
271 }
272 }
273 }
274 fn read_field(cursor: &mut StateCursor) -> Option<Self> {
275 let tag = u8::read_field(cursor)?;
276 if tag == 0 {
277 Some(None)
278 } else {
279 Some(Some(T::read_field(cursor)?))
280 }
281 }
282}
283
284// ---------------------------------------------------------------------------
285// StateBinding - typed wrapper for editor state access
286// ---------------------------------------------------------------------------
287
288use crate::editor::PluginContext;
289use std::sync::Arc;
290
291/// Typed state binding for editors.
292///
293/// Wraps the `get_state`/`set_state` closures from `PluginContext` with
294/// typed serialization. Caches the deserialized state to avoid repeated
295/// deserialization each frame.
296///
297/// ```ignore
298/// struct MyEditor {
299/// state: StateBinding<PersistentState>,
300/// }
301///
302/// // In open():
303/// self.state = StateBinding::new(&context);
304///
305/// // In state_changed():
306/// self.state.sync();
307///
308/// // Reading:
309/// let name = &self.state.get().instance_name;
310///
311/// // Writing:
312/// self.state.update(|s| s.instance_name = new_name);
313/// ```
314pub struct StateBinding<T: State> {
315 cached: T,
316 get_state: Arc<dyn Fn() -> Vec<u8> + Send + Sync>,
317 set_state: Arc<dyn Fn(Vec<u8>) + Send + Sync>,
318}
319
320impl<T: State> StateBinding<T> {
321 /// Create a new binding from a [`PluginContext`]. Generic over the
322 /// context's `<P>` since `StateBinding` cares only about the
323 /// `get_state` / `set_state` channel on the underlying
324 /// `EditorBridge`, never about parameter typing.
325 #[must_use]
326 pub fn new<P: ?Sized>(context: &PluginContext<P>) -> Self {
327 let bridge_for_get = Arc::clone(context.bridge());
328 let bridge_for_set = Arc::clone(context.bridge());
329 let mut binding = Self {
330 cached: T::default(),
331 get_state: Arc::new(move || bridge_for_get.get_state()),
332 set_state: Arc::new(move |data| bridge_for_set.set_state(data)),
333 };
334 binding.sync();
335 binding
336 }
337
338 /// Re-read state from the plugin. Call this from `state_changed()`.
339 pub fn sync(&mut self) {
340 let data = (self.get_state)();
341 if !data.is_empty()
342 && let Some(s) = T::deserialize(&data)
343 {
344 self.cached = s;
345 }
346 }
347
348 /// Get the current cached state.
349 pub fn get(&self) -> &T {
350 &self.cached
351 }
352
353 /// Modify state and write it back to the plugin.
354 pub fn update(&mut self, f: impl FnOnce(&mut T)) {
355 f(&mut self.cached);
356 let data = self.cached.serialize();
357 (self.set_state)(data);
358 }
359}
360
361impl<T: State> Default for StateBinding<T> {
362 /// Construct an **unwired** binding: `get()` returns `T::default()`
363 /// and `update()` *silently discards* the new state. Only useful
364 /// as a placeholder before the editor is opened; replace with
365 /// [`StateBinding::new(&context)`](StateBinding::new) inside
366 /// `Editor::open` once a [`PluginContext`] is available. If you
367 /// see writes vanishing, check that the binding has been wired up
368 /// before you call `update`.
369 fn default() -> Self {
370 Self {
371 cached: T::default(),
372 get_state: Arc::new(Vec::new),
373 set_state: Arc::new(|_| {}),
374 }
375 }
376}
377
378// ---------------------------------------------------------------------------
379// Tests
380// ---------------------------------------------------------------------------
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn primitives_round_trip() {
388 let mut buf = Vec::new();
389 42u32.write_field(&mut buf);
390 2.5f64.write_field(&mut buf);
391 true.write_field(&mut buf);
392
393 let mut cursor = StateCursor::new(&buf);
394 assert_eq!(u32::read_field(&mut cursor), Some(42));
395 assert_eq!(f64::read_field(&mut cursor), Some(2.5));
396 assert_eq!(bool::read_field(&mut cursor), Some(true));
397 }
398
399 #[test]
400 fn string_round_trip() {
401 let mut buf = Vec::new();
402 "hello world".to_string().write_field(&mut buf);
403
404 let mut cursor = StateCursor::new(&buf);
405 assert_eq!(
406 String::read_field(&mut cursor),
407 Some("hello world".to_string())
408 );
409 }
410
411 #[test]
412 fn vec_round_trip() {
413 let mut buf = Vec::new();
414 vec![1u32, 2, 3].write_field(&mut buf);
415
416 let mut cursor = StateCursor::new(&buf);
417 assert_eq!(Vec::<u32>::read_field(&mut cursor), Some(vec![1, 2, 3]));
418 }
419
420 #[test]
421 fn option_round_trip() {
422 let mut buf = Vec::new();
423 Some(42u32).write_field(&mut buf);
424 None::<u32>.write_field(&mut buf);
425
426 let mut cursor = StateCursor::new(&buf);
427 assert_eq!(Option::<u32>::read_field(&mut cursor), Some(Some(42)));
428 assert_eq!(Option::<u32>::read_field(&mut cursor), Some(None));
429 }
430
431 #[test]
432 fn nested_vec_string() {
433 let mut buf = Vec::new();
434 let v = vec!["foo".to_string(), "bar".to_string()];
435 v.write_field(&mut buf);
436
437 let mut cursor = StateCursor::new(&buf);
438 assert_eq!(Vec::<String>::read_field(&mut cursor), Some(v));
439 }
440
441 #[test]
442 fn persist_field_lock_round_trip() {
443 let src = std::sync::RwLock::new("guitar bus".to_string());
444 let mut buf = Vec::new();
445 src.persist_write(&mut buf);
446
447 let dst = std::sync::RwLock::new(String::new());
448 dst.persist_read(&mut StateCursor::new(&buf));
449 assert_eq!(*dst.read().unwrap(), "guitar bus");
450
451 let m_src = std::sync::Mutex::new(vec![1u32, 2, 3]);
452 let mut m_buf = Vec::new();
453 m_src.persist_write(&mut m_buf);
454 let m_dst = std::sync::Mutex::new(Vec::<u32>::new());
455 m_dst.persist_read(&mut StateCursor::new(&m_buf));
456 assert_eq!(*m_dst.lock().unwrap(), vec![1, 2, 3]);
457 }
458
459 #[test]
460 fn persist_field_atomic_cell_round_trip() {
461 let src = AtomicCell::new(0.75f32);
462 let mut buf = Vec::new();
463 src.persist_write(&mut buf);
464
465 let dst = AtomicCell::new(0.0f32);
466 dst.persist_read(&mut StateCursor::new(&buf));
467 // Bit-exact: the value round-trips through `to_le_bytes`.
468 assert_eq!(dst.load().to_bits(), 0.75f32.to_bits());
469 }
470
471 #[test]
472 fn persist_field_leaves_value_on_short_read() {
473 // A truncated blob must not clobber the current value.
474 let dst = AtomicCell::new(42u32);
475 dst.persist_read(&mut StateCursor::new(&[0xFF, 0xFF]));
476 assert_eq!(dst.load(), 42);
477 }
478}