Skip to main content

maolan_engine/
render_plan.rs

1//! Compiled, immutable audio render plan — Phase 2 of `LOCKLESS.md`.
2//!
3//! A `RenderPlan` is the flattened, topologically ordered form of the engine's
4//! per-cycle task graph plus the `AudioIO` port network. It is built on the
5//! control thread (see the builder contract in `LOCKLESS.md`), published with
6//! `arc_swap`, and executed by the cycle executor with count-up dependency
7//! counters. Nothing in it is behind a mutex: while a plan is alive it never
8//! mutates.
9//!
10//! Key invariants (checked by [`RenderPlan::verify`] after every compile):
11//!
12//! - **Single-producer chains.** Every arena buffer has at least one writer,
13//!   and multiple writers of the same buffer always form a dependency chain
14//!   (e.g. an input `Sum` node writes a track input, then the track task adds
15//!   clip audio in place). Two nodes that could run concurrently never write
16//!   the same buffer.
17//! - **Topological order.** Except for `forced` nodes (feedback loops, broken
18//!   deliberately — the same situation today's `finished`-flag scheduler
19//!   resolves with its force-progress fallback), every edge points from a
20//!   lower node index to a higher one, so sequential execution is a plain
21//!   iteration and the arena can be split safely at each output index.
22
23use 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;
33use std::sync::atomic::{AtomicUsize, Ordering};
34
35/// A plan shared across the dispatcher, workers and hardware drivers.
36///
37/// The `Owned` wrapper defers the actual free of the plan (the whole buffer
38/// arena) to the builder thread's `basedrop::Collector`: the final `Arc` drop
39/// may land on a worker thread (in-flight jobs hold clones), but it only
40/// queues the memory for reclamation instead of freeing it inline.
41pub type SharedPlan = Arc<basedrop::Owned<RenderPlan>>;
42/// The atomically published plan slot. The dispatcher pulls with
43/// `load_full()` at cycle start; the builder stores newly compiled plans.
44pub type PlanSlot = arc_swap::ArcSwap<basedrop::Owned<RenderPlan>>;
45
46/// Index into [`RenderPlan::buffers`].
47pub type BufferId = u32;
48/// Index into [`RenderPlan::nodes`].
49pub type NodeId = u32;
50
51/// What a plan node does when executed.
52#[derive(Debug)]
53pub enum Op {
54    /// Fill `output` with silence for an unconnected consumer port.
55    Zero { output: BufferId },
56    /// Sum all `inputs` into `output` for a connected consumer port.
57    Sum {
58        inputs: Vec<BufferId>,
59        delays: Vec<UnsafeCell<DelayLine>>,
60        output: BufferId,
61    },
62    /// Engine task: track / folder section / plugin processing. Keeps the
63    /// transitional `TrackHandle` until Phase 5's `TrackRt` split lands;
64    /// `ins`/`outs` are the arena buffers this task reads (post-`Sum`) and
65    /// produces.
66    Task {
67        task: ProcessTask,
68        ins: Vec<BufferId>,
69        outs: Vec<BufferId>,
70    },
71    /// Hardware input bridge: the driver thread writes `output` directly each
72    /// block (JACK `copy_audio_inputs`, `fill_ports_from_interleaved_buffer`).
73    /// A pure source — no plan node produces it.
74    HwInput { channel: usize, output: BufferId },
75}
76
77/// A compiled, immutable render plan. Owns the whole buffer arena.
78#[derive(Debug)]
79pub struct RenderPlan {
80    /// Samples per port buffer (the driver block size at compile time).
81    pub buffer_size: usize,
82    /// The arena: one buffer per `AudioIO` port, owned by the plan. Replaces
83    /// shared mutable buffer wrappers on the real-time path.
84    ///
85    /// Interior mutability is required because workers execute disjoint nodes
86    /// of the same plan concurrently. Soundness rests on the
87    /// single-producer-chain invariant enforced by [`RenderPlan::verify`]:
88    /// two nodes that may run concurrently never touch the same buffer, and
89    /// every buffer access goes through the node currently executing on this
90    /// thread. This is the one audited `unsafe` cell of the design.
91    pub buffers: Vec<UnsafeCell<Vec<f32>>>,
92    pub buffer_latencies: Vec<AtomicUsize>,
93    /// Nodes in topological order (producers before consumers).
94    pub nodes: Vec<Op>,
95    /// `indegree[i]` = number of nodes that must finish before `nodes[i]`.
96    pub indegree: Vec<u32>,
97    /// `dependents[i]` = nodes that may run once `nodes[i]` has finished.
98    pub dependents: Vec<Vec<NodeId>>,
99    /// Nodes with `indegree == 0` — the cycle seeds.
100    pub sources: Vec<NodeId>,
101    /// `(hw channel, buffer)` — the driver fills these before dispatch.
102    pub hw_in_map: Vec<(usize, BufferId)>,
103    /// `(buffer, hw channel)` — the driver drains these after the cycle.
104    pub hw_out_map: Vec<(BufferId, usize)>,
105    /// `Arc` pointer identity of each `AudioIO` port → its arena buffer.
106    /// Transition aid so control code can resolve ports without re-walking.
107    pub port_map: HashMap<usize, BufferId>,
108    /// `(writer task, reader task)` pairs derived from MIDI connections
109    /// (Phase 3, see `LOCKLESS.md`). Event payloads stay in the `MIDIIO`
110    /// port buffers; these edges only guarantee that a port's producer task
111    /// completes before any task that merges it. Checked by `verify()`.
112    pub midi_edges: Vec<(NodeId, NodeId)>,
113    /// Nodes whose dependencies may never be satisfied (feedback loops).
114    /// The executor force-completes them after the task timeout, mirroring
115    /// today's `!progressed` fallback in the dynamic scheduler.
116    pub forced: Vec<NodeId>,
117}
118
119// Safety: the only interior mutability is the buffer arena. Access to it goes
120// through `buffer`/`buffer_ptr`, whose safety contract is the
121// single-producer-chain invariant checked by `verify()`: concurrently running
122// nodes never touch the same buffer, and each buffer access is performed by
123// the thread executing the node that owns it this cycle. Everything else in
124// the plan is plain immutable data (plus `Arc` handles that are already
125// `Send + Sync`).
126unsafe impl Sync for RenderPlan {}
127
128#[derive(Clone, Debug)]
129pub struct DelayLine {
130    buffer: Vec<f32>,
131    pos: usize,
132    delay: usize,
133}
134
135impl DelayLine {
136    pub fn new() -> Self {
137        Self {
138            buffer: Vec::new(),
139            pos: 0,
140            delay: 0,
141        }
142    }
143
144    pub fn process(&mut self, input: &[f32], delay: usize, output: &mut [f32], add: bool) {
145        if self.delay != delay || self.buffer.len() != delay {
146            self.process_with_transition(input, delay, output, add);
147            return;
148        }
149        self.process_direct(input, delay, output, add);
150    }
151
152    fn process_with_transition(
153        &mut self,
154        input: &[f32],
155        delay: usize,
156        output: &mut [f32],
157        add: bool,
158    ) {
159        let mut old_line = self.clone();
160        let mut old_output = vec![0.0; output.len()];
161        old_line.process_direct(input, self.delay, &mut old_output, false);
162
163        self.reset(delay);
164        let mut new_output = vec![0.0; output.len()];
165        self.process_direct(input, delay, &mut new_output, false);
166
167        let frames = output.len().min(input.len());
168        let fade_frames = frames.clamp(1, 128);
169        for frame in 0..frames {
170            let t = ((frame + 1) as f32 / fade_frames as f32).clamp(0.0, 1.0);
171            let sample = old_output[frame] * (1.0 - t) + new_output[frame] * t;
172            if add {
173                output[frame] += sample;
174            } else {
175                output[frame] = sample;
176            }
177        }
178        if !add && frames < output.len() {
179            output[frames..].fill(0.0);
180        }
181    }
182
183    fn reset(&mut self, delay: usize) {
184        self.delay = delay;
185        self.pos = 0;
186        self.buffer.resize(delay, 0.0);
187        self.buffer.fill(0.0);
188    }
189
190    fn process_direct(&mut self, input: &[f32], delay: usize, output: &mut [f32], add: bool) {
191        if delay == 0 {
192            if add {
193                crate::simd::add_sanitized_inplace(output, input);
194            } else {
195                crate::simd::copy_sanitized_inplace(output, input);
196                if input.len() < output.len() {
197                    output[input.len()..].fill(0.0);
198                }
199            }
200            self.delay = 0;
201            self.buffer.clear();
202            self.pos = 0;
203            return;
204        }
205
206        let frames = output.len().min(input.len());
207        for frame in 0..frames {
208            let delayed = self.buffer[self.pos];
209            self.buffer[self.pos] = input[frame];
210            self.pos += 1;
211            if self.pos == self.buffer.len() {
212                self.pos = 0;
213            }
214            let delayed = if delayed.is_finite() { delayed } else { 0.0 };
215            if add {
216                output[frame] += delayed;
217            } else {
218                output[frame] = delayed;
219            }
220        }
221        if !add && frames < output.len() {
222            output[frames..].fill(0.0);
223        }
224    }
225}
226
227impl Default for DelayLine {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233impl RenderPlan {
234    /// Mutable access to an arena buffer, as a raw pointer.
235    ///
236    /// Returns a pointer rather than a `&mut` because the aliasing discipline
237    /// is dynamic (enforced by the plan's dependency graph, not the borrow
238    /// checker) — this is the same shape as `UnsafeCell::get`.
239    ///
240    /// # Safety
241    /// The caller must be executing (or have already completed) the unique
242    /// node chain that writes buffer `id` in this cycle, per the plan's
243    /// single-producer-chain invariant: no other concurrently running node
244    /// may read or write the same buffer. The returned pointer is valid for
245    /// the lifetime of the plan.
246    pub unsafe fn buffer_ptr(&self, id: BufferId) -> *mut Vec<f32> {
247        self.buffers[id as usize].get()
248    }
249
250    /// Read access to an arena buffer.
251    ///
252    /// # Safety
253    /// Same discipline as [`RenderPlan::buffer_ptr`]: the buffer's producer
254    /// chain must have completed, and no concurrent writer may exist.
255    pub unsafe fn buffer(&self, id: BufferId) -> &[f32] {
256        unsafe { &*self.buffers[id as usize].get() }
257    }
258
259    pub fn buffer_latency(&self, id: BufferId) -> usize {
260        self.buffer_latencies[id as usize].load(Ordering::Acquire)
261    }
262
263    pub fn set_buffer_latency(&self, id: BufferId, latency: usize) {
264        self.buffer_latencies[id as usize].store(latency, Ordering::Release);
265    }
266
267    /// Number of arena buffers.
268    pub fn buffer_count(&self) -> usize {
269        self.buffers.len()
270    }
271    /// Compile the current topology (tracks, folders, plugins, port wiring,
272    /// HW bridges) into an immutable plan. Runs on the control thread; may
273    /// allocate freely.
274    ///
275    /// Track visit order is sorted by name so plans are deterministic — the
276    /// legacy scheduler iterated `State.tracks` in HashMap order.
277    pub fn compile(
278        state: &StateSnapshot,
279        hw_inputs: &[Arc<AudioIO>],
280        hw_outputs: &[Arc<AudioIO>],
281        buffer_size: usize,
282    ) -> Self {
283        let mut b = Builder::new(buffer_size);
284        b.add_hw(hw_inputs, hw_outputs);
285
286        let mut ordered: Vec<(String, TrackHandle)> = state
287            .tracks
288            .iter()
289            .map(|(name, track)| (name.clone(), track.clone()))
290            .collect();
291        ordered.sort_by(|a, b| a.0.cmp(&b.0));
292
293        for (_name, track) in &ordered {
294            if track.lock().parent_track.is_some() {
295                continue;
296            }
297            b.append_track(track.clone(), None);
298        }
299
300        b.finish()
301    }
302
303    /// Verify the plan invariants (see the module docs). Called by `compile`
304    /// (violations are logged) and by tests. Returns the first violation.
305    pub fn verify(&self) -> Result<(), String> {
306        let forced: HashSet<NodeId> = self.forced.iter().copied().collect();
307
308        // Non-forced edges must point forward (topological order).
309        for (from, dependents) in self.dependents.iter().enumerate() {
310            for &to in dependents {
311                if from as NodeId >= to
312                    && !(forced.contains(&(from as NodeId)) && forced.contains(&to))
313                {
314                    return Err(format!("edge {from} -> {to} violates topological order"));
315                }
316            }
317        }
318
319        // MIDI edges (producer task -> merging task) obey the same order.
320        for &(from, to) in &self.midi_edges {
321            if from >= to && !(forced.contains(&from) && forced.contains(&to)) {
322                return Err(format!(
323                    "midi edge {from} -> {to} violates topological order"
324                ));
325            }
326        }
327
328        // Collect writers per buffer. `Track` and `FolderInput` tasks also
329        // write their input buffers in place (clip audio is mixed into the
330        // summed input today), so they count as chained writers of `ins`.
331        let mut writers: HashMap<BufferId, Vec<NodeId>> = HashMap::new();
332        for (idx, op) in self.nodes.iter().enumerate() {
333            let idx = idx as NodeId;
334            match op {
335                Op::Zero { output } | Op::Sum { output, .. } | Op::HwInput { output, .. } => {
336                    writers.entry(*output).or_default().push(idx);
337                }
338                Op::Task { task, ins, outs } => {
339                    let writes_ins =
340                        matches!(task, ProcessTask::Track(_) | ProcessTask::FolderInput(_));
341                    for b in outs {
342                        writers.entry(*b).or_default().push(idx);
343                    }
344                    if writes_ins {
345                        for b in ins {
346                            writers.entry(*b).or_default().push(idx);
347                        }
348                    }
349                }
350            }
351        }
352
353        for buffer in 0..self.buffers.len() as BufferId {
354            let ws = writers.get(&buffer).cloned().unwrap_or_default();
355            if ws.is_empty() {
356                return Err(format!("buffer {buffer} has no writer"));
357            }
358            // Multiple writers must be chain-ordered: each consecutive pair
359            // (in node order) must have a dependency path between them.
360            let mut sorted = ws;
361            sorted.sort_unstable();
362            for pair in sorted.windows(2) {
363                if !self.reachable(pair[0], pair[1]) {
364                    return Err(format!(
365                        "buffer {buffer} written by unordered nodes {} ({:?}) and {} ({:?})",
366                        pair[0],
367                        self.nodes[pair[0] as usize],
368                        pair[1],
369                        self.nodes[pair[1] as usize]
370                    ));
371                }
372            }
373        }
374        Ok(())
375    }
376
377    /// Is there a dependency path from `from` to `to`?
378    fn reachable(&self, from: NodeId, to: NodeId) -> bool {
379        if from == to {
380            return true;
381        }
382        let mut seen = HashSet::new();
383        let mut queue = VecDeque::from([from]);
384        seen.insert(from);
385        while let Some(n) = queue.pop_front() {
386            for &d in &self.dependents[n as usize] {
387                if d == to {
388                    return true;
389                }
390                if seen.insert(d) {
391                    queue.push_back(d);
392                }
393            }
394        }
395        false
396    }
397}
398
399/// Mutable compile-time state. Not part of the plan.
400struct Builder {
401    buffer_size: usize,
402    buffers: Vec<UnsafeCell<Vec<f32>>>,
403    port_map: HashMap<usize, BufferId>,
404    nodes: Vec<Op>,
405    edges: HashSet<(NodeId, NodeId)>,
406    /// Buffers that need a `Sum`/`Zero` node (consumer ports).
407    consumer_ports: Vec<Arc<AudioIO>>,
408    /// Task nodes that read each consumer buffer.
409    port_readers: HashMap<BufferId, Vec<NodeId>>,
410    /// Task nodes that also write each consumer buffer in place.
411    port_inplace_writers: HashMap<BufferId, Vec<NodeId>>,
412    /// Producer node per buffer, filled as producer nodes are created.
413    producer: HashMap<BufferId, NodeId>,
414    hw_in_map: Vec<(usize, BufferId)>,
415    hw_out_map: Vec<(BufferId, usize)>,
416    /// `Arc` pointer identity of a MIDI port → the task that writes its
417    /// event buffer this cycle.
418    midi_writers: HashMap<usize, NodeId>,
419    /// `Arc` pointer identity of a MIDI port → the task that reads/merges it.
420    midi_readers: HashMap<usize, NodeId>,
421    /// Every registered MIDI port; walked in `finish` to derive MIDI edges.
422    midi_ports: Vec<Arc<crate::midi::io::MIDIIO>>,
423    midi_edges: Vec<(NodeId, NodeId)>,
424}
425
426impl Builder {
427    fn new(buffer_size: usize) -> Self {
428        Self {
429            buffer_size,
430            buffers: Vec::new(),
431            port_map: HashMap::new(),
432            nodes: Vec::new(),
433            edges: HashSet::new(),
434            consumer_ports: Vec::new(),
435            port_readers: HashMap::new(),
436            port_inplace_writers: HashMap::new(),
437            producer: HashMap::new(),
438            hw_in_map: Vec::new(),
439            hw_out_map: Vec::new(),
440            midi_writers: HashMap::new(),
441            midi_readers: HashMap::new(),
442            midi_ports: Vec::new(),
443            midi_edges: Vec::new(),
444        }
445    }
446
447    /// Register a track's own MIDI ports: inputs are written and read by the
448    /// track's first task (folder input / track body), outputs by its last
449    /// task (folder output / track body).
450    fn register_midi_track_ports(&mut self, t: &TrackData, first: NodeId, last: NodeId) {
451        for p in &t.midi.ins {
452            let key = Arc::as_ptr(p) as usize;
453            self.midi_writers.insert(key, first);
454            self.midi_readers.insert(key, first);
455            self.midi_ports.push(p.clone());
456        }
457        for p in &t.midi.outs {
458            let key = Arc::as_ptr(p) as usize;
459            self.midi_writers.insert(key, last);
460            self.midi_readers.insert(key, last);
461            self.midi_ports.push(p.clone());
462        }
463    }
464
465    /// Register a plugin's MIDI ports at the task that processes it (its own
466    /// node for folder plugins, the track task for inline plugins).
467    fn register_plugin_midi_ports(
468        &mut self,
469        t: &TrackData,
470        kind: PluginKind,
471        index: usize,
472        node: NodeId,
473    ) {
474        let (midi_ins, midi_outs): (
475            Vec<Arc<crate::midi::io::MIDIIO>>,
476            Vec<Arc<crate::midi::io::MIDIIO>>,
477        ) = match kind {
478            PluginKind::Clap => {
479                let proc = t.clap_plugins[index].processor.clone();
480                (
481                    proc.midi_input_ports().to_vec(),
482                    proc.midi_output_ports().to_vec(),
483                )
484            }
485            PluginKind::Vst3 => {
486                let proc = t.vst3_plugins[index].processor.clone();
487                (
488                    proc.midi_input_ports().to_vec(),
489                    proc.midi_output_ports().to_vec(),
490                )
491            }
492            #[cfg(unix)]
493            PluginKind::Lv2 => {
494                let proc = t.lv2_plugins[index].processor.clone();
495                (
496                    proc.midi_input_ports().to_vec(),
497                    proc.midi_output_ports().to_vec(),
498                )
499            }
500        };
501        for p in midi_ins {
502            let key = Arc::as_ptr(&p) as usize;
503            self.midi_writers.insert(key, node);
504            self.midi_readers.insert(key, node);
505            self.midi_ports.push(p);
506        }
507        for p in midi_outs {
508            let key = Arc::as_ptr(&p) as usize;
509            self.midi_writers.insert(key, node);
510            self.midi_ports.push(p);
511        }
512    }
513
514    /// Arena buffer for a port, registering it (at silence) on first sight.
515    fn buffer_for(&mut self, port: &Arc<AudioIO>) -> BufferId {
516        let key = Arc::as_ptr(port) as usize;
517        if let Some(&id) = self.port_map.get(&key) {
518            return id;
519        }
520        let id = self.buffers.len() as BufferId;
521        self.buffers
522            .push(UnsafeCell::new(vec![0.0; self.buffer_size]));
523        self.port_map.insert(key, id);
524        id
525    }
526
527    fn push_node(&mut self, op: Op) -> NodeId {
528        self.nodes.push(op);
529        (self.nodes.len() - 1) as NodeId
530    }
531
532    fn add_hw(&mut self, hw_inputs: &[Arc<AudioIO>], hw_outputs: &[Arc<AudioIO>]) {
533        for (channel, port) in hw_inputs.iter().enumerate() {
534            let output = self.buffer_for(port);
535            let node = self.push_node(Op::HwInput { channel, output });
536            self.producer.insert(output, node);
537            self.hw_in_map.push((channel, output));
538        }
539        for (channel, port) in hw_outputs.iter().enumerate() {
540            let buffer = self.buffer_for(port);
541            self.consumer_ports.push(port.clone());
542            self.hw_out_map.push((buffer, channel));
543        }
544    }
545
546    /// Mirror of the legacy `append_track_tasks`: emits the task nodes for a
547    /// track (folder sections, plugins, children) and returns the first and
548    /// last node of the track's subgraph, for chaining by the caller.
549    fn append_track(
550        &mut self,
551        track: TrackHandle,
552        predecessor: Option<NodeId>,
553    ) -> (NodeId, NodeId) {
554        let t = track.lock();
555        let ins: Vec<BufferId> = t.audio.ins.iter().map(|p| self.buffer_for(p)).collect();
556        let outs: Vec<BufferId> = t.audio.outs.iter().map(|p| self.buffer_for(p)).collect();
557        let metronome_source = t.metronome_source();
558        let metronome_out = metronome_source.as_ref().map(|p| self.buffer_for(p));
559        for p in &t.audio.ins {
560            self.consumer_ports.push(p.clone());
561        }
562
563        if t.is_folder {
564            let mut folder_input_outs = Vec::new();
565            if let Some(out) = metronome_out {
566                folder_input_outs.push(out);
567            }
568            let folder_input = self.push_node(Op::Task {
569                task: ProcessTask::FolderInput(track.clone()),
570                ins: ins.clone(),
571                outs: folder_input_outs,
572            });
573            if let Some(pred) = predecessor {
574                self.edges.insert((pred, folder_input));
575            }
576            self.register_task_ports(folder_input, &ins, true);
577            if let Some(out) = metronome_out {
578                self.producer.insert(out, folder_input);
579            }
580
581            let mut source_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
582            let mut target_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
583            source_keys.insert(ConnectableRef::TrackInput, folder_input);
584            target_keys.insert(ConnectableRef::TrackInput, folder_input);
585
586            let mut plugin_nodes: Vec<NodeId> = Vec::new();
587            for idx in 0..t.clap_plugins.len() {
588                let node = self.push_plugin(&track, &t, PluginKind::Clap, idx, folder_input);
589                let id = t.clap_plugins[idx].id;
590                source_keys.insert(ConnectableRef::ClapPlugin(id), node);
591                target_keys.insert(ConnectableRef::ClapPlugin(id), node);
592                plugin_nodes.push(node);
593            }
594            for idx in 0..t.vst3_plugins.len() {
595                let node = self.push_plugin(&track, &t, PluginKind::Vst3, idx, folder_input);
596                let id = t.vst3_plugins[idx].id;
597                source_keys.insert(ConnectableRef::Vst3Plugin(id), node);
598                target_keys.insert(ConnectableRef::Vst3Plugin(id), node);
599                plugin_nodes.push(node);
600            }
601            #[cfg(unix)]
602            for idx in 0..t.lv2_plugins.len() {
603                let node = self.push_plugin(&track, &t, PluginKind::Lv2, idx, folder_input);
604                let id = t.lv2_plugins[idx].id;
605                source_keys.insert(ConnectableRef::Lv2Plugin(id), node);
606                target_keys.insert(ConnectableRef::Lv2Plugin(id), node);
607                plugin_nodes.push(node);
608            }
609
610            let mut child_lasts: Vec<NodeId> = Vec::new();
611            for child_track in &t.child_tracks {
612                let (child_first, child_last) =
613                    self.append_track(child_track.clone(), Some(folder_input));
614                let child_name = child_track.lock().name.clone();
615                source_keys.insert(ConnectableRef::ChildTrack(child_name.clone()), child_last);
616                target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first);
617                child_lasts.push(child_last);
618            }
619
620            let folder_output = self.push_node(Op::Task {
621                task: ProcessTask::FolderOutput(track.clone()),
622                ins: Vec::new(),
623                outs: outs.clone(),
624            });
625            self.edges.insert((folder_input, folder_output));
626            for &p in &plugin_nodes {
627                self.edges.insert((p, folder_output));
628            }
629            for &c in &child_lasts {
630                self.edges.insert((c, folder_output));
631            }
632            for &out in &outs {
633                self.producer.insert(out, folder_output);
634            }
635            self.register_midi_track_ports(&t, folder_input, folder_output);
636
637            // Cross-connectable edges within this folder's routing graph,
638            // exactly as the legacy builder derived them.
639            for conn in t.connectable_connections() {
640                let ConnectableConnection { from, to, .. } = conn;
641                let (Some(&source), Some(&target)) = (source_keys.get(&from), target_keys.get(&to))
642                else {
643                    continue;
644                };
645                if source != target {
646                    self.edges.insert((source, target));
647                }
648            }
649
650            (folder_input, folder_output)
651        } else {
652            let mut task_outs = outs.clone();
653            if let Some(out) = metronome_out {
654                task_outs.push(out);
655            }
656            let task = self.push_node(Op::Task {
657                task: ProcessTask::Track(track.clone()),
658                ins: ins.clone(),
659                outs: task_outs,
660            });
661            if let Some(pred) = predecessor {
662                self.edges.insert((pred, task));
663            }
664            self.register_task_ports(task, &ins, true);
665            for &out in &outs {
666                self.producer.insert(out, task);
667            }
668            if let Some(out) = metronome_out {
669                self.producer.insert(out, task);
670            }
671            self.register_midi_track_ports(&t, task, task);
672            // Inline plugins are processed inside the track task body.
673            for idx in 0..t.clap_plugins.len() {
674                self.register_plugin_midi_ports(&t, PluginKind::Clap, idx, task);
675            }
676            for idx in 0..t.vst3_plugins.len() {
677                self.register_plugin_midi_ports(&t, PluginKind::Vst3, idx, task);
678            }
679            #[cfg(unix)]
680            for idx in 0..t.lv2_plugins.len() {
681                self.register_plugin_midi_ports(&t, PluginKind::Lv2, idx, task);
682            }
683            (task, task)
684        }
685    }
686
687    fn push_plugin(
688        &mut self,
689        track: &TrackHandle,
690        t: &TrackData,
691        kind: PluginKind,
692        index: usize,
693        folder_input: NodeId,
694    ) -> NodeId {
695        let (input_ports, output_ports): (Vec<Arc<AudioIO>>, Vec<Arc<AudioIO>>) = match kind {
696            PluginKind::Clap => {
697                let proc = t.clap_plugins[index].processor.clone();
698                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
699            }
700            PluginKind::Vst3 => {
701                let proc = t.vst3_plugins[index].processor.clone();
702                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
703            }
704            #[cfg(unix)]
705            PluginKind::Lv2 => {
706                let proc = t.lv2_plugins[index].processor.clone();
707                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
708            }
709        };
710        for p in &input_ports {
711            self.consumer_ports.push(p.clone());
712        }
713        let pins: Vec<BufferId> = input_ports.iter().map(|p| self.buffer_for(p)).collect();
714        let pouts: Vec<BufferId> = output_ports.iter().map(|p| self.buffer_for(p)).collect();
715        let node = self.push_node(Op::Task {
716            task: ProcessTask::Plugin {
717                track: track.clone(),
718                kind,
719                index,
720            },
721            ins: pins.clone(),
722            outs: pouts.clone(),
723        });
724        self.edges.insert((folder_input, node));
725        self.register_task_ports(node, &pins, false);
726        for &out in &pouts {
727            self.producer.insert(out, node);
728        }
729        self.register_plugin_midi_ports(t, kind, index, node);
730        node
731    }
732
733    /// Record which task reads (and optionally writes in place) each port.
734    fn register_task_ports(&mut self, node: NodeId, ins: &[BufferId], in_place: bool) {
735        for &b in ins {
736            self.port_readers.entry(b).or_default().push(node);
737            if in_place {
738                self.port_inplace_writers.entry(b).or_default().push(node);
739                self.producer.insert(b, node);
740            }
741        }
742    }
743
744    /// Create the `Sum`/`Zero` nodes for every consumer port, wire the edges,
745    /// topologically sort, and freeze into a `RenderPlan`.
746    fn finish(mut self) -> RenderPlan {
747        // The same consumer port can be registered more than once (e.g. a
748        // plugin input port referenced by multiple routing records). Each port
749        // owns exactly one arena buffer, so only one Sum/Zero node may write
750        // it; deduplicate by `Arc` pointer identity before emitting nodes.
751        let mut seen = HashSet::new();
752        self.consumer_ports
753            .retain(|p| seen.insert(Arc::as_ptr(p) as usize));
754
755        for port in self.consumer_ports.clone() {
756            let output = self.buffer_for(&port);
757            let readers = self.port_readers.get(&output).cloned().unwrap_or_default();
758            let sources: Vec<BufferId> = {
759                let conns = port.connections();
760                conns
761                    .iter()
762                    .filter_map(|p| {
763                        let src = self.buffer_for(p);
764                        match self.producer.get(&src) {
765                            Some(producer) if readers.contains(producer) => None,
766                            _ => Some(src),
767                        }
768                    })
769                    .collect()
770            };
771            let node = if sources.is_empty() {
772                self.push_node(Op::Zero { output })
773            } else {
774                let node = self.push_node(Op::Sum {
775                    inputs: sources.clone(),
776                    delays: sources
777                        .iter()
778                        .map(|_| UnsafeCell::new(DelayLine::new()))
779                        .collect(),
780                    output,
781                });
782                for src in sources {
783                    match self.producer.get(&src) {
784                        Some(&prod) => {
785                            self.edges.insert((prod, node));
786                        }
787                        None => {
788                            tracing::warn!(
789                                "render plan: connection source for buffer {src} has no producer; \
790                                 treating as silent"
791                            );
792                        }
793                    }
794                }
795                node
796            };
797            // Every task reading this port runs after its Sum/Zero node.
798            for reader in readers {
799                self.edges.insert((node, reader));
800            }
801        }
802
803        // MIDI edges: for every registered port, order each source's writer
804        // task before the task that merges this port (Phase 3). Unregistered
805        // sources (e.g. ports outside any track) are skipped — their writers
806        // are serialized by the cycle boundary.
807        for port in self.midi_ports.clone() {
808            let key = Arc::as_ptr(&port) as usize;
809            let Some(&reader) = self.midi_readers.get(&key) else {
810                continue;
811            };
812            for source in port.sources() {
813                let src_key = Arc::as_ptr(&source) as usize;
814                let Some(&writer) = self.midi_writers.get(&src_key) else {
815                    continue;
816                };
817                if writer != reader && self.edges.insert((writer, reader)) {
818                    self.midi_edges.push((writer, reader));
819                }
820            }
821        }
822
823        let n = self.nodes.len();
824        let (order, forced) = topo_sort(n, &self.edges);
825        let mut remap = vec![0u32; n];
826        for (new_idx, &old_idx) in order.iter().enumerate() {
827            remap[old_idx as usize] = new_idx as NodeId;
828        }
829
830        let mut nodes = Vec::with_capacity(n);
831        for &old_idx in &order {
832            nodes.push(std::mem::replace(
833                &mut self.nodes[old_idx as usize],
834                Op::Zero { output: 0 },
835            ));
836        }
837
838        let mut indegree = vec![0u32; n];
839        let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
840        for &(from, to) in &self.edges {
841            let (from, to) = (remap[from as usize], remap[to as usize]);
842            indegree[to as usize] += 1;
843            dependents[from as usize].push(to);
844        }
845        let sources: Vec<NodeId> = (0..n as NodeId)
846            .filter(|&i| indegree[i as usize] == 0)
847            .collect();
848        let forced: Vec<NodeId> = forced.iter().map(|&f| remap[f as usize]).collect();
849        let midi_edges: Vec<(NodeId, NodeId)> = self
850            .midi_edges
851            .iter()
852            .map(|&(from, to)| (remap[from as usize], remap[to as usize]))
853            .collect();
854
855        let buffer_count = self.buffers.len();
856        let plan = RenderPlan {
857            buffer_size: self.buffer_size,
858            buffers: self.buffers,
859            buffer_latencies: (0..buffer_count).map(|_| AtomicUsize::new(0)).collect(),
860            nodes,
861            indegree,
862            dependents,
863            sources,
864            hw_in_map: self.hw_in_map,
865            hw_out_map: self.hw_out_map,
866            port_map: self.port_map,
867            midi_edges,
868            forced,
869        };
870        if let Err(e) = plan.verify() {
871            tracing::error!("render plan invariant violation: {e}");
872        }
873        plan
874    }
875}
876
877/// Kahn's algorithm. Returns the topological order of all nodes — nodes left
878/// over after the algorithm (feedback loops) are appended at the end and also
879/// returned separately as `forced`.
880fn topo_sort(n: usize, edges: &HashSet<(NodeId, NodeId)>) -> (Vec<NodeId>, Vec<NodeId>) {
881    let mut indegree = vec![0u32; n];
882    let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
883    for &(from, to) in edges {
884        indegree[to as usize] += 1;
885        dependents[from as usize].push(to);
886    }
887    let mut queue: VecDeque<NodeId> = (0..n as NodeId)
888        .filter(|&i| indegree[i as usize] == 0)
889        .collect();
890    let mut order = Vec::with_capacity(n);
891    while let Some(node) = queue.pop_front() {
892        order.push(node);
893        for &d in &dependents[node as usize] {
894            indegree[d as usize] -= 1;
895            if indegree[d as usize] == 0 {
896                queue.push_back(d);
897            }
898        }
899    }
900    let placed: HashSet<NodeId> = order.iter().copied().collect();
901    let forced: Vec<NodeId> = (0..n as NodeId).filter(|i| !placed.contains(i)).collect();
902    order.extend(forced.iter().copied());
903    (order, forced)
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use crate::connectable::connect_audio;
910    use crate::state::State;
911
912    fn make_track(name: &str, ins: usize, outs: usize) -> TrackHandle {
913        Arc::new(Track::new(name.to_string(), ins, outs, 0, 0, 64, 48_000.0))
914    }
915
916    fn state_with(tracks: Vec<TrackHandle>) -> StateSnapshot {
917        let mut state = State::default();
918        for t in tracks {
919            state.tracks.insert(t.lock().name.clone(), t);
920        }
921        state.snapshot()
922    }
923
924    /// Connect `a`'s output `a_port` to `b`'s input `b_port`.
925    fn connect(a: &TrackHandle, a_port: usize, b: &TrackHandle, b_port: usize) {
926        let src = a.lock();
927        let dst = b.lock();
928        connect_audio(&*src, a_port, &*dst, b_port).expect("connect");
929    }
930
931    fn task_nodes(plan: &RenderPlan, name: &str) -> Vec<usize> {
932        plan.nodes
933            .iter()
934            .enumerate()
935            .filter_map(|(i, op)| match op {
936                Op::Task { task, .. } => {
937                    let track = match task {
938                        ProcessTask::Track(t)
939                        | ProcessTask::FolderInput(t)
940                        | ProcessTask::FolderOutput(t) => t,
941                        ProcessTask::Plugin { track, .. } => track,
942                    };
943                    if track.lock().name == name {
944                        Some(i)
945                    } else {
946                        None
947                    }
948                }
949                _ => None,
950            })
951            .collect()
952    }
953
954    fn task_node(plan: &RenderPlan, name: &str, want: fn(&ProcessTask) -> bool) -> usize {
955        plan.nodes
956            .iter()
957            .enumerate()
958            .find_map(|(i, op)| match op {
959                Op::Task { task, .. } => {
960                    let track = match task {
961                        ProcessTask::Track(t)
962                        | ProcessTask::FolderInput(t)
963                        | ProcessTask::FolderOutput(t) => t,
964                        ProcessTask::Plugin { track, .. } => track,
965                    };
966                    if track.lock().name == name && want(task) {
967                        Some(i)
968                    } else {
969                        None
970                    }
971                }
972                _ => None,
973            })
974            .expect("task node not found")
975    }
976
977    fn sum_nodes(plan: &RenderPlan) -> Vec<(usize, Vec<BufferId>, BufferId)> {
978        plan.nodes
979            .iter()
980            .enumerate()
981            .filter_map(|(i, op)| match op {
982                Op::Sum { inputs, output, .. } => Some((i, inputs.clone(), *output)),
983                _ => None,
984            })
985            .collect()
986    }
987
988    fn zero_count(plan: &RenderPlan) -> usize {
989        plan.nodes
990            .iter()
991            .filter(|op| matches!(op, Op::Zero { .. }))
992            .count()
993    }
994
995    fn is_track(t: &ProcessTask) -> bool {
996        matches!(t, ProcessTask::Track(_))
997    }
998    fn is_folder_input(t: &ProcessTask) -> bool {
999        matches!(t, ProcessTask::FolderInput(_))
1000    }
1001    fn is_folder_output(t: &ProcessTask) -> bool {
1002        matches!(t, ProcessTask::FolderOutput(_))
1003    }
1004
1005    #[test]
1006    fn producer_chain_orders_zero_track_sum_track() {
1007        let a = make_track("a", 1, 1);
1008        let b = make_track("b", 1, 1);
1009        connect(&a, 0, &b, 0);
1010        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
1011        plan.verify().expect("invariants");
1012
1013        let sums = sum_nodes(&plan);
1014        assert_eq!(sums.len(), 1, "one connected input -> one Sum");
1015        assert_eq!(sums[0].1.len(), 1);
1016
1017        let task_a = task_node(&plan, "a", is_track);
1018        let task_b = task_node(&plan, "b", is_track);
1019        let sum = sums[0].0;
1020        assert_eq!(zero_count(&plan), 1, "a's unconnected input -> Zero");
1021        let zero = plan
1022            .nodes
1023            .iter()
1024            .position(|op| matches!(op, Op::Zero { .. }))
1025            .expect("zero node");
1026
1027        assert!(zero < task_a, "Zero before the task that reads it");
1028        assert!(task_a < sum, "producer before the Sum of its consumer");
1029        assert!(sum < task_b, "Sum before the consuming track");
1030        assert_eq!(plan.sources, vec![zero as NodeId]);
1031        assert_eq!(plan.indegree[task_b], 1);
1032        assert!(plan.forced.is_empty());
1033    }
1034
1035    #[test]
1036    fn default_passthrough_stays_inside_track_task() {
1037        let track = make_track("t", 1, 1);
1038        let plan = RenderPlan::compile(&state_with(vec![track]), &[], &[], 64);
1039        plan.verify().expect("invariants");
1040
1041        let task = task_node(&plan, "t", is_track);
1042        let zeros = plan
1043            .nodes
1044            .iter()
1045            .enumerate()
1046            .filter_map(|(idx, op)| match op {
1047                Op::Zero { .. } => Some(idx),
1048                _ => None,
1049            })
1050            .collect::<Vec<_>>();
1051
1052        assert_eq!(zeros.len(), 1, "track input is not fed by its own output");
1053        assert!(zeros[0] < task, "input zero before the track task");
1054        assert!(plan.forced.is_empty());
1055    }
1056
1057    #[test]
1058    fn two_sources_insert_sum_with_two_inputs() {
1059        let a = make_track("a", 0, 1);
1060        let b = make_track("b", 0, 1);
1061        let c = make_track("c", 1, 1);
1062        connect(&a, 0, &c, 0);
1063        connect(&b, 0, &c, 0);
1064        let plan = RenderPlan::compile(&state_with(vec![a, b, c]), &[], &[], 64);
1065        plan.verify().expect("invariants");
1066
1067        let sums = sum_nodes(&plan);
1068        assert_eq!(sums.len(), 1);
1069        assert_eq!(sums[0].1.len(), 2, "both sources summed");
1070        assert_eq!(plan.indegree[sums[0].0], 2);
1071
1072        let task_a = task_node(&plan, "a", is_track);
1073        let task_b = task_node(&plan, "b", is_track);
1074        assert!(task_a < sums[0].0 && task_b < sums[0].0);
1075        // No Zero nodes: c's input is connected, a and b have no inputs.
1076        assert_eq!(zero_count(&plan), 0);
1077        assert_eq!(plan.sources.len(), 2, "two root tracks are sources");
1078    }
1079
1080    #[test]
1081    fn metronome_source_is_produced_by_track_task() {
1082        let metronome = make_track("metronome", 0, 1);
1083        let source = {
1084            let mut track = metronome.lock();
1085            let (source, changed) = track.ensure_metronome_source(64);
1086            assert!(changed);
1087            source.expect("metronome source")
1088        };
1089        let plan = RenderPlan::compile(&state_with(vec![metronome]), &[], &[], 64);
1090        plan.verify().expect("invariants");
1091
1092        let source_key = Arc::as_ptr(&source) as usize;
1093        let source_buffer = *plan.port_map.get(&source_key).expect("source buffer");
1094        let task = task_node(&plan, "metronome", is_track);
1095
1096        match &plan.nodes[task] {
1097            Op::Task { outs, .. } => assert!(outs.contains(&source_buffer)),
1098            _ => unreachable!(),
1099        }
1100    }
1101
1102    #[test]
1103    fn folder_track_emits_input_child_output_chain() {
1104        let folder = make_track("folder", 1, 1);
1105        let child = make_track("child", 1, 1);
1106        folder.lock().is_folder = true;
1107        child.lock().parent_track = Some("folder".to_string());
1108        folder.lock().child_tracks.push(child.clone());
1109        let plan = RenderPlan::compile(&state_with(vec![folder, child]), &[], &[], 64);
1110        plan.verify().expect("invariants");
1111
1112        let fi = task_node(&plan, "folder", is_folder_input);
1113        let fo = task_node(&plan, "folder", is_folder_output);
1114        let child_task = task_node(&plan, "child", is_track);
1115        assert!(fi < child_task, "folder input before child");
1116        assert!(child_task < fo, "child before folder output");
1117        assert!(plan.dependents[fi].contains(&(child_task as NodeId)));
1118        assert!(plan.dependents[child_task].contains(&(fo as NodeId)));
1119        // Only the folder shows up at the top level; the child is not a root.
1120        assert_eq!(task_nodes(&plan, "child").len(), 1);
1121    }
1122
1123    #[test]
1124    fn midi_only_connection_inserts_ordering_edge() {
1125        // Two tracks with no audio at all, linked only by a MIDI connection.
1126        let a = Arc::new(Track::new("a".to_string(), 0, 0, 0, 1, 64, 48_000.0));
1127        let b = Arc::new(Track::new("b".to_string(), 0, 0, 1, 0, 64, 48_000.0));
1128        let a_out = a.lock().midi.outs[0].clone();
1129        let b_in = b.lock().midi.ins[0].clone();
1130        crate::midi::io::MIDIIO::connect(&a_out, &b_in);
1131
1132        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
1133        plan.verify().expect("invariants");
1134
1135        let task_a = task_node(&plan, "a", is_track);
1136        let task_b = task_node(&plan, "b", is_track);
1137        assert_eq!(plan.midi_edges, vec![(task_a as NodeId, task_b as NodeId)]);
1138        assert!(task_a < task_b, "producer task ordered before consumer");
1139        assert!(plan.dependents[task_a].contains(&(task_b as NodeId)));
1140        assert!(plan.forced.is_empty());
1141    }
1142
1143    #[test]
1144    fn feedback_cycle_is_broken_and_marked_forced() {
1145        let a = make_track("a", 1, 1);
1146        let b = make_track("b", 1, 1);
1147        connect(&a, 0, &b, 0);
1148        connect(&b, 0, &a, 0);
1149        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
1150
1151        // Two tasks + two Sums, all in the cycle: nothing is a source.
1152        assert_eq!(plan.nodes.len(), 4);
1153        assert!(plan.sources.is_empty());
1154        assert_eq!(plan.forced.len(), 4, "whole cycle marked forced");
1155        // verify() tolerates forced nodes (edges among them may point any way).
1156        plan.verify().expect("invariants tolerate forced cycle");
1157    }
1158
1159    #[test]
1160    fn hw_bridges_become_source_and_sink_nodes() {
1161        let t = make_track("t", 1, 1);
1162        let hw_in = Arc::new(AudioIO::new(64));
1163        let hw_out = Arc::new(AudioIO::new(64));
1164        // Route: hw_in -> track input, track output -> hw_out.
1165        {
1166            let track = t.lock();
1167            AudioIO::connect(&hw_in, &track.audio.ins[0]);
1168            AudioIO::connect(&track.audio.outs[0], &hw_out);
1169        }
1170        let plan = RenderPlan::compile(
1171            &state_with(vec![t]),
1172            std::slice::from_ref(&hw_in),
1173            std::slice::from_ref(&hw_out),
1174            64,
1175        );
1176        plan.verify().expect("invariants");
1177
1178        let hw_node = plan
1179            .nodes
1180            .iter()
1181            .position(|op| matches!(op, Op::HwInput { .. }))
1182            .expect("HwInput node");
1183        assert_eq!(plan.hw_in_map.len(), 1);
1184        assert_eq!(plan.hw_out_map.len(), 1);
1185        let (chan, buf) = plan.hw_in_map[0];
1186        assert_eq!(chan, 0);
1187        match &plan.nodes[hw_node] {
1188            Op::HwInput { output, .. } => assert_eq!(*output, buf),
1189            _ => unreachable!(),
1190        }
1191        // hw_in is a true source feeding the track input's Sum.
1192        assert!(plan.sources.contains(&(hw_node as NodeId)));
1193        let sums = sum_nodes(&plan);
1194        assert_eq!(sums.len(), 2, "track input sum + hw_out bridge sum");
1195        // hw_out bridge output buffer is mapped for draining.
1196        let (out_buf, out_chan) = plan.hw_out_map[0];
1197        assert_eq!(out_chan, 0);
1198        assert!(sums.iter().any(|(_, _, output)| *output == out_buf));
1199        assert!(plan.forced.is_empty());
1200    }
1201
1202    /// Build a plan by hand for `verify` negative tests.
1203    fn hand_plan(
1204        buffers: usize,
1205        nodes: Vec<Op>,
1206        indegree: Vec<u32>,
1207        dependents: Vec<Vec<NodeId>>,
1208        sources: Vec<NodeId>,
1209    ) -> RenderPlan {
1210        RenderPlan {
1211            buffer_size: 64,
1212            buffers: (0..buffers)
1213                .map(|_| UnsafeCell::new(vec![0.0; 64]))
1214                .collect(),
1215            buffer_latencies: (0..buffers).map(|_| AtomicUsize::new(0)).collect(),
1216            nodes,
1217            indegree,
1218            dependents,
1219            sources,
1220            hw_in_map: vec![],
1221            hw_out_map: vec![],
1222            port_map: HashMap::new(),
1223            midi_edges: vec![],
1224            forced: vec![],
1225        }
1226    }
1227
1228    #[test]
1229    fn verify_rejects_backward_edge() {
1230        let plan = hand_plan(
1231            2,
1232            vec![
1233                Op::Sum {
1234                    inputs: vec![1],
1235                    delays: vec![UnsafeCell::new(DelayLine::new())],
1236                    output: 0,
1237                },
1238                Op::HwInput {
1239                    channel: 0,
1240                    output: 1,
1241                },
1242            ],
1243            vec![1, 0],
1244            vec![vec![], vec![0]],
1245            vec![1],
1246        );
1247        // Node 1 (HwInput) sits after node 0 (Sum) but feeds it: fine topo-wise
1248        // (1 -> 0 is backward!) — this must be rejected.
1249        assert!(plan.verify().is_err());
1250    }
1251
1252    #[test]
1253    fn verify_rejects_racing_writers() {
1254        // Two Sum nodes write the same buffer with no path between them.
1255        let plan = hand_plan(
1256            3,
1257            vec![
1258                Op::HwInput {
1259                    channel: 0,
1260                    output: 1,
1261                },
1262                Op::HwInput {
1263                    channel: 1,
1264                    output: 2,
1265                },
1266                Op::Sum {
1267                    inputs: vec![1],
1268                    delays: vec![UnsafeCell::new(DelayLine::new())],
1269                    output: 0,
1270                },
1271                Op::Sum {
1272                    inputs: vec![2],
1273                    delays: vec![UnsafeCell::new(DelayLine::new())],
1274                    output: 0,
1275                },
1276            ],
1277            vec![2, 0, 0, 0],
1278            vec![vec![], vec![2], vec![], vec![]],
1279            vec![0, 1],
1280        );
1281        let err = plan.verify().expect_err("racing writers must fail");
1282        assert!(err.contains("unordered nodes"));
1283    }
1284
1285    #[test]
1286    fn buffers_are_sized_and_silent() {
1287        let t = make_track("t", 2, 1);
1288        let plan = RenderPlan::compile(&state_with(vec![t]), &[], &[], 256);
1289        assert_eq!(plan.buffer_size, 256);
1290        // 2 ins + 1 out = 3 port buffers.
1291        assert_eq!(plan.buffer_count(), 3);
1292        for i in 0..plan.buffer_count() as BufferId {
1293            // Safety: test thread, no node is executing.
1294            let buf = unsafe { plan.buffer(i) };
1295            assert_eq!(buf.len(), 256);
1296            assert!(buf.iter().all(|&s| s == 0.0));
1297        }
1298        // Two unconnected inputs -> two Zero nodes.
1299        assert_eq!(zero_count(&plan), 2);
1300        assert_eq!(plan.port_map.len(), 3);
1301    }
1302}