1use crate::audio::io::AudioIO;
24use crate::connectable::{ConnectableConnection, ConnectableRef};
25use crate::message::{PluginKind, ProcessTask};
26use crate::state::{StateSnapshot, TrackHandle};
27#[cfg(test)]
28use crate::track::Track;
29use crate::track::TrackData;
30use std::cell::UnsafeCell;
31use std::collections::{HashMap, HashSet, VecDeque};
32use std::sync::Arc;
33
34pub type SharedPlan = Arc<basedrop::Owned<RenderPlan>>;
41pub type PlanSlot = arc_swap::ArcSwap<basedrop::Owned<RenderPlan>>;
44
45pub type BufferId = u32;
47pub type NodeId = u32;
49
50#[derive(Debug)]
52pub enum Op {
53 Zero { output: BufferId },
55 Sum {
57 inputs: Vec<BufferId>,
58 output: BufferId,
59 },
60 Task {
65 task: ProcessTask,
66 ins: Vec<BufferId>,
67 outs: Vec<BufferId>,
68 },
69 HwInput { channel: usize, output: BufferId },
73}
74
75#[derive(Debug)]
77pub struct RenderPlan {
78 pub buffer_size: usize,
80 pub buffers: Vec<UnsafeCell<Vec<f32>>>,
90 pub nodes: Vec<Op>,
92 pub indegree: Vec<u32>,
94 pub dependents: Vec<Vec<NodeId>>,
96 pub sources: Vec<NodeId>,
98 pub hw_in_map: Vec<(usize, BufferId)>,
100 pub hw_out_map: Vec<(BufferId, usize)>,
102 pub port_map: HashMap<usize, BufferId>,
105 pub midi_edges: Vec<(NodeId, NodeId)>,
110 pub forced: Vec<NodeId>,
114}
115
116unsafe impl Sync for RenderPlan {}
124
125impl RenderPlan {
126 pub unsafe fn buffer_ptr(&self, id: BufferId) -> *mut Vec<f32> {
139 self.buffers[id as usize].get()
140 }
141
142 pub unsafe fn buffer(&self, id: BufferId) -> &[f32] {
148 unsafe { &*self.buffers[id as usize].get() }
149 }
150
151 pub fn buffer_count(&self) -> usize {
153 self.buffers.len()
154 }
155 pub fn compile(
162 state: &StateSnapshot,
163 hw_inputs: &[Arc<AudioIO>],
164 hw_outputs: &[Arc<AudioIO>],
165 buffer_size: usize,
166 ) -> Self {
167 let mut b = Builder::new(buffer_size);
168 b.add_hw(hw_inputs, hw_outputs);
169
170 let mut ordered: Vec<(String, TrackHandle)> = state
171 .tracks
172 .iter()
173 .map(|(name, track)| (name.clone(), track.clone()))
174 .collect();
175 ordered.sort_by(|a, b| a.0.cmp(&b.0));
176
177 for (_name, track) in &ordered {
178 if track.lock().parent_track.is_some() {
179 continue;
180 }
181 b.append_track(track.clone(), None);
182 }
183
184 b.finish()
185 }
186
187 pub fn verify(&self) -> Result<(), String> {
190 let forced: HashSet<NodeId> = self.forced.iter().copied().collect();
191
192 for (from, dependents) in self.dependents.iter().enumerate() {
194 for &to in dependents {
195 if from as NodeId >= to
196 && !(forced.contains(&(from as NodeId)) && forced.contains(&to))
197 {
198 return Err(format!("edge {from} -> {to} violates topological order"));
199 }
200 }
201 }
202
203 for &(from, to) in &self.midi_edges {
205 if from >= to && !(forced.contains(&from) && forced.contains(&to)) {
206 return Err(format!(
207 "midi edge {from} -> {to} violates topological order"
208 ));
209 }
210 }
211
212 let mut writers: HashMap<BufferId, Vec<NodeId>> = HashMap::new();
216 for (idx, op) in self.nodes.iter().enumerate() {
217 let idx = idx as NodeId;
218 match op {
219 Op::Zero { output } | Op::Sum { output, .. } | Op::HwInput { output, .. } => {
220 writers.entry(*output).or_default().push(idx);
221 }
222 Op::Task { task, ins, outs } => {
223 let writes_ins =
224 matches!(task, ProcessTask::Track(_) | ProcessTask::FolderInput(_));
225 for b in outs {
226 writers.entry(*b).or_default().push(idx);
227 }
228 if writes_ins {
229 for b in ins {
230 writers.entry(*b).or_default().push(idx);
231 }
232 }
233 }
234 }
235 }
236
237 for buffer in 0..self.buffers.len() as BufferId {
238 let ws = writers.get(&buffer).cloned().unwrap_or_default();
239 if ws.is_empty() {
240 return Err(format!("buffer {buffer} has no writer"));
241 }
242 let mut sorted = ws;
245 sorted.sort_unstable();
246 for pair in sorted.windows(2) {
247 if !self.reachable(pair[0], pair[1]) {
248 return Err(format!(
249 "buffer {buffer} written by unordered nodes {} and {}",
250 pair[0], pair[1]
251 ));
252 }
253 }
254 }
255 Ok(())
256 }
257
258 fn reachable(&self, from: NodeId, to: NodeId) -> bool {
260 if from == to {
261 return true;
262 }
263 let mut seen = HashSet::new();
264 let mut queue = VecDeque::from([from]);
265 seen.insert(from);
266 while let Some(n) = queue.pop_front() {
267 for &d in &self.dependents[n as usize] {
268 if d == to {
269 return true;
270 }
271 if seen.insert(d) {
272 queue.push_back(d);
273 }
274 }
275 }
276 false
277 }
278}
279
280struct Builder {
282 buffer_size: usize,
283 buffers: Vec<UnsafeCell<Vec<f32>>>,
284 port_map: HashMap<usize, BufferId>,
285 nodes: Vec<Op>,
286 edges: HashSet<(NodeId, NodeId)>,
287 consumer_ports: Vec<Arc<AudioIO>>,
289 port_readers: HashMap<BufferId, Vec<NodeId>>,
291 port_inplace_writers: HashMap<BufferId, Vec<NodeId>>,
293 producer: HashMap<BufferId, NodeId>,
295 hw_in_map: Vec<(usize, BufferId)>,
296 hw_out_map: Vec<(BufferId, usize)>,
297 midi_writers: HashMap<usize, NodeId>,
300 midi_readers: HashMap<usize, NodeId>,
302 midi_ports: Vec<Arc<crate::midi::io::MIDIIO>>,
304 midi_edges: Vec<(NodeId, NodeId)>,
305}
306
307impl Builder {
308 fn new(buffer_size: usize) -> Self {
309 Self {
310 buffer_size,
311 buffers: Vec::new(),
312 port_map: HashMap::new(),
313 nodes: Vec::new(),
314 edges: HashSet::new(),
315 consumer_ports: Vec::new(),
316 port_readers: HashMap::new(),
317 port_inplace_writers: HashMap::new(),
318 producer: HashMap::new(),
319 hw_in_map: Vec::new(),
320 hw_out_map: Vec::new(),
321 midi_writers: HashMap::new(),
322 midi_readers: HashMap::new(),
323 midi_ports: Vec::new(),
324 midi_edges: Vec::new(),
325 }
326 }
327
328 fn register_midi_track_ports(&mut self, t: &TrackData, first: NodeId, last: NodeId) {
332 for p in &t.midi.ins {
333 let key = Arc::as_ptr(p) as usize;
334 self.midi_writers.insert(key, first);
335 self.midi_readers.insert(key, first);
336 self.midi_ports.push(p.clone());
337 }
338 for p in &t.midi.outs {
339 let key = Arc::as_ptr(p) as usize;
340 self.midi_writers.insert(key, last);
341 self.midi_readers.insert(key, last);
342 self.midi_ports.push(p.clone());
343 }
344 }
345
346 fn register_plugin_midi_ports(
349 &mut self,
350 t: &TrackData,
351 kind: PluginKind,
352 index: usize,
353 node: NodeId,
354 ) {
355 let (midi_ins, midi_outs): (
356 Vec<Arc<crate::midi::io::MIDIIO>>,
357 Vec<Arc<crate::midi::io::MIDIIO>>,
358 ) = match kind {
359 PluginKind::Clap => {
360 let proc = t.clap_plugins[index].processor.clone();
361 (
362 proc.midi_input_ports().to_vec(),
363 proc.midi_output_ports().to_vec(),
364 )
365 }
366 PluginKind::Vst3 => {
367 let proc = t.vst3_plugins[index].processor.clone();
368 (
369 proc.midi_input_ports().to_vec(),
370 proc.midi_output_ports().to_vec(),
371 )
372 }
373 #[cfg(all(unix, not(target_os = "macos")))]
374 PluginKind::Lv2 => {
375 let proc = t.lv2_plugins[index].processor.clone();
376 (
377 proc.midi_input_ports().to_vec(),
378 proc.midi_output_ports().to_vec(),
379 )
380 }
381 };
382 for p in midi_ins {
383 let key = Arc::as_ptr(&p) as usize;
384 self.midi_writers.insert(key, node);
385 self.midi_readers.insert(key, node);
386 self.midi_ports.push(p);
387 }
388 for p in midi_outs {
389 let key = Arc::as_ptr(&p) as usize;
390 self.midi_writers.insert(key, node);
391 self.midi_ports.push(p);
392 }
393 }
394
395 fn buffer_for(&mut self, port: &Arc<AudioIO>) -> BufferId {
397 let key = Arc::as_ptr(port) as usize;
398 if let Some(&id) = self.port_map.get(&key) {
399 return id;
400 }
401 let id = self.buffers.len() as BufferId;
402 self.buffers
403 .push(UnsafeCell::new(vec![0.0; self.buffer_size]));
404 self.port_map.insert(key, id);
405 id
406 }
407
408 fn push_node(&mut self, op: Op) -> NodeId {
409 self.nodes.push(op);
410 (self.nodes.len() - 1) as NodeId
411 }
412
413 fn add_hw(&mut self, hw_inputs: &[Arc<AudioIO>], hw_outputs: &[Arc<AudioIO>]) {
414 for (channel, port) in hw_inputs.iter().enumerate() {
415 let output = self.buffer_for(port);
416 let node = self.push_node(Op::HwInput { channel, output });
417 self.producer.insert(output, node);
418 self.hw_in_map.push((channel, output));
419 }
420 for (channel, port) in hw_outputs.iter().enumerate() {
421 let buffer = self.buffer_for(port);
422 self.consumer_ports.push(port.clone());
423 self.hw_out_map.push((buffer, channel));
424 }
425 }
426
427 fn append_track(
431 &mut self,
432 track: TrackHandle,
433 predecessor: Option<NodeId>,
434 ) -> (NodeId, NodeId) {
435 let t = track.lock();
436 let ins: Vec<BufferId> = t.audio.ins.iter().map(|p| self.buffer_for(p)).collect();
437 let outs: Vec<BufferId> = t.audio.outs.iter().map(|p| self.buffer_for(p)).collect();
438 let metronome_source = t.metronome_source();
439 let metronome_out = metronome_source.as_ref().map(|p| self.buffer_for(p));
440 for p in &t.audio.ins {
441 self.consumer_ports.push(p.clone());
442 }
443
444 if t.is_folder {
445 let mut folder_input_outs = Vec::new();
446 if let Some(out) = metronome_out {
447 folder_input_outs.push(out);
448 }
449 let folder_input = self.push_node(Op::Task {
450 task: ProcessTask::FolderInput(track.clone()),
451 ins: ins.clone(),
452 outs: folder_input_outs,
453 });
454 if let Some(pred) = predecessor {
455 self.edges.insert((pred, folder_input));
456 }
457 self.register_task_ports(folder_input, &ins, true);
458 if let Some(out) = metronome_out {
459 self.producer.insert(out, folder_input);
460 }
461
462 let mut source_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
463 let mut target_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
464 source_keys.insert(ConnectableRef::TrackInput, folder_input);
465 target_keys.insert(ConnectableRef::TrackInput, folder_input);
466
467 let mut plugin_nodes: Vec<NodeId> = Vec::new();
468 for idx in 0..t.clap_plugins.len() {
469 let node = self.push_plugin(&track, &t, PluginKind::Clap, idx, folder_input);
470 let id = t.clap_plugins[idx].id;
471 source_keys.insert(ConnectableRef::ClapPlugin(id), node);
472 target_keys.insert(ConnectableRef::ClapPlugin(id), node);
473 plugin_nodes.push(node);
474 }
475 for idx in 0..t.vst3_plugins.len() {
476 let node = self.push_plugin(&track, &t, PluginKind::Vst3, idx, folder_input);
477 let id = t.vst3_plugins[idx].id;
478 source_keys.insert(ConnectableRef::Vst3Plugin(id), node);
479 target_keys.insert(ConnectableRef::Vst3Plugin(id), node);
480 plugin_nodes.push(node);
481 }
482 #[cfg(all(unix, not(target_os = "macos")))]
483 for idx in 0..t.lv2_plugins.len() {
484 let node = self.push_plugin(&track, &t, PluginKind::Lv2, idx, folder_input);
485 let id = t.lv2_plugins[idx].id;
486 source_keys.insert(ConnectableRef::Lv2Plugin(id), node);
487 target_keys.insert(ConnectableRef::Lv2Plugin(id), node);
488 plugin_nodes.push(node);
489 }
490
491 let mut child_lasts: Vec<NodeId> = Vec::new();
492 for child_track in &t.child_tracks {
493 let (child_first, child_last) =
494 self.append_track(child_track.clone(), Some(folder_input));
495 let child_name = child_track.lock().name.clone();
496 source_keys.insert(ConnectableRef::ChildTrack(child_name.clone()), child_last);
497 target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first);
498 child_lasts.push(child_last);
499 }
500
501 let folder_output = self.push_node(Op::Task {
502 task: ProcessTask::FolderOutput(track.clone()),
503 ins: Vec::new(),
504 outs: outs.clone(),
505 });
506 self.edges.insert((folder_input, folder_output));
507 for &p in &plugin_nodes {
508 self.edges.insert((p, folder_output));
509 }
510 for &c in &child_lasts {
511 self.edges.insert((c, folder_output));
512 }
513 for &out in &outs {
514 self.producer.insert(out, folder_output);
515 }
516 self.register_midi_track_ports(&t, folder_input, folder_output);
517
518 for conn in t.connectable_connections() {
521 let ConnectableConnection { from, to, .. } = conn;
522 let (Some(&source), Some(&target)) = (source_keys.get(&from), target_keys.get(&to))
523 else {
524 continue;
525 };
526 if source != target {
527 self.edges.insert((source, target));
528 }
529 }
530
531 (folder_input, folder_output)
532 } else {
533 let mut task_outs = outs.clone();
534 if let Some(out) = metronome_out {
535 task_outs.push(out);
536 }
537 let task = self.push_node(Op::Task {
538 task: ProcessTask::Track(track.clone()),
539 ins: ins.clone(),
540 outs: task_outs,
541 });
542 if let Some(pred) = predecessor {
543 self.edges.insert((pred, task));
544 }
545 self.register_task_ports(task, &ins, true);
546 for &out in &outs {
547 self.producer.insert(out, task);
548 }
549 if let Some(out) = metronome_out {
550 self.producer.insert(out, task);
551 }
552 self.register_midi_track_ports(&t, task, task);
553 for idx in 0..t.clap_plugins.len() {
555 self.register_plugin_midi_ports(&t, PluginKind::Clap, idx, task);
556 }
557 for idx in 0..t.vst3_plugins.len() {
558 self.register_plugin_midi_ports(&t, PluginKind::Vst3, idx, task);
559 }
560 #[cfg(all(unix, not(target_os = "macos")))]
561 for idx in 0..t.lv2_plugins.len() {
562 self.register_plugin_midi_ports(&t, PluginKind::Lv2, idx, task);
563 }
564 (task, task)
565 }
566 }
567
568 fn push_plugin(
569 &mut self,
570 track: &TrackHandle,
571 t: &TrackData,
572 kind: PluginKind,
573 index: usize,
574 folder_input: NodeId,
575 ) -> NodeId {
576 let (input_ports, output_ports): (Vec<Arc<AudioIO>>, Vec<Arc<AudioIO>>) = match kind {
577 PluginKind::Clap => {
578 let proc = t.clap_plugins[index].processor.clone();
579 (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
580 }
581 PluginKind::Vst3 => {
582 let proc = t.vst3_plugins[index].processor.clone();
583 (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
584 }
585 #[cfg(all(unix, not(target_os = "macos")))]
586 PluginKind::Lv2 => {
587 let proc = t.lv2_plugins[index].processor.clone();
588 (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
589 }
590 };
591 for p in &input_ports {
592 self.consumer_ports.push(p.clone());
593 }
594 let pins: Vec<BufferId> = input_ports.iter().map(|p| self.buffer_for(p)).collect();
595 let pouts: Vec<BufferId> = output_ports.iter().map(|p| self.buffer_for(p)).collect();
596 let node = self.push_node(Op::Task {
597 task: ProcessTask::Plugin {
598 track: track.clone(),
599 kind,
600 index,
601 },
602 ins: pins.clone(),
603 outs: pouts.clone(),
604 });
605 self.edges.insert((folder_input, node));
606 self.register_task_ports(node, &pins, false);
607 for &out in &pouts {
608 self.producer.insert(out, node);
609 }
610 self.register_plugin_midi_ports(t, kind, index, node);
611 node
612 }
613
614 fn register_task_ports(&mut self, node: NodeId, ins: &[BufferId], in_place: bool) {
616 for &b in ins {
617 self.port_readers.entry(b).or_default().push(node);
618 if in_place {
619 self.port_inplace_writers.entry(b).or_default().push(node);
620 self.producer.insert(b, node);
621 }
622 }
623 }
624
625 fn finish(mut self) -> RenderPlan {
628 for port in self.consumer_ports.clone() {
629 let output = self.buffer_for(&port);
630 let sources: Vec<BufferId> = {
631 let conns = port.connections();
632 conns.iter().map(|p| self.buffer_for(p)).collect()
633 };
634 let node = if sources.is_empty() {
635 self.push_node(Op::Zero { output })
636 } else {
637 let node = self.push_node(Op::Sum {
638 inputs: sources.clone(),
639 output,
640 });
641 for src in sources {
642 match self.producer.get(&src) {
643 Some(&prod) => {
644 self.edges.insert((prod, node));
645 }
646 None => {
647 tracing::warn!(
648 "render plan: connection source for buffer {src} has no producer; \
649 treating as silent"
650 );
651 }
652 }
653 }
654 node
655 };
656 if let Some(readers) = self.port_readers.get(&output).cloned() {
658 for reader in readers {
659 self.edges.insert((node, reader));
660 }
661 }
662 }
663
664 for port in self.midi_ports.clone() {
669 let key = Arc::as_ptr(&port) as usize;
670 let Some(&reader) = self.midi_readers.get(&key) else {
671 continue;
672 };
673 for source in port.sources() {
674 let src_key = Arc::as_ptr(&source) as usize;
675 let Some(&writer) = self.midi_writers.get(&src_key) else {
676 continue;
677 };
678 if writer != reader && self.edges.insert((writer, reader)) {
679 self.midi_edges.push((writer, reader));
680 }
681 }
682 }
683
684 let n = self.nodes.len();
685 let (order, forced) = topo_sort(n, &self.edges);
686 let mut remap = vec![0u32; n];
687 for (new_idx, &old_idx) in order.iter().enumerate() {
688 remap[old_idx as usize] = new_idx as NodeId;
689 }
690
691 let mut nodes = Vec::with_capacity(n);
692 for &old_idx in &order {
693 nodes.push(std::mem::replace(
694 &mut self.nodes[old_idx as usize],
695 Op::Zero { output: 0 },
696 ));
697 }
698
699 let mut indegree = vec![0u32; n];
700 let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
701 for &(from, to) in &self.edges {
702 let (from, to) = (remap[from as usize], remap[to as usize]);
703 indegree[to as usize] += 1;
704 dependents[from as usize].push(to);
705 }
706 let sources: Vec<NodeId> = (0..n as NodeId)
707 .filter(|&i| indegree[i as usize] == 0)
708 .collect();
709 let forced: Vec<NodeId> = forced.iter().map(|&f| remap[f as usize]).collect();
710 let midi_edges: Vec<(NodeId, NodeId)> = self
711 .midi_edges
712 .iter()
713 .map(|&(from, to)| (remap[from as usize], remap[to as usize]))
714 .collect();
715
716 let plan = RenderPlan {
717 buffer_size: self.buffer_size,
718 buffers: self.buffers,
719 nodes,
720 indegree,
721 dependents,
722 sources,
723 hw_in_map: self.hw_in_map,
724 hw_out_map: self.hw_out_map,
725 port_map: self.port_map,
726 midi_edges,
727 forced,
728 };
729 if let Err(e) = plan.verify() {
730 tracing::error!("render plan invariant violation: {e}");
731 }
732 plan
733 }
734}
735
736fn topo_sort(n: usize, edges: &HashSet<(NodeId, NodeId)>) -> (Vec<NodeId>, Vec<NodeId>) {
740 let mut indegree = vec![0u32; n];
741 let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
742 for &(from, to) in edges {
743 indegree[to as usize] += 1;
744 dependents[from as usize].push(to);
745 }
746 let mut queue: VecDeque<NodeId> = (0..n as NodeId)
747 .filter(|&i| indegree[i as usize] == 0)
748 .collect();
749 let mut order = Vec::with_capacity(n);
750 while let Some(node) = queue.pop_front() {
751 order.push(node);
752 for &d in &dependents[node as usize] {
753 indegree[d as usize] -= 1;
754 if indegree[d as usize] == 0 {
755 queue.push_back(d);
756 }
757 }
758 }
759 let placed: HashSet<NodeId> = order.iter().copied().collect();
760 let forced: Vec<NodeId> = (0..n as NodeId).filter(|i| !placed.contains(i)).collect();
761 order.extend(forced.iter().copied());
762 (order, forced)
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768 use crate::connectable::connect_audio;
769 use crate::state::State;
770
771 fn make_track(name: &str, ins: usize, outs: usize) -> TrackHandle {
772 Arc::new(Track::new(name.to_string(), ins, outs, 0, 0, 64, 48_000.0))
773 }
774
775 fn state_with(tracks: Vec<TrackHandle>) -> StateSnapshot {
776 let mut state = State::default();
777 for t in tracks {
778 state.tracks.insert(t.lock().name.clone(), t);
779 }
780 state.snapshot()
781 }
782
783 fn connect(a: &TrackHandle, a_port: usize, b: &TrackHandle, b_port: usize) {
785 let src = a.lock();
786 let dst = b.lock();
787 connect_audio(&*src, a_port, &*dst, b_port).expect("connect");
788 }
789
790 fn task_nodes(plan: &RenderPlan, name: &str) -> Vec<usize> {
791 plan.nodes
792 .iter()
793 .enumerate()
794 .filter_map(|(i, op)| match op {
795 Op::Task { task, .. } => {
796 let track = match task {
797 ProcessTask::Track(t)
798 | ProcessTask::FolderInput(t)
799 | ProcessTask::FolderOutput(t) => t,
800 ProcessTask::Plugin { track, .. } => track,
801 };
802 if track.lock().name == name {
803 Some(i)
804 } else {
805 None
806 }
807 }
808 _ => None,
809 })
810 .collect()
811 }
812
813 fn task_node(plan: &RenderPlan, name: &str, want: fn(&ProcessTask) -> bool) -> usize {
814 plan.nodes
815 .iter()
816 .enumerate()
817 .find_map(|(i, op)| match op {
818 Op::Task { task, .. } => {
819 let track = match task {
820 ProcessTask::Track(t)
821 | ProcessTask::FolderInput(t)
822 | ProcessTask::FolderOutput(t) => t,
823 ProcessTask::Plugin { track, .. } => track,
824 };
825 if track.lock().name == name && want(task) {
826 Some(i)
827 } else {
828 None
829 }
830 }
831 _ => None,
832 })
833 .expect("task node not found")
834 }
835
836 fn sum_nodes(plan: &RenderPlan) -> Vec<(usize, Vec<BufferId>, BufferId)> {
837 plan.nodes
838 .iter()
839 .enumerate()
840 .filter_map(|(i, op)| match op {
841 Op::Sum { inputs, output } => Some((i, inputs.clone(), *output)),
842 _ => None,
843 })
844 .collect()
845 }
846
847 fn zero_count(plan: &RenderPlan) -> usize {
848 plan.nodes
849 .iter()
850 .filter(|op| matches!(op, Op::Zero { .. }))
851 .count()
852 }
853
854 fn is_track(t: &ProcessTask) -> bool {
855 matches!(t, ProcessTask::Track(_))
856 }
857 fn is_folder_input(t: &ProcessTask) -> bool {
858 matches!(t, ProcessTask::FolderInput(_))
859 }
860 fn is_folder_output(t: &ProcessTask) -> bool {
861 matches!(t, ProcessTask::FolderOutput(_))
862 }
863
864 #[test]
865 fn producer_chain_orders_zero_track_sum_track() {
866 let a = make_track("a", 1, 1);
867 let b = make_track("b", 1, 1);
868 connect(&a, 0, &b, 0);
869 let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
870 plan.verify().expect("invariants");
871
872 let sums = sum_nodes(&plan);
873 assert_eq!(sums.len(), 1, "one connected input -> one Sum");
874 assert_eq!(sums[0].1.len(), 1);
875
876 let task_a = task_node(&plan, "a", is_track);
877 let task_b = task_node(&plan, "b", is_track);
878 let sum = sums[0].0;
879 assert_eq!(zero_count(&plan), 1, "a's unconnected input -> Zero");
880 let zero = plan
881 .nodes
882 .iter()
883 .position(|op| matches!(op, Op::Zero { .. }))
884 .expect("zero node");
885
886 assert!(zero < task_a, "Zero before the task that reads it");
887 assert!(task_a < sum, "producer before the Sum of its consumer");
888 assert!(sum < task_b, "Sum before the consuming track");
889 assert_eq!(plan.sources, vec![zero as NodeId]);
890 assert_eq!(plan.indegree[task_b], 1);
891 assert!(plan.forced.is_empty());
892 }
893
894 #[test]
895 fn two_sources_insert_sum_with_two_inputs() {
896 let a = make_track("a", 0, 1);
897 let b = make_track("b", 0, 1);
898 let c = make_track("c", 1, 1);
899 connect(&a, 0, &c, 0);
900 connect(&b, 0, &c, 0);
901 let plan = RenderPlan::compile(&state_with(vec![a, b, c]), &[], &[], 64);
902 plan.verify().expect("invariants");
903
904 let sums = sum_nodes(&plan);
905 assert_eq!(sums.len(), 1);
906 assert_eq!(sums[0].1.len(), 2, "both sources summed");
907 assert_eq!(plan.indegree[sums[0].0], 2);
908
909 let task_a = task_node(&plan, "a", is_track);
910 let task_b = task_node(&plan, "b", is_track);
911 assert!(task_a < sums[0].0 && task_b < sums[0].0);
912 assert_eq!(zero_count(&plan), 0);
914 assert_eq!(plan.sources.len(), 2, "two root tracks are sources");
915 }
916
917 #[test]
918 fn metronome_source_is_produced_by_track_task() {
919 let metronome = make_track("metronome", 0, 1);
920 let source = {
921 let mut track = metronome.lock();
922 let (source, changed) = track.ensure_metronome_source(64);
923 assert!(changed);
924 source.expect("metronome source")
925 };
926 let plan = RenderPlan::compile(&state_with(vec![metronome]), &[], &[], 64);
927 plan.verify().expect("invariants");
928
929 let source_key = Arc::as_ptr(&source) as usize;
930 let source_buffer = *plan.port_map.get(&source_key).expect("source buffer");
931 let task = task_node(&plan, "metronome", is_track);
932
933 match &plan.nodes[task] {
934 Op::Task { outs, .. } => assert!(outs.contains(&source_buffer)),
935 _ => unreachable!(),
936 }
937 }
938
939 #[test]
940 fn folder_track_emits_input_child_output_chain() {
941 let folder = make_track("folder", 1, 1);
942 let child = make_track("child", 1, 1);
943 folder.lock().is_folder = true;
944 child.lock().parent_track = Some("folder".to_string());
945 folder.lock().child_tracks.push(child.clone());
946 let plan = RenderPlan::compile(&state_with(vec![folder, child]), &[], &[], 64);
947 plan.verify().expect("invariants");
948
949 let fi = task_node(&plan, "folder", is_folder_input);
950 let fo = task_node(&plan, "folder", is_folder_output);
951 let child_task = task_node(&plan, "child", is_track);
952 assert!(fi < child_task, "folder input before child");
953 assert!(child_task < fo, "child before folder output");
954 assert!(plan.dependents[fi].contains(&(child_task as NodeId)));
955 assert!(plan.dependents[child_task].contains(&(fo as NodeId)));
956 assert_eq!(task_nodes(&plan, "child").len(), 1);
958 }
959
960 #[test]
961 fn midi_only_connection_inserts_ordering_edge() {
962 let a = Arc::new(Track::new("a".to_string(), 0, 0, 0, 1, 64, 48_000.0));
964 let b = Arc::new(Track::new("b".to_string(), 0, 0, 1, 0, 64, 48_000.0));
965 let a_out = a.lock().midi.outs[0].clone();
966 let b_in = b.lock().midi.ins[0].clone();
967 crate::midi::io::MIDIIO::connect(&a_out, &b_in);
968
969 let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
970 plan.verify().expect("invariants");
971
972 let task_a = task_node(&plan, "a", is_track);
973 let task_b = task_node(&plan, "b", is_track);
974 assert_eq!(plan.midi_edges, vec![(task_a as NodeId, task_b as NodeId)]);
975 assert!(task_a < task_b, "producer task ordered before consumer");
976 assert!(plan.dependents[task_a].contains(&(task_b as NodeId)));
977 assert!(plan.forced.is_empty());
978 }
979
980 #[test]
981 fn feedback_cycle_is_broken_and_marked_forced() {
982 let a = make_track("a", 1, 1);
983 let b = make_track("b", 1, 1);
984 connect(&a, 0, &b, 0);
985 connect(&b, 0, &a, 0);
986 let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
987
988 assert_eq!(plan.nodes.len(), 4);
990 assert!(plan.sources.is_empty());
991 assert_eq!(plan.forced.len(), 4, "whole cycle marked forced");
992 plan.verify().expect("invariants tolerate forced cycle");
994 }
995
996 #[test]
997 fn hw_bridges_become_source_and_sink_nodes() {
998 let t = make_track("t", 1, 1);
999 let hw_in = Arc::new(AudioIO::new(64));
1000 let hw_out = Arc::new(AudioIO::new(64));
1001 {
1003 let track = t.lock();
1004 AudioIO::connect(&hw_in, &track.audio.ins[0]);
1005 AudioIO::connect(&track.audio.outs[0], &hw_out);
1006 }
1007 let plan = RenderPlan::compile(
1008 &state_with(vec![t]),
1009 std::slice::from_ref(&hw_in),
1010 std::slice::from_ref(&hw_out),
1011 64,
1012 );
1013 plan.verify().expect("invariants");
1014
1015 let hw_node = plan
1016 .nodes
1017 .iter()
1018 .position(|op| matches!(op, Op::HwInput { .. }))
1019 .expect("HwInput node");
1020 assert_eq!(plan.hw_in_map.len(), 1);
1021 assert_eq!(plan.hw_out_map.len(), 1);
1022 let (chan, buf) = plan.hw_in_map[0];
1023 assert_eq!(chan, 0);
1024 match &plan.nodes[hw_node] {
1025 Op::HwInput { output, .. } => assert_eq!(*output, buf),
1026 _ => unreachable!(),
1027 }
1028 assert!(plan.sources.contains(&(hw_node as NodeId)));
1030 let sums = sum_nodes(&plan);
1031 assert_eq!(sums.len(), 2, "track input sum + hw_out bridge sum");
1032 let (out_buf, out_chan) = plan.hw_out_map[0];
1034 assert_eq!(out_chan, 0);
1035 assert!(sums.iter().any(|(_, _, output)| *output == out_buf));
1036 }
1037
1038 fn hand_plan(
1040 buffers: usize,
1041 nodes: Vec<Op>,
1042 indegree: Vec<u32>,
1043 dependents: Vec<Vec<NodeId>>,
1044 sources: Vec<NodeId>,
1045 ) -> RenderPlan {
1046 RenderPlan {
1047 buffer_size: 64,
1048 buffers: (0..buffers)
1049 .map(|_| UnsafeCell::new(vec![0.0; 64]))
1050 .collect(),
1051 nodes,
1052 indegree,
1053 dependents,
1054 sources,
1055 hw_in_map: vec![],
1056 hw_out_map: vec![],
1057 port_map: HashMap::new(),
1058 midi_edges: vec![],
1059 forced: vec![],
1060 }
1061 }
1062
1063 #[test]
1064 fn verify_rejects_backward_edge() {
1065 let plan = hand_plan(
1066 2,
1067 vec![
1068 Op::Sum {
1069 inputs: vec![1],
1070 output: 0,
1071 },
1072 Op::HwInput {
1073 channel: 0,
1074 output: 1,
1075 },
1076 ],
1077 vec![1, 0],
1078 vec![vec![], vec![0]],
1079 vec![1],
1080 );
1081 assert!(plan.verify().is_err());
1084 }
1085
1086 #[test]
1087 fn verify_rejects_racing_writers() {
1088 let plan = hand_plan(
1090 3,
1091 vec![
1092 Op::HwInput {
1093 channel: 0,
1094 output: 1,
1095 },
1096 Op::HwInput {
1097 channel: 1,
1098 output: 2,
1099 },
1100 Op::Sum {
1101 inputs: vec![1],
1102 output: 0,
1103 },
1104 Op::Sum {
1105 inputs: vec![2],
1106 output: 0,
1107 },
1108 ],
1109 vec![2, 0, 0, 0],
1110 vec![vec![], vec![2], vec![], vec![]],
1111 vec![0, 1],
1112 );
1113 let err = plan.verify().expect_err("racing writers must fail");
1114 assert!(err.contains("unordered nodes"));
1115 }
1116
1117 #[test]
1118 fn buffers_are_sized_and_silent() {
1119 let t = make_track("t", 2, 1);
1120 let plan = RenderPlan::compile(&state_with(vec![t]), &[], &[], 256);
1121 assert_eq!(plan.buffer_size, 256);
1122 assert_eq!(plan.buffer_count(), 3);
1124 for i in 0..plan.buffer_count() as BufferId {
1125 let buf = unsafe { plan.buffer(i) };
1127 assert_eq!(buf.len(), 256);
1128 assert!(buf.iter().all(|&s| s == 0.0));
1129 }
1130 assert_eq!(zero_count(&plan), 2);
1132 assert_eq!(plan.port_map.len(), 3);
1133 }
1134}