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], pair[1]
367                    ));
368                }
369            }
370        }
371        Ok(())
372    }
373
374    /// Is there a dependency path from `from` to `to`?
375    fn reachable(&self, from: NodeId, to: NodeId) -> bool {
376        if from == to {
377            return true;
378        }
379        let mut seen = HashSet::new();
380        let mut queue = VecDeque::from([from]);
381        seen.insert(from);
382        while let Some(n) = queue.pop_front() {
383            for &d in &self.dependents[n as usize] {
384                if d == to {
385                    return true;
386                }
387                if seen.insert(d) {
388                    queue.push_back(d);
389                }
390            }
391        }
392        false
393    }
394}
395
396/// Mutable compile-time state. Not part of the plan.
397struct Builder {
398    buffer_size: usize,
399    buffers: Vec<UnsafeCell<Vec<f32>>>,
400    port_map: HashMap<usize, BufferId>,
401    nodes: Vec<Op>,
402    edges: HashSet<(NodeId, NodeId)>,
403    /// Buffers that need a `Sum`/`Zero` node (consumer ports).
404    consumer_ports: Vec<Arc<AudioIO>>,
405    /// Task nodes that read each consumer buffer.
406    port_readers: HashMap<BufferId, Vec<NodeId>>,
407    /// Task nodes that also write each consumer buffer in place.
408    port_inplace_writers: HashMap<BufferId, Vec<NodeId>>,
409    /// Producer node per buffer, filled as producer nodes are created.
410    producer: HashMap<BufferId, NodeId>,
411    hw_in_map: Vec<(usize, BufferId)>,
412    hw_out_map: Vec<(BufferId, usize)>,
413    /// `Arc` pointer identity of a MIDI port → the task that writes its
414    /// event buffer this cycle.
415    midi_writers: HashMap<usize, NodeId>,
416    /// `Arc` pointer identity of a MIDI port → the task that reads/merges it.
417    midi_readers: HashMap<usize, NodeId>,
418    /// Every registered MIDI port; walked in `finish` to derive MIDI edges.
419    midi_ports: Vec<Arc<crate::midi::io::MIDIIO>>,
420    midi_edges: Vec<(NodeId, NodeId)>,
421}
422
423impl Builder {
424    fn new(buffer_size: usize) -> Self {
425        Self {
426            buffer_size,
427            buffers: Vec::new(),
428            port_map: HashMap::new(),
429            nodes: Vec::new(),
430            edges: HashSet::new(),
431            consumer_ports: Vec::new(),
432            port_readers: HashMap::new(),
433            port_inplace_writers: HashMap::new(),
434            producer: HashMap::new(),
435            hw_in_map: Vec::new(),
436            hw_out_map: Vec::new(),
437            midi_writers: HashMap::new(),
438            midi_readers: HashMap::new(),
439            midi_ports: Vec::new(),
440            midi_edges: Vec::new(),
441        }
442    }
443
444    /// Register a track's own MIDI ports: inputs are written and read by the
445    /// track's first task (folder input / track body), outputs by its last
446    /// task (folder output / track body).
447    fn register_midi_track_ports(&mut self, t: &TrackData, first: NodeId, last: NodeId) {
448        for p in &t.midi.ins {
449            let key = Arc::as_ptr(p) as usize;
450            self.midi_writers.insert(key, first);
451            self.midi_readers.insert(key, first);
452            self.midi_ports.push(p.clone());
453        }
454        for p in &t.midi.outs {
455            let key = Arc::as_ptr(p) as usize;
456            self.midi_writers.insert(key, last);
457            self.midi_readers.insert(key, last);
458            self.midi_ports.push(p.clone());
459        }
460    }
461
462    /// Register a plugin's MIDI ports at the task that processes it (its own
463    /// node for folder plugins, the track task for inline plugins).
464    fn register_plugin_midi_ports(
465        &mut self,
466        t: &TrackData,
467        kind: PluginKind,
468        index: usize,
469        node: NodeId,
470    ) {
471        let (midi_ins, midi_outs): (
472            Vec<Arc<crate::midi::io::MIDIIO>>,
473            Vec<Arc<crate::midi::io::MIDIIO>>,
474        ) = match kind {
475            PluginKind::Clap => {
476                let proc = t.clap_plugins[index].processor.clone();
477                (
478                    proc.midi_input_ports().to_vec(),
479                    proc.midi_output_ports().to_vec(),
480                )
481            }
482            PluginKind::Vst3 => {
483                let proc = t.vst3_plugins[index].processor.clone();
484                (
485                    proc.midi_input_ports().to_vec(),
486                    proc.midi_output_ports().to_vec(),
487                )
488            }
489            #[cfg(all(unix, not(target_os = "macos")))]
490            PluginKind::Lv2 => {
491                let proc = t.lv2_plugins[index].processor.clone();
492                (
493                    proc.midi_input_ports().to_vec(),
494                    proc.midi_output_ports().to_vec(),
495                )
496            }
497        };
498        for p in midi_ins {
499            let key = Arc::as_ptr(&p) as usize;
500            self.midi_writers.insert(key, node);
501            self.midi_readers.insert(key, node);
502            self.midi_ports.push(p);
503        }
504        for p in midi_outs {
505            let key = Arc::as_ptr(&p) as usize;
506            self.midi_writers.insert(key, node);
507            self.midi_ports.push(p);
508        }
509    }
510
511    /// Arena buffer for a port, registering it (at silence) on first sight.
512    fn buffer_for(&mut self, port: &Arc<AudioIO>) -> BufferId {
513        let key = Arc::as_ptr(port) as usize;
514        if let Some(&id) = self.port_map.get(&key) {
515            return id;
516        }
517        let id = self.buffers.len() as BufferId;
518        self.buffers
519            .push(UnsafeCell::new(vec![0.0; self.buffer_size]));
520        self.port_map.insert(key, id);
521        id
522    }
523
524    fn push_node(&mut self, op: Op) -> NodeId {
525        self.nodes.push(op);
526        (self.nodes.len() - 1) as NodeId
527    }
528
529    fn add_hw(&mut self, hw_inputs: &[Arc<AudioIO>], hw_outputs: &[Arc<AudioIO>]) {
530        for (channel, port) in hw_inputs.iter().enumerate() {
531            let output = self.buffer_for(port);
532            let node = self.push_node(Op::HwInput { channel, output });
533            self.producer.insert(output, node);
534            self.hw_in_map.push((channel, output));
535        }
536        for (channel, port) in hw_outputs.iter().enumerate() {
537            let buffer = self.buffer_for(port);
538            self.consumer_ports.push(port.clone());
539            self.hw_out_map.push((buffer, channel));
540        }
541    }
542
543    /// Mirror of the legacy `append_track_tasks`: emits the task nodes for a
544    /// track (folder sections, plugins, children) and returns the first and
545    /// last node of the track's subgraph, for chaining by the caller.
546    fn append_track(
547        &mut self,
548        track: TrackHandle,
549        predecessor: Option<NodeId>,
550    ) -> (NodeId, NodeId) {
551        let t = track.lock();
552        let ins: Vec<BufferId> = t.audio.ins.iter().map(|p| self.buffer_for(p)).collect();
553        let outs: Vec<BufferId> = t.audio.outs.iter().map(|p| self.buffer_for(p)).collect();
554        let metronome_source = t.metronome_source();
555        let metronome_out = metronome_source.as_ref().map(|p| self.buffer_for(p));
556        for p in &t.audio.ins {
557            self.consumer_ports.push(p.clone());
558        }
559
560        if t.is_folder {
561            let mut folder_input_outs = Vec::new();
562            if let Some(out) = metronome_out {
563                folder_input_outs.push(out);
564            }
565            let folder_input = self.push_node(Op::Task {
566                task: ProcessTask::FolderInput(track.clone()),
567                ins: ins.clone(),
568                outs: folder_input_outs,
569            });
570            if let Some(pred) = predecessor {
571                self.edges.insert((pred, folder_input));
572            }
573            self.register_task_ports(folder_input, &ins, true);
574            if let Some(out) = metronome_out {
575                self.producer.insert(out, folder_input);
576            }
577
578            let mut source_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
579            let mut target_keys: HashMap<ConnectableRef, NodeId> = HashMap::new();
580            source_keys.insert(ConnectableRef::TrackInput, folder_input);
581            target_keys.insert(ConnectableRef::TrackInput, folder_input);
582
583            let mut plugin_nodes: Vec<NodeId> = Vec::new();
584            for idx in 0..t.clap_plugins.len() {
585                let node = self.push_plugin(&track, &t, PluginKind::Clap, idx, folder_input);
586                let id = t.clap_plugins[idx].id;
587                source_keys.insert(ConnectableRef::ClapPlugin(id), node);
588                target_keys.insert(ConnectableRef::ClapPlugin(id), node);
589                plugin_nodes.push(node);
590            }
591            for idx in 0..t.vst3_plugins.len() {
592                let node = self.push_plugin(&track, &t, PluginKind::Vst3, idx, folder_input);
593                let id = t.vst3_plugins[idx].id;
594                source_keys.insert(ConnectableRef::Vst3Plugin(id), node);
595                target_keys.insert(ConnectableRef::Vst3Plugin(id), node);
596                plugin_nodes.push(node);
597            }
598            #[cfg(all(unix, not(target_os = "macos")))]
599            for idx in 0..t.lv2_plugins.len() {
600                let node = self.push_plugin(&track, &t, PluginKind::Lv2, idx, folder_input);
601                let id = t.lv2_plugins[idx].id;
602                source_keys.insert(ConnectableRef::Lv2Plugin(id), node);
603                target_keys.insert(ConnectableRef::Lv2Plugin(id), node);
604                plugin_nodes.push(node);
605            }
606
607            let mut child_lasts: Vec<NodeId> = Vec::new();
608            for child_track in &t.child_tracks {
609                let (child_first, child_last) =
610                    self.append_track(child_track.clone(), Some(folder_input));
611                let child_name = child_track.lock().name.clone();
612                source_keys.insert(ConnectableRef::ChildTrack(child_name.clone()), child_last);
613                target_keys.insert(ConnectableRef::ChildTrack(child_name), child_first);
614                child_lasts.push(child_last);
615            }
616
617            let folder_output = self.push_node(Op::Task {
618                task: ProcessTask::FolderOutput(track.clone()),
619                ins: Vec::new(),
620                outs: outs.clone(),
621            });
622            self.edges.insert((folder_input, folder_output));
623            for &p in &plugin_nodes {
624                self.edges.insert((p, folder_output));
625            }
626            for &c in &child_lasts {
627                self.edges.insert((c, folder_output));
628            }
629            for &out in &outs {
630                self.producer.insert(out, folder_output);
631            }
632            self.register_midi_track_ports(&t, folder_input, folder_output);
633
634            // Cross-connectable edges within this folder's routing graph,
635            // exactly as the legacy builder derived them.
636            for conn in t.connectable_connections() {
637                let ConnectableConnection { from, to, .. } = conn;
638                let (Some(&source), Some(&target)) = (source_keys.get(&from), target_keys.get(&to))
639                else {
640                    continue;
641                };
642                if source != target {
643                    self.edges.insert((source, target));
644                }
645            }
646
647            (folder_input, folder_output)
648        } else {
649            let mut task_outs = outs.clone();
650            if let Some(out) = metronome_out {
651                task_outs.push(out);
652            }
653            let task = self.push_node(Op::Task {
654                task: ProcessTask::Track(track.clone()),
655                ins: ins.clone(),
656                outs: task_outs,
657            });
658            if let Some(pred) = predecessor {
659                self.edges.insert((pred, task));
660            }
661            self.register_task_ports(task, &ins, true);
662            for &out in &outs {
663                self.producer.insert(out, task);
664            }
665            if let Some(out) = metronome_out {
666                self.producer.insert(out, task);
667            }
668            self.register_midi_track_ports(&t, task, task);
669            // Inline plugins are processed inside the track task body.
670            for idx in 0..t.clap_plugins.len() {
671                self.register_plugin_midi_ports(&t, PluginKind::Clap, idx, task);
672            }
673            for idx in 0..t.vst3_plugins.len() {
674                self.register_plugin_midi_ports(&t, PluginKind::Vst3, idx, task);
675            }
676            #[cfg(all(unix, not(target_os = "macos")))]
677            for idx in 0..t.lv2_plugins.len() {
678                self.register_plugin_midi_ports(&t, PluginKind::Lv2, idx, task);
679            }
680            (task, task)
681        }
682    }
683
684    fn push_plugin(
685        &mut self,
686        track: &TrackHandle,
687        t: &TrackData,
688        kind: PluginKind,
689        index: usize,
690        folder_input: NodeId,
691    ) -> NodeId {
692        let (input_ports, output_ports): (Vec<Arc<AudioIO>>, Vec<Arc<AudioIO>>) = match kind {
693            PluginKind::Clap => {
694                let proc = t.clap_plugins[index].processor.clone();
695                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
696            }
697            PluginKind::Vst3 => {
698                let proc = t.vst3_plugins[index].processor.clone();
699                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
700            }
701            #[cfg(all(unix, not(target_os = "macos")))]
702            PluginKind::Lv2 => {
703                let proc = t.lv2_plugins[index].processor.clone();
704                (proc.audio_inputs().to_vec(), proc.audio_outputs().to_vec())
705            }
706        };
707        for p in &input_ports {
708            self.consumer_ports.push(p.clone());
709        }
710        let pins: Vec<BufferId> = input_ports.iter().map(|p| self.buffer_for(p)).collect();
711        let pouts: Vec<BufferId> = output_ports.iter().map(|p| self.buffer_for(p)).collect();
712        let node = self.push_node(Op::Task {
713            task: ProcessTask::Plugin {
714                track: track.clone(),
715                kind,
716                index,
717            },
718            ins: pins.clone(),
719            outs: pouts.clone(),
720        });
721        self.edges.insert((folder_input, node));
722        self.register_task_ports(node, &pins, false);
723        for &out in &pouts {
724            self.producer.insert(out, node);
725        }
726        self.register_plugin_midi_ports(t, kind, index, node);
727        node
728    }
729
730    /// Record which task reads (and optionally writes in place) each port.
731    fn register_task_ports(&mut self, node: NodeId, ins: &[BufferId], in_place: bool) {
732        for &b in ins {
733            self.port_readers.entry(b).or_default().push(node);
734            if in_place {
735                self.port_inplace_writers.entry(b).or_default().push(node);
736                self.producer.insert(b, node);
737            }
738        }
739    }
740
741    /// Create the `Sum`/`Zero` nodes for every consumer port, wire the edges,
742    /// topologically sort, and freeze into a `RenderPlan`.
743    fn finish(mut self) -> RenderPlan {
744        for port in self.consumer_ports.clone() {
745            let output = self.buffer_for(&port);
746            let sources: Vec<BufferId> = {
747                let conns = port.connections();
748                conns.iter().map(|p| self.buffer_for(p)).collect()
749            };
750            let node = if sources.is_empty() {
751                self.push_node(Op::Zero { output })
752            } else {
753                let node = self.push_node(Op::Sum {
754                    inputs: sources.clone(),
755                    delays: sources
756                        .iter()
757                        .map(|_| UnsafeCell::new(DelayLine::new()))
758                        .collect(),
759                    output,
760                });
761                for src in sources {
762                    match self.producer.get(&src) {
763                        Some(&prod) => {
764                            self.edges.insert((prod, node));
765                        }
766                        None => {
767                            tracing::warn!(
768                                "render plan: connection source for buffer {src} has no producer; \
769                                 treating as silent"
770                            );
771                        }
772                    }
773                }
774                node
775            };
776            // Every task reading this port runs after its Sum/Zero node.
777            if let Some(readers) = self.port_readers.get(&output).cloned() {
778                for reader in readers {
779                    self.edges.insert((node, reader));
780                }
781            }
782        }
783
784        // MIDI edges: for every registered port, order each source's writer
785        // task before the task that merges this port (Phase 3). Unregistered
786        // sources (e.g. ports outside any track) are skipped — their writers
787        // are serialized by the cycle boundary.
788        for port in self.midi_ports.clone() {
789            let key = Arc::as_ptr(&port) as usize;
790            let Some(&reader) = self.midi_readers.get(&key) else {
791                continue;
792            };
793            for source in port.sources() {
794                let src_key = Arc::as_ptr(&source) as usize;
795                let Some(&writer) = self.midi_writers.get(&src_key) else {
796                    continue;
797                };
798                if writer != reader && self.edges.insert((writer, reader)) {
799                    self.midi_edges.push((writer, reader));
800                }
801            }
802        }
803
804        let n = self.nodes.len();
805        let (order, forced) = topo_sort(n, &self.edges);
806        let mut remap = vec![0u32; n];
807        for (new_idx, &old_idx) in order.iter().enumerate() {
808            remap[old_idx as usize] = new_idx as NodeId;
809        }
810
811        let mut nodes = Vec::with_capacity(n);
812        for &old_idx in &order {
813            nodes.push(std::mem::replace(
814                &mut self.nodes[old_idx as usize],
815                Op::Zero { output: 0 },
816            ));
817        }
818
819        let mut indegree = vec![0u32; n];
820        let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
821        for &(from, to) in &self.edges {
822            let (from, to) = (remap[from as usize], remap[to as usize]);
823            indegree[to as usize] += 1;
824            dependents[from as usize].push(to);
825        }
826        let sources: Vec<NodeId> = (0..n as NodeId)
827            .filter(|&i| indegree[i as usize] == 0)
828            .collect();
829        let forced: Vec<NodeId> = forced.iter().map(|&f| remap[f as usize]).collect();
830        let midi_edges: Vec<(NodeId, NodeId)> = self
831            .midi_edges
832            .iter()
833            .map(|&(from, to)| (remap[from as usize], remap[to as usize]))
834            .collect();
835
836        let buffer_count = self.buffers.len();
837        let plan = RenderPlan {
838            buffer_size: self.buffer_size,
839            buffers: self.buffers,
840            buffer_latencies: (0..buffer_count).map(|_| AtomicUsize::new(0)).collect(),
841            nodes,
842            indegree,
843            dependents,
844            sources,
845            hw_in_map: self.hw_in_map,
846            hw_out_map: self.hw_out_map,
847            port_map: self.port_map,
848            midi_edges,
849            forced,
850        };
851        if let Err(e) = plan.verify() {
852            tracing::error!("render plan invariant violation: {e}");
853        }
854        plan
855    }
856}
857
858/// Kahn's algorithm. Returns the topological order of all nodes — nodes left
859/// over after the algorithm (feedback loops) are appended at the end and also
860/// returned separately as `forced`.
861fn topo_sort(n: usize, edges: &HashSet<(NodeId, NodeId)>) -> (Vec<NodeId>, Vec<NodeId>) {
862    let mut indegree = vec![0u32; n];
863    let mut dependents: Vec<Vec<NodeId>> = vec![Vec::new(); n];
864    for &(from, to) in edges {
865        indegree[to as usize] += 1;
866        dependents[from as usize].push(to);
867    }
868    let mut queue: VecDeque<NodeId> = (0..n as NodeId)
869        .filter(|&i| indegree[i as usize] == 0)
870        .collect();
871    let mut order = Vec::with_capacity(n);
872    while let Some(node) = queue.pop_front() {
873        order.push(node);
874        for &d in &dependents[node as usize] {
875            indegree[d as usize] -= 1;
876            if indegree[d as usize] == 0 {
877                queue.push_back(d);
878            }
879        }
880    }
881    let placed: HashSet<NodeId> = order.iter().copied().collect();
882    let forced: Vec<NodeId> = (0..n as NodeId).filter(|i| !placed.contains(i)).collect();
883    order.extend(forced.iter().copied());
884    (order, forced)
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890    use crate::connectable::connect_audio;
891    use crate::state::State;
892
893    fn make_track(name: &str, ins: usize, outs: usize) -> TrackHandle {
894        Arc::new(Track::new(name.to_string(), ins, outs, 0, 0, 64, 48_000.0))
895    }
896
897    fn state_with(tracks: Vec<TrackHandle>) -> StateSnapshot {
898        let mut state = State::default();
899        for t in tracks {
900            state.tracks.insert(t.lock().name.clone(), t);
901        }
902        state.snapshot()
903    }
904
905    /// Connect `a`'s output `a_port` to `b`'s input `b_port`.
906    fn connect(a: &TrackHandle, a_port: usize, b: &TrackHandle, b_port: usize) {
907        let src = a.lock();
908        let dst = b.lock();
909        connect_audio(&*src, a_port, &*dst, b_port).expect("connect");
910    }
911
912    fn task_nodes(plan: &RenderPlan, name: &str) -> Vec<usize> {
913        plan.nodes
914            .iter()
915            .enumerate()
916            .filter_map(|(i, op)| match op {
917                Op::Task { task, .. } => {
918                    let track = match task {
919                        ProcessTask::Track(t)
920                        | ProcessTask::FolderInput(t)
921                        | ProcessTask::FolderOutput(t) => t,
922                        ProcessTask::Plugin { track, .. } => track,
923                    };
924                    if track.lock().name == name {
925                        Some(i)
926                    } else {
927                        None
928                    }
929                }
930                _ => None,
931            })
932            .collect()
933    }
934
935    fn task_node(plan: &RenderPlan, name: &str, want: fn(&ProcessTask) -> bool) -> usize {
936        plan.nodes
937            .iter()
938            .enumerate()
939            .find_map(|(i, op)| match op {
940                Op::Task { task, .. } => {
941                    let track = match task {
942                        ProcessTask::Track(t)
943                        | ProcessTask::FolderInput(t)
944                        | ProcessTask::FolderOutput(t) => t,
945                        ProcessTask::Plugin { track, .. } => track,
946                    };
947                    if track.lock().name == name && want(task) {
948                        Some(i)
949                    } else {
950                        None
951                    }
952                }
953                _ => None,
954            })
955            .expect("task node not found")
956    }
957
958    fn sum_nodes(plan: &RenderPlan) -> Vec<(usize, Vec<BufferId>, BufferId)> {
959        plan.nodes
960            .iter()
961            .enumerate()
962            .filter_map(|(i, op)| match op {
963                Op::Sum { inputs, output, .. } => Some((i, inputs.clone(), *output)),
964                _ => None,
965            })
966            .collect()
967    }
968
969    fn zero_count(plan: &RenderPlan) -> usize {
970        plan.nodes
971            .iter()
972            .filter(|op| matches!(op, Op::Zero { .. }))
973            .count()
974    }
975
976    fn is_track(t: &ProcessTask) -> bool {
977        matches!(t, ProcessTask::Track(_))
978    }
979    fn is_folder_input(t: &ProcessTask) -> bool {
980        matches!(t, ProcessTask::FolderInput(_))
981    }
982    fn is_folder_output(t: &ProcessTask) -> bool {
983        matches!(t, ProcessTask::FolderOutput(_))
984    }
985
986    #[test]
987    fn producer_chain_orders_zero_track_sum_track() {
988        let a = make_track("a", 1, 1);
989        let b = make_track("b", 1, 1);
990        connect(&a, 0, &b, 0);
991        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
992        plan.verify().expect("invariants");
993
994        let sums = sum_nodes(&plan);
995        assert_eq!(sums.len(), 1, "one connected input -> one Sum");
996        assert_eq!(sums[0].1.len(), 1);
997
998        let task_a = task_node(&plan, "a", is_track);
999        let task_b = task_node(&plan, "b", is_track);
1000        let sum = sums[0].0;
1001        assert_eq!(zero_count(&plan), 1, "a's unconnected input -> Zero");
1002        let zero = plan
1003            .nodes
1004            .iter()
1005            .position(|op| matches!(op, Op::Zero { .. }))
1006            .expect("zero node");
1007
1008        assert!(zero < task_a, "Zero before the task that reads it");
1009        assert!(task_a < sum, "producer before the Sum of its consumer");
1010        assert!(sum < task_b, "Sum before the consuming track");
1011        assert_eq!(plan.sources, vec![zero as NodeId]);
1012        assert_eq!(plan.indegree[task_b], 1);
1013        assert!(plan.forced.is_empty());
1014    }
1015
1016    #[test]
1017    fn two_sources_insert_sum_with_two_inputs() {
1018        let a = make_track("a", 0, 1);
1019        let b = make_track("b", 0, 1);
1020        let c = make_track("c", 1, 1);
1021        connect(&a, 0, &c, 0);
1022        connect(&b, 0, &c, 0);
1023        let plan = RenderPlan::compile(&state_with(vec![a, b, c]), &[], &[], 64);
1024        plan.verify().expect("invariants");
1025
1026        let sums = sum_nodes(&plan);
1027        assert_eq!(sums.len(), 1);
1028        assert_eq!(sums[0].1.len(), 2, "both sources summed");
1029        assert_eq!(plan.indegree[sums[0].0], 2);
1030
1031        let task_a = task_node(&plan, "a", is_track);
1032        let task_b = task_node(&plan, "b", is_track);
1033        assert!(task_a < sums[0].0 && task_b < sums[0].0);
1034        // No Zero nodes: c's input is connected, a and b have no inputs.
1035        assert_eq!(zero_count(&plan), 0);
1036        assert_eq!(plan.sources.len(), 2, "two root tracks are sources");
1037    }
1038
1039    #[test]
1040    fn metronome_source_is_produced_by_track_task() {
1041        let metronome = make_track("metronome", 0, 1);
1042        let source = {
1043            let mut track = metronome.lock();
1044            let (source, changed) = track.ensure_metronome_source(64);
1045            assert!(changed);
1046            source.expect("metronome source")
1047        };
1048        let plan = RenderPlan::compile(&state_with(vec![metronome]), &[], &[], 64);
1049        plan.verify().expect("invariants");
1050
1051        let source_key = Arc::as_ptr(&source) as usize;
1052        let source_buffer = *plan.port_map.get(&source_key).expect("source buffer");
1053        let task = task_node(&plan, "metronome", is_track);
1054
1055        match &plan.nodes[task] {
1056            Op::Task { outs, .. } => assert!(outs.contains(&source_buffer)),
1057            _ => unreachable!(),
1058        }
1059    }
1060
1061    #[test]
1062    fn folder_track_emits_input_child_output_chain() {
1063        let folder = make_track("folder", 1, 1);
1064        let child = make_track("child", 1, 1);
1065        folder.lock().is_folder = true;
1066        child.lock().parent_track = Some("folder".to_string());
1067        folder.lock().child_tracks.push(child.clone());
1068        let plan = RenderPlan::compile(&state_with(vec![folder, child]), &[], &[], 64);
1069        plan.verify().expect("invariants");
1070
1071        let fi = task_node(&plan, "folder", is_folder_input);
1072        let fo = task_node(&plan, "folder", is_folder_output);
1073        let child_task = task_node(&plan, "child", is_track);
1074        assert!(fi < child_task, "folder input before child");
1075        assert!(child_task < fo, "child before folder output");
1076        assert!(plan.dependents[fi].contains(&(child_task as NodeId)));
1077        assert!(plan.dependents[child_task].contains(&(fo as NodeId)));
1078        // Only the folder shows up at the top level; the child is not a root.
1079        assert_eq!(task_nodes(&plan, "child").len(), 1);
1080    }
1081
1082    #[test]
1083    fn midi_only_connection_inserts_ordering_edge() {
1084        // Two tracks with no audio at all, linked only by a MIDI connection.
1085        let a = Arc::new(Track::new("a".to_string(), 0, 0, 0, 1, 64, 48_000.0));
1086        let b = Arc::new(Track::new("b".to_string(), 0, 0, 1, 0, 64, 48_000.0));
1087        let a_out = a.lock().midi.outs[0].clone();
1088        let b_in = b.lock().midi.ins[0].clone();
1089        crate::midi::io::MIDIIO::connect(&a_out, &b_in);
1090
1091        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
1092        plan.verify().expect("invariants");
1093
1094        let task_a = task_node(&plan, "a", is_track);
1095        let task_b = task_node(&plan, "b", is_track);
1096        assert_eq!(plan.midi_edges, vec![(task_a as NodeId, task_b as NodeId)]);
1097        assert!(task_a < task_b, "producer task ordered before consumer");
1098        assert!(plan.dependents[task_a].contains(&(task_b as NodeId)));
1099        assert!(plan.forced.is_empty());
1100    }
1101
1102    #[test]
1103    fn feedback_cycle_is_broken_and_marked_forced() {
1104        let a = make_track("a", 1, 1);
1105        let b = make_track("b", 1, 1);
1106        connect(&a, 0, &b, 0);
1107        connect(&b, 0, &a, 0);
1108        let plan = RenderPlan::compile(&state_with(vec![a, b]), &[], &[], 64);
1109
1110        // Two tasks + two Sums, all in the cycle: nothing is a source.
1111        assert_eq!(plan.nodes.len(), 4);
1112        assert!(plan.sources.is_empty());
1113        assert_eq!(plan.forced.len(), 4, "whole cycle marked forced");
1114        // verify() tolerates forced nodes (edges among them may point any way).
1115        plan.verify().expect("invariants tolerate forced cycle");
1116    }
1117
1118    #[test]
1119    fn hw_bridges_become_source_and_sink_nodes() {
1120        let t = make_track("t", 1, 1);
1121        let hw_in = Arc::new(AudioIO::new(64));
1122        let hw_out = Arc::new(AudioIO::new(64));
1123        // Route: hw_in -> track input, track output -> hw_out.
1124        {
1125            let track = t.lock();
1126            AudioIO::connect(&hw_in, &track.audio.ins[0]);
1127            AudioIO::connect(&track.audio.outs[0], &hw_out);
1128        }
1129        let plan = RenderPlan::compile(
1130            &state_with(vec![t]),
1131            std::slice::from_ref(&hw_in),
1132            std::slice::from_ref(&hw_out),
1133            64,
1134        );
1135        plan.verify().expect("invariants");
1136
1137        let hw_node = plan
1138            .nodes
1139            .iter()
1140            .position(|op| matches!(op, Op::HwInput { .. }))
1141            .expect("HwInput node");
1142        assert_eq!(plan.hw_in_map.len(), 1);
1143        assert_eq!(plan.hw_out_map.len(), 1);
1144        let (chan, buf) = plan.hw_in_map[0];
1145        assert_eq!(chan, 0);
1146        match &plan.nodes[hw_node] {
1147            Op::HwInput { output, .. } => assert_eq!(*output, buf),
1148            _ => unreachable!(),
1149        }
1150        // hw_in is a true source feeding the track input's Sum.
1151        assert!(plan.sources.contains(&(hw_node as NodeId)));
1152        let sums = sum_nodes(&plan);
1153        assert_eq!(sums.len(), 2, "track input sum + hw_out bridge sum");
1154        // hw_out bridge output buffer is mapped for draining.
1155        let (out_buf, out_chan) = plan.hw_out_map[0];
1156        assert_eq!(out_chan, 0);
1157        assert!(sums.iter().any(|(_, _, output)| *output == out_buf));
1158    }
1159
1160    /// Build a plan by hand for `verify` negative tests.
1161    fn hand_plan(
1162        buffers: usize,
1163        nodes: Vec<Op>,
1164        indegree: Vec<u32>,
1165        dependents: Vec<Vec<NodeId>>,
1166        sources: Vec<NodeId>,
1167    ) -> RenderPlan {
1168        RenderPlan {
1169            buffer_size: 64,
1170            buffers: (0..buffers)
1171                .map(|_| UnsafeCell::new(vec![0.0; 64]))
1172                .collect(),
1173            buffer_latencies: (0..buffers).map(|_| AtomicUsize::new(0)).collect(),
1174            nodes,
1175            indegree,
1176            dependents,
1177            sources,
1178            hw_in_map: vec![],
1179            hw_out_map: vec![],
1180            port_map: HashMap::new(),
1181            midi_edges: vec![],
1182            forced: vec![],
1183        }
1184    }
1185
1186    #[test]
1187    fn verify_rejects_backward_edge() {
1188        let plan = hand_plan(
1189            2,
1190            vec![
1191                Op::Sum {
1192                    inputs: vec![1],
1193                    delays: vec![UnsafeCell::new(DelayLine::new())],
1194                    output: 0,
1195                },
1196                Op::HwInput {
1197                    channel: 0,
1198                    output: 1,
1199                },
1200            ],
1201            vec![1, 0],
1202            vec![vec![], vec![0]],
1203            vec![1],
1204        );
1205        // Node 1 (HwInput) sits after node 0 (Sum) but feeds it: fine topo-wise
1206        // (1 -> 0 is backward!) — this must be rejected.
1207        assert!(plan.verify().is_err());
1208    }
1209
1210    #[test]
1211    fn verify_rejects_racing_writers() {
1212        // Two Sum nodes write the same buffer with no path between them.
1213        let plan = hand_plan(
1214            3,
1215            vec![
1216                Op::HwInput {
1217                    channel: 0,
1218                    output: 1,
1219                },
1220                Op::HwInput {
1221                    channel: 1,
1222                    output: 2,
1223                },
1224                Op::Sum {
1225                    inputs: vec![1],
1226                    delays: vec![UnsafeCell::new(DelayLine::new())],
1227                    output: 0,
1228                },
1229                Op::Sum {
1230                    inputs: vec![2],
1231                    delays: vec![UnsafeCell::new(DelayLine::new())],
1232                    output: 0,
1233                },
1234            ],
1235            vec![2, 0, 0, 0],
1236            vec![vec![], vec![2], vec![], vec![]],
1237            vec![0, 1],
1238        );
1239        let err = plan.verify().expect_err("racing writers must fail");
1240        assert!(err.contains("unordered nodes"));
1241    }
1242
1243    #[test]
1244    fn buffers_are_sized_and_silent() {
1245        let t = make_track("t", 2, 1);
1246        let plan = RenderPlan::compile(&state_with(vec![t]), &[], &[], 256);
1247        assert_eq!(plan.buffer_size, 256);
1248        // 2 ins + 1 out = 3 port buffers.
1249        assert_eq!(plan.buffer_count(), 3);
1250        for i in 0..plan.buffer_count() as BufferId {
1251            // Safety: test thread, no node is executing.
1252            let buf = unsafe { plan.buffer(i) };
1253            assert_eq!(buf.len(), 256);
1254            assert!(buf.iter().all(|&s| s == 0.0));
1255        }
1256        // Two unconnected inputs -> two Zero nodes.
1257        assert_eq!(zero_count(&plan), 2);
1258        assert_eq!(plan.port_map.len(), 3);
1259    }
1260}