Keep one VST3 controller-sync path after merging #306's latest

The landing branch already syncs host-set values to the controller before
saving state (lock-free atomics + EditorLink::sync_param); the PR's later
ControllerPending table did the same, so keep one. Keeps the PR's new
'a loaded state replaces earlier edits' assertion, which passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 06:26:54 -04:00
parent 7a6df4cbbd
commit baea184bd1
3 changed files with 2 additions and 61 deletions

View File

@ -2722,11 +2722,10 @@ mod tests {
let mut eng = MixEngine::new(48_000.0, 512); let mut eng = MixEngine::new(48_000.0, 512);
eng.set_external_instances(true); eng.set_external_instances(true);
let sets = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let sets = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let make = |db: f32| { let make = |blob: &[u8]| {
let mut gain = soundcraft_dsp::create("gain").unwrap(); let mut gain = soundcraft_dsp::create("gain").unwrap();
gain.prepare(48_000.0, 512, 1); gain.prepare(48_000.0, 512, 1);
gain.set_param("gain", db); Box::new(Stateful { inner: gain, blob: blob.to_vec(), sets: Arc::clone(&sets) })
Box::new(Stateful { inner: gain, sets: Arc::clone(&sets) })
}; };
eng.adopt(vec![PreparedInstance { track: t, slot: 0, id: "clap:test.fake".into(), ch: 1, plugin: make(b"old") }]); eng.adopt(vec![PreparedInstance { track: t, slot: 0, id: "clap:test.fake".into(), ch: 1, plugin: make(b"old") }]);
render_block(&mut eng, &s, 0); render_block(&mut eng, &s, 0);

View File

@ -1587,48 +1587,6 @@ fn rect_size(r: &sb::ViewRect) -> (f64, f64) {
(f64::from(w), f64::from(h)) (f64::from(w), f64::from(h))
} }
/// Normalized values the host set (automation, the session) that the edit controller hasn't been
/// told yet, one slot per parameter. The audio thread fills a slot without locking or allocating;
/// [`EditorLink::save_state`] sends what is waiting before it reads the controller's state, so
/// values the controller keeps in its own state are saved as set.
pub(crate) struct ControllerPending {
ids: Vec<u32>,
slots: Vec<AtomicU64>,
}
/// An empty slot: a NaN bit pattern, which `set` never stores.
const NO_VALUE: u64 = u64::MAX;
impl ControllerPending {
pub fn new(ids: &[u32]) -> ControllerPending {
ControllerPending { ids: ids.to_vec(), slots: ids.iter().map(|_| AtomicU64::new(NO_VALUE)).collect() }
}
/// Notes the value of parameter `i` (index into the plugin's parameter list).
pub fn set(&self, i: usize, n: f64) {
if let Some(s) = self.slots.get(i)
&& n.is_finite()
{
s.store(n.clamp(0.0, 1.0).to_bits(), Ordering::Release);
}
}
/// Forgets every waiting value (a loaded state replaces them).
pub fn clear(&self) {
for s in &self.slots {
s.store(NO_VALUE, Ordering::Release);
}
}
/// Takes the waiting values as `(ParamID, normalized)`.
fn take(&self) -> impl Iterator<Item = (u32, f64)> + '_ {
self.ids.iter().zip(&self.slots).filter_map(|(&id, s)| {
let b = s.swap(NO_VALUE, Ordering::AcqRel);
(b != NO_VALUE).then(|| (id, f64::from_bits(b)))
})
}
}
#[derive(Default)] #[derive(Default)]
struct EditorInner { struct EditorInner {
/// `None` once the instance is gone. /// `None` once the instance is gone.
@ -1638,8 +1596,6 @@ struct EditorInner {
view: Option<ComPtr<sb::IPlugView>>, view: Option<ComPtr<sb::IPlugView>>,
frame: Option<ComWrapper<PlugFrame>>, frame: Option<ComWrapper<PlugFrame>>,
window: Option<window::HostWindow>, window: Option<window::HostWindow>,
/// Host-set values for the controller, sent before its state is read.
pending: Option<Arc<ControllerPending>>,
} }
/// The link between an instance and its editor and state handles. The instance may process on /// The link between an instance and its editor and state handles. The instance may process on
@ -1767,13 +1723,6 @@ impl EditorLink {
pub fn save_state(&self) -> Option<Vec<u8>> { pub fn save_state(&self) -> Option<Vec<u8>> {
let g = self.lock(); let g = self.lock();
let component = g.component.as_ref()?; let component = g.component.as_ref()?;
if let (Some(c), Some(p)) = (&g.controller, &g.pending) {
for (id, n) in p.take() {
// SAFETY: valid controller (cleared under this lock before the instance
// terminates); UI thread, as `getState` below.
unsafe { c.setParamNormalized(id, n) };
}
}
// SAFETY: valid component (the instance clears it under this lock before it terminates); // SAFETY: valid component (the instance clears it under this lock before it terminates);
// the stream lives in `read_state` for the whole call. // the stream lives in `read_state` for the whole call.
let comp = Instance::read_state(|s| unsafe { component.getState(s) })?; let comp = Instance::read_state(|s| unsafe { component.getState(s) })?;
@ -1791,11 +1740,6 @@ impl EditorLink {
Some(out) Some(out)
} }
/// Shares the instance's table of host-set values with the state reader.
pub fn set_controller_pending(&self, p: Arc<ControllerPending>) {
self.lock().pending = Some(p);
}
/// Closes the editor. Main thread only (no-op elsewhere). /// Closes the editor. Main thread only (no-op elsewhere).
pub fn close(&self) { pub fn close(&self) {
if window::on_main_thread() { if window::on_main_thread() {

View File

@ -114,7 +114,6 @@ impl Vst3Plugin {
}) })
.collect(); .collect();
p.values = values; p.values = values;
p.inst.state_link().set_controller_pending(Arc::clone(&p.controller_pending));
p p
} }
@ -235,7 +234,6 @@ impl Plugin for Vst3Plugin {
} else if self.pending.len() < self.pending.capacity() { } else if self.pending.len() < self.pending.capacity() {
self.pending.push((vid, n)); self.pending.push((vid, n));
} }
self.controller_pending.set(i, n);
true true
} }