vst3_host/realtime.rs
1//! Lock-free real-time plugin runner.
2//!
3//! [`Vst3Host::play`](crate::Vst3Host::play) / [`simple::play`](crate::simple::play) are the
4//! friendly path: they wrap the plugin in an `Arc<Mutex<Plugin>>` and the audio callback
5//! locks it. That's correctness-first but not hard-real-time — a control-thread call can
6//! contend with the audio thread for the lock.
7//!
8//! [`RealtimePluginRunner`] is the serious path *alongside* it. The runner **owns** the
9//! plugin on the audio thread; control commands (MIDI, parameter changes) are delivered over
10//! a lock-free SPSC ring and applied at the start of each block. The audio callback never
11//! takes a lock a control thread could be holding, so it can't be blocked by `set_parameter`
12//! or `send_midi`.
13//!
14//! ```no_run
15//! use vst3_host::{simple, realtime::RealtimePluginRunner, midi::MidiChannel, audio::AudioBuffers};
16//! # fn main() -> vst3_host::Result<()> {
17//! let plugin = simple::load_plugin("/path/synth.vst3")?;
18//! let (mut runner, mut control) = RealtimePluginRunner::new(plugin, 1024);
19//! runner.start()?;
20//!
21//! // From any thread: queue control changes without locking the audio thread.
22//! control.send_midi(vst3_host::midi::MidiEvent::NoteOn { channel: MidiChannel::Ch1, note: 60, velocity: 100 });
23//!
24//! // On the audio thread (e.g. your device callback): drain commands + render, no locks.
25//! let mut buffers = AudioBuffers::new(0, 2, 512, 48_000.0);
26//! runner.process(&mut buffers)?;
27//!
28//! // Stop the audio side, then perform thread-affine COM teardown here.
29//! runner.stop()?;
30//! drop(runner);
31//! let _destroyed = control.service_teardown();
32//! # Ok(())
33//! # }
34//! ```
35
36use crate::{audio::AudioBuffers, error::Result, midi::MidiEvent, plugin::Plugin};
37use rtrb::{Consumer, Producer, RingBuffer};
38use std::{
39 mem::ManuallyDrop,
40 sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendError},
41 thread::{self, ThreadId},
42};
43
44/// Whether `value` is a usable normalized parameter value: finite and within `0.0..=1.0`.
45///
46/// Checked on the control thread before a parameter change is queued. A bad value that reached
47/// the ring would be rejected inside the plugin on the *audio* thread, where the rejection
48/// allocates an error string and is then discarded — long after the caller was told the change
49/// had been accepted.
50pub(crate) fn is_normalized(value: f64) -> bool {
51 value.is_finite() && (0.0..=1.0).contains(&value)
52}
53
54/// Drain at most one ring's worth of queued commands from `rx`, handing each to `apply`.
55/// Returns how many were applied.
56///
57/// The bound is what makes this safe to call from an audio callback: the ring never fills while
58/// a drain keeps pace with it, so an unbounded `while let Ok(..) = pop()` lets a control thread
59/// pushing in a tight loop pin the callback for as long as it keeps pushing. Commands still
60/// queued when the budget runs out are applied on the next block.
61pub(crate) fn drain_commands<T>(rx: &mut Consumer<T>, mut apply: impl FnMut(T)) -> usize {
62 let budget = rx.buffer().capacity();
63 let mut applied = 0;
64 for _ in 0..budget {
65 let Ok(command) = rx.pop() else {
66 break;
67 };
68 apply(command);
69 applied += 1;
70 }
71 applied
72}
73
74/// A runtime transport change applied to the plugin's host `ProcessContext` on the audio
75/// thread, taking effect on the next block. Shared by the lock-free runner and the
76/// mutex-based playback path so both apply transport mutation the same way.
77#[derive(Clone, Copy)]
78pub(crate) enum TransportCommand {
79 /// Set the transport tempo (BPM).
80 Tempo(f64),
81 /// Set the transport time signature (`numerator`, `denominator`).
82 TimeSignature(i32, i32),
83 /// Toggle the transport playing state.
84 Playing(bool),
85}
86
87impl TransportCommand {
88 /// Apply this transport change to the plugin, ignoring errors as the audio thread does for
89 /// all queued control. The value was validated on the control thread before being queued.
90 pub(crate) fn apply(self, plugin: &mut Plugin) {
91 match self {
92 TransportCommand::Tempo(bpm) => {
93 let _ = plugin.set_tempo(bpm);
94 }
95 TransportCommand::TimeSignature(num, den) => {
96 let _ = plugin.set_time_signature(num, den);
97 }
98 TransportCommand::Playing(playing) => {
99 let _ = plugin.set_playing(playing);
100 }
101 }
102 }
103}
104
105/// A control command applied to the plugin on the audio thread.
106enum RtCommand {
107 /// Deliver a MIDI event at `offset` samples into the next block.
108 Midi { event: MidiEvent, offset: i32 },
109 /// Set a normalized parameter value on the next block.
110 ///
111 /// Applying this only adds a point to the processor's next input-parameter queue.
112 /// `IEditController::setParamNormalized` is never invoked from the audio callback.
113 Param { id: u32, value: f64 },
114 /// Apply a transport change (tempo / time signature / playing) on the next block.
115 Transport(TransportCommand),
116}
117
118/// Owns a [`Plugin`] on the audio thread and applies queued control commands before each
119/// process block. Pair with an [`RtControl`] (returned from [`Self::new`]) to drive it from
120/// other threads.
121///
122/// # Real-time safety
123///
124/// In steady state [`process`](Self::process) is **allocation-free and `Drop`-free**: once
125/// warmed up it performs no heap allocation, reallocation, or free per block, even while
126/// parameter changes and MIDI (in and out) are flowing. This holds under two conditions:
127///
128/// - **Fixed buffer size** — pass an [`AudioBuffers`] sized to the configured block size and
129/// don't resize it between calls (a smaller block is fine; growth reallocates).
130/// - **In-process** — the runner hosts the plugin in-process; the process-isolation path
131/// marshals audio over IPC and is not allocation-free.
132///
133/// This is verified by `tests/alloc_tests.rs` (a counting global allocator asserts zero
134/// alloc/realloc/free over a steady-state run driving parameters and MIDI). The host cannot
135/// guarantee the *plugin's* own `process()` is allocation-free — that is the plugin's
136/// responsibility; the guarantee is about the host code around it.
137///
138/// # Threading model
139///
140/// Queued parameter and mapped-MIDI commands populate the processor's input parameter queues on
141/// the audio thread and park the same values for `IEditController`, which is a main-thread-domain
142/// interface: the plugin applies them when a control thread next touches it (see
143/// [`RtControl::set_parameter`]). No controller call is ever made from this runner.
144///
145/// It is **not yet fully lock-free**: `process` still takes a few short, uncontended mutexes
146/// per block (the parameter-change and event queues, and the level meter). They are uncontended
147/// while the runner owns the plugin, but a hard-real-time deployment should treat lock removal
148/// as pending work. Output MIDI is already lock-free, though: take a
149/// [`OutputMidiConsumer`](crate::OutputMidiConsumer) via
150/// [`Plugin::output_midi_handle`](crate::Plugin::output_midi_handle) before moving the plugin
151/// into the runner, then drain emitted events from your UI thread while the audio thread pushes.
152pub struct RealtimePluginRunner {
153 plugin: Option<Plugin>,
154 rx: Consumer<RtCommand>,
155 teardown_tx: SyncSender<ManuallyDrop<Plugin>>,
156}
157
158/// A `Send` handle for pushing MIDI and parameter changes to a [`RealtimePluginRunner`]
159/// without locking. The runner lives on the audio thread; this handle may move between threads,
160/// but plugin teardown is serviced only on the thread where [`RealtimePluginRunner::new`] created
161/// it.
162pub struct RtControl {
163 tx: Producer<RtCommand>,
164 /// Count of commands dropped because the queue was full (observability).
165 dropped: u64,
166 teardown: OwnerThreadTeardown<Plugin>,
167}
168
169/// The receive half of a one-slot teardown handoff.
170///
171/// Values are wrapped in `ManuallyDrop` before they enter the channel. This is important:
172/// destroying a disconnected receiver normally drops queued values on whichever thread drops
173/// the receiver. Here an off-owner drop leaks queued values instead, preserving the plugin's COM
174/// and module-unload thread affinity.
175struct OwnerThreadTeardown<T> {
176 rx: Receiver<ManuallyDrop<T>>,
177 owner_thread: ThreadId,
178}
179
180impl<T> OwnerThreadTeardown<T> {
181 fn service_one(&mut self) -> bool {
182 if thread::current().id() != self.owner_thread {
183 return false;
184 }
185 match self.rx.try_recv() {
186 Ok(value) => {
187 drop(ManuallyDrop::into_inner(value));
188 true
189 }
190 Err(TryRecvError::Empty | TryRecvError::Disconnected) => false,
191 }
192 }
193}
194
195impl<T> Drop for OwnerThreadTeardown<T> {
196 fn drop(&mut self) {
197 if thread::current().id() != self.owner_thread {
198 return;
199 }
200 while self.service_one() {}
201 }
202}
203
204fn teardown_handoff<T>() -> (SyncSender<ManuallyDrop<T>>, OwnerThreadTeardown<T>) {
205 let (tx, rx) = sync_channel(1);
206 (
207 tx,
208 OwnerThreadTeardown {
209 rx,
210 owner_thread: thread::current().id(),
211 },
212 )
213}
214
215/// Hand a value to its owner thread without blocking. Both error variants deliberately leak the
216/// `ManuallyDrop` payload when the receiver is gone or the one-slot queue is occupied.
217fn try_handoff_teardown<T>(tx: &SyncSender<ManuallyDrop<T>>, value: T) -> bool {
218 match tx.try_send(ManuallyDrop::new(value)) {
219 Ok(()) => true,
220 Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => false,
221 }
222}
223
224impl RealtimePluginRunner {
225 /// Build a runner that owns `plugin`, plus the [`RtControl`] handle to drive it.
226 ///
227 /// `command_capacity` is the maximum number of MIDI/parameter commands that can be
228 /// queued between two [`process`](Self::process) calls; pushes beyond it are dropped
229 /// (reported by the `RtControl` methods returning `false`). Size it for your block rate
230 /// and worst-case control burst (e.g. 1024).
231 ///
232 /// Call this on the same control thread that loaded the plugin. If the runner is later
233 /// dropped on an audio thread, the plugin is handed back to the returned [`RtControl`] for
234 /// destruction on this thread. Call [`RtControl::service_teardown`] after the runner has
235 /// stopped, or drop the control on this thread.
236 pub fn new(plugin: Plugin, command_capacity: usize) -> (Self, RtControl) {
237 let (tx, rx) = RingBuffer::new(command_capacity.max(1));
238 let (teardown_tx, teardown) = teardown_handoff();
239 (
240 Self {
241 plugin: Some(plugin),
242 rx,
243 teardown_tx,
244 },
245 RtControl {
246 tx,
247 dropped: 0,
248 teardown,
249 },
250 )
251 }
252
253 /// Begin processing. Call once before the first [`process`](Self::process).
254 pub fn start(&mut self) -> Result<()> {
255 self.plugin
256 .as_mut()
257 .expect("runner plugin missing")
258 .start_processing()
259 }
260
261 /// Stop processing.
262 pub fn stop(&mut self) -> Result<()> {
263 self.plugin
264 .as_mut()
265 .expect("runner plugin missing")
266 .stop_processing()
267 }
268
269 /// Drain queued control commands and render one block.
270 ///
271 /// Call this from the audio thread (e.g. inside your device callback). It performs only
272 /// the lock-free queue drain plus the plugin's own processing — it never blocks on a lock
273 /// a control thread could hold.
274 ///
275 /// The drain is bounded by the command queue's capacity. A control thread pushing in a
276 /// tight loop refills the queue as fast as this drains it, so an unbounded drain would pin
277 /// the audio callback; anything still queued is applied on the next block instead.
278 pub fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
279 let plugin = self.plugin.as_mut().expect("runner plugin missing");
280 let rx = &mut self.rx;
281 drain_commands(rx, |command| match command {
282 RtCommand::Midi { event, offset } => {
283 let _ = plugin.send_midi_event_at(event, offset);
284 }
285 RtCommand::Param { id, value } => {
286 let _ = plugin.queue_processor_parameter_at(id, value, 0);
287 }
288 RtCommand::Transport(change) => {
289 change.apply(plugin);
290 }
291 });
292 plugin.process_audio(buffers)
293 }
294
295 /// Borrow the underlying plugin (e.g. to read parameters or info). Do **not** call this
296 /// from the audio thread while another thread might also touch the plugin.
297 pub fn plugin(&self) -> &Plugin {
298 self.plugin.as_ref().expect("runner plugin missing")
299 }
300
301 /// Recover the owned plugin, consuming the runner.
302 pub fn into_plugin(mut self) -> Plugin {
303 self.plugin.take().expect("runner plugin missing")
304 }
305}
306
307impl Drop for RealtimePluginRunner {
308 fn drop(&mut self) {
309 let Some(plugin) = self.plugin.take() else {
310 return;
311 };
312 // Never destroy the plugin on this (possibly real-time) thread. The bounded handoff is
313 // nonblocking; a disconnected/full queue leaks rather than running COM termination or
314 // unloading executable code here.
315 let _ = try_handoff_teardown(&self.teardown_tx, plugin);
316 }
317}
318
319impl RtControl {
320 /// Destroy a plugin handed back by a dropped [`RealtimePluginRunner`].
321 ///
322 /// This call never waits for the runner. It returns `true` only when a pending plugin was
323 /// destroyed. It must be called on the thread where [`RealtimePluginRunner::new`] created
324 /// this control; calls from any other thread return `false` and leave the handoff queued.
325 ///
326 /// Dropping `RtControl` on its creation thread services any pending handoff automatically.
327 /// Dropping it elsewhere deliberately leaks a pending plugin rather than releasing COM
328 /// objects and unloading the plugin bundle on the wrong thread.
329 pub fn service_teardown(&mut self) -> bool {
330 self.teardown.service_one()
331 }
332
333 /// Queue a MIDI event for the next block (at block start). Returns `false` if the command
334 /// queue is full (the event is dropped rather than blocking the caller).
335 pub fn send_midi(&mut self, event: MidiEvent) -> bool {
336 self.send_midi_at(event, 0)
337 }
338
339 /// Queue a MIDI event scheduled at `sample_offset` samples into the next block, for
340 /// sample-accurate sequencing. A negative offset is floored to `0`; `process()` clamps it
341 /// into the actual (possibly shorter) block. Returns `false` if the queue is full.
342 pub fn send_midi_at(&mut self, event: MidiEvent, sample_offset: i32) -> bool {
343 let ok = self
344 .tx
345 .push(RtCommand::Midi {
346 event,
347 offset: sample_offset.max(0),
348 })
349 .is_ok();
350 self.track(ok)
351 }
352
353 /// Queue a normalized parameter change for the next block. `value` must be finite and
354 /// within `0.0..=1.0`; an invalid value is rejected here (returns `false`) rather than
355 /// queued, so the caller learns about it instead of the audio thread silently discarding
356 /// it. Returns `false` if the queue is full.
357 ///
358 /// # The editor catches up later
359 ///
360 /// The audio thread applies the value to the plugin's DSP, but `IEditController` belongs to
361 /// the main-thread domain, so the plugin's *own editor* (and
362 /// [`Plugin::get_parameter`](crate::Plugin::get_parameter),
363 /// [`format_parameter`](crate::Plugin::format_parameter) and saved state) is updated from
364 /// the control thread instead. That happens the next time the control thread touches the
365 /// plugin — reading a parameter, draining
366 /// [`Plugin::get_parameter_changes`](crate::Plugin::get_parameter_changes), or calling
367 /// [`Plugin::service_host_requests`](crate::Plugin::service_host_requests). A host that
368 /// polls the plugin every UI frame (the usual editor loop) never notices the gap; a host
369 /// that never calls back in will see a stale editor. The queue is bounded and drops its
370 /// oldest entry when full, so the newest value for a parameter always wins.
371 pub fn set_parameter(&mut self, id: u32, value: f64) -> bool {
372 if !is_normalized(value) {
373 return false;
374 }
375 let ok = self.tx.push(RtCommand::Param { id, value }).is_ok();
376 self.track(ok)
377 }
378
379 /// Queue a transport tempo change (BPM) for the next block. `bpm` must be finite and
380 /// greater than `0`; an invalid value is rejected (returns `false`) rather than queued.
381 /// Returns `false` if the queue is full.
382 pub fn set_tempo(&mut self, bpm: f64) -> bool {
383 if !(bpm.is_finite() && bpm > 0.0) {
384 return false;
385 }
386 let ok = self
387 .tx
388 .push(RtCommand::Transport(TransportCommand::Tempo(bpm)))
389 .is_ok();
390 self.track(ok)
391 }
392
393 /// Queue a transport time-signature change for the next block. `denominator` must be one
394 /// of `1, 2, 4, 8, 16` and `numerator` must be positive; an invalid value is rejected
395 /// (returns `false`). Returns `false` if the queue is full.
396 pub fn set_time_signature(&mut self, numerator: i32, denominator: i32) -> bool {
397 if numerator <= 0 || !matches!(denominator, 1 | 2 | 4 | 8 | 16) {
398 return false;
399 }
400 let ok = self
401 .tx
402 .push(RtCommand::Transport(TransportCommand::TimeSignature(
403 numerator,
404 denominator,
405 )))
406 .is_ok();
407 self.track(ok)
408 }
409
410 /// Queue a transport playing-state toggle for the next block. Returns `false` if the queue
411 /// is full.
412 pub fn set_playing(&mut self, playing: bool) -> bool {
413 let ok = self
414 .tx
415 .push(RtCommand::Transport(TransportCommand::Playing(playing)))
416 .is_ok();
417 self.track(ok)
418 }
419
420 /// Total number of commands dropped because the queue was full since this control was
421 /// created. A persistently rising count means the queue capacity is too small for the
422 /// control rate.
423 pub fn dropped_command_count(&self) -> u64 {
424 self.dropped
425 }
426
427 fn track(&mut self, ok: bool) -> bool {
428 if !ok {
429 self.dropped += 1;
430 }
431 ok
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::midi::MidiChannel;
439 use std::sync::{
440 atomic::{AtomicUsize, Ordering},
441 Arc, Mutex,
442 };
443
444 fn test_control(tx: Producer<RtCommand>) -> RtControl {
445 let (_teardown_tx, teardown) = teardown_handoff();
446 RtControl {
447 tx,
448 dropped: 0,
449 teardown,
450 }
451 }
452
453 #[test]
454 fn control_queue_reports_full_without_blocking() {
455 // A tiny capacity makes the drop-on-full behavior observable without a plugin.
456 let (tx, _rx) = RingBuffer::<RtCommand>::new(2);
457 let mut control = test_control(tx);
458 assert!(control.set_parameter(1, 0.5));
459 assert!(control.set_parameter(1, 0.6));
460 // Third push exceeds capacity (nothing has been drained) → dropped, not blocked.
461 assert!(!control.set_parameter(1, 0.7));
462 assert!(!control.send_midi(crate::midi::MidiEvent::NoteOn {
463 channel: crate::midi::MidiChannel::Ch1,
464 note: 60,
465 velocity: 100
466 }));
467 assert_eq!(control.dropped_command_count(), 2);
468 }
469
470 #[test]
471 fn transport_commands_round_trip_through_the_ring() {
472 let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
473 let mut control = test_control(tx);
474
475 assert!(control.set_tempo(140.0));
476 assert!(control.set_time_signature(7, 8));
477 assert!(control.set_playing(false));
478
479 // The three transport commands arrive in order, carrying their payloads intact.
480 match rx.pop().expect("tempo queued") {
481 RtCommand::Transport(TransportCommand::Tempo(bpm)) => assert_eq!(bpm, 140.0),
482 _ => panic!("expected tempo transport command"),
483 }
484 match rx.pop().expect("time sig queued") {
485 RtCommand::Transport(TransportCommand::TimeSignature(n, d)) => {
486 assert_eq!((n, d), (7, 8))
487 }
488 _ => panic!("expected time-signature transport command"),
489 }
490 match rx.pop().expect("playing queued") {
491 RtCommand::Transport(TransportCommand::Playing(p)) => assert!(!p),
492 _ => panic!("expected playing transport command"),
493 }
494 }
495
496 #[test]
497 fn midi_offset_round_trips_through_the_ring() {
498 let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
499 let mut control = test_control(tx);
500
501 assert!(control.send_midi_at(
502 MidiEvent::NoteOn {
503 channel: MidiChannel::Ch1,
504 note: 60,
505 velocity: 100,
506 },
507 128,
508 ));
509 // send_midi is the offset-0 convenience.
510 assert!(control.send_midi(MidiEvent::NoteOff {
511 channel: MidiChannel::Ch1,
512 note: 60,
513 velocity: 0,
514 }));
515
516 match rx.pop().expect("scheduled note queued") {
517 RtCommand::Midi { offset, .. } => assert_eq!(offset, 128),
518 _ => panic!("expected a MIDI command"),
519 }
520 match rx.pop().expect("block-start note queued") {
521 RtCommand::Midi { offset, .. } => assert_eq!(offset, 0),
522 _ => panic!("expected a MIDI command"),
523 }
524 }
525
526 /// `set_parameter` documents normalized values. A NaN or a `7.3` that reached the ring is
527 /// rejected on the *audio* thread — allocating an error string there and vanishing silently
528 /// after the caller was told the change succeeded.
529 #[test]
530 fn out_of_range_parameter_values_are_rejected_not_queued() {
531 let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
532 let mut control = test_control(tx);
533
534 for bad in [
535 f64::NAN,
536 f64::INFINITY,
537 f64::NEG_INFINITY,
538 -0.1,
539 1.000_001,
540 7.3,
541 ] {
542 assert!(!control.set_parameter(1, bad), "{bad} must be rejected");
543 }
544 assert!(rx.pop().is_err(), "no invalid value reached the ring");
545 // Rejected on validation, not because the queue was full.
546 assert_eq!(control.dropped_command_count(), 0);
547
548 // Both endpoints of the normalized range are valid.
549 assert!(control.set_parameter(1, 0.0));
550 assert!(control.set_parameter(1, 1.0));
551 assert_eq!(rx.slots(), 2);
552 }
553
554 #[test]
555 fn is_normalized_accepts_exactly_the_unit_interval() {
556 assert!(is_normalized(0.0) && is_normalized(0.5) && is_normalized(1.0));
557 for bad in [
558 f64::NAN,
559 f64::INFINITY,
560 f64::NEG_INFINITY,
561 -1e-9,
562 1.0 + 1e-9,
563 ] {
564 assert!(!is_normalized(bad), "{bad} is not normalized");
565 }
566 }
567
568 /// An unbounded `while let Ok(..) = pop()` on the audio thread can be pinned indefinitely by
569 /// a control thread that pushes as fast as the callback drains: the ring never fills, so the
570 /// loop never ends. The drain is capped at one ring's worth per block instead.
571 #[test]
572 fn drain_commands_stops_after_one_ring_even_while_the_producer_refills() {
573 let (mut tx, mut rx) = RingBuffer::<u32>::new(4);
574 for i in 0..4 {
575 tx.push(i).expect("ring holds 4");
576 }
577
578 // Refill from inside the drain, standing in for the tight-loop control thread.
579 let mut seen = Vec::new();
580 let applied = drain_commands(&mut rx, |command| {
581 seen.push(command);
582 let _ = tx.push(100 + command);
583 });
584
585 assert_eq!(applied, 4, "exactly one ring's worth per call");
586 assert_eq!(seen, vec![0, 1, 2, 3]);
587 assert_eq!(
588 rx.slots(),
589 4,
590 "the commands pushed during the drain wait for the next block"
591 );
592 }
593
594 #[test]
595 fn drain_commands_stops_early_on_an_empty_ring() {
596 let (mut tx, mut rx) = RingBuffer::<u32>::new(64);
597 tx.push(7).expect("room");
598 let mut seen = Vec::new();
599 assert_eq!(drain_commands(&mut rx, |c| seen.push(c)), 1);
600 assert_eq!(seen, vec![7]);
601 assert_eq!(drain_commands(&mut rx, |c| seen.push(c)), 0);
602 }
603
604 #[test]
605 fn invalid_transport_values_are_rejected_not_queued() {
606 let (tx, _rx) = RingBuffer::<RtCommand>::new(8);
607 let mut control = test_control(tx);
608 // Non-positive / non-finite tempo and malformed time signatures never reach the ring.
609 assert!(!control.set_tempo(0.0));
610 assert!(!control.set_tempo(f64::NAN));
611 assert!(!control.set_time_signature(0, 4));
612 assert!(!control.set_time_signature(4, 3));
613 // Rejected on validation, not because the queue was full.
614 assert_eq!(control.dropped_command_count(), 0);
615 }
616
617 struct DropProbe {
618 drops: Arc<AtomicUsize>,
619 threads: Arc<Mutex<Vec<ThreadId>>>,
620 }
621
622 impl Drop for DropProbe {
623 fn drop(&mut self) {
624 self.drops.fetch_add(1, Ordering::SeqCst);
625 self.threads
626 .lock()
627 .expect("drop thread log")
628 .push(thread::current().id());
629 }
630 }
631
632 fn drop_probe() -> (DropProbe, Arc<AtomicUsize>, Arc<Mutex<Vec<ThreadId>>>) {
633 let drops = Arc::new(AtomicUsize::new(0));
634 let threads = Arc::new(Mutex::new(Vec::new()));
635 (
636 DropProbe {
637 drops: Arc::clone(&drops),
638 threads: Arc::clone(&threads),
639 },
640 drops,
641 threads,
642 )
643 }
644
645 #[test]
646 fn teardown_is_serviced_only_on_the_captured_owner_thread() {
647 let owner = thread::current().id();
648 let (teardown_tx, teardown) = teardown_handoff();
649 let (probe, drops, threads) = drop_probe();
650 assert!(try_handoff_teardown(&teardown_tx, probe));
651
652 let mut teardown = thread::spawn(move || {
653 let mut teardown = teardown;
654 assert!(!teardown.service_one());
655 teardown
656 })
657 .join()
658 .expect("non-owner service thread");
659
660 assert_eq!(drops.load(Ordering::SeqCst), 0);
661 assert!(teardown.service_one());
662 assert_eq!(drops.load(Ordering::SeqCst), 1);
663 assert_eq!(*threads.lock().expect("drop thread log"), vec![owner]);
664 }
665
666 #[test]
667 fn dropping_teardown_receiver_off_owner_leaks_queued_value() {
668 let (teardown_tx, teardown) = teardown_handoff();
669 let (probe, drops, _threads) = drop_probe();
670 assert!(try_handoff_teardown(&teardown_tx, probe));
671
672 thread::spawn(move || drop(teardown))
673 .join()
674 .expect("off-owner drop thread");
675 drop(teardown_tx);
676
677 assert_eq!(
678 drops.load(Ordering::SeqCst),
679 0,
680 "thread-affine value must not be destroyed off its owner thread"
681 );
682 }
683
684 #[test]
685 fn disconnected_or_full_handoff_leaks_instead_of_dropping_the_value() {
686 let (disconnected_tx, disconnected_rx) = teardown_handoff();
687 drop(disconnected_rx);
688 let (disconnected_probe, disconnected_drops, _) = drop_probe();
689 assert!(!try_handoff_teardown(&disconnected_tx, disconnected_probe));
690 assert_eq!(disconnected_drops.load(Ordering::SeqCst), 0);
691
692 let (full_tx, mut full_rx) = teardown_handoff();
693 let (queued_probe, queued_drops, _) = drop_probe();
694 let (overflow_probe, overflow_drops, _) = drop_probe();
695 assert!(try_handoff_teardown(&full_tx, queued_probe));
696 assert!(!try_handoff_teardown(&full_tx, overflow_probe));
697 assert_eq!(overflow_drops.load(Ordering::SeqCst), 0);
698 assert!(full_rx.service_one());
699 assert_eq!(queued_drops.load(Ordering::SeqCst), 1);
700 }
701}