Skip to main content

firewheel_graph/graph/
compiler.rs

1use alloc::{collections::VecDeque, rc::Rc};
2use firewheel_core::node::{AudioNodeInfoInner, DynAudioNode, NodeID};
3use smallvec::SmallVec;
4use thunderdome::Arena;
5
6#[cfg(not(feature = "std"))]
7use bevy_platform::prelude::{Box, Vec, vec};
8
9use crate::error::CompileGraphError;
10
11mod schedule;
12
13pub(crate) use schedule::{CompiledSchedule, NodeHeapData, ProcessNodeInfo, ScheduleHeapData};
14use schedule::{InBufferAssignment, OutBufferAssignment, PreProcNode, ScheduledNode};
15
16pub struct NodeEntry {
17    pub id: NodeID,
18    pub info: AudioNodeInfoInner,
19    /// In order to be compatible with nodes hosting CLAP plugins,
20    /// this field must remain !Send
21    pub dyn_node: Box<dyn DynAudioNode>,
22    pub processor_constructed: bool,
23    /// The edges connected to this node's input ports.
24    incoming: SmallVec<[Edge; 4]>,
25    /// The edges connected to this node's output ports.
26    outgoing: SmallVec<[Edge; 4]>,
27}
28
29impl NodeEntry {
30    pub fn new(mut info: AudioNodeInfoInner, dyn_node: Box<dyn DynAudioNode>) -> Self {
31        if info.channel_config.num_outputs.get() == 0 {
32            info.in_place_buffers = false;
33        }
34
35        Self {
36            id: NodeID::DANGLING,
37            info,
38            dyn_node,
39            processor_constructed: false,
40            incoming: SmallVec::new(),
41            outgoing: SmallVec::new(),
42        }
43    }
44}
45
46/// The index of an input/output port on a particular node.
47pub type PortIdx = u32;
48
49/// A globally unique identifier for an [Edge].
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct EdgeID(pub(super) thunderdome::Index);
52
53/// An [Edge] is a connection from source node and port to a
54/// destination node and port.
55#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
56pub struct Edge {
57    pub id: EdgeID,
58    /// The ID of the source node used by this edge.
59    pub src_node: NodeID,
60    /// The ID of the source port used by this edge.
61    pub src_port: PortIdx,
62    /// The ID of the destination node used by this edge.
63    pub dst_node: NodeID,
64    /// The ID of the destination port used by this edge.
65    pub dst_port: PortIdx,
66}
67
68/// A reference to an abstract buffer during buffer allocation.
69#[derive(Debug, Clone, Copy)]
70struct BufferRef {
71    /// The index of the buffer
72    idx: usize,
73    /// The generation, or the nth time this buffer has
74    /// been assigned to a different edge in the graph.
75    generation: usize,
76}
77
78/// An allocator for managing and reusing [BufferRef]s.
79#[derive(Debug, Clone)]
80struct BufferAllocator {
81    /// A list of free buffers that may be reallocated
82    free_list: Vec<BufferRef>,
83    /// The maximum number of buffers used
84    count: usize,
85}
86
87impl BufferAllocator {
88    /// Create a new allocator, `num_types` defines the number
89    /// of buffer types we may allocate.
90    fn new(initial_capacity: usize) -> Self {
91        Self {
92            free_list: Vec::with_capacity(initial_capacity),
93            count: 0,
94        }
95    }
96
97    /// Acquire a new buffer
98    fn acquire(&mut self) -> Rc<BufferRef> {
99        let entry = self.free_list.pop().unwrap_or_else(|| {
100            let idx = self.count;
101            self.count += 1;
102            BufferRef { idx, generation: 0 }
103        });
104        Rc::new(BufferRef {
105            idx: entry.idx,
106            generation: entry.generation,
107        })
108    }
109
110    /// Release a BufferRef
111    fn release(&mut self, buffer_ref: Rc<BufferRef>) {
112        if Rc::strong_count(&buffer_ref) == 1 {
113            self.free_list.push(BufferRef {
114                idx: buffer_ref.idx,
115                generation: buffer_ref.generation + 1,
116            });
117        }
118    }
119
120    /// Consume the allocator to return the maximum number of buffers used
121    fn num_buffers(self) -> usize {
122        self.count
123    }
124}
125
126/// Main compilation algorithm
127pub fn compile(
128    nodes: &mut Arena<NodeEntry>,
129    edges: &mut Arena<Edge>,
130    graph_in_id: NodeID,
131    graph_out_id: NodeID,
132    max_block_frames: usize,
133    prev_buffer_capacity: usize,
134) -> Result<CompiledSchedule, CompileGraphError> {
135    Ok(GraphIR::preprocess(
136        nodes,
137        edges,
138        graph_in_id,
139        graph_out_id,
140        max_block_frames,
141        prev_buffer_capacity,
142    )
143    .sort_topologically(true)?
144    .solve_buffer_requirements()?
145    .merge())
146}
147
148pub fn cycle_detected<'a>(
149    nodes: &'a mut Arena<NodeEntry>,
150    edges: &'a mut Arena<Edge>,
151    graph_in_id: NodeID,
152    graph_out_id: NodeID,
153) -> bool {
154    matches!(
155        GraphIR::preprocess(nodes, edges, graph_in_id, graph_out_id, 0, 0)
156            .sort_topologically(false),
157        Err(CompileGraphError::CycleDetected)
158    )
159}
160
161/// Internal IR used by the compiler algorithm. Built incrementally
162/// via the compiler passes.
163struct GraphIR<'a> {
164    nodes: &'a mut Arena<NodeEntry>,
165    edges: &'a mut Arena<Edge>,
166
167    /// Nodes with zero inputs and outputs are "pre process nodes" that get
168    /// processed before all other nodes.
169    pre_proc_nodes: Vec<PreProcNode>,
170    /// The topologically sorted schedule of the graph. Built internally.
171    schedule: Vec<ScheduledNode>,
172    /// The maximum number of buffers used.
173    max_num_buffers: usize,
174
175    graph_in_id: NodeID,
176    graph_out_id: NodeID,
177    max_in_buffers: usize,
178    max_out_buffers: usize,
179    max_block_frames: usize,
180
181    prev_buffer_capacity: usize,
182}
183
184impl<'a> GraphIR<'a> {
185    /// Construct a [GraphIR] instance from lists of nodes and edges, building
186    /// up the adjacency table and creating an empty schedule.
187    fn preprocess(
188        nodes: &'a mut Arena<NodeEntry>,
189        edges: &'a mut Arena<Edge>,
190        graph_in_id: NodeID,
191        graph_out_id: NodeID,
192        max_block_frames: usize,
193        prev_buffer_capacity: usize,
194    ) -> Self {
195        assert!(nodes.contains(graph_in_id.0));
196        assert!(nodes.contains(graph_out_id.0));
197
198        for (_, node) in nodes.iter_mut() {
199            node.incoming.clear();
200            node.outgoing.clear();
201        }
202
203        for (_, edge) in edges.iter() {
204            nodes[edge.src_node.0].outgoing.push(*edge);
205            nodes[edge.dst_node.0].incoming.push(*edge);
206
207            debug_assert_ne!(edge.src_node, graph_out_id);
208            debug_assert_ne!(edge.dst_node, graph_in_id);
209        }
210
211        Self {
212            nodes,
213            edges,
214            pre_proc_nodes: vec![],
215            schedule: vec![],
216            max_num_buffers: 0,
217            graph_in_id,
218            graph_out_id,
219            max_in_buffers: 0,
220            max_out_buffers: 0,
221            max_block_frames,
222            prev_buffer_capacity,
223        }
224    }
225
226    /// Sort the nodes topologically using Kahn's algorithm.
227    /// <https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/>
228    fn sort_topologically(mut self, build_schedule: bool) -> Result<Self, CompileGraphError> {
229        let mut in_degree = vec![0i32; self.nodes.capacity()];
230        let mut queue = VecDeque::with_capacity(self.nodes.len());
231
232        if build_schedule {
233            self.schedule.reserve(self.nodes.len());
234        }
235
236        let mut num_visited = 0;
237
238        // Calculate in-degree of each vertex
239        for (_, node_entry) in self.nodes.iter() {
240            for edge in node_entry.outgoing.iter() {
241                in_degree[edge.dst_node.0.slot() as usize] += 1;
242            }
243        }
244
245        // Make sure that the graph in node is the first entry in the
246        // schedule. Otherwise a different root node could overwrite
247        // the buffers assigned to the graph in node.
248        queue.push_back(self.graph_in_id.0.slot());
249
250        // Enqueue all other nodes with 0 in-degree
251        for (_, node_entry) in self.nodes.iter() {
252            if node_entry.incoming.is_empty() && node_entry.id.0.slot() != self.graph_in_id.0.slot()
253            {
254                // If the number of inputs and outputs on a node is zero, then it
255                // is a "pre process" node.
256                if node_entry.info.channel_config.is_empty() {
257                    self.pre_proc_nodes.push(PreProcNode {
258                        id: node_entry.id,
259                        debug_name: node_entry.info.debug_name,
260                    });
261
262                    num_visited += 1;
263                } else {
264                    queue.push_back(node_entry.id.0.slot());
265                }
266            }
267        }
268
269        // BFS traversal
270        while let Some(node_slot) = queue.pop_front() {
271            num_visited += 1;
272
273            let (_, node_entry) = self.nodes.get_by_slot(node_slot).unwrap();
274
275            // Reduce in-degree of adjacent nodes
276            for edge in node_entry.outgoing.iter() {
277                in_degree[edge.dst_node.0.slot() as usize] -= 1;
278
279                // If in-degree becomes 0, enqueue it
280                if in_degree[edge.dst_node.0.slot() as usize] == 0 {
281                    queue.push_back(edge.dst_node.0.slot());
282                }
283            }
284
285            if build_schedule && node_slot != self.graph_out_id.0.slot() {
286                self.schedule.push(ScheduledNode::new(
287                    node_entry.id,
288                    node_entry.info.debug_name,
289                    node_entry.info.in_place_buffers,
290                ));
291            }
292        }
293
294        if build_schedule {
295            // Make sure that the graph out node is the last entry in the
296            // schedule by waiting to push it after all other nodes have
297            // been pushed. Otherwise a different leaf node could overwrite
298            // the buffers assigned to the graph out node.
299            self.schedule
300                .push(ScheduledNode::new(self.graph_out_id, "graph_out", false));
301        }
302
303        // If not all vertices are visited, cycle
304        if num_visited != self.nodes.len() {
305            return Err(CompileGraphError::CycleDetected);
306        }
307
308        Ok(self)
309    }
310
311    fn solve_buffer_requirements(mut self) -> Result<Self, CompileGraphError> {
312        let mut allocator = BufferAllocator::new(64);
313        let mut assignment_table: Arena<Rc<BufferRef>> =
314            Arena::with_capacity(self.edges.capacity());
315        let mut buffers_to_release: Vec<Rc<BufferRef>> = Vec::with_capacity(64);
316
317        for entry in &mut self.schedule {
318            // Collect the inputs to the algorithm, the incoming/outgoing edges of this node.
319
320            let node_entry = &self.nodes[entry.id.0];
321
322            let num_inputs = node_entry.info.channel_config.num_inputs.get() as usize;
323            let num_outputs = node_entry.info.channel_config.num_outputs.get() as usize;
324
325            buffers_to_release.clear();
326            if buffers_to_release.capacity() < num_inputs + num_outputs {
327                buffers_to_release
328                    .reserve(num_inputs + num_outputs - buffers_to_release.capacity());
329            }
330
331            entry.input_buffers.reserve_exact(num_inputs);
332            entry.output_buffers.reserve_exact(num_outputs);
333
334            for port_idx in 0..num_inputs as u32 {
335                let edges: SmallVec<[&Edge; 4]> = node_entry
336                    .incoming
337                    .iter()
338                    .filter(|edge| edge.dst_port == port_idx)
339                    .collect();
340
341                entry
342                    .in_connected_mask
343                    .set_channel(port_idx as usize, !edges.is_empty());
344
345                if edges.is_empty() {
346                    // Case 1: The port is an input and it is unconnected. Acquire a buffer, and
347                    //         assign it. The buffer must be cleared. Release the buffer once the
348                    //         node assignments are done.
349                    let buffer = allocator.acquire();
350                    entry.input_buffers.push(InBufferAssignment {
351                        buffer_index: buffer.idx,
352                        //generation: buffer.generation,
353                        should_clear: true,
354                    });
355                    buffers_to_release.push(buffer);
356                } else if edges.len() == 1 {
357                    // Case 2: The port is an input, and has exactly one incoming edge. Lookup the
358                    //         corresponding buffer and assign it. Buffer should not be cleared.
359                    //         Release the buffer once the node assignments are done.
360                    let buffer = assignment_table
361                        .remove(edges[0].id.0)
362                        .expect("No buffer assigned to edge!");
363                    entry.input_buffers.push(InBufferAssignment {
364                        buffer_index: buffer.idx,
365                        //generation: buffer.generation,
366                        should_clear: false,
367                    });
368                    buffers_to_release.push(buffer);
369                } else {
370                    // Case 3: The port is an input with multiple incoming edges. Compute the
371                    //         summing point, and assign the input buffer assignment to the output
372                    //         of the summing point.
373
374                    let sum_buffer = allocator.acquire();
375                    let sum_output = OutBufferAssignment {
376                        buffer_index: sum_buffer.idx,
377                        //generation: sum_buffer.generation,
378                    };
379
380                    // The sum inputs are the corresponding output buffers of the incoming edges.
381                    let sum_inputs = edges
382                        .iter()
383                        .map(|edge| {
384                            let buf = assignment_table
385                                .remove(edge.id.0)
386                                .expect("No buffer assigned to edge!");
387                            let assignment = InBufferAssignment {
388                                buffer_index: buf.idx,
389                                //generation: buf.generation,
390                                should_clear: false,
391                            };
392                            allocator.release(buf);
393                            assignment
394                        })
395                        .collect();
396
397                    entry.sum_inputs.push(InsertedSum {
398                        input_buffers: sum_inputs,
399                        output_buffer: sum_output,
400                    });
401
402                    // This node's input buffer is the sum output buffer. Release it once the node
403                    // assignments are done.
404                    entry.input_buffers.push(InBufferAssignment {
405                        buffer_index: sum_output.buffer_index,
406                        //generation: sum_output.generation,
407                        should_clear: false,
408                    });
409
410                    buffers_to_release.push(sum_buffer);
411                }
412            }
413
414            for port_idx in 0..num_outputs as u32 {
415                let edges: SmallVec<[&Edge; 4]> = node_entry
416                    .outgoing
417                    .iter()
418                    .filter(|edge| edge.src_port == port_idx)
419                    .collect();
420
421                entry
422                    .out_connected_mask
423                    .set_channel(port_idx as usize, !edges.is_empty());
424
425                if edges.is_empty() {
426                    // Case 1: The port is an output and it is unconnected. Acquire a buffer and
427                    //         assign it. The buffer does not need to be cleared. Release the
428                    //         buffer once the node assignments are done.
429                    let buffer = allocator.acquire();
430                    entry.output_buffers.push(OutBufferAssignment {
431                        buffer_index: buffer.idx,
432                        //generation: buffer.generation,
433                    });
434                    buffers_to_release.push(buffer);
435                } else {
436                    // Case 2: The port is an output. Acquire a buffer, and add to the assignment
437                    //         table with any corresponding edge IDs. For each edge, update the
438                    //         assigned buffer table. Buffer should not be cleared or released.
439                    let buffer = allocator.acquire();
440                    for edge in &edges {
441                        assignment_table.insert_at(edge.id.0, Rc::clone(&buffer));
442                    }
443                    entry.output_buffers.push(OutBufferAssignment {
444                        buffer_index: buffer.idx,
445                        //generation: buffer.generation,
446                    });
447                }
448            }
449
450            for buffer in buffers_to_release.drain(..) {
451                allocator.release(buffer);
452            }
453
454            self.max_in_buffers = self.max_in_buffers.max(num_inputs);
455            self.max_out_buffers = self.max_out_buffers.max(num_outputs);
456        }
457
458        self.max_num_buffers = allocator.num_buffers();
459        Ok(self)
460    }
461
462    /// Merge the GraphIR into a [CompiledSchedule].
463    fn merge(self) -> CompiledSchedule {
464        CompiledSchedule::new(
465            self.pre_proc_nodes,
466            self.schedule,
467            self.max_num_buffers,
468            self.max_out_buffers,
469            self.max_block_frames,
470            self.graph_in_id,
471            self.prev_buffer_capacity,
472        )
473    }
474}
475
476#[derive(Debug, Clone)]
477struct InsertedSum {
478    input_buffers: SmallVec<[InBufferAssignment; 4]>,
479    output_buffer: OutBufferAssignment,
480}