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    /// Fence: `write_*_blocking` only queues param changes for the data
93    /// thread; the barrier ack proves they have been applied.
94    fn params_applied(handle: &PluginRuntimeHandle) {
95        assert!(
96            handle.wait_params_applied(std::time::Duration::from_secs(10)),
97            "data thread did not apply queued param changes"
98        );
99    }
100
101    fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
102        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
103        while !cond() {
104            assert!(
105                std::time::Instant::now() < deadline,
106                "timed out waiting for {what}"
107            );
108            std::thread::sleep(std::time::Duration::from_millis(2));
109        }
110    }
111
112    #[test]
113    fn test_processor_stores_and_passes_through() {
114        let mut proc = StdArraysProcessor::new();
115        let pool = NDArrayPool::new(1_000_000);
116
117        let arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
118        let result = proc.process_array(&arr, &pool);
119        assert_eq!(result.output_arrays.len(), 1);
120
121        let latest = proc.data_handle().lock().clone();
122        assert!(latest.is_some());
123    }
124
125    #[test]
126    fn test_std_arrays_runtime() {
127        let pool = Arc::new(NDArrayPool::new(1_000_000));
128        let wiring = Arc::new(WiringRegistry::new());
129        let (handle, data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
130
131        // Plugins default to disabled — enable for test
132        handle
133            .port_runtime()
134            .port_handle()
135            .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
136            .unwrap();
137        params_applied(&handle);
138
139        let rt = tokio::runtime::Builder::new_current_thread()
140            .enable_all()
141            .build()
142            .unwrap();
143        rt.block_on(handle.array_sender().publish(make_array(42)));
144        wait_until("StdArrays to store the published array", || {
145            data.lock().as_ref().is_some_and(|a| a.unique_id == 42)
146        });
147    }
148
149    #[test]
150    fn test_std_arrays_initial_array_callbacks_off() {
151        // C NDPluginStdArrays.cpp:343 sets NDArrayCallbacks=0 in the
152        // constructor; the initial param a client reads must be 0, not 1.
153        let pool = Arc::new(NDArrayPool::new(1_000_000));
154        let wiring = Arc::new(WiringRegistry::new());
155        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
156
157        let val = handle
158            .port_runtime()
159            .port_handle()
160            .read_int32_blocking(handle.ndarray_params.array_callbacks, 0)
161            .unwrap();
162        assert_eq!(val, 0, "StdArrays initial NDArrayCallbacks must be 0");
163    }
164
165    #[test]
166    fn test_terminal_plugins_do_not_do_array_callbacks() {
167        // C disables NDArrayCallbacks in the constructor of NDPluginStdArrays
168        // (:343), NDPluginAttribute (:203), and NDPluginFile (:948, base of
169        // every file writer). Each Rust counterpart overrides
170        // does_array_callbacks() to false so the initial param reflects this.
171        use crate::attribute::AttributeProcessor;
172        use crate::file_hdf5::Hdf5FileProcessor;
173        use crate::file_jpeg::JpegFileProcessor;
174        use crate::file_magick::MagickFileProcessor;
175        use crate::file_netcdf::NetcdfFileProcessor;
176        use crate::file_nexus::NexusFileProcessor;
177        use crate::file_tiff::TiffFileProcessor;
178        use crate::passthrough::PassthroughProcessor;
179
180        assert!(!StdArraysProcessor::new().does_array_callbacks());
181        assert!(!AttributeProcessor::new("attr", 1).does_array_callbacks());
182        assert!(!Hdf5FileProcessor::new().does_array_callbacks());
183        assert!(!JpegFileProcessor::new(85).does_array_callbacks());
184        assert!(!TiffFileProcessor::new().does_array_callbacks());
185        assert!(!NetcdfFileProcessor::new().does_array_callbacks());
186        assert!(!NexusFileProcessor::new().does_array_callbacks());
187        assert!(!MagickFileProcessor::new().does_array_callbacks());
188
189        // A non-terminal plugin keeps the default: it does deliver downstream.
190        assert!(PassthroughProcessor::new("NDPluginProcess").does_array_callbacks());
191    }
192
193    #[test]
194    fn test_std_arrays_serves_waveform_with_callbacks_off() {
195        // C NDPluginStdArrays fires its typed-array waveform callbacks
196        // (NDPluginStdArrays.cpp:71-73) regardless of NDArrayCallbacks — and it
197        // defaults NDArrayCallbacks=0. So the ArrayData waveform on I/O Intr
198        // must still update with NDArrayCallbacks=0. This guards against the
199        // STD_ARRAY_DATA interrupt being gated by the downstream-delivery flag.
200        use asyn_rs::param::ParamValue;
201
202        let pool = Arc::new(NDArrayPool::new(1_000_000));
203        let wiring = Arc::new(WiringRegistry::new());
204        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
205
206        let port = handle.port_runtime().port_handle();
207        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
208            .unwrap();
209        // Force NDArrayCallbacks=0 at runtime (StdArrays' C default) — the
210        // waveform output must still fire.
211        port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
212            .unwrap();
213        params_applied(&handle);
214
215        let mut rx = port.interrupts().subscribe_async();
216
217        let rt = tokio::runtime::Builder::new_current_thread()
218            .enable_all()
219            .build()
220            .unwrap();
221        rt.block_on(handle.array_sender().publish(make_array(7)));
222        // ArrayCounter==1 ⟹ the frame was processed and its param batch
223        // (including the waveform interrupts) flushed.
224        wait_until("StdArrays to process the frame", || {
225            port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
226                .is_ok_and(|v| v == 1)
227        });
228
229        // make_array(7) is a 4-element UInt8 array → the STD_ARRAY_DATA
230        // interrupt carries it as an Int8Array; the dimensions interrupt carries
231        // an Int32Array. Finding an Int8Array interrupt proves the waveform was
232        // served despite NDArrayCallbacks=0.
233        let mut served = false;
234        loop {
235            match rx.try_recv() {
236                Ok(v) => {
237                    if matches!(v.value, ParamValue::Int8Array(_)) {
238                        served = true;
239                    }
240                }
241                Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => continue,
242                Err(_) => break,
243            }
244        }
245        assert!(
246            served,
247            "StdArrays must serve STD_ARRAY_DATA with NDArrayCallbacks=0"
248        );
249    }
250
251    #[test]
252    fn test_std_arrays_throttled_frame_does_not_advance_array_counter() {
253        // C NDPluginStdArrays.cpp:202-211 decrements NDArrayCounter when its
254        // MaxByteRate throttle drops the waveform output, so a throttled frame
255        // leaves ArrayCounter unchanged (ImageJ etc. see no new data) while
256        // DroppedArrays advances.
257
258        let pool = Arc::new(NDArrayPool::new(1_000_000));
259        let wiring = Arc::new(WiringRegistry::new());
260        let (handle, _data, _jh) = create_std_arrays_runtime("IMAGE1", pool, "", wiring);
261        let port = handle.port_runtime().port_handle();
262        port.write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
263            .unwrap();
264        // MaxByteRate = 4 bytes/sec; make_array is a 4-byte UInt8 array. The
265        // bucket starts full at 4, so the first frame passes and the next
266        // (sent before a meaningful refill) is throttled.
267        port.write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 4.0)
268            .unwrap();
269        params_applied(&handle);
270
271        let rt = tokio::runtime::Builder::new_current_thread()
272            .enable_all()
273            .build()
274            .unwrap();
275
276        rt.block_on(handle.array_sender().publish(make_array(1)));
277        wait_until("first frame to be served and counted", || {
278            port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
279                .is_ok_and(|v| v == 1)
280        });
281        let counter_after_first = port
282            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
283            .unwrap();
284
285        rt.block_on(handle.array_sender().publish(make_array(2)));
286        // The throttled frame leaves ArrayCounter unchanged, so wait on the
287        // observable it DOES advance: DroppedArrays.
288        wait_until("throttled frame to advance DroppedArrays", || {
289            port.read_int32_blocking(handle.plugin_params.dropped_output_arrays, 0)
290                .is_ok_and(|v| v == 1)
291        });
292        let counter_after_second = port
293            .read_int32_blocking(handle.ndarray_params.array_counter, 0)
294            .unwrap();
295        let dropped = port
296            .read_int32_blocking(handle.plugin_params.dropped_output_arrays, 0)
297            .unwrap();
298
299        assert_eq!(counter_after_first, 1, "first frame is served and counted");
300        assert_eq!(
301            counter_after_second, 1,
302            "throttled frame must NOT advance ArrayCounter"
303        );
304        assert_eq!(dropped, 1, "throttled frame advances DroppedArrays");
305    }
306}