Skip to main content

rill_graph/
graph.rs

1use crate::factory::NodeFactory;
2use std::sync::Arc;
3
4use rill_core::buffer::{FixedBuffer, ResourceRegistry, TapeLoop};
5use rill_core::io::{IoCapture, IoDriver, IoPlayback};
6use rill_core::math::Transcendental;
7use rill_core::queues::CommandEnum;
8use rill_core::queues::SetParameter;
9use rill_core::time::{ClockTick, RenderContext, SystemClock};
10use rill_core::traits::port::Port;
11use rill_core::traits::processable::Processable;
12use rill_core::traits::{Node, NodeId, NodeVariant, Params, ProcessResult};
13use rill_core_actor::{Actor, ActorRef, ActorSystem};
14use std::cell::{RefCell, UnsafeCell};
15use std::collections::{HashSet, VecDeque};
16use std::rc::Rc;
17use std::sync::atomic::AtomicBool;
18
19// ============================================================================
20// Internal routing metadata
21// ============================================================================
22
23// ============================================================================
24// Build Errors
25// ============================================================================
26
27/// Errors that can occur during graph construction.
28#[derive(Debug, Clone)]
29pub enum BuildError {
30    /// A cycle was detected in the signal edge graph.
31    CycleDetected,
32    /// Backend creation failed.
33    Backend(String),
34    /// Factory registration error (unknown node type).
35    Registry(String),
36}
37
38impl std::fmt::Display for BuildError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::CycleDetected => write!(f, "graph cycle detected"),
42            Self::Backend(msg) => write!(f, "backend error: {msg}"),
43            Self::Registry(msg) => write!(f, "registry error: {msg}"),
44        }
45    }
46}
47
48// ============================================================================
49// Graph Builder
50// ============================================================================
51
52// ============================================================================
53// Node Storage
54// ============================================================================
55
56/// A deferred node recipe — constructed at [`build`](GraphBuilder::build) time.
57struct NodeRecipe<T: Transcendental, const BUF_SIZE: usize> {
58    type_name: String,
59    id: NodeId,
60    params: Params,
61    routing_entries: Vec<(usize, usize, f32)>,
62    _phantom: std::marker::PhantomData<(T, [(); BUF_SIZE])>,
63}
64
65/// Temporary holder during build — wraps a constructed node for wiring.
66struct NodeEntry<T: Transcendental, const BUF_SIZE: usize> {
67    node: NodeVariant<T, BUF_SIZE>,
68}
69
70// ============================================================================
71// GraphBuilder (Mutable Construction)
72// ============================================================================
73
74/// A named resource (tape loop) shared between nodes in the graph.
75#[derive(Clone)]
76pub struct GraphResource {
77    /// Unique name referenced by node parameters.
78    pub name: String,
79    /// Resource kind string (`"tape"`).
80    pub kind: String,
81    /// Capacity in samples (for `"tape"` kind).
82    pub capacity: usize,
83}
84
85/// Mutable builder for an immutable signal graph.
86///
87/// Stores deferred node recipes until [`build`](Self::build), which
88/// constructs all nodes, wires connections, and performs topological
89/// sort.  This keeps `GraphBuilder` `Send` — all non‑`Send` data
90/// is constructed inside the target thread.
91///
92/// # Node factory
93///
94/// The builder holds an [`Arc<NodeFactory>`] for constructing nodes by
95/// type name. Nodes registered via [`add_node_with_id`](Self::add_node_with_id)
96/// are only validated and constructed at [`build`](Self::build) time.
97pub struct GraphBuilder<T: Transcendental, const BUF_SIZE: usize> {
98    recipes: Vec<NodeRecipe<T, BUF_SIZE>>,
99    signal_edges: Vec<(usize, usize, usize, usize)>,
100    control_edges: Vec<(usize, usize, usize, usize)>,
101    clock_edges: Vec<(usize, usize, usize, usize)>,
102    feedback_edges: Vec<(usize, usize, usize, usize)>,
103    resources: Vec<GraphResource>,
104    /// Shared node factory (required, from Runtime).
105    factory: Arc<NodeFactory<T, BUF_SIZE>>,
106    /// Sample rate override. When set, used in [`build`](Self::build).
107    /// Populated from [`GraphDef::sample_rate`] during deserialization.
108    sample_rate: Option<f32>,
109    /// Parent RackCase ActorRef — Graph sends ClockTick here.
110    parent_ref: Option<ActorRef<CommandEnum>>,
111}
112
113impl<T: Transcendental, const BUF_SIZE: usize> GraphBuilder<T, BUF_SIZE> {
114    /// Create a new empty graph builder.
115    pub fn new(factory: Arc<NodeFactory<T, BUF_SIZE>>) -> Self {
116        Self {
117            recipes: Vec::new(),
118            signal_edges: Vec::new(),
119            control_edges: Vec::new(),
120            clock_edges: Vec::new(),
121            feedback_edges: Vec::new(),
122            resources: Vec::new(),
123            factory,
124            sample_rate: None,
125            parent_ref: None,
126        }
127    }
128
129    /// Add a node by type name using the internal factory.
130    ///
131    /// The type must be registered in the factory before [`build`](Self::build)
132    /// is called.
133    ///
134    /// Returns the index of the newly added node.
135    pub fn add_node(&mut self, type_name: &str, params: &Params) -> usize {
136        let id = NodeId(self.recipes.len() as u32);
137        self.add_node_with_id(type_name, params, id)
138    }
139
140    /// Add a node with an explicit [`NodeId`].
141    ///
142    /// Like [`add_node`](Self::add_node) but uses the provided `id`.
143    /// Important for serialization where external references depend on
144    /// exact IDs.
145    pub fn add_node_with_id(&mut self, type_name: &str, params: &Params, id: NodeId) -> usize {
146        let idx = self.recipes.len();
147        self.recipes.push(NodeRecipe {
148            type_name: type_name.to_string(),
149            id,
150            params: params.clone(),
151            routing_entries: Vec::new(),
152            _phantom: std::marker::PhantomData,
153        });
154        idx
155    }
156
157    /// Store a routing matrix entry to be applied at build time.
158    pub fn add_routing_entry(&mut self, idx: usize, from: usize, to: usize, gain: f32) {
159        if let Some(recipe) = self.recipes.get_mut(idx) {
160            recipe.routing_entries.push((from, to, gain));
161        }
162    }
163
164    /// Register a named resource (tape loop, buffer, etc.).
165    pub fn add_resource(&mut self, resource: GraphResource) {
166        self.resources.push(resource);
167    }
168
169    /// Number of nodes added to the builder so far.
170    pub fn node_count(&self) -> usize {
171        self.recipes.len()
172    }
173
174    /// Set the sample rate for this builder.
175    pub fn set_sample_rate(&mut self, sr: f32) {
176        self.sample_rate = Some(sr);
177    }
178
179    /// Set the parent RackCase actor reference (Graph → parent ClockTick).
180    pub fn set_parent_ref(&mut self, parent: ActorRef<CommandEnum>) {
181        self.parent_ref = Some(parent);
182    }
183
184    /// Connect signal ports.
185    pub fn connect_signal(
186        &mut self,
187        from_node: usize,
188        from_port: usize,
189        to_node: usize,
190        to_port: usize,
191    ) {
192        self.signal_edges
193            .push((from_node, from_port, to_node, to_port));
194    }
195
196    /// Connect control ports (modulation values).
197    pub fn connect_control(
198        &mut self,
199        from_node: usize,
200        from_port: usize,
201        to_node: usize,
202        to_port: usize,
203    ) {
204        self.control_edges
205            .push((from_node, from_port, to_node, to_port));
206    }
207
208    /// Connect clock ports (timing events).
209    pub fn connect_clock(
210        &mut self,
211        from_node: usize,
212        from_port: usize,
213        to_node: usize,
214        to_port: usize,
215    ) {
216        self.clock_edges
217            .push((from_node, from_port, to_node, to_port));
218    }
219
220    /// Connect feedback ports (delay lines, state carryover).
221    pub fn connect_feedback(
222        &mut self,
223        from_node: usize,
224        from_port: usize,
225        to_node: usize,
226        to_port: usize,
227    ) {
228        self.feedback_edges
229            .push((from_node, from_port, to_node, to_port));
230    }
231
232    /// Build the graph.
233    ///
234    /// Creates backends for nodes that have a backend name set (via
235    /// `SourceDef::backend` / `SinkDef::backend` or the builder's default).  Finds the active
236    /// (driver) node and stores its index for [`Graph::run`].
237    pub fn build(self, system: &ActorSystem) -> Result<Graph<T, BUF_SIZE>, BuildError> {
238        // Phase 1: Construct all nodes from recipes
239        let mut node_entries: Vec<NodeEntry<T, BUF_SIZE>> = Vec::with_capacity(self.recipes.len());
240        for recipe in &self.recipes {
241            let node = self
242                .factory
243                .construct(&recipe.type_name, recipe.id, &recipe.params)
244                .map_err(|e| BuildError::Registry(format!("{e}")))?;
245            node_entries.push(NodeEntry { node });
246        }
247
248        // Apply pre-configured routing entries
249        for (idx, node) in node_entries.iter_mut().enumerate() {
250            for &(from, to, gain) in &self.recipes[idx].routing_entries {
251                if let NodeVariant::Router(ref mut router) = node.node {
252                    router.set_connection(from, to, T::from_f32(gain)).ok();
253                }
254            }
255        }
256
257        let num_nodes = node_entries.len();
258
259        // --- Phase 2: adjacency for Kahn (signal edges only) ---
260        let mut in_degree = vec![0usize; num_nodes];
261        let mut out_edges: Vec<Vec<(usize, usize, usize)>> = vec![Vec::new(); num_nodes];
262
263        for &(from_n, from_p, to_n, to_p) in &self.signal_edges {
264            in_degree[to_n] += 1;
265            out_edges[from_n].push((from_p, to_n, to_p));
266        }
267
268        // Categorize root nodes (in_degree == 0) by type_name
269        let recording_roots: Vec<usize> = in_degree
270            .iter()
271            .enumerate()
272            .filter(|(i, &d)| d == 0 && self.recipes[*i].type_name == "rill/input")
273            .map(|(i, _)| i)
274            .collect();
275        let playback_roots: Vec<usize> = in_degree
276            .iter()
277            .enumerate()
278            .filter(|(i, &d)| d == 0 && self.recipes[*i].type_name != "rill/input")
279            .map(|(i, _)| i)
280            .collect();
281
282        // --- Kahn's algorithm ---
283        let mut queue: VecDeque<usize> = in_degree
284            .iter()
285            .enumerate()
286            .filter(|(_, &d)| d == 0)
287            .map(|(i, _)| i)
288            .collect();
289
290        let mut topo = Vec::with_capacity(num_nodes);
291        let mut indeg = in_degree;
292        while let Some(idx) = queue.pop_front() {
293            topo.push(idx);
294            for &(_, to_n, _) in &out_edges[idx] {
295                indeg[to_n] -= 1;
296                if indeg[to_n] == 0 {
297                    queue.push_back(to_n);
298                }
299            }
300        }
301
302        if topo.len() != num_nodes {
303            return Err(BuildError::CycleDetected);
304        }
305
306        // --- collect nodes into final Vec ---
307        let mut nodes: Vec<NodeVariant<T, BUF_SIZE>> =
308            node_entries.into_iter().map(|e| e.node).collect();
309
310        // --- Phase 5: port pointer wiring on the final nodes Vec ---
311        for &(from_n, from_p, to_n, to_p) in &self.signal_edges {
312            if let Some(port) = nodes[from_n].output_port_mut(from_p) {
313                port.add_downstream(to_n, to_p);
314            }
315            let in_ptr: *mut Port<T, BUF_SIZE> = nodes[to_n]
316                .input_port_mut(to_p)
317                .map(|p| p as *mut Port<T, BUF_SIZE>)
318                .unwrap_or(std::ptr::null_mut());
319            let out_ptr: *mut Port<T, BUF_SIZE> = nodes[from_n]
320                .output_port_mut(from_p)
321                .map(|p| p as *mut Port<T, BUF_SIZE>)
322                .unwrap_or(std::ptr::null_mut());
323            if !in_ptr.is_null() && !out_ptr.is_null() {
324                #[allow(unsafe_code)]
325                unsafe {
326                    (*out_ptr).add_downstream_input_ptr(in_ptr);
327                }
328            }
329        }
330
331        // --- chain membership (recording vs playback) ---
332        //
333        // Recording roots are I/O input nodes (type "rill/input").
334        let has_split_chain = !recording_roots.is_empty() && !playback_roots.is_empty();
335
336        let mut recording_set: HashSet<usize> = HashSet::new();
337        let mut playback_set: HashSet<usize> = HashSet::new();
338
339        if has_split_chain {
340            recording_set = recording_roots.iter().copied().collect();
341            let mut queue: VecDeque<usize> = recording_roots.iter().copied().collect();
342            while let Some(node) = queue.pop_front() {
343                for &(_, to_n, _) in &out_edges[node] {
344                    if recording_set.insert(to_n) {
345                        queue.push_back(to_n);
346                    }
347                }
348            }
349
350            playback_set = playback_roots.iter().copied().collect();
351            let mut queue: VecDeque<usize> = playback_roots.iter().copied().collect();
352            while let Some(node) = queue.pop_front() {
353                for &(_, to_n, _) in &out_edges[node] {
354                    if playback_set.insert(to_n) {
355                        queue.push_back(to_n);
356                    }
357                }
358            }
359
360            // Intersection → playback wins
361            for node in recording_set.clone() {
362                if playback_set.contains(&node) && !recording_roots.contains(&node) {
363                    recording_set.remove(&node);
364                }
365            }
366        }
367
368        // --- downstream_nodes (chain-filtered) ---
369        for &(from_n, from_p, to_n, _) in &self.signal_edges {
370            let parent: *mut NodeVariant<T, BUF_SIZE> = &mut nodes[to_n];
371            if let Some(port) = nodes[from_n].output_port_mut(from_p) {
372                if has_split_chain {
373                    let same_chain = (recording_set.contains(&from_n)
374                        && recording_set.contains(&to_n))
375                        || (playback_set.contains(&from_n) && playback_set.contains(&to_n));
376                    if !same_chain {
377                        continue;
378                    }
379                }
380                port.add_downstream_node(parent);
381            }
382        }
383
384        // --- upstream_node (pull-model, same-chain only) ---
385        for &(from_n, _from_p, to_n, to_p) in &self.signal_edges {
386            let same_chain = if has_split_chain {
387                (recording_set.contains(&from_n) && recording_set.contains(&to_n))
388                    || (playback_set.contains(&from_n) && playback_set.contains(&to_n))
389            } else {
390                true
391            };
392            if same_chain {
393                let src: *mut NodeVariant<T, BUF_SIZE> = &mut nodes[from_n];
394                if let Some(port) = nodes[to_n].input_port_mut(to_p) {
395                    port.set_upstream_node(src);
396                }
397            }
398        }
399
400        // --- upstream_buffer (zero-copy alias, exclusive 1:1 edges only) ---
401        //
402        // An input port may read its upstream output buffer directly (no copy)
403        // ONLY when the edge is exclusive: the source output has exactly one
404        // consumer AND the input has exactly one producer. Fan-out branches
405        // must each receive an independent copy so downstream processing is
406        // isolated (per the zero-copy rules); fan-in ports need their own
407        // buffer too. Both leave `upstream_buffer` as `None` → materialized.
408        let mut out_degree: std::collections::HashMap<(usize, usize), usize> =
409            std::collections::HashMap::new();
410        let mut in_degree_port: std::collections::HashMap<(usize, usize), usize> =
411            std::collections::HashMap::new();
412        for &(from_n, from_p, to_n, to_p) in &self.signal_edges {
413            *out_degree.entry((from_n, from_p)).or_insert(0) += 1;
414            *in_degree_port.entry((to_n, to_p)).or_insert(0) += 1;
415        }
416        for &(from_n, from_p, to_n, to_p) in &self.signal_edges {
417            let exclusive = out_degree.get(&(from_n, from_p)) == Some(&1)
418                && in_degree_port.get(&(to_n, to_p)) == Some(&1);
419            let upstream = if exclusive {
420                nodes[from_n]
421                    .output_port(from_p)
422                    .map(|p| p.buffer() as *const FixedBuffer<T, BUF_SIZE>)
423            } else {
424                None
425            };
426            if let Some(port) = nodes[to_n].input_port_mut(to_p) {
427                port.set_upstream_buffer(upstream);
428            }
429        }
430
431        // --- feedback buffers ---
432        for &(from_n, from_p, to_n, to_p) in &self.feedback_edges {
433            if let Some(port) = nodes[from_n].output_port_mut(from_p) {
434                port.init_feedback_buffer();
435                port.add_feedback_downstream(to_n, to_p);
436            }
437            if let Some(port) = nodes[to_n].input_port_mut(to_p) {
438                port.init_feedback_buffer();
439            }
440        }
441        for &(from_n, from_p, to_n, to_p) in &self.feedback_edges {
442            let ptr = nodes[to_n]
443                .input_port(to_p)
444                .map(|p| p.feedback_buffer_ptr());
445            if let Some(port) = nodes[from_n].output_port_mut(from_p) {
446                if let Some(p) = ptr {
447                    port.add_feedback_ptr(p);
448                }
449            }
450        }
451
452        // Allocate named shared resources (tape loops) and hand out capability
453        // handles. The registry is build-time only: nodes keep their handles,
454        // which reference-count the resource, so it is dropped after resolution.
455        let mut registry = ResourceRegistry::new();
456        for r in &self.resources {
457            if r.kind == "tape" {
458                if let Some(tape) = TapeLoop::<T>::new(r.capacity) {
459                    registry.register_tape(&r.name, tape);
460                }
461            }
462        }
463        for entry in nodes.iter_mut() {
464            entry.resolve_resources(&mut registry);
465        }
466
467        let allocated = self.resources.clone();
468
469        // Compute node pointers for branch-chain processing
470        let rec_ptrs: Vec<_> = recording_roots
471            .iter()
472            .map(|&i| &mut nodes[i] as *mut _)
473            .collect();
474
475        // Real sink = last Sink node in topological order. Using the actual
476        // Sink (rather than `topo.last()`) is robust against extra signal-DAG
477        // leaves introduced by feedback-source branches (e.g. an effect chain
478        // inside a tape feedback loop terminates on a feedback edge, becoming a
479        // second leaf that could otherwise be mistaken for the sink).
480        let sink_idx = if has_split_chain {
481            topo.iter()
482                .rev()
483                .copied()
484                .find(|&i| matches!(nodes[i], NodeVariant::Sink(_)))
485                .or_else(|| topo.last().copied())
486        } else {
487            None
488        };
489        let sink_ptr = match sink_idx {
490            Some(i) => &mut nodes[i] as *mut _,
491            None => std::ptr::null_mut(),
492        };
493
494        // Feedback-branch nodes: signal-ancestors of feedback-edge sources that
495        // are processed by NEITHER the recording push NOR the playback pull.
496        // In split mode the pull only processes nodes upstream of the sink, so
497        // side-branch nodes feeding a feedback edge (e.g. effects inside a tape
498        // feedback loop) would never run and their `snapshot_feedback` would
499        // never fire. They are processed explicitly, in topological order,
500        // after the pull.
501        let feedback_ptrs: Vec<*mut NodeVariant<T, BUF_SIZE>> =
502            if has_split_chain && !self.feedback_edges.is_empty() {
503                let mut rev: Vec<Vec<usize>> = vec![Vec::new(); num_nodes];
504                for &(f, _, t, _) in &self.signal_edges {
505                    rev[t].push(f);
506                }
507                // Nodes that can reach the sink (already processed by the pull).
508                let mut sink_reachable = vec![false; num_nodes];
509                if let Some(si) = sink_idx {
510                    let mut q = VecDeque::new();
511                    q.push_back(si);
512                    sink_reachable[si] = true;
513                    while let Some(n) = q.pop_front() {
514                        for &u in &rev[n] {
515                            if !sink_reachable[u] {
516                                sink_reachable[u] = true;
517                                q.push_back(u);
518                            }
519                        }
520                    }
521                }
522                // Signal-ancestors (inclusive) of feedback-edge sources.
523                let mut in_branch = vec![false; num_nodes];
524                let mut q = VecDeque::new();
525                for &(from_n, _, _, _) in &self.feedback_edges {
526                    if !in_branch[from_n] {
527                        in_branch[from_n] = true;
528                        q.push_back(from_n);
529                    }
530                }
531                while let Some(n) = q.pop_front() {
532                    for &u in &rev[n] {
533                        if !in_branch[u] {
534                            in_branch[u] = true;
535                            q.push_back(u);
536                        }
537                    }
538                }
539                topo.iter()
540                    .copied()
541                    .filter(|&i| in_branch[i] && !sink_reachable[i] && !recording_set.contains(&i))
542                    .map(|i| &mut nodes[i] as *mut _)
543                    .collect()
544            } else {
545                Vec::new()
546            };
547
548        // Wrap nodes in Rc<UnsafeCell<Vec<>>> — port pointers already valid in this Vec.
549        let nodes: Rc<UnsafeCell<Vec<NodeVariant<T, BUF_SIZE>>>> = Rc::new(UnsafeCell::new(nodes));
550
551        let pending_params: PendingParams = Rc::new(RefCell::new(Vec::new()));
552
553        let actor = system.spawn("graph", {
554            let n = nodes.clone();
555            let pending = pending_params.clone();
556            #[allow(unsafe_code)]
557            move |msg: CommandEnum| {
558                if let CommandEnum::SetParameter(param) = msg {
559                    if param.sample_pos.is_some() {
560                        // Sample-accurate: defer to the block containing sample_pos.
561                        pending.borrow_mut().push(param);
562                        return;
563                    }
564                    let idx = param.port.node_id().inner() as usize;
565                    unsafe {
566                        let nv = &mut *n.get();
567                        if idx < nv.len() {
568                            let _ = nv[idx].set_parameter(&param.parameter, param.value);
569                        }
570                    }
571                }
572            }
573        });
574
575        let actor_ref = actor.actor_ref();
576
577        Ok(Graph {
578            nodes,
579            topo_order: topo,
580            resources: allocated,
581            current_tick: ClockTick::new(
582                0,
583                BUF_SIZE as u32,
584                self.sample_rate.unwrap_or(44100.0),
585                String::new(),
586            ),
587            recording_roots: recording_roots.clone(),
588            playback_roots: playback_roots.clone(),
589            recording_ptrs: rec_ptrs,
590            sink_ptr,
591            feedback_ptrs,
592            actor: Some(actor),
593            actor_ref,
594            parent_ref: self.parent_ref.clone(),
595            system_clock: None,
596            pending_params,
597        })
598    }
599}
600
601// ============================================================================
602// Graph (Static DAG)
603// ============================================================================
604
605/// Shared queue of sample-accurate parameter changes awaiting application.
606///
607/// Populated by the graph actor handler when a [`SetParameter`] carries a
608/// `sample_pos`; drained per processing block by [`apply_due_params`].
609type PendingParams = Rc<RefCell<Vec<SetParameter>>>;
610
611/// Apply all pending sample-accurate parameter changes that are due by
612/// `chunk_end` (the absolute sample position just past the current block).
613///
614/// Changes are applied in ascending `sample_pos` order; anything scheduled for
615/// a later block stays queued. This is what makes parameter automation land at
616/// the right sample position regardless of how the backend batches blocks into
617/// I/O callbacks.
618#[allow(unsafe_code)]
619fn apply_due_params<T: Transcendental, const BUF_SIZE: usize>(
620    nodes: &UnsafeCell<Vec<NodeVariant<T, BUF_SIZE>>>,
621    pending: &RefCell<Vec<SetParameter>>,
622    chunk_end: u64,
623) {
624    let mut pend = pending.borrow_mut();
625    if pend.is_empty() {
626        return;
627    }
628    pend.sort_by_key(|p| p.sample_pos.unwrap_or(0));
629    let split = pend.partition_point(|p| p.sample_pos.is_none_or(|sp| sp < chunk_end));
630    if split == 0 {
631        return;
632    }
633    unsafe {
634        let nv = &mut *nodes.get();
635        for p in pend.drain(0..split) {
636            let idx = p.port.node_id().inner() as usize;
637            if idx < nv.len() {
638                let _ = nv[idx].set_parameter(&p.parameter, p.value);
639            }
640        }
641    }
642}
643
644/// Owned parts of a [`Graph`] returned by `into_parts` (test only).
645#[cfg(test)]
646type GraphParts<T, const BUF_SIZE: usize> = (Vec<NodeVariant<T, BUF_SIZE>>, Vec<usize>, ClockTick);
647
648/// Immutable signal graph with static DAG topology.
649///
650/// Once built the graph cannot be modified. The graph owns no processing
651/// logic — it is a pure topology description. Processing is driven by
652/// port-level methods (`pre_process`, `snapshot_feedback`, `propagate`)
653/// called from external code (e.g. a real-time signal callback or an
654/// offline renderer).
655pub struct Graph<T: Transcendental, const BUF_SIZE: usize> {
656    nodes: Rc<UnsafeCell<Vec<NodeVariant<T, BUF_SIZE>>>>,
657    topo_order: Vec<usize>,
658    recording_roots: Vec<usize>,
659    playback_roots: Vec<usize>,
660    recording_ptrs: Vec<*mut NodeVariant<T, BUF_SIZE>>,
661    sink_ptr: *mut NodeVariant<T, BUF_SIZE>,
662    feedback_ptrs: Vec<*mut NodeVariant<T, BUF_SIZE>>,
663    current_tick: ClockTick,
664    pub(crate) resources: Vec<GraphResource>,
665    actor: Option<Actor<CommandEnum>>,
666    actor_ref: ActorRef<CommandEnum>,
667    parent_ref: Option<ActorRef<CommandEnum>>,
668    /// Optional shared system clock, updated by external sync sources (MIDI, JACK transport).
669    /// When set, the I/O callback reads BPM from it and creates `ClockTick::with_tempo`.
670    pub system_clock: Option<Arc<SystemClock>>,
671    /// Sample-accurate parameter changes awaiting application (shared with the actor handler).
672    pending_params: PendingParams,
673}
674
675/// Owned processing state extracted from a [`Graph`].
676///
677/// Holds the parts needed for the I/O callback loop: the actor mailbox
678/// for draining `SetParameter` commands, the node array, and routing
679/// metadata.  The state is `!Send + !Sync` — it stays on the I/O thread.
680///
681/// Created via [`Graph::into_processing_state`].
682pub struct ProcessingState<T: Transcendental, const BUF_SIZE: usize> {
683    actor: Actor<CommandEnum>,
684    nodes: Rc<UnsafeCell<Vec<NodeVariant<T, BUF_SIZE>>>>,
685    recording_roots: Vec<usize>,
686    playback_roots: Vec<usize>,
687    recording_ptrs: Vec<*mut NodeVariant<T, BUF_SIZE>>,
688    sink_ptr: *mut NodeVariant<T, BUF_SIZE>,
689    feedback_ptrs: Vec<*mut NodeVariant<T, BUF_SIZE>>,
690    parent_ref: Option<ActorRef<CommandEnum>>,
691    system_clock: Option<Arc<SystemClock>>,
692    /// Sample rate the graph nodes are currently initialised for.
693    ///
694    /// The graph has no clock of its own — it runs entirely inside the backend
695    /// process callback and adopts the rate carried by each [`ClockTick`]. When
696    /// a backend reports a hardware rate that differs from the rate the nodes
697    /// were built with (e.g. JACK locked to 48 kHz while the graph was
698    /// configured for 44.1 kHz), the nodes are re-initialised on the first tick
699    /// so DSP (chip clocks, filter coefficients, …) matches the real rate.
700    sample_rate: f32,
701    /// Sample-accurate parameter changes awaiting application (shared with the actor handler).
702    pending_params: PendingParams,
703}
704
705impl<T: Transcendental, const BUF_SIZE: usize> ProcessingState<T, BUF_SIZE> {
706    /// Re-initialise every node for a new sample rate.
707    ///
708    /// Called from [`process_block`](Self::process_block) when the driving
709    /// [`ClockTick`] reports a rate different from the one the graph is
710    /// currently initialised for.
711    #[allow(unsafe_code)]
712    fn reinit_sample_rate(&mut self, sample_rate: f32) {
713        unsafe {
714            let nv = &mut *self.nodes.get();
715            for node in nv.iter_mut() {
716                node.init(sample_rate);
717            }
718        }
719        self.sample_rate = sample_rate;
720    }
721
722    /// Process one block of signal data driven by an external [`ClockTick`].
723    ///
724    /// Processes all root nodes (recording + playback) — used when there's no
725    /// split between input and output streams (single-stream backends).
726    #[allow(unsafe_code)]
727    pub fn process_block(&mut self, tick: &ClockTick) -> ProcessResult<()> {
728        if tick.sample_rate > 0.0 && (tick.sample_rate - self.sample_rate).abs() > 0.5 {
729            self.reinit_sample_rate(tick.sample_rate);
730        }
731        self.actor.drain();
732        apply_due_params(
733            &self.nodes,
734            &self.pending_params,
735            tick.sample_pos + tick.samples_since_last as u64,
736        );
737        let mut ctx = if let Some(ref clock) = self.system_clock {
738            RenderContext::with_tempo(
739                tick.sample_pos,
740                tick.samples_since_last,
741                tick.sample_rate,
742                clock.bpm() as f32,
743            )
744        } else {
745            RenderContext::new(tick.sample_pos, tick.samples_since_last, tick.sample_rate)
746        };
747        ctx.speed_ratio = tick.speed_ratio;
748        unsafe {
749            let nv = &mut *self.nodes.get();
750            for &root in self
751                .recording_roots
752                .iter()
753                .chain(self.playback_roots.iter())
754            {
755                let _ = nv[root].process_block(&ctx, tick);
756                for po in 0..nv[root].num_signal_outputs() {
757                    if let Some(port) = nv[root].output_port(po) {
758                        let _ = port.propagate(&ctx, tick);
759                    }
760                }
761            }
762        }
763        Ok(())
764    }
765
766    /// Send a ClockTick to the parent actor (rack fan-out).
767    ///
768    /// Called by the backend's process callback at the appropriate time
769    /// (once per I/O callback for standard backends, once per DMA buffer
770    /// for chunking backends).
771    pub fn send_clock_tick(&self, tick: &ClockTick) {
772        if tick.is_final {
773            if let Some(ref parent) = self.parent_ref {
774                parent.send(CommandEnum::ClockTick(tick.clone()));
775            }
776        }
777    }
778
779    /// Wire capture/playback backends into Source/Sink nodes after graph construction.
780    ///
781    /// Must be called after `into_processing_state()` and before the driver starts.
782    /// Only Source nodes respond to `set_capture`; only Sink nodes respond to
783    /// `set_playback`.  Processor and Router nodes ignore both.
784    #[allow(unsafe_code)]
785    pub fn wire_backends(
786        &mut self,
787        capture: Option<Arc<dyn IoCapture>>,
788        playback: Option<Arc<dyn IoPlayback>>,
789    ) {
790        unsafe {
791            let nv = &mut *self.nodes.get();
792            for node in nv.iter_mut() {
793                if let Some(ref c) = capture {
794                    if let NodeVariant::Source(src) = node {
795                        src.set_capture(c.clone())
796                    }
797                }
798                if let Some(ref p) = playback {
799                    if let NodeVariant::Sink(sink) = node {
800                        sink.set_playback(p.clone())
801                    }
802                }
803            }
804        }
805    }
806
807    /// Run this processing state with a pre-created driver backend.
808    ///
809    /// Consumes `self`, wires the process callback, enters the I/O loop.
810    /// The `running` flag controls shutdown.
811    pub fn run_with_driver(
812        mut self,
813        driver: Arc<dyn IoDriver>,
814        running: Arc<AtomicBool>,
815    ) -> Result<(), String> {
816        self.actor.drain();
817        let use_split = !self.recording_roots.is_empty() && !self.playback_roots.is_empty();
818        if use_split {
819            let mut actor = self.actor;
820            let rec_ptrs = self.recording_ptrs;
821            let sink = self.sink_ptr;
822            let fb_ptrs = self.feedback_ptrs;
823            let clock = self.system_clock;
824            let parent = self.parent_ref;
825
826            let clock_rec = clock.clone();
827            driver.set_input_process_callback(Box::new(move |tick: &ClockTick| {
828                actor.drain();
829                if let Some(ref c) = clock_rec {
830                    let ctx = RenderContext::with_tempo(
831                        tick.sample_pos,
832                        tick.samples_since_last,
833                        tick.sample_rate,
834                        c.bpm() as f32,
835                    );
836                    p_forward(&rec_ptrs, &ctx, tick);
837                } else {
838                    let ctx = RenderContext::new(
839                        tick.sample_pos,
840                        tick.samples_since_last,
841                        tick.sample_rate,
842                    );
843                    p_forward(&rec_ptrs, &ctx, tick);
844                }
845            }));
846            driver.set_process_callback(Box::new(move |tick: &ClockTick| {
847                if let Some(ref c) = clock {
848                    let mut ctx = RenderContext::with_tempo(
849                        tick.sample_pos,
850                        tick.samples_since_last,
851                        tick.sample_rate,
852                        c.bpm() as f32,
853                    );
854                    ctx.speed_ratio = tick.speed_ratio;
855                    p_pull(sink, &ctx, tick);
856                    p_process_branch(&fb_ptrs, &ctx, tick);
857                } else {
858                    let mut ctx = RenderContext::new(
859                        tick.sample_pos,
860                        tick.samples_since_last,
861                        tick.sample_rate,
862                    );
863                    ctx.speed_ratio = tick.speed_ratio;
864                    p_pull(sink, &ctx, tick);
865                    p_process_branch(&fb_ptrs, &ctx, tick);
866                }
867                if tick.is_final {
868                    if let Some(ref p) = parent {
869                        p.send(CommandEnum::ClockTick(tick.clone()));
870                    }
871                }
872            }));
873            driver.run(running.clone())?;
874        } else {
875            driver.set_process_callback(Box::new(move |tick: &ClockTick| {
876                let _ = self.process_block(tick);
877                self.send_clock_tick(tick);
878            }));
879            driver.run(running.clone())?;
880        }
881        while running.load(std::sync::atomic::Ordering::Acquire) {
882            std::thread::park();
883        }
884        let _ = driver.stop();
885        Ok(())
886    }
887}
888
889// ============================================================================
890// Pointer-based chain processing (used by split-chain run_with_driver closures)
891// ============================================================================
892
893/// Forward propagate from recording roots.
894#[allow(unsafe_code)]
895fn p_forward<T: Transcendental, const BUF_SIZE: usize>(
896    roots: &[*mut NodeVariant<T, BUF_SIZE>],
897    ctx: &RenderContext,
898    tick: &ClockTick,
899) {
900    for &root in roots {
901        unsafe {
902            let nv = &mut *root;
903            let _ = nv.process_block(ctx, tick);
904            for po in 0..nv.num_signal_outputs() {
905                if let Some(port) = nv.output_port(po) {
906                    let _ = port.propagate(ctx, tick);
907                }
908            }
909        }
910    }
911}
912
913/// Pull-model playback chain: start from sink, recursively process upstream.
914#[allow(unsafe_code)]
915fn p_pull<T: Transcendental, const BUF_SIZE: usize>(
916    sink: *mut NodeVariant<T, BUF_SIZE>,
917    ctx: &RenderContext,
918    tick: &ClockTick,
919) {
920    if sink.is_null() {
921        return;
922    }
923    unsafe {
924        p_pull_recurse(&mut *sink, ctx, tick);
925    }
926}
927
928#[allow(unsafe_code)]
929fn p_pull_recurse<T: Transcendental, const BUF_SIZE: usize>(
930    node: &mut NodeVariant<T, BUF_SIZE>,
931    ctx: &RenderContext,
932    tick: &ClockTick,
933) {
934    for pi in 0..node.num_signal_inputs() {
935        if let Some(p) = node.input_port_mut(pi) {
936            p.pre_process();
937        }
938    }
939    for pi in 0..node.num_signal_inputs() {
940        if let Some(p) = node.input_port(pi) {
941            let src = p.upstream_node();
942            if !src.is_null() {
943                unsafe {
944                    p_pull_recurse(&mut *src, ctx, tick);
945                }
946            }
947        }
948    }
949    let _ = node.process_block(ctx, tick);
950    for po in 0..node.num_signal_outputs() {
951        if let Some(p) = node.output_port_mut(po) {
952            p.snapshot_feedback();
953        }
954    }
955    for po in 0..node.num_signal_outputs() {
956        if let Some(port) = node.output_port(po) {
957            let buf = port.buffer();
958            for &in_ptr in port.downstream_input_ptrs() {
959                unsafe {
960                    let ip = &mut *in_ptr;
961                    if !ip.is_zero_copy() {
962                        let _ = ip.run_action(Some(buf.as_array()));
963                    }
964                    ip.set_data_received(true);
965                }
966            }
967        }
968    }
969}
970
971/// Process a topologically-ordered list of feedback-branch nodes.
972///
973/// These are side-branch nodes (e.g. effects inside a tape feedback loop) that
974/// feed a feedback edge but do not reach the sink, so the playback pull never
975/// processes them. Their inputs were already filled by the pull (their upstream
976/// producers are on the sink path); here each is processed in order so its
977/// output is produced and `snapshot_feedback` captures it into the downstream
978/// feedback buffer for the next block.
979#[allow(unsafe_code)]
980fn p_process_branch<T: Transcendental, const BUF_SIZE: usize>(
981    branch: &[*mut NodeVariant<T, BUF_SIZE>],
982    ctx: &RenderContext,
983    tick: &ClockTick,
984) {
985    for &np in branch {
986        unsafe {
987            let node = &mut *np;
988            for pi in 0..node.num_signal_inputs() {
989                if let Some(p) = node.input_port_mut(pi) {
990                    p.pre_process();
991                }
992            }
993            let _ = node.process_block(ctx, tick);
994            for po in 0..node.num_signal_outputs() {
995                if let Some(p) = node.output_port_mut(po) {
996                    p.snapshot_feedback();
997                }
998            }
999            for po in 0..node.num_signal_outputs() {
1000                if let Some(port) = node.output_port(po) {
1001                    let buf = port.buffer();
1002                    for &in_ptr in port.downstream_input_ptrs() {
1003                        let ip = &mut *in_ptr;
1004                        if !ip.is_zero_copy() {
1005                            let _ = ip.run_action(Some(buf.as_array()));
1006                        }
1007                        ip.set_data_received(true);
1008                    }
1009                }
1010            }
1011        }
1012    }
1013}
1014
1015impl<T: Transcendental, const BUF_SIZE: usize> Graph<T, BUF_SIZE> {
1016    // ========================================================================
1017    // Accessors
1018    // ========================================================================
1019
1020    /// Borrow the node array (read-only).
1021    #[allow(unsafe_code)]
1022    pub fn nodes(&self) -> &[NodeVariant<T, BUF_SIZE>] {
1023        unsafe { &*self.nodes.get() }
1024    }
1025
1026    /// Return the current clock tick.
1027    pub fn current_tick(&self) -> ClockTick {
1028        self.current_tick.clone()
1029    }
1030
1031    /// Return the number of nodes in the graph.
1032    #[allow(unsafe_code)]
1033    pub fn node_count(&self) -> usize {
1034        unsafe { (*self.nodes.get()).len() }
1035    }
1036
1037    /// Return the topological ordering of node indices.
1038    pub fn topo_order(&self) -> &[usize] {
1039        &self.topo_order
1040    }
1041
1042    #[allow(dead_code)]
1043    pub(crate) fn sample_rate(&self) -> f32 {
1044        self.current_tick.sample_rate
1045    }
1046
1047    /// Access the named resources (tape loops, etc.) allocated for this graph.
1048    #[allow(dead_code)]
1049    pub fn resources(&self) -> &[GraphResource] {
1050        &self.resources
1051    }
1052
1053    /// Process one block of signal data driven by an external [`ClockTick`].
1054    ///
1055    /// Called from the backend's process callback. Performs:
1056    ///
1057    /// 1. Drains the graph's actor mailbox (applies queued `SetParameter`s).
1058    /// 2. Creates a [`RenderContext`] from the tick.
1059    /// 3. Calls `process_block` on each root node and recursively
1060    ///    propagates through the DAG via [`Port::propagate`].
1061    ///
1062    /// The graph is `!Send + !Sync` — it stays on the I/O callback thread.
1063    #[allow(unsafe_code)]
1064    pub fn process_block(&mut self, tick: &ClockTick) -> ProcessResult<()> {
1065        if let Some(ref mut actor) = self.actor {
1066            actor.drain();
1067        }
1068        apply_due_params(
1069            &self.nodes,
1070            &self.pending_params,
1071            tick.sample_pos + tick.samples_since_last as u64,
1072        );
1073        let ctx = if let Some(ref clock) = self.system_clock {
1074            RenderContext::with_tempo(
1075                tick.sample_pos,
1076                tick.samples_since_last,
1077                tick.sample_rate,
1078                clock.bpm() as f32,
1079            )
1080        } else {
1081            RenderContext::new(tick.sample_pos, tick.samples_since_last, tick.sample_rate)
1082        };
1083        self.current_tick = tick.clone();
1084        unsafe {
1085            let nv = &mut *self.nodes.get();
1086            for &root in self
1087                .recording_roots
1088                .iter()
1089                .chain(self.playback_roots.iter())
1090            {
1091                let _ = nv[root].process_block(&ctx, tick);
1092                for po in 0..nv[root].num_signal_outputs() {
1093                    if let Some(port) = nv[root].output_port(po) {
1094                        let _ = port.propagate(&ctx, tick);
1095                    }
1096                }
1097            }
1098        }
1099        Ok(())
1100    }
1101
1102    /// Consume the graph and return a [`ProcessingState`] that owns all
1103    /// parts needed for the I/O callback loop.
1104    ///
1105    /// `ProcessingState` is `!Send + !Sync` — it stays on the I/O thread
1106    /// and is moved into the backend's process callback closure.
1107    pub fn into_processing_state(mut self) -> ProcessingState<T, BUF_SIZE> {
1108        let actor = self.actor.take().expect("graph actor missing");
1109        ProcessingState {
1110            actor,
1111            nodes: self.nodes,
1112            recording_roots: self.recording_roots,
1113            playback_roots: self.playback_roots,
1114            recording_ptrs: self.recording_ptrs,
1115            sink_ptr: self.sink_ptr,
1116            feedback_ptrs: self.feedback_ptrs,
1117            parent_ref: self.parent_ref,
1118            system_clock: self.system_clock,
1119            sample_rate: self.current_tick.sample_rate,
1120            pending_params: self.pending_params,
1121        }
1122    }
1123
1124    /// Obtain an [`ActorRef`] for sending commands to this graph.
1125    pub fn handle(&self) -> ActorRef<CommandEnum> {
1126        self.actor_ref.clone()
1127    }
1128
1129    /// Consume the graph and return its owned parts (test only).
1130    #[cfg(test)]
1131    pub fn into_parts(self) -> GraphParts<T, BUF_SIZE> {
1132        let Self {
1133            nodes,
1134            topo_order,
1135            current_tick,
1136            resources: _,
1137            recording_roots: _,
1138            playback_roots: _,
1139            recording_ptrs: _,
1140            sink_ptr: _,
1141            feedback_ptrs: _,
1142            actor,
1143            actor_ref: _,
1144            parent_ref: _,
1145            system_clock: _,
1146            pending_params: _,
1147        } = self;
1148        drop(actor);
1149        let nodes = Rc::try_unwrap(nodes).unwrap().into_inner();
1150        (nodes, topo_order, current_tick)
1151    }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::*;
1157    use rill_core::math::Transcendental;
1158    use rill_core::time::RenderContext;
1159
1160    use rill_core::traits::{
1161        Node, NodeCategory, NodeId, NodeMetadata, NodeState, ParamValue, ParameterId, Port,
1162        ProcessResult, Processor, Sink, Source,
1163    };
1164    use rill_core_actor::ActorSystem;
1165    use std::sync::Arc;
1166
1167    fn test_system() -> ActorSystem {
1168        ActorSystem::new()
1169    }
1170
1171    fn test_factory<const B: usize>() -> Arc<NodeFactory<f32, B>> {
1172        let mut f = NodeFactory::<f32, B>::new();
1173
1174        f.register_fn("test/const", |id, params| {
1175            let value = params.get_f32("value", 1.0);
1176            let mut node = ConstantSource::<f32, B>::new(id, value, params.sample_rate);
1177            node.init(params.sample_rate);
1178            NodeVariant::Source(Box::new(node))
1179        });
1180
1181        f.register_fn("test/gain", |id, params| {
1182            let gain = params.get_f32("gain", 1.0);
1183            let mut node = GainProcessor::<f32, B>::new(id, params.sample_rate, gain);
1184            node.init(params.sample_rate);
1185            NodeVariant::Processor(Box::new(node))
1186        });
1187
1188        f.register_fn("test/capture", |id, params| {
1189            let mut node = CaptureSink::<f32, B>::new(id, params.sample_rate);
1190            node.init(params.sample_rate);
1191            NodeVariant::Sink(Box::new(node))
1192        });
1193
1194        Arc::new(f)
1195    }
1196
1197    fn test_builder<const B: usize>(factory: &Arc<NodeFactory<f32, B>>) -> GraphBuilder<f32, B> {
1198        GraphBuilder::new(factory.clone())
1199    }
1200
1201    fn test_params(sample_rate: f32) -> Params {
1202        let mut p = Params::new(sample_rate);
1203        p.insert("value".to_string(), ParamValue::Float(sample_rate));
1204        p
1205    }
1206
1207    // ------------------------------------------------------------------------
1208    // Test node implementations
1209    // ------------------------------------------------------------------------
1210
1211    pub(crate) struct ConstantSource<T: Transcendental, const B: usize> {
1212        id: NodeId,
1213        value: T,
1214        state: NodeState<T, B>,
1215        output: Port<T, B>,
1216    }
1217
1218    impl<T: Transcendental, const B: usize> ConstantSource<T, B> {
1219        pub fn new(id: NodeId, value: T, sample_rate: f32) -> Self {
1220            let state = NodeState::new(sample_rate);
1221            let output = Port::output(id, 0, "out");
1222            Self {
1223                id,
1224                value,
1225                state,
1226                output,
1227            }
1228        }
1229    }
1230
1231    impl<T: Transcendental, const B: usize> Node<T, B> for ConstantSource<T, B> {
1232        fn id(&self) -> NodeId {
1233            self.id
1234        }
1235        fn set_id(&mut self, id: NodeId) {
1236            self.id = id;
1237        }
1238        fn metadata(&self) -> NodeMetadata {
1239            NodeMetadata {
1240                name: "ConstantSource".into(),
1241                type_name: Some("test/const".into()),
1242                category: NodeCategory::Source,
1243                description: String::new(),
1244                author: String::new(),
1245                version: String::new(),
1246                parameters: vec![],
1247                signal_inputs: 0,
1248                signal_outputs: 1,
1249                control_inputs: 0,
1250                control_outputs: 0,
1251                clock_inputs: 0,
1252                clock_outputs: 0,
1253                feedback_ports: 0,
1254            }
1255        }
1256        fn init(&mut self, _: f32) {}
1257        fn reset(&mut self) {}
1258        fn get_parameter(&self, _: &ParameterId) -> Option<ParamValue> {
1259            None
1260        }
1261        fn set_parameter(&mut self, _: &ParameterId, _: ParamValue) -> ProcessResult<()> {
1262            Ok(())
1263        }
1264        fn control_port(&self, _: usize) -> Option<&Port<T, B>> {
1265            None
1266        }
1267        fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
1268            None
1269        }
1270        fn output_port(&self, i: usize) -> Option<&Port<T, B>> {
1271            if i == 0 {
1272                Some(&self.output)
1273            } else {
1274                None
1275            }
1276        }
1277        fn output_port_mut(&mut self, i: usize) -> Option<&mut Port<T, B>> {
1278            if i == 0 {
1279                Some(&mut self.output)
1280            } else {
1281                None
1282            }
1283        }
1284        fn num_signal_outputs(&self) -> usize {
1285            1
1286        }
1287        fn input_port(&self, _: usize) -> Option<&Port<T, B>> {
1288            None
1289        }
1290        fn input_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
1291            None
1292        }
1293        fn state(&self) -> &NodeState<T, B> {
1294            &self.state
1295        }
1296        fn state_mut(&mut self) -> &mut NodeState<T, B> {
1297            &mut self.state
1298        }
1299    }
1300
1301    impl<T: Transcendental, const B: usize> Source<T, B> for ConstantSource<T, B> {
1302        fn generate(
1303            &mut self,
1304            _: &RenderContext,
1305            _: &[T],
1306            _: &[RenderContext],
1307            _: &ClockTick,
1308        ) -> ProcessResult<()> {
1309            self.output.write().fill(self.value);
1310            Ok(())
1311        }
1312    }
1313
1314    // ------------------------------------------------------------------------
1315    // GainProcessor
1316    // ------------------------------------------------------------------------
1317
1318    pub(crate) struct GainProcessor<T: Transcendental, const B: usize> {
1319        id: NodeId,
1320        gain: T,
1321        state: NodeState<T, B>,
1322        input: Port<T, B>,
1323        output: Port<T, B>,
1324    }
1325
1326    impl<T: Transcendental, const B: usize> GainProcessor<T, B> {
1327        pub fn new(id: NodeId, sample_rate: f32, gain: T) -> Self {
1328            let state = NodeState::new(sample_rate);
1329            let input = Port::input(id, 0, "in");
1330            let output = Port::output(id, 0, "out");
1331            Self {
1332                id,
1333                gain,
1334                state,
1335                input,
1336                output,
1337            }
1338        }
1339    }
1340
1341    impl<T: Transcendental, const B: usize> Node<T, B> for GainProcessor<T, B> {
1342        fn id(&self) -> NodeId {
1343            self.id
1344        }
1345        fn set_id(&mut self, id: NodeId) {
1346            self.id = id;
1347        }
1348        fn metadata(&self) -> NodeMetadata {
1349            NodeMetadata {
1350                name: "GainProcessor".into(),
1351                type_name: Some("test/gain".into()),
1352                category: NodeCategory::Processor,
1353                description: String::new(),
1354                author: String::new(),
1355                version: String::new(),
1356                parameters: vec![],
1357                signal_inputs: 1,
1358                signal_outputs: 1,
1359                control_inputs: 0,
1360                control_outputs: 0,
1361                clock_inputs: 0,
1362                clock_outputs: 0,
1363                feedback_ports: 0,
1364            }
1365        }
1366        fn init(&mut self, _: f32) {}
1367        fn reset(&mut self) {}
1368        fn get_parameter(&self, _: &ParameterId) -> Option<ParamValue> {
1369            None
1370        }
1371        fn set_parameter(&mut self, _: &ParameterId, _: ParamValue) -> ProcessResult<()> {
1372            Ok(())
1373        }
1374        fn control_port(&self, _: usize) -> Option<&Port<T, B>> {
1375            None
1376        }
1377        fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
1378            None
1379        }
1380        fn input_port(&self, i: usize) -> Option<&Port<T, B>> {
1381            if i == 0 {
1382                Some(&self.input)
1383            } else {
1384                None
1385            }
1386        }
1387        fn input_port_mut(&mut self, i: usize) -> Option<&mut Port<T, B>> {
1388            if i == 0 {
1389                Some(&mut self.input)
1390            } else {
1391                None
1392            }
1393        }
1394        fn num_signal_outputs(&self) -> usize {
1395            1
1396        }
1397        fn num_signal_inputs(&self) -> usize {
1398            1
1399        }
1400        fn output_port(&self, i: usize) -> Option<&Port<T, B>> {
1401            if i == 0 {
1402                Some(&self.output)
1403            } else {
1404                None
1405            }
1406        }
1407        fn output_port_mut(&mut self, i: usize) -> Option<&mut Port<T, B>> {
1408            if i == 0 {
1409                Some(&mut self.output)
1410            } else {
1411                None
1412            }
1413        }
1414        fn state(&self) -> &NodeState<T, B> {
1415            &self.state
1416        }
1417        fn state_mut(&mut self) -> &mut NodeState<T, B> {
1418            &mut self.state
1419        }
1420    }
1421
1422    impl<T: Transcendental, const B: usize> Processor<T, B> for GainProcessor<T, B> {
1423        fn process(
1424            &mut self,
1425            _: &RenderContext,
1426            _: &[&[T; B]],
1427            _: &[T],
1428            _: &[RenderContext],
1429            _: &[&[T; B]],
1430        ) -> ProcessResult<()> {
1431            let src = self.input.read();
1432            let buf = self.output.write();
1433            for i in 0..B {
1434                buf[i] = src[i] * self.gain;
1435            }
1436            Ok(())
1437        }
1438    }
1439
1440    // ------------------------------------------------------------------------
1441    // CaptureSink — captures first sample of each block
1442    // ------------------------------------------------------------------------
1443
1444    pub(crate) struct CaptureSink<T: Transcendental, const B: usize> {
1445        id: NodeId,
1446        state: NodeState<T, B>,
1447        input: Port<T, B>,
1448    }
1449
1450    impl<T: Transcendental, const B: usize> CaptureSink<T, B> {
1451        pub fn new(id: NodeId, sample_rate: f32) -> Self {
1452            let state = NodeState::new(sample_rate);
1453            let input = Port::input(id, 0, "in");
1454            Self { id, state, input }
1455        }
1456    }
1457
1458    impl<T: Transcendental, const B: usize> Node<T, B> for CaptureSink<T, B> {
1459        fn id(&self) -> NodeId {
1460            self.id
1461        }
1462        fn set_id(&mut self, id: NodeId) {
1463            self.id = id;
1464        }
1465        fn metadata(&self) -> NodeMetadata {
1466            NodeMetadata {
1467                name: "CaptureSink".into(),
1468                type_name: Some("test/capture".into()),
1469                category: NodeCategory::Sink,
1470                description: String::new(),
1471                author: String::new(),
1472                version: String::new(),
1473                parameters: vec![],
1474                signal_inputs: 1,
1475                signal_outputs: 0,
1476                control_inputs: 0,
1477                control_outputs: 0,
1478                clock_inputs: 0,
1479                clock_outputs: 0,
1480                feedback_ports: 0,
1481            }
1482        }
1483        fn init(&mut self, _: f32) {}
1484        fn reset(&mut self) {}
1485        fn get_parameter(&self, _: &ParameterId) -> Option<ParamValue> {
1486            None
1487        }
1488        fn set_parameter(&mut self, _: &ParameterId, _: ParamValue) -> ProcessResult<()> {
1489            Ok(())
1490        }
1491        fn control_port(&self, _: usize) -> Option<&Port<T, B>> {
1492            None
1493        }
1494        fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
1495            None
1496        }
1497        fn output_port(&self, _: usize) -> Option<&Port<T, B>> {
1498            None
1499        }
1500        fn output_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
1501            None
1502        }
1503        fn input_port(&self, i: usize) -> Option<&Port<T, B>> {
1504            if i == 0 {
1505                Some(&self.input)
1506            } else {
1507                None
1508            }
1509        }
1510        fn input_port_mut(&mut self, i: usize) -> Option<&mut Port<T, B>> {
1511            if i == 0 {
1512                Some(&mut self.input)
1513            } else {
1514                None
1515            }
1516        }
1517        fn num_signal_inputs(&self) -> usize {
1518            1
1519        }
1520        fn state(&self) -> &NodeState<T, B> {
1521            &self.state
1522        }
1523        fn state_mut(&mut self) -> &mut NodeState<T, B> {
1524            &mut self.state
1525        }
1526    }
1527
1528    impl<T: Transcendental, const B: usize> Sink<T, B> for CaptureSink<T, B> {
1529        fn consume(
1530            &mut self,
1531            _: &RenderContext,
1532            _: &[&[T; B]],
1533            _: &[T],
1534            _: &[RenderContext],
1535            _: &[&[T; B]],
1536            _: &ClockTick,
1537        ) -> ProcessResult<()> {
1538            Ok(())
1539        }
1540    }
1541
1542    // ------------------------------------------------------------------------
1543    // Graph signal flow tests
1544    // ------------------------------------------------------------------------
1545
1546    const BUF: usize = 64;
1547
1548    #[test]
1549    fn test_fanout_branches_are_independent_not_zero_copy() {
1550        // One source output feeding two consumers is a fan-out: each branch
1551        // must own an independent buffer so downstream processing is isolated.
1552        let factory = test_factory::<BUF>();
1553        let mut builder = test_builder::<BUF>(&factory);
1554        let system = test_system();
1555
1556        let src = builder.add_node("test/const", &test_params(44100.0));
1557        let a = builder.add_node("test/gain", &test_params(44100.0));
1558        let b = builder.add_node("test/gain", &test_params(44100.0));
1559        builder.connect_signal(src, 0, a, 0);
1560        builder.connect_signal(src, 0, b, 0);
1561
1562        let graph = builder.build(&system).unwrap();
1563        let nodes = graph.nodes();
1564        assert!(
1565            !nodes[a].input_port(0).unwrap().is_zero_copy(),
1566            "fan-out branch A must not alias the shared source buffer"
1567        );
1568        assert!(
1569            !nodes[b].input_port(0).unwrap().is_zero_copy(),
1570            "fan-out branch B must not alias the shared source buffer"
1571        );
1572        assert!(!nodes[a].input_port(0).unwrap().has_upstream_buffer());
1573        assert!(!nodes[b].input_port(0).unwrap().has_upstream_buffer());
1574    }
1575
1576    #[test]
1577    fn test_linear_chain_edge_is_zero_copy() {
1578        // An exclusive 1:1 edge is safe to alias (single consumer).
1579        let factory = test_factory::<BUF>();
1580        let mut builder = test_builder::<BUF>(&factory);
1581        let system = test_system();
1582
1583        let src = builder.add_node("test/const", &test_params(44100.0));
1584        let g = builder.add_node("test/gain", &test_params(44100.0));
1585        builder.connect_signal(src, 0, g, 0);
1586
1587        let graph = builder.build(&system).unwrap();
1588        let nodes = graph.nodes();
1589        assert!(
1590            nodes[g].input_port(0).unwrap().is_zero_copy(),
1591            "exclusive 1:1 edge should be zero-copy"
1592        );
1593    }
1594
1595    #[test]
1596    #[allow(unsafe_code)]
1597    fn test_graph_source_to_sink() {
1598        let factory = test_factory::<BUF>();
1599        let mut builder = test_builder::<BUF>(&factory);
1600        let system = test_system();
1601
1602        let src_idx = builder.add_node("test/const", &test_params(44100.0));
1603        let snk_idx = builder.add_node("test/capture", &test_params(44100.0));
1604        builder.connect_signal(src_idx, 0, snk_idx, 0);
1605
1606        let graph = builder.build(&system).unwrap();
1607        let source_idx = graph
1608            .recording_roots
1609            .first()
1610            .or(graph.playback_roots.first())
1611            .copied()
1612            .unwrap_or(0);
1613
1614        let ctx = RenderContext::new(0, BUF as u32, 44100.0);
1615        let tick = ClockTick::new(0, BUF as u32, 44100.0, String::new());
1616        let nodes = graph.nodes.clone();
1617        unsafe {
1618            let nv = &mut *nodes.get();
1619            nv[source_idx].process_block(&ctx, &tick).unwrap();
1620            if let Some(port) = nv[source_idx].output_port(0) {
1621                port.propagate(&ctx, &tick).unwrap();
1622            }
1623        }
1624        unsafe {
1625            let nv = &*nodes.get();
1626            let val = nv[snk_idx]
1627                .input_port(0)
1628                .unwrap()
1629                .signal_buffer()
1630                .as_array()[0];
1631            assert!(val != 0.0, "signal should have propagated, got {}", val);
1632        }
1633    }
1634
1635    #[test]
1636    #[allow(unsafe_code)]
1637    fn test_graph_source_proc_sink() {
1638        let factory = test_factory::<BUF>();
1639        let mut builder = test_builder::<BUF>(&factory);
1640        let system = test_system();
1641
1642        let mut params = test_params(44100.0);
1643        params.insert("value".to_string(), ParamValue::Float(5.0));
1644        let src_idx = builder.add_node("test/const", &params);
1645
1646        let mut gain_params = test_params(44100.0);
1647        gain_params.insert("gain".to_string(), ParamValue::Float(3.0));
1648        let proc_idx = builder.add_node("test/gain", &gain_params);
1649
1650        let snk_idx = builder.add_node("test/capture", &test_params(44100.0));
1651
1652        builder.connect_signal(src_idx, 0, proc_idx, 0);
1653        builder.connect_signal(proc_idx, 0, snk_idx, 0);
1654
1655        let graph = builder.build(&system).unwrap();
1656        let source_idx = graph
1657            .recording_roots
1658            .first()
1659            .or(graph.playback_roots.first())
1660            .copied()
1661            .unwrap_or(0);
1662
1663        eprintln!("topo: {:?}", graph.topo_order);
1664        eprintln!("source_idx: {source_idx}, src_idx: {src_idx}, proc_idx: {proc_idx}, snk_idx: {snk_idx}");
1665
1666        let ctx = RenderContext::new(0, BUF as u32, 44100.0);
1667        let tick = ClockTick::new(0, BUF as u32, 44100.0, String::new());
1668        let nodes = graph.nodes.clone();
1669        unsafe {
1670            let nv = &mut *nodes.get();
1671            eprintln!(
1672                "node types: src={:?}, proc={:?}, snk={:?}",
1673                std::mem::discriminant(&nv[0]),
1674                std::mem::discriminant(&nv[1]),
1675                std::mem::discriminant(&nv[2]),
1676            );
1677
1678            let _ = nv[source_idx].process_block(&ctx, &tick);
1679            let src_val = nv[source_idx].output_port(0).unwrap().read()[0];
1680            eprintln!("source output: {src_val}");
1681
1682            let out_port = nv[source_idx].output_port(0).unwrap();
1683            eprintln!(
1684                "source output port downstream_nodes: {}",
1685                out_port.downstream_nodes().len()
1686            );
1687            eprintln!(
1688                "source output port downstream_input_ptrs: {}",
1689                out_port.downstream_input_ptrs().len()
1690            );
1691
1692            // Check processor output port connections BEFORE propagate
1693            {
1694                let proc_port = nv[proc_idx].output_port(0).unwrap();
1695                eprintln!(
1696                    "PROC OUT port downstream_nodes: {}",
1697                    proc_port.downstream_nodes().len()
1698                );
1699                eprintln!(
1700                    "PROC OUT port downstream_input_ptrs: {}",
1701                    proc_port.downstream_input_ptrs().len()
1702                );
1703                for (i, &dn) in proc_port.downstream().iter().enumerate() {
1704                    eprintln!("  downstream[{}]: (node={}, port={})", i, dn.0, dn.1);
1705                }
1706            }
1707
1708            // --- BUFFER ADDRESS DEBUG ---
1709            let src_out = nv[source_idx].output_port(0).unwrap();
1710            let proc_in = nv[proc_idx].input_port(0).unwrap();
1711            let proc_out = nv[proc_idx].output_port(0).unwrap();
1712            let snk_in = nv[snk_idx].input_port(0).unwrap();
1713            eprintln!("BUFFER ADDRESSES:");
1714            eprintln!("  src output buf:  {:p}", src_out.read().as_ptr());
1715            eprintln!("  proc input buf:  {:p}", proc_in.read().as_ptr());
1716            eprintln!("  proc output buf: {:p}", proc_out.read().as_ptr());
1717            eprintln!("  snk input buf:   {:p}", snk_in.read().as_ptr());
1718            eprintln!(
1719                "  proc_in.has_upstream_buffer(): {}",
1720                proc_in.has_upstream_buffer()
1721            );
1722            eprintln!(
1723                "  snk_in.has_upstream_buffer(): {}",
1724                snk_in.has_upstream_buffer()
1725            );
1726            // --- END DEBUG ---
1727
1728            out_port.propagate(&ctx, &tick).unwrap();
1729
1730            // --- AFTER PROPAGATE: debug buffer values ---
1731            {
1732                let nv = &*nodes.get();
1733                let snk_in = nv[snk_idx].input_port(0).unwrap();
1734                eprintln!("AFTER propagate - snk input buf[0]: {}", snk_in.read()[0]);
1735            }
1736
1737            let sink_buf = nv[snk_idx]
1738                .input_port(0)
1739                .unwrap()
1740                .signal_buffer()
1741                .as_array();
1742            eprintln!("SINK input port buffer first sample: {}", sink_buf[0]);
1743
1744            // Check processor output port propagation
1745            let proc_out_port = nv[proc_idx].output_port(0).unwrap();
1746            eprintln!(
1747                "proc output port downstream_nodes: {}",
1748                proc_out_port.downstream_nodes().len()
1749            );
1750            eprintln!(
1751                "proc output port downstream_input_ptrs: {}",
1752                proc_out_port.downstream_input_ptrs().len()
1753            );
1754
1755            // Sink
1756            let sink_val = nv[snk_idx]
1757                .input_port(0)
1758                .unwrap()
1759                .signal_buffer()
1760                .as_array()[0];
1761            eprintln!("sink input AFTER propagate: {sink_val}");
1762
1763            assert!(
1764                (sink_val - 15.0).abs() < 1e-4,
1765                "expected 15.0, got {}",
1766                sink_val
1767            );
1768        }
1769    }
1770
1771    /// A feedback-branch node (feeds a feedback edge but does not reach the
1772    /// sink) must be processed every block in split mode via `p_process_branch`.
1773    /// Before the fix these side-branch nodes were never run, so their
1774    /// `snapshot_feedback` never fired and feedback loops stayed silent.
1775    #[test]
1776    #[allow(unsafe_code)]
1777    fn test_split_processes_feedback_branch() {
1778        let mut f = NodeFactory::<f32, BUF>::new();
1779        f.register_fn("rill/input", |id, params| {
1780            let mut n = ConstantSource::<f32, BUF>::new(id, 0.0, params.sample_rate);
1781            n.init(params.sample_rate);
1782            NodeVariant::Source(Box::new(n))
1783        });
1784        f.register_fn("test/const", |id, params| {
1785            let v = params.get_f32("value", 1.0);
1786            let mut n = ConstantSource::<f32, BUF>::new(id, v, params.sample_rate);
1787            n.init(params.sample_rate);
1788            NodeVariant::Source(Box::new(n))
1789        });
1790        f.register_fn("test/gain", |id, params| {
1791            let g = params.get_f32("gain", 1.0);
1792            let mut n = GainProcessor::<f32, BUF>::new(id, params.sample_rate, g);
1793            n.init(params.sample_rate);
1794            NodeVariant::Processor(Box::new(n))
1795        });
1796        f.register_fn("test/capture", |id, params| {
1797            let mut n = CaptureSink::<f32, BUF>::new(id, params.sample_rate);
1798            n.init(params.sample_rate);
1799            NodeVariant::Sink(Box::new(n))
1800        });
1801        let factory = Arc::new(f);
1802        let mut builder = test_builder::<BUF>(&factory);
1803        let system = test_system();
1804
1805        let rec_in = builder.add_node("rill/input", &test_params(44100.0)); // 0 (recording root)
1806        let mut cparams = test_params(44100.0);
1807        cparams.insert("value", ParamValue::Float(2.0));
1808        let play = builder.add_node("test/const", &cparams); // 1 (playback root)
1809        let sink = builder.add_node("test/capture", &test_params(44100.0)); // 2
1810        let branch = builder.add_node("test/gain", &test_params(44100.0)); // 3 (feedback branch)
1811        let rec_proc = builder.add_node("test/gain", &test_params(44100.0)); // 4 (recording)
1812
1813        builder.connect_signal(play, 0, sink, 0); // playback: const -> sink
1814        builder.connect_signal(play, 0, branch, 0); // branch: const -> gain(branch)
1815        builder.connect_signal(rec_in, 0, rec_proc, 0); // recording: input -> gain
1816        builder.connect_feedback(branch, 0, rec_proc, 0); // branch -> recording (feedback)
1817
1818        let graph = builder.build(&system).unwrap();
1819        assert_eq!(
1820            graph.feedback_ptrs.len(),
1821            1,
1822            "the feedback-branch node must be detected at build time"
1823        );
1824
1825        let ctx = RenderContext::new(0, BUF as u32, 44100.0);
1826        for i in 0..3u64 {
1827            let tick = ClockTick::new(i * BUF as u64, BUF as u32, 44100.0, String::new());
1828            p_forward(&graph.recording_ptrs, &ctx, &tick);
1829            p_pull(graph.sink_ptr, &ctx, &tick);
1830            p_process_branch(&graph.feedback_ptrs, &ctx, &tick);
1831        }
1832
1833        unsafe {
1834            let nv = &*graph.nodes.get();
1835            let branch_out = nv[branch].output_port(0).unwrap().read()[0];
1836            assert!(
1837                (branch_out - 2.0).abs() < 1e-4,
1838                "feedback-branch node was not processed (out={branch_out}, expected 2.0)"
1839            );
1840        }
1841    }
1842}