1use crate::message::{PluginKind, ProcessTask};
25use crate::render_plan::{NodeId, Op, PlanSlot, SharedPlan};
26use crate::state::TrackHandle;
27#[cfg(test)]
28use crate::track::Track;
29use std::sync::Arc;
30use std::sync::atomic::Ordering;
31use std::time::{Duration, Instant};
32
33#[derive(Clone)]
38pub struct NodeJob {
39 pub epoch: u64,
40 pub plan: SharedPlan,
41 pub node: NodeId,
42}
43
44impl std::fmt::Debug for NodeJob {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("NodeJob")
47 .field("epoch", &self.epoch)
48 .field("node", &self.node)
49 .finish()
50 }
51}
52
53#[derive(Debug, Default)]
56pub struct ForceOutcome {
57 pub jobs: Vec<NodeJob>,
60 pub silenced: Vec<NodeId>,
62 pub cycle_complete: bool,
64}
65
66pub struct CycleExecutor {
69 slot: Arc<PlanSlot>,
70 plan: SharedPlan,
71 cycle: u64,
74 epoch: u64,
76 deps_completed: Vec<u64>,
77 dispatched: Vec<u64>,
79 completed: Vec<u64>,
81 started_at: Vec<Option<Instant>>,
83 pending: usize,
85 cycle_started_at: Instant,
86 forced_dispatched: bool,
88}
89
90impl CycleExecutor {
91 pub fn new(slot: Arc<PlanSlot>) -> Self {
92 let plan = slot.load_full();
93 let now = Instant::now();
94 let n = plan.nodes.len();
95 Self {
96 slot,
97 plan,
98 cycle: 0,
99 epoch: 0,
100 deps_completed: vec![0; n],
101 dispatched: vec![0; n],
102 completed: vec![0; n],
103 started_at: vec![None; n],
104 pending: 0,
105 cycle_started_at: now,
106 forced_dispatched: false,
107 }
108 }
109
110 pub fn epoch(&self) -> u64 {
111 self.epoch
112 }
113
114 pub fn plan(&self) -> &SharedPlan {
115 &self.plan
116 }
117
118 pub fn cycle_complete(&self) -> bool {
119 self.pending == 0
120 }
121
122 fn pull_plan(&mut self) {
125 let new = self.slot.load_full();
126 if Arc::ptr_eq(&new, &self.plan) {
127 return;
128 }
129 self.plan = new;
130 self.epoch = self.epoch.wrapping_add(1);
131 let n = self.plan.nodes.len();
132 self.deps_completed.clear();
135 self.deps_completed
136 .extend(self.plan.indegree.iter().map(|&d| self.cycle * d as u64));
137 self.dispatched.clear();
138 self.dispatched.resize(n, 0);
139 self.completed.clear();
140 self.completed.resize(n, 0);
141 self.started_at.clear();
142 self.started_at.resize(n, None);
143 }
144
145 pub fn start_cycle(&mut self, now: Instant) -> Vec<NodeJob> {
147 self.pull_plan();
148 self.cycle += 1;
149 self.pending = self.plan.nodes.len();
150 self.cycle_started_at = now;
151 self.forced_dispatched = false;
152 let cycle = self.cycle;
153 let sources = self.plan.sources.clone();
154 sources
155 .iter()
156 .map(|&node| self.dispatch(node, cycle, now))
157 .collect()
158 }
159
160 pub fn on_node_done(&mut self, epoch: u64, node: NodeId, now: Instant) -> (Vec<NodeJob>, bool) {
163 if epoch != self.epoch {
164 return (Vec::new(), self.cycle_complete());
165 }
166 if self.completed[node as usize] == self.cycle {
167 return (Vec::new(), self.cycle_complete());
170 }
171 let jobs = self.complete_node(node, now);
172 let complete = self.cycle_complete();
173 (jobs, complete)
174 }
175
176 pub fn force_timeouts(&mut self, now: Instant, timeout: Duration) -> ForceOutcome {
180 let mut outcome = ForceOutcome::default();
181 if self.cycle_complete() {
182 return outcome;
183 }
184 let cycle = self.cycle;
185 let mut timed_out = Vec::new();
186 for node in 0..self.plan.nodes.len() as NodeId {
187 let idx = node as usize;
188 if self.dispatched[idx] != cycle || self.completed[idx] == cycle {
189 continue;
190 }
191 let Some(started) = self.started_at[idx] else {
192 continue;
193 };
194 if now.duration_since(started) >= timeout {
195 timed_out.push(node);
196 }
197 }
198 for node in timed_out {
199 self.silence_node(node);
200 outcome.silenced.push(node);
201 outcome.jobs.extend(self.complete_node(node, now));
202 }
203 if !self.forced_dispatched && now.duration_since(self.cycle_started_at) >= timeout {
204 self.forced_dispatched = true;
205 for &node in &self.plan.forced.clone() {
206 if self.dispatched[node as usize] != cycle {
207 outcome.jobs.push(self.dispatch(node, cycle, now));
208 }
209 }
210 }
211 outcome.cycle_complete = self.cycle_complete();
212 outcome
213 }
214
215 pub fn abandon_node(&mut self, node: NodeId, now: Instant) -> ForceOutcome {
218 let mut outcome = ForceOutcome::default();
219 if self.completed[node as usize] == self.cycle {
220 outcome.cycle_complete = self.cycle_complete();
221 return outcome;
222 }
223 self.silence_node(node);
224 outcome.silenced.push(node);
225 outcome.jobs = self.complete_node(node, now);
226 outcome.cycle_complete = self.cycle_complete();
227 outcome
228 }
229
230 fn dispatch(&mut self, node: NodeId, cycle: u64, now: Instant) -> NodeJob {
232 let idx = node as usize;
233 self.dispatched[idx] = cycle;
234 self.started_at[idx] = Some(now);
235 NodeJob {
236 epoch: self.epoch,
237 plan: self.plan.clone(),
238 node,
239 }
240 }
241
242 fn complete_node(&mut self, node: NodeId, now: Instant) -> Vec<NodeJob> {
245 let idx = node as usize;
246 self.completed[idx] = self.cycle;
247 self.started_at[idx] = None;
248 self.pending = self.pending.saturating_sub(1);
249 let cycle = self.cycle;
250 let mut jobs = Vec::new();
251 let dependents = self.plan.dependents[idx].clone();
252 for dep in dependents {
253 let dep_idx = dep as usize;
254 self.deps_completed[dep_idx] += 1;
255 let threshold = cycle * self.plan.indegree[dep_idx] as u64;
256 if self.dispatched[dep_idx] != cycle && self.deps_completed[dep_idx] == threshold {
257 jobs.push(self.dispatch(dep, cycle, now));
258 }
259 }
260 jobs
261 }
262
263 fn silence_node(&mut self, node: NodeId) {
267 let op = &self.plan.nodes[node as usize];
268 let (outs, task) = match op {
269 Op::Zero { output } => (vec![*output], None),
270 Op::Sum { output, .. } => (vec![*output], None),
271 Op::HwInput { output, .. } => (vec![*output], None),
272 Op::Task { task, outs, .. } => (outs.clone(), Some(task)),
273 };
274 for buf in outs {
275 unsafe { &mut *self.plan.buffer_ptr(buf) }.fill(0.0);
279 }
280 let Some(task) = task else {
281 return;
282 };
283 let track = match task {
284 ProcessTask::Track(t) | ProcessTask::FolderInput(t) | ProcessTask::FolderOutput(t) => {
285 t.clone()
286 }
287 ProcessTask::Plugin { track, .. } => track.clone(),
288 };
289 silence_task_ports(&track, task);
290 }
291}
292
293fn silence_task_ports(track: &TrackHandle, task: &ProcessTask) {
296 let t = track.lock();
297 match task {
298 ProcessTask::Track(_) | ProcessTask::FolderOutput(_) => t.audio.outs.clone(),
299 ProcessTask::FolderInput(_) => Vec::new(),
300 ProcessTask::Plugin { kind, index, .. } => match kind {
301 PluginKind::Clap => t
302 .clap_plugins
303 .get(*index)
304 .map(|p| p.processor.audio_outputs().to_vec())
305 .unwrap_or_default(),
306 PluginKind::Vst3 => t
307 .vst3_plugins
308 .get(*index)
309 .map(|p| p.processor.audio_outputs().to_vec())
310 .unwrap_or_default(),
311 #[cfg(all(unix, not(target_os = "macos")))]
312 PluginKind::Lv2 => t
313 .lv2_plugins
314 .get(*index)
315 .map(|p| p.processor.audio_outputs().to_vec())
316 .unwrap_or_default(),
317 },
318 }
319 .iter()
320 .for_each(|out| {
321 out.finished.store(true, Ordering::Release);
322 });
323 t.audio.set_processing(false);
324 t.audio.set_finished(true);
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::render_plan::RenderPlan;
331 use std::cell::UnsafeCell;
332 use std::collections::HashMap;
333 use std::sync::Mutex;
334
335 struct TestPlanSlot {
336 collector: Option<basedrop::Collector>,
337 slot: Option<Arc<PlanSlot>>,
338 }
339
340 impl TestPlanSlot {
341 fn new(plan: RenderPlan) -> Self {
342 let collector = basedrop::Collector::new();
343 let owned = basedrop::Owned::new(&collector.handle(), plan);
344 Self {
345 collector: Some(collector),
346 slot: Some(Arc::new(PlanSlot::from_pointee(owned))),
347 }
348 }
349
350 fn slot(&self) -> Arc<PlanSlot> {
351 self.slot.as_ref().expect("test slot").clone()
352 }
353
354 fn store(&self, plan: RenderPlan) {
355 let owned = basedrop::Owned::new(
356 &self.collector.as_ref().expect("test collector").handle(),
357 plan,
358 );
359 self.slot
360 .as_ref()
361 .expect("test slot")
362 .store(Arc::new(owned));
363 }
364 }
365
366 impl Drop for TestPlanSlot {
367 fn drop(&mut self) {
368 self.slot.take();
369 let Some(mut collector) = self.collector.take() else {
370 return;
371 };
372 collector.collect();
373 let _ = collector.try_cleanup();
374 }
375 }
376
377 fn slot_with(plan: RenderPlan) -> TestPlanSlot {
378 TestPlanSlot::new(plan)
379 }
380
381 fn chain_plan(track: &TrackHandle) -> RenderPlan {
384 let nodes = vec![
385 Op::Task {
386 task: ProcessTask::Track(track.clone()),
387 ins: vec![],
388 outs: vec![0],
389 },
390 Op::Task {
391 task: ProcessTask::Track(track.clone()),
392 ins: vec![],
393 outs: vec![1],
394 },
395 Op::Sum {
396 inputs: vec![0, 1],
397 delays: vec![
398 UnsafeCell::new(crate::render_plan::DelayLine::new()),
399 UnsafeCell::new(crate::render_plan::DelayLine::new()),
400 ],
401 output: 2,
402 },
403 Op::Task {
404 task: ProcessTask::Track(track.clone()),
405 ins: vec![2],
406 outs: vec![3],
407 },
408 ];
409 RenderPlan {
410 buffer_size: 8,
411 buffers: (0..4).map(|_| UnsafeCell::new(vec![0.0; 8])).collect(),
412 buffer_latencies: (0..4)
413 .map(|_| std::sync::atomic::AtomicUsize::new(0))
414 .collect(),
415 nodes,
416 indegree: vec![0, 0, 2, 1],
417 dependents: vec![vec![2], vec![2], vec![3], vec![]],
418 sources: vec![0, 1],
419 hw_in_map: vec![],
420 hw_out_map: vec![],
421 port_map: HashMap::new(),
422 midi_edges: vec![],
423 forced: vec![],
424 }
425 }
426
427 fn make_track(name: &str) -> TrackHandle {
428 Arc::new(Track::new(name.to_string(), 1, 1, 0, 0, 8, 48_000.0))
429 }
430
431 #[test]
432 fn counters_dispatch_in_dependency_order_exactly_once() {
433 let track = make_track("t");
434 let slot_guard = slot_with(chain_plan(&track));
435 let slot = slot_guard.slot();
436 let mut exec = CycleExecutor::new(slot);
437 let now = Instant::now();
438
439 let jobs = exec.start_cycle(now);
440 assert_eq!(jobs.len(), 2, "two source tasks");
441 let mut seen: Vec<NodeId> = jobs.iter().map(|j| j.node).collect();
442 seen.sort_unstable();
443 assert_eq!(seen, vec![0, 1]);
444
445 let (jobs, complete) = exec.on_node_done(exec.epoch(), 0, now);
447 assert!(jobs.is_empty() && !complete);
448
449 let (jobs, complete) = exec.on_node_done(exec.epoch(), 1, now);
451 assert_eq!(jobs.len(), 1);
452 assert_eq!(jobs[0].node, 2);
453 assert!(!complete);
454 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
456 assert!(jobs.is_empty());
457
458 let (jobs, complete) = exec.on_node_done(exec.epoch(), 2, now);
459 assert_eq!(jobs.len(), 1);
460 assert_eq!(jobs[0].node, 3);
461 assert!(!complete);
462
463 let (jobs, complete) = exec.on_node_done(exec.epoch(), 3, now);
464 assert!(jobs.is_empty());
465 assert!(complete, "cycle complete after the sink");
466
467 let jobs = exec.start_cycle(now);
469 assert_eq!(jobs.len(), 2);
470 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
471 assert!(jobs.is_empty());
472 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
473 assert_eq!(jobs.len(), 1, "cycle 2 counters re-baselined correctly");
474 }
475
476 #[test]
477 fn swap_during_cycle_keeps_old_plan_until_boundary() {
478 let track = make_track("t");
479 let slot_guard = slot_with(chain_plan(&track));
480 let slot = slot_guard.slot();
481 let mut exec = CycleExecutor::new(slot.clone());
482 let now = Instant::now();
483
484 let jobs = exec.start_cycle(now);
485 assert_eq!(jobs.len(), 2);
486 let epoch0 = exec.epoch();
487
488 slot_guard.store(chain_plan(&track));
490
491 let (jobs, _) = exec.on_node_done(epoch0, 0, now);
493 assert!(jobs.is_empty());
494 let (jobs, _) = exec.on_node_done(epoch0, 1, now);
495 assert_eq!(jobs.len(), 1, "old plan still executes to completion");
496 assert_eq!(exec.epoch(), epoch0, "no swap before the boundary");
497
498 let (jobs, _) = exec.on_node_done(epoch0 + 1, 2, now);
500 assert!(jobs.is_empty());
501
502 let (jobs, _) = exec.on_node_done(epoch0, 2, now);
503 assert_eq!(jobs.len(), 1);
504 let (_, complete) = exec.on_node_done(epoch0, 3, now);
505 assert!(complete);
506
507 let jobs = exec.start_cycle(now);
509 assert_eq!(exec.epoch(), epoch0 + 1);
510 assert_eq!(jobs.len(), 2);
511 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
512 assert!(jobs.is_empty());
513 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
514 assert_eq!(jobs.len(), 1, "new plan runs after the boundary");
515 }
516
517 #[test]
521 fn swap_during_simulated_parallel_cycle_exactly_once() {
522 let track = make_track("t");
523 let slot_guard = slot_with(chain_plan(&track));
524 let slot = slot_guard.slot();
525 let exec = Mutex::new(CycleExecutor::new(slot.clone()));
526 let now = Instant::now();
527
528 let jobs = exec.lock().expect("lock").start_cycle(now);
529 let epoch0 = exec.lock().expect("lock").epoch();
530 slot_guard.store(chain_plan(&track));
532
533 let queue: Arc<Mutex<std::collections::VecDeque<NodeJob>>> =
536 Arc::new(Mutex::new(jobs.into_iter().collect()));
537 let executed: Arc<Mutex<Vec<NodeId>>> = Arc::new(Mutex::new(Vec::new()));
538 std::thread::scope(|s| {
539 for _ in 0..2 {
540 let queue = queue.clone();
541 let executed = executed.clone();
542 let exec = &exec;
543 s.spawn(move || {
544 loop {
545 let job = queue.lock().expect("lock").pop_front();
546 let Some(job) = job else {
547 break;
548 };
549 if let Op::Task { outs, .. } = &job.plan.nodes[job.node as usize]
552 && let Some(&buf) = outs.first()
553 {
554 let out = unsafe { &mut *job.plan.buffer_ptr(buf) };
557 out[0] = job.node as f32;
558 }
559 executed.lock().expect("lock").push(job.node);
560 std::thread::yield_now();
561 let (new_jobs, _) = exec
562 .lock()
563 .expect("lock")
564 .on_node_done(epoch0, job.node, now);
565 queue.lock().expect("lock").extend(new_jobs);
566 }
567 });
568 }
569 });
570
571 let mut counts = [0usize; 4];
572 for node in executed.lock().expect("lock").iter() {
573 counts[*node as usize] += 1;
574 }
575 assert_eq!(
576 counts,
577 [1, 1, 1, 1],
578 "every node executed exactly once across two racing workers"
579 );
580 assert!(exec.lock().expect("lock").cycle_complete());
581 }
582
583 #[test]
584 fn timeout_silences_node_outputs_and_completes_by_index() {
585 let track = make_track("t");
586 let slot_guard = slot_with(chain_plan(&track));
587 let slot = slot_guard.slot();
588 let mut exec = CycleExecutor::new(slot);
589 let now = Instant::now();
590 let timeout = Duration::from_millis(250);
591
592 let jobs = exec.start_cycle(now);
593 assert_eq!(jobs.len(), 2);
594 unsafe {
597 (&mut *exec.plan().buffer_ptr(0)).fill(1.0);
598 (&mut *exec.plan().buffer_ptr(1)).fill(2.0);
599 }
600
601 let later = now + timeout + Duration::from_millis(1);
603 let outcome = exec.force_timeouts(later, timeout);
604 assert_eq!(outcome.silenced, vec![0, 1], "both sources timed out");
605 unsafe {
607 assert!(exec.plan().buffer(0).iter().all(|&s| s == 0.0));
608 assert!(exec.plan().buffer(1).iter().all(|&s| s == 0.0));
609 }
610 assert_eq!(outcome.jobs.len(), 1);
612 assert_eq!(outcome.jobs[0].node, 2);
613 assert!(!outcome.cycle_complete);
614
615 let t = track.lock();
617 assert!(t.audio.finished());
618 assert!(!t.audio.processing());
619 for out in &t.audio.outs {
620 assert!(out.finished.load(Ordering::Acquire));
621 }
622 let (jobs, _) = exec.on_node_done(exec.epoch(), 2, later);
624 assert_eq!(jobs.len(), 1);
625 let outcome = exec.force_timeouts(later, timeout);
626 assert!(outcome.silenced.is_empty());
627 let (_, complete) = exec.on_node_done(exec.epoch(), 3, later);
628 assert!(complete);
629 }
630
631 #[test]
632 fn abandon_node_completes_with_silence() {
633 let track = make_track("t");
634 let slot_guard = slot_with(chain_plan(&track));
635 let slot = slot_guard.slot();
636 let mut exec = CycleExecutor::new(slot);
637 let now = Instant::now();
638
639 let jobs = exec.start_cycle(now);
640 assert_eq!(jobs.len(), 2);
641 unsafe { (&mut *exec.plan().buffer_ptr(0)).fill(3.0) };
642
643 let outcome = exec.abandon_node(0, now);
644 assert_eq!(outcome.silenced, vec![0]);
645 unsafe {
646 assert!(exec.plan().buffer(0).iter().all(|&s| s == 0.0));
647 }
648 let outcome = exec.abandon_node(0, now);
650 assert!(outcome.silenced.is_empty());
651 assert!(outcome.jobs.is_empty());
652
653 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
654 assert_eq!(jobs.len(), 1, "abandon + done crosses the sum threshold");
655 }
656
657 #[test]
658 fn forced_feedback_nodes_dispatch_after_timeout() {
659 let track = make_track("t");
660 let mut plan = chain_plan(&track);
661 plan.indegree[3] = 2;
663 plan.forced = vec![3];
664 let slot_guard = slot_with(plan);
665 let slot = slot_guard.slot();
666 let mut exec = CycleExecutor::new(slot);
667 let now = Instant::now();
668 let timeout = Duration::from_millis(250);
669
670 let jobs = exec.start_cycle(now);
671 assert_eq!(jobs.len(), 2);
672 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
674 assert!(jobs.is_empty());
675 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
676 assert_eq!(jobs.len(), 1);
677 let (jobs, complete) = exec.on_node_done(exec.epoch(), 2, now);
678 assert!(jobs.is_empty() && !complete);
679
680 let outcome = exec.force_timeouts(now + Duration::from_millis(10), timeout);
682 assert!(outcome.jobs.is_empty());
683 let outcome = exec.force_timeouts(now + timeout + Duration::from_millis(1), timeout);
685 assert_eq!(outcome.jobs.len(), 1);
686 assert_eq!(outcome.jobs[0].node, 3);
687
688 let (_, complete) = exec.on_node_done(exec.epoch(), 3, now);
689 assert!(complete);
690 }
691
692 #[test]
693 fn stale_epoch_completions_are_dropped() {
694 let track = make_track("t");
695 let slot_guard = slot_with(chain_plan(&track));
696 let slot = slot_guard.slot();
697 let mut exec = CycleExecutor::new(slot);
698 let now = Instant::now();
699
700 let jobs = exec.start_cycle(now);
701 assert_eq!(jobs.len(), 2);
702 let (jobs, _) = exec.on_node_done(exec.epoch() + 7, 0, now);
703 assert!(jobs.is_empty());
704 let (jobs, _) = exec.on_node_done(exec.epoch().wrapping_sub(1), 1, now);
705 assert!(jobs.is_empty());
706 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
708 assert!(jobs.is_empty());
709 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
710 assert_eq!(jobs.len(), 1);
711 }
712}