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 output: 2,
398 },
399 Op::Task {
400 task: ProcessTask::Track(track.clone()),
401 ins: vec![2],
402 outs: vec![3],
403 },
404 ];
405 RenderPlan {
406 buffer_size: 8,
407 buffers: (0..4).map(|_| UnsafeCell::new(vec![0.0; 8])).collect(),
408 nodes,
409 indegree: vec![0, 0, 2, 1],
410 dependents: vec![vec![2], vec![2], vec![3], vec![]],
411 sources: vec![0, 1],
412 hw_in_map: vec![],
413 hw_out_map: vec![],
414 port_map: HashMap::new(),
415 midi_edges: vec![],
416 forced: vec![],
417 }
418 }
419
420 fn make_track(name: &str) -> TrackHandle {
421 Arc::new(Track::new(name.to_string(), 1, 1, 0, 0, 8, 48_000.0))
422 }
423
424 #[test]
425 fn counters_dispatch_in_dependency_order_exactly_once() {
426 let track = make_track("t");
427 let slot_guard = slot_with(chain_plan(&track));
428 let slot = slot_guard.slot();
429 let mut exec = CycleExecutor::new(slot);
430 let now = Instant::now();
431
432 let jobs = exec.start_cycle(now);
433 assert_eq!(jobs.len(), 2, "two source tasks");
434 let mut seen: Vec<NodeId> = jobs.iter().map(|j| j.node).collect();
435 seen.sort_unstable();
436 assert_eq!(seen, vec![0, 1]);
437
438 let (jobs, complete) = exec.on_node_done(exec.epoch(), 0, now);
440 assert!(jobs.is_empty() && !complete);
441
442 let (jobs, complete) = exec.on_node_done(exec.epoch(), 1, now);
444 assert_eq!(jobs.len(), 1);
445 assert_eq!(jobs[0].node, 2);
446 assert!(!complete);
447 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
449 assert!(jobs.is_empty());
450
451 let (jobs, complete) = exec.on_node_done(exec.epoch(), 2, now);
452 assert_eq!(jobs.len(), 1);
453 assert_eq!(jobs[0].node, 3);
454 assert!(!complete);
455
456 let (jobs, complete) = exec.on_node_done(exec.epoch(), 3, now);
457 assert!(jobs.is_empty());
458 assert!(complete, "cycle complete after the sink");
459
460 let jobs = exec.start_cycle(now);
462 assert_eq!(jobs.len(), 2);
463 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
464 assert!(jobs.is_empty());
465 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
466 assert_eq!(jobs.len(), 1, "cycle 2 counters re-baselined correctly");
467 }
468
469 #[test]
470 fn swap_during_cycle_keeps_old_plan_until_boundary() {
471 let track = make_track("t");
472 let slot_guard = slot_with(chain_plan(&track));
473 let slot = slot_guard.slot();
474 let mut exec = CycleExecutor::new(slot.clone());
475 let now = Instant::now();
476
477 let jobs = exec.start_cycle(now);
478 assert_eq!(jobs.len(), 2);
479 let epoch0 = exec.epoch();
480
481 slot_guard.store(chain_plan(&track));
483
484 let (jobs, _) = exec.on_node_done(epoch0, 0, now);
486 assert!(jobs.is_empty());
487 let (jobs, _) = exec.on_node_done(epoch0, 1, now);
488 assert_eq!(jobs.len(), 1, "old plan still executes to completion");
489 assert_eq!(exec.epoch(), epoch0, "no swap before the boundary");
490
491 let (jobs, _) = exec.on_node_done(epoch0 + 1, 2, now);
493 assert!(jobs.is_empty());
494
495 let (jobs, _) = exec.on_node_done(epoch0, 2, now);
496 assert_eq!(jobs.len(), 1);
497 let (_, complete) = exec.on_node_done(epoch0, 3, now);
498 assert!(complete);
499
500 let jobs = exec.start_cycle(now);
502 assert_eq!(exec.epoch(), epoch0 + 1);
503 assert_eq!(jobs.len(), 2);
504 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
505 assert!(jobs.is_empty());
506 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
507 assert_eq!(jobs.len(), 1, "new plan runs after the boundary");
508 }
509
510 #[test]
514 fn swap_during_simulated_parallel_cycle_exactly_once() {
515 let track = make_track("t");
516 let slot_guard = slot_with(chain_plan(&track));
517 let slot = slot_guard.slot();
518 let exec = Mutex::new(CycleExecutor::new(slot.clone()));
519 let now = Instant::now();
520
521 let jobs = exec.lock().expect("lock").start_cycle(now);
522 let epoch0 = exec.lock().expect("lock").epoch();
523 slot_guard.store(chain_plan(&track));
525
526 let queue: Arc<Mutex<std::collections::VecDeque<NodeJob>>> =
529 Arc::new(Mutex::new(jobs.into_iter().collect()));
530 let executed: Arc<Mutex<Vec<NodeId>>> = Arc::new(Mutex::new(Vec::new()));
531 std::thread::scope(|s| {
532 for _ in 0..2 {
533 let queue = queue.clone();
534 let executed = executed.clone();
535 let exec = &exec;
536 s.spawn(move || {
537 loop {
538 let job = queue.lock().expect("lock").pop_front();
539 let Some(job) = job else {
540 break;
541 };
542 if let Op::Task { outs, .. } = &job.plan.nodes[job.node as usize]
545 && let Some(&buf) = outs.first()
546 {
547 let out = unsafe { &mut *job.plan.buffer_ptr(buf) };
550 out[0] = job.node as f32;
551 }
552 executed.lock().expect("lock").push(job.node);
553 std::thread::yield_now();
554 let (new_jobs, _) = exec
555 .lock()
556 .expect("lock")
557 .on_node_done(epoch0, job.node, now);
558 queue.lock().expect("lock").extend(new_jobs);
559 }
560 });
561 }
562 });
563
564 let mut counts = [0usize; 4];
565 for node in executed.lock().expect("lock").iter() {
566 counts[*node as usize] += 1;
567 }
568 assert_eq!(
569 counts,
570 [1, 1, 1, 1],
571 "every node executed exactly once across two racing workers"
572 );
573 assert!(exec.lock().expect("lock").cycle_complete());
574 }
575
576 #[test]
577 fn timeout_silences_node_outputs_and_completes_by_index() {
578 let track = make_track("t");
579 let slot_guard = slot_with(chain_plan(&track));
580 let slot = slot_guard.slot();
581 let mut exec = CycleExecutor::new(slot);
582 let now = Instant::now();
583 let timeout = Duration::from_millis(250);
584
585 let jobs = exec.start_cycle(now);
586 assert_eq!(jobs.len(), 2);
587 unsafe {
590 (&mut *exec.plan().buffer_ptr(0)).fill(1.0);
591 (&mut *exec.plan().buffer_ptr(1)).fill(2.0);
592 }
593
594 let later = now + timeout + Duration::from_millis(1);
596 let outcome = exec.force_timeouts(later, timeout);
597 assert_eq!(outcome.silenced, vec![0, 1], "both sources timed out");
598 unsafe {
600 assert!(exec.plan().buffer(0).iter().all(|&s| s == 0.0));
601 assert!(exec.plan().buffer(1).iter().all(|&s| s == 0.0));
602 }
603 assert_eq!(outcome.jobs.len(), 1);
605 assert_eq!(outcome.jobs[0].node, 2);
606 assert!(!outcome.cycle_complete);
607
608 let t = track.lock();
610 assert!(t.audio.finished());
611 assert!(!t.audio.processing());
612 for out in &t.audio.outs {
613 assert!(out.finished.load(Ordering::Acquire));
614 }
615 let (jobs, _) = exec.on_node_done(exec.epoch(), 2, later);
617 assert_eq!(jobs.len(), 1);
618 let outcome = exec.force_timeouts(later, timeout);
619 assert!(outcome.silenced.is_empty());
620 let (_, complete) = exec.on_node_done(exec.epoch(), 3, later);
621 assert!(complete);
622 }
623
624 #[test]
625 fn abandon_node_completes_with_silence() {
626 let track = make_track("t");
627 let slot_guard = slot_with(chain_plan(&track));
628 let slot = slot_guard.slot();
629 let mut exec = CycleExecutor::new(slot);
630 let now = Instant::now();
631
632 let jobs = exec.start_cycle(now);
633 assert_eq!(jobs.len(), 2);
634 unsafe { (&mut *exec.plan().buffer_ptr(0)).fill(3.0) };
635
636 let outcome = exec.abandon_node(0, now);
637 assert_eq!(outcome.silenced, vec![0]);
638 unsafe {
639 assert!(exec.plan().buffer(0).iter().all(|&s| s == 0.0));
640 }
641 let outcome = exec.abandon_node(0, now);
643 assert!(outcome.silenced.is_empty());
644 assert!(outcome.jobs.is_empty());
645
646 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
647 assert_eq!(jobs.len(), 1, "abandon + done crosses the sum threshold");
648 }
649
650 #[test]
651 fn forced_feedback_nodes_dispatch_after_timeout() {
652 let track = make_track("t");
653 let mut plan = chain_plan(&track);
654 plan.indegree[3] = 2;
656 plan.forced = vec![3];
657 let slot_guard = slot_with(plan);
658 let slot = slot_guard.slot();
659 let mut exec = CycleExecutor::new(slot);
660 let now = Instant::now();
661 let timeout = Duration::from_millis(250);
662
663 let jobs = exec.start_cycle(now);
664 assert_eq!(jobs.len(), 2);
665 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
667 assert!(jobs.is_empty());
668 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
669 assert_eq!(jobs.len(), 1);
670 let (jobs, complete) = exec.on_node_done(exec.epoch(), 2, now);
671 assert!(jobs.is_empty() && !complete);
672
673 let outcome = exec.force_timeouts(now + Duration::from_millis(10), timeout);
675 assert!(outcome.jobs.is_empty());
676 let outcome = exec.force_timeouts(now + timeout + Duration::from_millis(1), timeout);
678 assert_eq!(outcome.jobs.len(), 1);
679 assert_eq!(outcome.jobs[0].node, 3);
680
681 let (_, complete) = exec.on_node_done(exec.epoch(), 3, now);
682 assert!(complete);
683 }
684
685 #[test]
686 fn stale_epoch_completions_are_dropped() {
687 let track = make_track("t");
688 let slot_guard = slot_with(chain_plan(&track));
689 let slot = slot_guard.slot();
690 let mut exec = CycleExecutor::new(slot);
691 let now = Instant::now();
692
693 let jobs = exec.start_cycle(now);
694 assert_eq!(jobs.len(), 2);
695 let (jobs, _) = exec.on_node_done(exec.epoch() + 7, 0, now);
696 assert!(jobs.is_empty());
697 let (jobs, _) = exec.on_node_done(exec.epoch().wrapping_sub(1), 1, now);
698 assert!(jobs.is_empty());
699 let (jobs, _) = exec.on_node_done(exec.epoch(), 0, now);
701 assert!(jobs.is_empty());
702 let (jobs, _) = exec.on_node_done(exec.epoch(), 1, now);
703 assert_eq!(jobs.len(), 1);
704 }
705}