hexodsp/nodes/
node_prog.rs

1// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
2// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
3// See README.md and COPYING for details.
4
5use crate::dsp::{Node, ProcBuf, SAtom};
6use std::cell::RefCell;
7use triple_buffer::{Input, Output, TripleBuffer};
8
9thread_local! {
10    pub static NODE_PROG_ID_COUNTER: RefCell<usize> = RefCell::new(1);
11}
12
13#[derive(Debug, Clone)]
14pub struct ModOp {
15    amount: f32,
16    modbuf: ProcBuf,
17    outbuf: ProcBuf,
18    inbuf: ProcBuf,
19}
20
21impl Drop for ModOp {
22    fn drop(&mut self) {
23        self.modbuf.free();
24    }
25}
26
27impl ModOp {
28    pub fn new() -> Self {
29        Self {
30            amount: 0.0,
31            modbuf: ProcBuf::new(),
32            outbuf: ProcBuf::null(),
33            inbuf: ProcBuf::null(),
34        }
35    }
36
37    pub fn set_amt(&mut self, amt: f32) {
38        self.amount = amt;
39    }
40
41    pub fn lock(&mut self, inbuf: ProcBuf, outbuf: ProcBuf) -> ProcBuf {
42        self.inbuf = inbuf;
43        self.outbuf = outbuf;
44        self.modbuf
45    }
46
47    pub fn unlock(&mut self) {
48        self.outbuf = ProcBuf::null();
49        self.inbuf = ProcBuf::null();
50    }
51
52    #[inline]
53    pub fn process(&mut self, nframes: usize) {
54        let modbuf = &mut self.modbuf;
55        let inbuf = &mut self.inbuf;
56        let outbuf = &mut self.outbuf;
57
58        if inbuf.is_null() {
59            return;
60        }
61
62        for frame in 0..nframes {
63            modbuf.write(frame, inbuf.read(frame) + (self.amount * outbuf.read(frame)));
64        }
65    }
66}
67
68/// Step in a `NodeProg` that stores the to be
69/// executed node and output operations.
70#[derive(Debug, Clone)]
71pub struct NodeOp {
72    /// Stores the index of the node
73    pub idx: u8,
74    /// Stores the reference to the node.
75    pub node: Node,
76    /// Output index and length of the node:
77    pub out_idxlen: (usize, usize),
78    /// Input index and length of the node:
79    pub in_idxlen: (usize, usize),
80    /// Atom data index and length of the node:
81    pub at_idxlen: (usize, usize),
82    /// ModOp index and length of the node:
83    pub mod_idxlen: (usize, usize),
84    /// Input indices,
85    /// (<out vec index>, <own node input index>,
86    ///  (<mod index into NodeProg::modops>, <mod amt>))
87    pub inputs: Vec<(usize, usize, Option<usize>)>,
88    /// A bit mask which indicates which of the input ports are actually
89    /// used/connected to some output.
90    pub in_connected: u64,
91    /// A bit mask which indicates which of the output ports are actually
92    /// used/connected to some input.
93    pub out_connected: u64,
94}
95
96impl NodeOp {
97    pub fn in_idx_belongs_to_nodeop(&self, idx: usize) -> bool {
98        idx >= self.in_idxlen.0 && idx < self.in_idxlen.1
99    }
100
101    pub fn set_in_idx_connected_flag(&mut self, global_idx: usize) {
102        if !self.in_idx_belongs_to_nodeop(global_idx) {
103            return;
104        }
105
106        let local_idx = global_idx - self.in_idxlen.0;
107        self.in_connected |= 0x1 << local_idx;
108    }
109
110    pub fn out_idx_belongs_to_nodeop(&self, idx: usize) -> bool {
111        idx >= self.out_idxlen.0 && idx < self.out_idxlen.1
112    }
113
114    pub fn set_out_idx_connected_flag(&mut self, global_idx: usize) {
115        if !self.out_idx_belongs_to_nodeop(global_idx) {
116            return;
117        }
118
119        let local_idx = global_idx - self.out_idxlen.0;
120        self.out_connected |= 0x1 << local_idx;
121    }
122}
123
124impl std::fmt::Display for NodeOp {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(
127            f,
128            "Op(i={} out=({}-{}|{:x}) in=({}-{}|{:x}) at=({}-{}) mod=({}-{})",
129            self.idx,
130            self.out_idxlen.0,
131            self.out_idxlen.1,
132            self.out_connected,
133            self.in_idxlen.0,
134            self.in_idxlen.1,
135            self.in_connected,
136            self.at_idxlen.0,
137            self.at_idxlen.1,
138            self.mod_idxlen.0,
139            self.mod_idxlen.1
140        )?;
141
142        for i in self.inputs.iter() {
143            write!(f, " cpy=(o{} => i{})", i.0, i.1)?;
144        }
145
146        for i in self.inputs.iter() {
147            if let Some(idx) = i.2 {
148                write!(f, " mod={}", idx)?;
149            }
150        }
151
152        write!(f, ")")
153    }
154}
155
156/// A node graph execution program. It comes with buffers
157/// for the inputs, outputs and node parameters (knob values).
158#[derive(Debug)]
159pub struct NodeProg {
160    /// The input vector stores the smoothed values of the params.
161    /// It is not used directly, but will be merged into the `cur_inp`
162    /// field together with the assigned outputs.
163    pub inp: Vec<ProcBuf>,
164
165    /// The temporary input vector that is initialized from `inp`
166    /// and is then merged with the associated outputs.
167    pub cur_inp: Vec<ProcBuf>,
168
169    /// The output vector, holding all the node outputs.
170    pub out: Vec<ProcBuf>,
171
172    /// The param vector, holding all parameter inputs of the
173    /// nodes, such as knob settings.
174    pub params: Vec<f32>,
175
176    /// The atom vector, holding all non automatable parameter inputs
177    /// of the nodes, such as samples or integer settings.
178    pub atoms: Vec<SAtom>,
179
180    /// The node operations that are executed in the order they appear in this
181    /// vector.
182    pub prog: Vec<NodeOp>,
183
184    /// The modulators for the input parameters.
185    pub modops: Vec<ModOp>,
186
187    /// A marker, that checks if we can still swap buffers with
188    /// with other NodeProg instances. This is usally set if the ProcBuf pointers
189    /// have been copied into `cur_inp`. You can call `unlock_buffers` to
190    /// clear `locked_buffers`:
191    pub locked_buffers: bool,
192
193    /// Holds the input end of a triple buffer that is used
194    /// to publish the most recent output values to the frontend.
195    pub out_feedback: Input<Vec<f32>>,
196
197    /// Temporary hold for the producer for the `out_feedback`:
198    pub out_fb_cons: Option<Output<Vec<f32>>>,
199
200    /// A unique ID assigned to the node prog. Mostly for debugging purposes.
201    /// You should only read this field.
202    pub unique_id: usize,
203}
204
205impl Drop for NodeProg {
206    fn drop(&mut self) {
207        for buf in self.inp.iter_mut() {
208            buf.free();
209        }
210
211        for buf in self.out.iter_mut() {
212            buf.free();
213        }
214    }
215}
216
217fn new_node_prog_id() -> usize {
218    NODE_PROG_ID_COUNTER.with(|cnt| {
219        let unique_id = *cnt.borrow();
220        *cnt.borrow_mut() += 1;
221        unique_id
222    })
223}
224
225impl NodeProg {
226    pub fn empty() -> Self {
227        let out_fb = vec![];
228        let tb = TripleBuffer::new(out_fb);
229        let (input_fb, output_fb) = tb.split();
230        Self {
231            out: vec![],
232            inp: vec![],
233            cur_inp: vec![],
234            params: vec![],
235            atoms: vec![],
236            prog: vec![],
237            modops: vec![],
238            out_feedback: input_fb,
239            out_fb_cons: Some(output_fb),
240            locked_buffers: false,
241            unique_id: new_node_prog_id(),
242        }
243    }
244
245    pub fn new(out_len: usize, inp_len: usize, at_len: usize, mod_len: usize) -> Self {
246        let mut out = vec![];
247        out.resize_with(out_len, ProcBuf::new);
248
249        let out_fb = vec![0.0; out_len];
250        let tb = TripleBuffer::new(out_fb);
251        let (input_fb, output_fb) = tb.split();
252
253        let mut inp = vec![];
254        inp.resize_with(inp_len, ProcBuf::new);
255        let mut cur_inp = vec![];
256        cur_inp.resize_with(inp_len, ProcBuf::null);
257
258        let mut params = vec![];
259        params.resize(inp_len, 0.0);
260        let mut atoms = vec![];
261        atoms.resize(at_len, SAtom::setting(0));
262        let mut modops = vec![];
263        modops.resize_with(mod_len, ModOp::new);
264
265        Self {
266            out,
267            inp,
268            cur_inp,
269            params,
270            atoms,
271            modops,
272            prog: vec![],
273            out_feedback: input_fb,
274            out_fb_cons: Some(output_fb),
275            locked_buffers: false,
276            unique_id: new_node_prog_id(),
277        }
278    }
279
280    pub fn take_feedback_consumer(&mut self) -> Option<Output<Vec<f32>>> {
281        self.out_fb_cons.take()
282    }
283
284    pub fn params_mut(&mut self) -> &mut [f32] {
285        &mut self.params
286    }
287
288    pub fn atoms_mut(&mut self) -> &mut [SAtom] {
289        &mut self.atoms
290    }
291
292    pub fn modops_mut(&mut self) -> &mut [ModOp] {
293        &mut self.modops
294    }
295
296    pub fn append_op(&mut self, mut node_op: NodeOp) {
297        for n_op in self.prog.iter_mut() {
298            if n_op.idx == node_op.idx {
299                return;
300            }
301        }
302
303        node_op.out_connected = 0x0;
304        node_op.in_connected = 0x0;
305        self.prog.push(node_op);
306    }
307
308    pub fn append_edge(
309        &mut self,
310        node_op: NodeOp,
311        inp_index: usize,
312        out_index: usize,
313        mod_index: Option<usize>,
314    ) {
315        for n_op in self.prog.iter_mut() {
316            if n_op.out_idx_belongs_to_nodeop(out_index) {
317                n_op.set_out_idx_connected_flag(out_index);
318            }
319        }
320
321        for n_op in self.prog.iter_mut() {
322            if n_op.idx == node_op.idx {
323                n_op.set_in_idx_connected_flag(inp_index);
324                n_op.inputs.push((out_index, inp_index, mod_index));
325                return;
326            }
327        }
328    }
329
330    /// This is called right after the [crate::nodes::NodeExecutor]
331    /// received this NodeProg from the [crate::nodes::NodeConfigurator].
332    /// It initializes internal buffers with parameter data.
333    pub fn initialize_input_buffers(&mut self) {
334        for param_idx in 0..self.params.len() {
335            let param_val = self.params[param_idx];
336            self.inp[param_idx].fill(param_val);
337        }
338    }
339
340    pub fn swap_previous_outputs(&mut self, prev_prog: &mut NodeProg) {
341        if self.locked_buffers {
342            self.unlock_buffers();
343        }
344
345        if prev_prog.locked_buffers {
346            prev_prog.unlock_buffers();
347        }
348
349        // XXX: Swapping is now safe, because the `cur_inp` field
350        //      no longer references to the buffers in `inp` or `out`.
351        for (old_inp_pb, new_inp_pb) in prev_prog.inp.iter_mut().zip(self.inp.iter_mut()) {
352            std::mem::swap(old_inp_pb, new_inp_pb);
353        }
354    }
355
356    pub fn unlock_buffers(&mut self) {
357        for buf in self.cur_inp.iter_mut() {
358            *buf = ProcBuf::null();
359        }
360        for modop in self.modops.iter_mut() {
361            modop.unlock();
362        }
363        self.locked_buffers = false;
364    }
365
366    pub fn assign_outputs(&mut self) {
367        for op in self.prog.iter() {
368            // First step is copying the ProcBufs to the `cur_inp` current
369            // input buffer vector. It holds the data for smoothed paramter
370            // inputs or just constant values since the last smoothing.
371            //
372            // Next we overwrite the input ProcBufs which have an
373            // assigned output buffer.
374            //
375            // ProcBuf has a raw pointer inside, and this copying
376            // is therefor very fast.
377            //
378            // XXX: This requires, that the graph is not cyclic,
379            // because otherwise we would write output buffers which
380            // are already accessed in the current iteration.
381            // This might lead to unexpected effects inside the process()
382            // call of the nodes.
383            let input_bufs = &mut self.cur_inp;
384            let out_bufs = &mut self.out;
385
386            let inp = op.in_idxlen;
387
388            // First step (refresh inputs):
389            input_bufs[inp.0..inp.1].copy_from_slice(&self.inp[inp.0..inp.1]);
390
391            // Second step (assign outputs):
392            for io in op.inputs.iter() {
393                input_bufs[io.1] = out_bufs[io.0];
394
395                if let Some(idx) = io.2 {
396                    input_bufs[io.1] = self.modops[idx].lock(self.inp[io.1], out_bufs[io.0]);
397                }
398            }
399        }
400
401        self.locked_buffers = true;
402    }
403}