Skip to main content

ad_plugins_rs/
std_arrays.rs

1use std::sync::Arc;
2
3use ad_core_rs::ndarray::NDArray;
4use ad_core_rs::ndarray_pool::NDArrayPool;
5use ad_core_rs::plugin::runtime::{NDPluginProcess, PluginRuntimeHandle, ProcessResult};
6use ad_core_rs::plugin::wiring::WiringRegistry;
7use parking_lot::Mutex;
8
9/// Pure processing logic: stores the latest array and passes it through.
10pub struct StdArraysProcessor {
11    latest_data: Arc<Mutex<Option<Arc<NDArray>>>>,
12}
13
14impl StdArraysProcessor {
15    pub fn new() -> Self {
16        Self {
17            latest_data: Arc::new(Mutex::new(None)),
18        }
19    }
20
21    /// Get a cloneable handle to the latest array.
22    pub fn data_handle(&self) -> Arc<Mutex<Option<Arc<NDArray>>>> {
23        self.latest_data.clone()
24    }
25}
26
27impl Default for StdArraysProcessor {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl NDPluginProcess for StdArraysProcessor {
34    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
35        let out = Arc::new(array.clone());
36        *self.latest_data.lock() = Some(out.clone());
37        ProcessResult::arrays(vec![out])
38    }
39
40    fn plugin_type(&self) -> &str {
41        "NDPluginStdArrays"
42    }
43
44    /// C `NDPluginStdArrays.cpp:343` sets `NDArrayCallbacks = 0`: this plugin
45    /// serves pixel data via the StdArray waveforms, not downstream callbacks.
46    fn does_array_callbacks(&self) -> bool {
47        false
48    }
49
50    fn array_data_handle(&self) -> Option<Arc<Mutex<Option<Arc<NDArray>>>>> {
51        Some(self.latest_data.clone())
52    }
53}
54
55/// Create a StdArrays plugin runtime.
56pub fn create_std_arrays_runtime(
57    port_name: &str,
58    pool: Arc<NDArrayPool>,
59    ndarray_port: &str,
60    wiring: Arc<WiringRegistry>,
61) -> (
62    PluginRuntimeHandle,
63    Arc<Mutex<Option<Arc<NDArray>>>>,
64    std::thread::JoinHandle<()>,
65) {
66    let processor = StdArraysProcessor::new();
67    let data_handle = processor.data_handle();
68
69    let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime(
70        port_name,
71        processor,
72        pool,
73        1, // LatestOnly semantics
74        ndarray_port,
75        wiring,
76    );
77
78    (handle, data_handle, data_jh)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use ad_core_rs::ndarray::{NDDataType, NDDimension};
85
86    fn make_array(id: i32) -> Arc<NDArray> {
87        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
88        arr.unique_id = id;
89        Arc::new(arr)
90    }
91
92    #[test]
93    fn test_processor_stores_and_passes_through() {
94        let mut proc = StdArraysProcessor::new();
95        let pool = NDArrayPool::new(1_000_000);
96
97        let arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
98        let result = proc.process_array(&arr, &pool);
99        assert_eq!(result.output_arrays.len(), 1);
100
101        let latest = proc.data_handle().lock().clone();
102        assert!(latest.is_some());
103    }
104
105    #[test]
106    fn test_std_arrays_runtime() {
107        let pool = Arc::new(NDArrayPool::new(1_000_000));
108        let wiring = Arc::new(WiringRegistry::new());
109        let (handle, data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
110
111        // Plugins default to disabled — enable for test
112        handle
113            .port_runtime()
114            .port_handle()
115            .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
116            .unwrap();
117        std::thread::sleep(std::time::Duration::from_millis(10));
118
119        let rt = tokio::runtime::Builder::new_current_thread()
120            .enable_all()
121            .build()
122            .unwrap();
123        rt.block_on(handle.array_sender().publish(make_array(42)));
124        std::thread::sleep(std::time::Duration::from_millis(100));
125
126        let latest = data.lock().clone();
127        assert!(latest.is_some());
128        assert_eq!(latest.unwrap().unique_id, 42);
129    }
130
131    #[test]
132    fn test_std_arrays_initial_array_callbacks_off() {
133        // C NDPluginStdArrays.cpp:343 sets NDArrayCallbacks=0 in the
134        // constructor; the initial param a client reads must be 0, not 1.
135        let pool = Arc::new(NDArrayPool::new(1_000_000));
136        let wiring = Arc::new(WiringRegistry::new());
137        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
138
139        let val = handle
140            .port_runtime()
141            .port_handle()
142            .read_int32_blocking(handle.ndarray_params.array_callbacks, 0)
143            .unwrap();
144        assert_eq!(val, 0, "StdArrays initial NDArrayCallbacks must be 0");
145    }
146
147    #[test]
148    fn test_terminal_plugins_do_not_do_array_callbacks() {
149        // C disables NDArrayCallbacks in the constructor of NDPluginStdArrays
150        // (:343), NDPluginAttribute (:203), and NDPluginFile (:948, base of
151        // every file writer). Each Rust counterpart overrides
152        // does_array_callbacks() to false so the initial param reflects this.
153        use crate::attribute::AttributeProcessor;
154        use crate::file_hdf5::Hdf5FileProcessor;
155        use crate::file_jpeg::JpegFileProcessor;
156        use crate::file_magick::MagickFileProcessor;
157        use crate::file_netcdf::NetcdfFileProcessor;
158        use crate::file_nexus::NexusFileProcessor;
159        use crate::file_tiff::TiffFileProcessor;
160        use crate::passthrough::PassthroughProcessor;
161
162        assert!(!StdArraysProcessor::new().does_array_callbacks());
163        assert!(!AttributeProcessor::new("attr", 1).does_array_callbacks());
164        assert!(!Hdf5FileProcessor::new().does_array_callbacks());
165        assert!(!JpegFileProcessor::new(85).does_array_callbacks());
166        assert!(!TiffFileProcessor::new().does_array_callbacks());
167        assert!(!NetcdfFileProcessor::new().does_array_callbacks());
168        assert!(!NexusFileProcessor::new().does_array_callbacks());
169        assert!(!MagickFileProcessor::new().does_array_callbacks());
170
171        // A non-terminal plugin keeps the default: it does deliver downstream.
172        assert!(PassthroughProcessor::new("NDPluginProcess").does_array_callbacks());
173    }
174
175    #[test]
176    fn test_std_arrays_serves_waveform_with_callbacks_off() {
177        // C NDPluginStdArrays fires its typed-array waveform callbacks
178        // (NDPluginStdArrays.cpp:71-73) regardless of NDArrayCallbacks — and it
179        // defaults NDArrayCallbacks=0. So the ArrayData waveform on I/O Intr
180        // must still update with NDArrayCallbacks=0. This guards against the
181        // STD_ARRAY_DATA interrupt being gated by the downstream-delivery flag.
182        use asyn_rs::param::ParamValue;
183        use std::time::Duration;
184
185        let pool = Arc::new(NDArrayPool::new(1_000_000));
186        let wiring = Arc::new(WiringRegistry::new());
187        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
188
189        let port = handle.port_runtime().port_handle();
190        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
191            .unwrap();
192        // Force NDArrayCallbacks=0 at runtime (StdArrays' C default) — the
193        // waveform output must still fire.
194        port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
195            .unwrap();
196        std::thread::sleep(Duration::from_millis(20));
197
198        let mut rx = port.interrupts().subscribe_async();
199
200        let rt = tokio::runtime::Builder::new_current_thread()
201            .enable_all()
202            .build()
203            .unwrap();
204        rt.block_on(handle.array_sender().publish(make_array(7)));
205        std::thread::sleep(Duration::from_millis(100));
206
207        // make_array(7) is a 4-element UInt8 array → the STD_ARRAY_DATA
208        // interrupt carries it as an Int8Array; the dimensions interrupt carries
209        // an Int32Array. Finding an Int8Array interrupt proves the waveform was
210        // served despite NDArrayCallbacks=0.
211        let mut served = false;
212        loop {
213            match rx.try_recv() {
214                Ok(v) => {
215                    if matches!(v.value, ParamValue::Int8Array(_)) {
216                        served = true;
217                    }
218                }
219                Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
220                Err(_) => break,
221            }
222        }
223        assert!(
224            served,
225            "StdArrays must serve STD_ARRAY_DATA with NDArrayCallbacks=0"
226        );
227    }
228
229    #[test]
230    fn test_std_arrays_throttled_frame_does_not_advance_array_counter() {
231        // C NDPluginStdArrays.cpp:202-211 decrements NDArrayCounter when its
232        // MaxByteRate throttle drops the waveform output, so a throttled frame
233        // leaves ArrayCounter unchanged (ImageJ etc. see no new data) while
234        // DroppedArrays advances.
235        use std::time::Duration;
236
237        let pool = Arc::new(NDArrayPool::new(1_000_000));
238        let wiring = Arc::new(WiringRegistry::new());
239        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
240        let port = handle.port_runtime().port_handle();
241        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
242            .unwrap();
243        // MaxByteRate = 4 bytes/sec; make_array is a 4-byte UInt8 array. The
244        // bucket starts full at 4, so the first frame passes and the next
245        // (sent before a meaningful refill) is throttled.
246        port.write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 4.0)
247            .unwrap();
248        std::thread::sleep(Duration::from_millis(20));
249
250        let rt = tokio::runtime::Builder::new_current_thread()
251            .enable_all()
252            .build()
253            .unwrap();
254
255        rt.block_on(handle.array_sender().publish(make_array(1)));
256        std::thread::sleep(Duration::from_millis(40));
257        let counter_after_first = port
258            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
259            .unwrap();
260
261        rt.block_on(handle.array_sender().publish(make_array(2)));
262        std::thread::sleep(Duration::from_millis(40));
263        let counter_after_second = port
264            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
265            .unwrap();
266        let dropped = port
267            .read_int32_blocking(handle.plugin_params.dropped_output_arrays, 0)
268            .unwrap();
269
270        assert_eq!(counter_after_first, 1, "first frame is served and counted");
271        assert_eq!(
272            counter_after_second, 1,
273            "throttled frame must NOT advance ArrayCounter"
274        );
275        assert_eq!(dropped, 1, "throttled frame advances DroppedArrays");
276    }
277}