Skip to main content

rill_lang/
graph_compiler.rs

1//! Compile a GraphIr into a CompiledGraph for zero-allocation execution.
2
3use std::collections::HashMap;
4
5use rill_core::buffer::{Buffer, FixedBuffer};
6use rill_core::math::Transcendental;
7use rill_core::traits::{Algorithm, MultichannelAlgorithm, ProcessResult};
8
9use crate::builtin::Registry;
10use crate::graph_ir::{EdgeKind, GraphIr};
11
12/// Owned algorithm variants — SISO or MIMO.
13pub enum AlgorithmVariant<T: Transcendental> {
14    /// Single-input, single-output block built-in.
15    Siso(Box<dyn crate::builtin::BlockBuiltin<T>>),
16    /// Multi-input, multi-output block built-in.
17    Mimo(Box<dyn rill_core::builtin::MultichannelBlockBuiltin<T>>),
18}
19
20/// One compiled graph node — owns its algorithm and buffer routing.
21pub struct NodeClosure<T: Transcendental, const BUF_SIZE: usize> {
22    pub(crate) algo: AlgorithmVariant<T>,
23    input_indices: Vec<usize>,
24    output_indices: Vec<usize>,
25    /// Pre-allocated, cleared+re-filled each tick — zero allocation.
26    input_slices: Vec<&'static [T]>,
27    output_slices: Vec<&'static mut [T]>,
28}
29
30impl<T: Transcendental, const BUF_SIZE: usize> NodeClosure<T, BUF_SIZE> {
31    /// Execute this node's algorithm, reading from and writing to the buffer pool.
32    #[allow(unsafe_code)]
33    pub fn execute(&mut self, buffers: &mut [FixedBuffer<T, BUF_SIZE>]) -> ProcessResult<()> {
34        match &mut self.algo {
35            AlgorithmVariant::Siso(algo) => {
36                let bufs_ptr = buffers.as_mut_ptr();
37                let input = if self.input_indices.is_empty() {
38                    None
39                } else {
40                    let in_buf = unsafe { &*bufs_ptr.add(self.input_indices[0]) };
41                    Some(in_buf.as_slice())
42                };
43                let out_buf = unsafe { &mut *bufs_ptr.add(self.output_indices[0]) };
44                Algorithm::process(algo.as_mut(), input, out_buf.as_mut_slice())?;
45            }
46            AlgorithmVariant::Mimo(algo) => {
47                let bufs_ptr = buffers.as_mut_ptr();
48                self.input_slices.clear();
49                for &idx in &self.input_indices {
50                    let buf = unsafe { &*bufs_ptr.add(idx) };
51                    self.input_slices
52                        .push(unsafe { std::mem::transmute::<&[T], &'static [T]>(buf.as_slice()) });
53                }
54                self.output_slices.clear();
55                for &idx in &self.output_indices {
56                    let buf = unsafe { &mut *bufs_ptr.add(idx) };
57                    self.output_slices.push(unsafe {
58                        std::mem::transmute::<&mut [T], &'static mut [T]>(buf.as_mut_slice())
59                    });
60                }
61                MultichannelAlgorithm::process(
62                    algo.as_mut(),
63                    &self.input_slices,
64                    &mut self.output_slices,
65                )?;
66            }
67        }
68        Ok(())
69    }
70
71    /// Set a parameter by index on the owned algorithm.
72    pub fn set_param(&mut self, index: usize, value: &rill_core::traits::ParamValue) {
73        match &mut self.algo {
74            AlgorithmVariant::Siso(algo) => algo.set_param(index, value),
75            AlgorithmVariant::Mimo(algo) => algo.set_param(index, value),
76        }
77    }
78
79    /// Reset the owned algorithm to its initial state.
80    pub fn reset(&mut self) {
81        match &mut self.algo {
82            AlgorithmVariant::Siso(algo) => Algorithm::reset(algo.as_mut()),
83            AlgorithmVariant::Mimo(algo) => MultichannelAlgorithm::reset(algo.as_mut()),
84        }
85    }
86}
87
88/// A compiled graph ready for zero-allocation execution.
89pub struct CompiledGraph<T: Transcendental, const BUF_SIZE: usize> {
90    /// Pre-allocated buffer pool (fixed-size, stack-friendly).
91    pub buffers: Vec<FixedBuffer<T, BUF_SIZE>>,
92    /// Compiled node closures in topological order.
93    pub nodes: Vec<NodeClosure<T, BUF_SIZE>>,
94    /// Number of graph input channels.
95    pub inputs: usize,
96    /// Number of graph output channels.
97    pub outputs: usize,
98    /// Buffer indices mapping to graph output channels.
99    pub output_mapping: Vec<usize>,
100    /// Node names in topological order (for anchor-based param routing).
101    pub node_names: Vec<String>,
102    /// Per-node parameter name → index mappings.
103    pub node_param_names: Vec<Vec<String>>,
104}
105
106/// Compile a GraphIr into a CompiledGraph.
107pub fn compile<T: Transcendental, const BUF_SIZE: usize>(
108    ir: &GraphIr,
109    registry: &Registry<T>,
110    sample_rate: f32,
111) -> Result<CompiledGraph<T, BUF_SIZE>, String> {
112    // 1. Build edge buffer mapping (same zero-copy sharing as current graph_lower)
113    let mut edge_buffers: HashMap<(String, usize, String, usize), usize> = HashMap::new();
114    let mut buffer_counter: usize = ir.inputs;
115    let mut output_bufs_per_node: Vec<Vec<usize>> = Vec::new();
116
117    for name in &ir.topo_order {
118        let node = ir.nodes.get(name).unwrap();
119        let mut output_bufs = Vec::new();
120        for _port in 0..node.arity.1 {
121            let buf = buffer_counter;
122            buffer_counter += 1;
123            output_bufs.push(buf);
124        }
125        output_bufs_per_node.push(output_bufs.clone());
126
127        for edge in &ir.edges {
128            if edge.from_node == *name && edge.kind == EdgeKind::Signal {
129                edge_buffers.insert(
130                    (
131                        edge.from_node.clone(),
132                        edge.from_port,
133                        edge.to_node.clone(),
134                        edge.to_port,
135                    ),
136                    output_bufs[edge.from_port],
137                );
138            }
139        }
140    }
141
142    let n_bufs = buffer_counter;
143
144    // 2. Build NodeClosures
145    let mut nodes: Vec<NodeClosure<T, BUF_SIZE>> = Vec::new();
146    let mut node_names: Vec<String> = Vec::new();
147    let mut node_param_names_sets: Vec<Vec<String>> = Vec::new();
148
149    for (idx, name) in ir.topo_order.iter().enumerate() {
150        let node = ir.nodes.get(name).unwrap();
151
152        let mut input_bufs: Vec<usize> = Vec::new();
153        for edge in &ir.edges {
154            if edge.to_node == *name && edge.kind == EdgeKind::Signal {
155                let key = (
156                    edge.from_node.clone(),
157                    edge.from_port,
158                    edge.to_node.clone(),
159                    edge.to_port,
160                );
161                if let Some(&buf) = edge_buffers.get(&key) {
162                    if input_bufs.len() <= edge.to_port {
163                        input_bufs.resize(edge.to_port + 1, 0);
164                    }
165                    input_bufs[edge.to_port] = buf;
166                }
167            }
168        }
169
170        if input_bufs.is_empty() && idx < ir.inputs {
171            for port in 0..node.arity.0.min(ir.inputs) {
172                input_bufs.push(port);
173            }
174        }
175
176        let output_bufs = output_bufs_per_node[idx].clone();
177        let n_in = input_bufs.len();
178        let n_out = output_bufs.len();
179
180        let param_names: Vec<String> = node.ir.params.iter().map(|p| p.name.clone()).collect();
181        node_param_names_sets.push(param_names);
182
183        let algo = if n_in <= 1 && n_out == 1 {
184            let prog =
185                crate::program::RillProgram::<T>::new_with(node.ir.clone(), registry, sample_rate)
186                    .map_err(|e| format!("program creation: {e}"))?;
187            AlgorithmVariant::Siso(Box::new(prog))
188        } else {
189            let bi = node
190                .ir
191                .builtins
192                .first()
193                .ok_or_else(|| format!("MIMO node '{}' has no builtins", name))?;
194            let entry = registry
195                .get(&bi.name)
196                .ok_or_else(|| format!("unknown builtin: {}", bi.name))?;
197
198            let mimo = entry
199                .build_multichannel_block(&bi.params, sample_rate)
200                .ok_or_else(|| {
201                    format!(
202                        "failed to build multichannel block: {}. Is it registered via register_multichannel_block?",
203                        bi.name
204                    )
205                })?;
206            AlgorithmVariant::Mimo(mimo)
207        };
208
209        nodes.push(NodeClosure {
210            algo,
211            input_indices: input_bufs,
212            output_indices: output_bufs,
213            input_slices: Vec::with_capacity(n_in),
214            output_slices: Vec::with_capacity(n_out),
215        });
216        node_names.push(name.clone());
217    }
218
219    // 3. Build output mapping (leaf nodes -> graph outputs)
220    let mut output_mapping = Vec::new();
221    for name in &ir.topo_order {
222        let is_leaf = !ir
223            .edges
224            .iter()
225            .any(|e| e.from_node == *name && e.kind == EdgeKind::Signal);
226        if is_leaf {
227            let idx = ir.topo_order.iter().position(|n| n == name).unwrap();
228            for &buf in &output_bufs_per_node[idx] {
229                output_mapping.push(buf);
230            }
231        }
232    }
233
234    let buffers = vec![FixedBuffer::<T, BUF_SIZE>::new(); n_bufs];
235
236    Ok(CompiledGraph {
237        buffers,
238        nodes,
239        inputs: ir.inputs,
240        outputs: ir.outputs,
241        output_mapping,
242        node_names,
243        node_param_names: node_param_names_sets,
244    })
245}