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