Skip to main content

rill_core/traits/
port.rs

1//! Port types and identifiers for the Rill ecosystem
2//!
3//! Ports are the connection points between nodes in the signal graph.
4//! Each output port owns a `FixedBuffer<T, BUF_SIZE>` and an optional `Action`
5//! that defines how data is produced. Input ports are connection endpoints
6//! that receive data from upstream output ports.
7
8use crate::buffer::{Buffer, FixedBuffer};
9use crate::math::vector::scalar::ScalarVector4;
10use crate::math::vector::traits::Vector as VecTrait;
11use crate::math::Transcendental;
12use crate::time::RenderContext;
13use crate::traits::algorithm::Algorithm;
14use crate::traits::node::NodeId;
15use crate::traits::processable::Processable;
16use crate::traits::{Node, ProcessResult};
17use std::fmt;
18
19// ============================================================================
20// Port Type
21// ============================================================================
22
23/// Type of a port - what kind of signal it carries
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum PortType {
26    /// Signal port - carries signal blocks (signal data, sensor data, etc.)
27    Signal,
28
29    /// Control signal port - carries modulation/automation
30    Control,
31
32    /// Clock signal port - carries timing information
33    Clock,
34
35    /// Feedback port - stores state between blocks
36    Feedback,
37
38    /// Parameter port - for node parameters (special)
39    Param,
40}
41
42impl PortType {
43    /// Get the name of the port type
44    pub const fn name(&self) -> &'static str {
45        match self {
46            Self::Signal => "signal",
47            Self::Control => "control",
48            Self::Clock => "clock",
49            Self::Feedback => "feedback",
50            Self::Param => "param",
51        }
52    }
53
54    /// Check if this port carries signal-rate signals
55    pub const fn is_signal_rate(&self) -> bool {
56        matches!(self, Self::Signal)
57    }
58
59    /// Check if this port carries control-rate signals
60    pub const fn is_control_rate(&self) -> bool {
61        matches!(self, Self::Control)
62    }
63
64    /// Check if this port carries clock signals
65    pub const fn is_clock(&self) -> bool {
66        matches!(self, Self::Clock)
67    }
68}
69
70impl fmt::Display for PortType {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{}", self.name())
73    }
74}
75
76// ============================================================================
77// Port Direction
78// ============================================================================
79
80/// Direction of a port (input or output)
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum PortDirection {
83    /// Input port (receives data into the node)
84    Input,
85
86    /// Output port (sends data out of the node)
87    Output,
88}
89
90impl PortDirection {
91    /// Get the name of the direction
92    pub const fn name(&self) -> &'static str {
93        match self {
94            Self::Input => "input",
95            Self::Output => "output",
96        }
97    }
98
99    /// Check if this is an input port
100    pub const fn is_input(&self) -> bool {
101        matches!(self, Self::Input)
102    }
103
104    /// Check if this is an output port
105    pub const fn is_output(&self) -> bool {
106        matches!(self, Self::Output)
107    }
108}
109
110impl fmt::Display for PortDirection {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{}", self.name())
113    }
114}
115
116// ============================================================================
117// Port ID
118// ============================================================================
119
120/// Unique identifier for a port within a graph
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122pub struct PortId {
123    node: NodeId,
124    port_type: PortType,
125    direction: PortDirection,
126    index: u16,
127}
128
129impl PortId {
130    /// Create a new port ID
131    pub const fn new(
132        node: NodeId,
133        port_type: PortType,
134        direction: PortDirection,
135        index: u16,
136    ) -> Self {
137        Self {
138            node,
139            port_type,
140            direction,
141            index,
142        }
143    }
144
145    // ========================================================================
146    // Signal Port Constructors
147    // ========================================================================
148
149    /// Create a new signal input port
150    pub const fn signal_in(node: NodeId, index: u16) -> Self {
151        Self::new(node, PortType::Signal, PortDirection::Input, index)
152    }
153
154    /// Create a new signal output port
155    pub const fn signal_out(node: NodeId, index: u16) -> Self {
156        Self::new(node, PortType::Signal, PortDirection::Output, index)
157    }
158
159    // ========================================================================
160    // Control Port Constructors
161    // ========================================================================
162
163    /// Create a new control input port
164    pub const fn control_in(node: NodeId, index: u16) -> Self {
165        Self::new(node, PortType::Control, PortDirection::Input, index)
166    }
167
168    /// Create a new control output port
169    pub const fn control_out(node: NodeId, index: u16) -> Self {
170        Self::new(node, PortType::Control, PortDirection::Output, index)
171    }
172
173    // ========================================================================
174    // Clock Port Constructors
175    // ========================================================================
176
177    /// Create a new clock input port
178    pub const fn clock_in(node: NodeId, index: u16) -> Self {
179        Self::new(node, PortType::Clock, PortDirection::Input, index)
180    }
181
182    /// Create a new clock output port
183    pub const fn clock_out(node: NodeId, index: u16) -> Self {
184        Self::new(node, PortType::Clock, PortDirection::Output, index)
185    }
186
187    // ========================================================================
188    // Feedback Port Constructors
189    // ========================================================================
190
191    /// Create a new feedback input port
192    pub const fn feedback_in(node: NodeId, index: u16) -> Self {
193        Self::new(node, PortType::Feedback, PortDirection::Input, index)
194    }
195
196    /// Create a new feedback output port
197    pub const fn feedback_out(node: NodeId, index: u16) -> Self {
198        Self::new(node, PortType::Feedback, PortDirection::Output, index)
199    }
200
201    // ========================================================================
202    // Parameter Port Constructors
203    // ========================================================================
204
205    /// Create a new parameter port (always input)
206    pub const fn param(node: NodeId, index: u16) -> Self {
207        Self::new(node, PortType::Param, PortDirection::Input, index)
208    }
209
210    // ========================================================================
211    // Getters
212    // ========================================================================
213
214    /// Get the node ID
215    pub const fn node_id(&self) -> NodeId {
216        self.node
217    }
218
219    /// Get the port type
220    pub const fn port_type(&self) -> PortType {
221        self.port_type
222    }
223
224    /// Get the port direction
225    pub const fn direction(&self) -> PortDirection {
226        self.direction
227    }
228
229    /// Get the port index
230    pub const fn index(&self) -> u16 {
231        self.index
232    }
233
234    // ========================================================================
235    // Predicates
236    // ========================================================================
237
238    /// Check if this is an input port
239    pub const fn is_input(&self) -> bool {
240        self.direction.is_input()
241    }
242
243    /// Check if this is an output port
244    pub const fn is_output(&self) -> bool {
245        self.direction.is_output()
246    }
247
248    /// Check if this is a signal port
249    pub const fn is_signal(&self) -> bool {
250        matches!(self.port_type, PortType::Signal)
251    }
252
253    /// Check if this is a control port
254    pub const fn is_control(&self) -> bool {
255        matches!(self.port_type, PortType::Control)
256    }
257
258    /// Check if this is a clock port
259    pub const fn is_clock(&self) -> bool {
260        matches!(self.port_type, PortType::Clock)
261    }
262
263    /// Check if this is a feedback port
264    pub const fn is_feedback(&self) -> bool {
265        matches!(self.port_type, PortType::Feedback)
266    }
267
268    /// Check if this is a parameter port
269    pub const fn is_param(&self) -> bool {
270        matches!(self.port_type, PortType::Param)
271    }
272}
273
274impl fmt::Display for PortId {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        write!(
277            f,
278            "Node({}).{}_{}[{}]",
279            self.node.inner(),
280            self.port_type.name(),
281            self.direction.name(),
282            self.index
283        )
284    }
285}
286
287// ============================================================================
288// Port Structure
289// ============================================================================
290
291/// A port on a node.
292///
293/// Each port has an owned `FixedBuffer<T, BUF_SIZE>` for its data and an optional
294/// `Action` that defines per-port processing. Output ports typically have
295/// an action; input ports may have one for preprocessing.
296///
297/// Ports can optionally participate in feedback edges:
298/// - On an output port in a feedback edge, `feedback_buffer` stores the
299///   previous block's output, snapshotted after DSP via `snapshot_feedback()`.
300/// - On an input port in a feedback edge, `feedback_buffer` holds the delayed
301///   feedback value that gets mixed into `buffer` by `pre_process()`.
302/// - `downstream` lists signal connections from this output port to input ports
303///   of other nodes, populated at build time by the graph builder.
304/// - `upstream_buffer` on input ports: direct pointer to the upstream output
305///   port's buffer for zero-copy routing on **exclusive 1:1** edges only.
306///   `None` for fan-in, fan-out, and feedback ports (each materializes an
307///   independent copy).
308///
309/// # Safety
310/// `upstream_buffer` is safe because the graph topology is immutable and
311/// processing is strictly single-threaded in topological order. The
312/// upstream output buffer is guaranteed to outlive the downstream input
313/// port that references it.
314pub struct Port<T: Transcendental, const BUF_SIZE: usize> {
315    /// Port identifier
316    pub id: PortId,
317    /// Port name
318    pub name: String,
319    /// Port direction (input/output)
320    pub direction: PortDirection,
321    /// Per-port processing algorithm (None for simple input ports)
322    action: Option<Box<dyn Algorithm<T>>>,
323    /// Pending command value from the control path
324    pending_command: Option<T>,
325    /// Owned signal buffer (for output ports and input ports without upstream).
326    ///
327    /// Private: nodes access signal data through [`read`](Self::read) /
328    /// [`write`](Self::write) / [`write_from`](Self::write_from) /
329    /// [`feedback`](Self::feedback), which encapsulate the zero-copy routing.
330    /// The engine uses [`buffer`](Self::buffer) / `buffer_mut`.
331    buffer: FixedBuffer<T, BUF_SIZE>,
332    /// Delayed feedback state (None if not on a feedback edge)
333    feedback_buffer: Option<FixedBuffer<T, BUF_SIZE>>,
334    /// Downstream signal connections: (target_node_index, target_port_index).
335    /// Used for serialization and by `GraphBuilder::build()`.
336    downstream: Vec<(usize, usize)>,
337    /// Direct pointers to downstream input ports. Filled by
338    /// `GraphBuilder::build()`. Used by `propagate` to copy data.
339    downstream_input_ptrs: Vec<*mut Port<T, BUF_SIZE>>,
340    /// Unique downstream nodes (one per target, deduplicated at build time).
341    /// Filled by `GraphBuilder::build()`. Used by `propagate` to recurse
342    /// into downstream nodes — no runtime deduplication needed.
343    downstream_nodes: Vec<*mut crate::traits::NodeVariant<T, BUF_SIZE>>,
344    /// Direct pointer to upstream output buffer for zero-copy routing.
345    /// `Some` only for an **exclusive 1:1** input port (its upstream output
346    /// has a single consumer and this input has a single producer).
347    /// `None` for output ports, fan-in, fan-out branches, or unconnected —
348    /// those materialize an independent copy.
349    /// Valid for the engine's lifetime.
350    upstream_buffer: Option<*const FixedBuffer<T, BUF_SIZE>>,
351    /// Feedback edge targets from this output port (for serialization)
352    feedback_downstream: Vec<(usize, usize)>,
353
354    /// Direct pointers to `feedback_buffer` on downstream input ports.
355    ///
356    /// Set by `GraphBuilder::build()` for feedback edges.
357    /// `snapshot_feedback()` copies its buffer into each target.
358    feedback_ptrs: Vec<*mut Option<FixedBuffer<T, BUF_SIZE>>>,
359
360    /// Whether this input port has received new data in the current graph cycle.
361    ///
362    /// Set by `propagate` when a downstream input port receives a buffer copy.
363    /// Consumer nodes (esp. Sinks) check this flag to decide whether all
364    /// input channels are fresh before producing output.
365    data_received: bool,
366
367    /// Pull-model: pointer to the upstream node that feeds this input port.
368    /// Only set for same-chain edges (recording→recording or playback→playback).
369    /// Used by the pull traversal in `process_playback_chain`.
370    upstream_node: *mut crate::traits::NodeVariant<T, BUF_SIZE>,
371}
372
373impl<T: Transcendental, const BUF_SIZE: usize> fmt::Debug for Port<T, BUF_SIZE> {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375        f.debug_struct("Port")
376            .field("id", &self.id)
377            .field("name", &self.name)
378            .field("direction", &self.direction)
379            .field("has_action", &self.action.is_some())
380            .field("has_feedback", &self.feedback_buffer.is_some())
381            .field("downstream_len", &self.downstream.len())
382            .finish()
383    }
384}
385
386impl<T: Transcendental, const BUF_SIZE: usize> Port<T, BUF_SIZE> {
387    /// Create a new signal output port
388    pub fn output(node_id: NodeId, index: u16, name: &str) -> Self {
389        Self {
390            id: PortId::signal_out(node_id, index),
391            name: name.to_string(),
392            direction: PortDirection::Output,
393            action: None,
394            pending_command: None,
395            buffer: FixedBuffer::new(),
396            feedback_buffer: None,
397            downstream: Vec::new(),
398            feedback_downstream: Vec::new(),
399            feedback_ptrs: Vec::new(),
400            downstream_input_ptrs: Vec::new(),
401            downstream_nodes: Vec::new(),
402            upstream_buffer: None,
403            upstream_node: std::ptr::null_mut(),
404            data_received: false,
405        }
406    }
407
408    /// Create a new signal input port
409    pub fn input(node_id: NodeId, index: u16, name: &str) -> Self {
410        Self {
411            id: PortId::signal_in(node_id, index),
412            name: name.to_string(),
413            direction: PortDirection::Input,
414            action: None,
415            pending_command: None,
416            buffer: FixedBuffer::new(),
417            feedback_buffer: None,
418            downstream: Vec::new(),
419            feedback_downstream: Vec::new(),
420            feedback_ptrs: Vec::new(),
421            downstream_input_ptrs: Vec::new(),
422            downstream_nodes: Vec::new(),
423            upstream_buffer: None,
424            upstream_node: std::ptr::null_mut(),
425            data_received: false,
426        }
427    }
428
429    /// Create a new control output port
430    pub fn control_output(node_id: NodeId, index: u16, name: &str) -> Self {
431        Self {
432            id: PortId::control_out(node_id, index),
433            name: name.to_string(),
434            direction: PortDirection::Output,
435            action: None,
436            pending_command: None,
437            buffer: FixedBuffer::new(),
438            feedback_buffer: None,
439            downstream: Vec::new(),
440            feedback_downstream: Vec::new(),
441            feedback_ptrs: Vec::new(),
442            downstream_input_ptrs: Vec::new(),
443            downstream_nodes: Vec::new(),
444            upstream_buffer: None,
445            upstream_node: std::ptr::null_mut(),
446            data_received: false,
447        }
448    }
449
450    /// Create a new control output port with an algorithm
451    pub fn control_output_with_action(
452        node_id: NodeId,
453        index: u16,
454        name: &str,
455        action: Box<dyn Algorithm<T>>,
456    ) -> Self {
457        Self {
458            id: PortId::control_out(node_id, index),
459            name: name.to_string(),
460            direction: PortDirection::Output,
461            action: Some(action),
462            pending_command: None,
463            buffer: FixedBuffer::new(),
464            feedback_buffer: None,
465            downstream: Vec::new(),
466            feedback_downstream: Vec::new(),
467            feedback_ptrs: Vec::new(),
468            downstream_input_ptrs: Vec::new(),
469            downstream_nodes: Vec::new(),
470            upstream_buffer: None,
471            upstream_node: std::ptr::null_mut(),
472            data_received: false,
473        }
474    }
475
476    /// Create a new control input port
477    pub fn control_input(node_id: NodeId, index: u16, name: &str) -> Self {
478        Self {
479            id: PortId::control_in(node_id, index),
480            name: name.to_string(),
481            direction: PortDirection::Input,
482            action: None,
483            pending_command: None,
484            buffer: FixedBuffer::new(),
485            feedback_buffer: None,
486            downstream: Vec::new(),
487            feedback_downstream: Vec::new(),
488            feedback_ptrs: Vec::new(),
489            downstream_input_ptrs: Vec::new(),
490            downstream_nodes: Vec::new(),
491            upstream_buffer: None,
492            upstream_node: std::ptr::null_mut(),
493            data_received: false,
494        }
495    }
496
497    /// Get the port ID
498    pub fn id(&self) -> PortId {
499        self.id
500    }
501
502    /// Get the port name
503    pub fn name(&self) -> &str {
504        &self.name
505    }
506
507    /// Check if port is an input
508    pub fn is_input(&self) -> bool {
509        self.direction.is_input()
510    }
511
512    /// Check if port is an output
513    pub fn is_output(&self) -> bool {
514        self.direction.is_output()
515    }
516
517    /// Low-level access to the port's own buffer (engine / I-O boundary).
518    ///
519    /// Nodes should prefer [`read`](Self::read): for a signal **input** this
520    /// returns the port's own buffer, which is *not* the effective input on a
521    /// zero-copy edge.
522    pub fn buffer(&self) -> &FixedBuffer<T, BUF_SIZE> {
523        &self.buffer
524    }
525
526    /// Low-level mutable access to the port's own buffer (crate-internal).
527    ///
528    /// Nodes should prefer [`write`](Self::write) / [`write_from`](Self::write_from).
529    pub(crate) fn buffer_mut(&mut self) -> &mut FixedBuffer<T, BUF_SIZE> {
530        &mut self.buffer
531    }
532
533    /// Read the effective input block for this port (zero-copy aware).
534    ///
535    /// On an exclusive 1:1 edge this is the upstream output buffer; otherwise
536    /// it is the port's own (materialized) buffer. This is the correct way for
537    /// a node to read a signal input.
538    #[inline]
539    pub fn read(&self) -> &[T; BUF_SIZE] {
540        self.signal_buffer().as_array()
541    }
542
543    /// Mutable access to this port's output block. The canonical way for a
544    /// node to write its output samples.
545    #[inline]
546    pub fn write(&mut self) -> &mut [T; BUF_SIZE] {
547        self.buffer_mut().as_mut_array()
548    }
549
550    /// Write this port's output block from a source array.
551    #[inline]
552    pub fn write_from(&mut self, src: &[T; BUF_SIZE]) {
553        self.write().copy_from_slice(src);
554    }
555
556    /// The delayed feedback block, if this port is on a feedback edge.
557    #[inline]
558    pub fn feedback(&self) -> Option<&[T; BUF_SIZE]> {
559        self.feedback_buffer.as_ref().map(|b| b.as_array())
560    }
561
562    // ========================================================================
563    // Sink-facing status
564    // ========================================================================
565
566    /// Whether this input port received fresh data in the current graph cycle.
567    #[inline]
568    pub fn data_received(&self) -> bool {
569        self.data_received
570    }
571
572    /// Set the `data_received` flag (sinks reset it after consuming).
573    #[inline]
574    pub fn set_data_received(&mut self, value: bool) {
575        self.data_received = value;
576    }
577
578    // ========================================================================
579    // Graph construction API (used by `GraphBuilder::build`)
580    //
581    // These wire the immutable topology once at build time. They are not
582    // intended for node authors — signal data flows through `read`/`write`.
583    // ========================================================================
584
585    /// Downstream signal connections `(target_node, target_port)` (serialization).
586    #[inline]
587    pub fn downstream(&self) -> &[(usize, usize)] {
588        &self.downstream
589    }
590
591    /// Feedback edge targets from this output port (serialization).
592    #[inline]
593    pub fn feedback_downstream(&self) -> &[(usize, usize)] {
594        &self.feedback_downstream
595    }
596
597    /// Direct pointers to downstream input ports (engine propagation).
598    #[inline]
599    pub fn downstream_input_ptrs(&self) -> &[*mut Port<T, BUF_SIZE>] {
600        &self.downstream_input_ptrs
601    }
602
603    /// Unique downstream nodes fed by this output port (engine propagation).
604    #[inline]
605    pub fn downstream_nodes(&self) -> &[*mut crate::traits::NodeVariant<T, BUF_SIZE>] {
606        &self.downstream_nodes
607    }
608
609    /// The upstream node feeding this input port (pull model), or null.
610    #[inline]
611    pub fn upstream_node(&self) -> *mut crate::traits::NodeVariant<T, BUF_SIZE> {
612        self.upstream_node
613    }
614
615    /// Whether this input port aliases an upstream buffer (exclusive 1:1 edge).
616    #[inline]
617    pub fn has_upstream_buffer(&self) -> bool {
618        self.upstream_buffer.is_some()
619    }
620
621    /// Record a downstream signal connection `(target_node, target_port)`.
622    #[inline]
623    pub fn add_downstream(&mut self, target_node: usize, target_port: usize) {
624        self.downstream.push((target_node, target_port));
625    }
626
627    /// Record a feedback edge target `(target_node, target_port)`.
628    #[inline]
629    pub fn add_feedback_downstream(&mut self, target_node: usize, target_port: usize) {
630        self.feedback_downstream.push((target_node, target_port));
631    }
632
633    /// Append a direct pointer to a downstream input port.
634    #[inline]
635    pub fn add_downstream_input_ptr(&mut self, ptr: *mut Port<T, BUF_SIZE>) {
636        self.downstream_input_ptrs.push(ptr);
637    }
638
639    /// Append a downstream node pointer, deduplicating on identity.
640    #[inline]
641    pub fn add_downstream_node(&mut self, node: *mut crate::traits::NodeVariant<T, BUF_SIZE>) {
642        let val = node as usize;
643        if !self.downstream_nodes.iter().any(|&p| p as usize == val) {
644            self.downstream_nodes.push(node);
645        }
646    }
647
648    /// Set the pull-model upstream node pointer.
649    #[inline]
650    pub fn set_upstream_node(&mut self, node: *mut crate::traits::NodeVariant<T, BUF_SIZE>) {
651        self.upstream_node = node;
652    }
653
654    /// Set the zero-copy upstream buffer alias (exclusive 1:1 edges only).
655    #[inline]
656    pub fn set_upstream_buffer(&mut self, buffer: Option<*const FixedBuffer<T, BUF_SIZE>>) {
657        self.upstream_buffer = buffer;
658    }
659
660    /// Allocate this port's feedback buffer (feedback edge endpoint).
661    #[inline]
662    pub fn init_feedback_buffer(&mut self) {
663        self.feedback_buffer = Some(FixedBuffer::new());
664    }
665
666    /// Raw pointer to this port's `feedback_buffer`, for wiring `feedback_ptrs`.
667    #[inline]
668    pub fn feedback_buffer_ptr(&self) -> *mut Option<FixedBuffer<T, BUF_SIZE>> {
669        &self.feedback_buffer as *const Option<FixedBuffer<T, BUF_SIZE>>
670            as *mut Option<FixedBuffer<T, BUF_SIZE>>
671    }
672
673    /// Append a pointer to a downstream input port's feedback buffer.
674    #[inline]
675    pub fn add_feedback_ptr(&mut self, ptr: *mut Option<FixedBuffer<T, BUF_SIZE>>) {
676        self.feedback_ptrs.push(ptr);
677    }
678
679    /// Whether this input port is a pure zero-copy passthrough.
680    ///
681    /// True when it aliases a single upstream output buffer
682    /// (`upstream_buffer` is set) and performs no per-port processing —
683    /// no `action`, no `pending_command`, and no feedback mixing. Such a
684    /// port is never materialized into its own `buffer`: the consumer reads
685    /// the upstream buffer directly through [`signal_buffer`](Self::signal_buffer).
686    #[inline]
687    pub fn is_zero_copy(&self) -> bool {
688        self.upstream_buffer.is_some()
689            && self.action.is_none()
690            && self.pending_command.is_none()
691            && self.feedback_buffer.is_none()
692    }
693
694    /// Get the effective signal buffer for this port.
695    ///
696    /// For a zero-copy input port returns the upstream output buffer directly.
697    /// Otherwise returns the local buffer (materialized by `run_action` /
698    /// feedback `pre_process`).
699    #[allow(unsafe_code)]
700    pub fn signal_buffer(&self) -> &FixedBuffer<T, BUF_SIZE> {
701        match self.upstream_buffer {
702            Some(ptr) if self.is_zero_copy() => unsafe { &*ptr },
703            _ => &self.buffer,
704        }
705    }
706
707    /// Pre-process this port before node DSP.
708    ///
709    /// For input ports on a feedback edge, mixes the delayed feedback
710    /// (from `feedback_buffer`) into the current `buffer`.
711    pub fn pre_process(&mut self) {
712        if let Some(ref fb) = self.feedback_buffer {
713            let arr = self.buffer.as_mut_array();
714            let fb_arr = fb.as_array();
715            let chunks = BUF_SIZE / 4;
716
717            for chunk in 0..chunks {
718                let o = chunk * 4;
719                let a = ScalarVector4::load(&arr[o..o + 4]);
720                let b = ScalarVector4::load(&fb_arr[o..o + 4]);
721                a.add(&b).store(&mut arr[o..o + 4]);
722            }
723
724            for i in chunks * 4..BUF_SIZE {
725                arr[i] += fb_arr[i];
726            }
727        }
728    }
729
730    /// Snapshot the buffer into `feedback_buffer` and propagate to
731    /// downstream input ports via `feedback_ptrs`.
732    ///
733    /// For output ports on a feedback edge, saves the current buffer
734    /// so it can be used as delayed feedback in the next block, then
735    /// copies it into each target input port's `feedback_buffer`.
736    /// No-op when `feedback_buffer` is `None`.
737    #[allow(unsafe_code)]
738    pub fn snapshot_feedback(&mut self) {
739        if let Some(ref mut fb) = self.feedback_buffer {
740            fb.copy_from(self.buffer.as_array());
741            for &ptr in &self.feedback_ptrs {
742                unsafe {
743                    if let Some(ref mut target) = *ptr {
744                        target.copy_from(fb.as_array());
745                    }
746                }
747            }
748        }
749    }
750
751    /// Propagate this port's own buffer to all downstream input ports.
752    ///
753    /// For each downstream input port that is **not** a zero-copy passthrough
754    /// (fan-in, feedback, or a port with its own `action`/`pending_command`),
755    /// materialize the data into that port's buffer via `run_action`.
756    /// Zero-copy passthrough ports are left untouched — their consumer reads
757    /// this output buffer directly through [`signal_buffer`](Self::signal_buffer),
758    /// so no copy is performed. Then process each downstream node and recurse
759    /// through its output ports.
760    ///
761    /// No heap allocations — `downstream_nodes` is pre‑filled at build time.
762    #[allow(unsafe_code)]
763    pub fn propagate(
764        &self,
765        ctx: &RenderContext,
766        tick: &crate::time::ClockTick,
767    ) -> ProcessResult<()> {
768        let buffer = self.buffer.as_array();
769        for &ptr in &self.downstream_input_ptrs {
770            unsafe {
771                let p = &mut *ptr;
772                if !p.is_zero_copy() {
773                    p.run_action(Some(buffer))?;
774                }
775                p.data_received = true;
776            }
777        }
778        for &parent in &self.downstream_nodes {
779            unsafe {
780                let nv = &mut *parent;
781                for pi in 0..nv.num_signal_inputs() {
782                    if let Some(p) = nv.input_port_mut(pi) {
783                        p.pre_process();
784                    }
785                }
786                nv.process_block(ctx, tick)?;
787                for po in 0..nv.num_signal_outputs() {
788                    if let Some(p) = nv.output_port_mut(po) {
789                        p.snapshot_feedback();
790                    }
791                }
792                for po in 0..nv.num_signal_outputs() {
793                    if let Some(p) = nv.output_port(po) {
794                        p.propagate(ctx, tick)?;
795                    }
796                }
797            }
798        }
799        Ok(())
800    }
801
802    /// Run the port's algorithm.
803    ///
804    /// Delivers any pending command via `Algorithm::apply_command()`, then
805    /// calls `Algorithm::process()` with the input and output slices.
806    /// When no algorithm is attached, the pending command value (if any)
807    /// is written directly into the buffer; otherwise input is passed
808    /// through or zero-filled.
809    pub fn run_action(
810        &mut self,
811        input: Option<&[T; BUF_SIZE]>,
812    ) -> crate::traits::ProcessResult<()> {
813        match &mut self.action {
814            Some(action) => {
815                // Deliver any pending command to the algorithm
816                if let Some(cmd) = self.pending_command.take() {
817                    action.apply_command(cmd);
818                }
819                let input_slice = input.map(|arr| arr.as_slice());
820                action.process(input_slice, self.buffer.as_mut_slice())
821            }
822            None => {
823                // No algorithm — use pending command value if set,
824                // otherwise pass through input or zero-fill.
825                if let Some(cmd) = self.pending_command.take() {
826                    self.buffer.fill(cmd);
827                } else if let Some(input_data) = input {
828                    self.buffer.copy_from(input_data);
829                } else {
830                    self.buffer.fill(T::ZERO);
831                }
832                Ok(())
833            }
834        }
835    }
836
837    /// Set a command value for this port.
838    ///
839    /// The value is stored as a pending command and delivered to the
840    /// algorithm (or written directly to the buffer) on the next
841    /// `run_action()` call.
842    pub fn set_value(&mut self, value: T) {
843        self.pending_command = Some(value);
844    }
845}
846
847// ============================================================================
848// Send / Sync
849// ============================================================================
850
851// SAFETY: `upstream_buffer` is a raw pointer to a buffer owned by another
852// Port in the same static graph. The graph is immutable during processing
853// and runs single-threaded in topological order. The pointer target
854// outlives the pointer for the entire processing session.
855#[allow(unsafe_code)]
856unsafe impl<T: Transcendental + Send, const BUF_SIZE: usize> Send for Port<T, BUF_SIZE> {}
857#[allow(unsafe_code)]
858unsafe impl<T: Transcendental + Sync, const BUF_SIZE: usize> Sync for Port<T, BUF_SIZE> {}
859
860// ============================================================================
861// Tests
862// ============================================================================
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867
868    #[test]
869    fn test_port_id_creation() {
870        let node = NodeId(42);
871
872        let signal_in = PortId::signal_in(node, 0);
873        assert_eq!(signal_in.port_type(), PortType::Signal);
874        assert!(signal_in.is_input());
875
876        let clock_out = PortId::clock_out(node, 0);
877        assert_eq!(clock_out.port_type(), PortType::Clock);
878        assert!(clock_out.is_output());
879
880        let feedback_in = PortId::feedback_in(node, 0);
881        assert_eq!(feedback_in.port_type(), PortType::Feedback);
882        assert!(feedback_in.is_input());
883    }
884}