nsi_ffi_wrap/output/mod.rs
1#![cfg_attr(feature = "nightly", doc(cfg(feature = "output")))]
2// The double-Box (`Box<Box<dyn Fn…>>`) pattern below is required for
3// crossing the C-FFI boundary: a `Box<dyn Fn…>` is a fat pointer, and the
4// outer Box makes it thin so it fits through `void*`. Clippy doesn't
5// recognise the FFI shape and flags it as `redundant_allocation`. Same for
6// the `new` constructor that returns a `*mut`-shaped state for ndspy to
7// keep -- it's not idiomatic Rust but it IS the C-API contract.
8#![allow(clippy::redundant_allocation)]
9#![allow(clippy::new_ret_no_self)]
10//! Output driver callbacks.
11//!
12//! This module provides type-safe, generic callback support for streaming pixel
13//! data during and after renders. Callbacks are generic over pixel type (`f32`,
14//! `u16`, `u8`, etc.) with zero runtime cost via monomorphization.
15//!
16//! ## Callback Types
17//!
18//! * [`FnOpen`] -- Called once when the output driver is opened by the renderer.
19//!
20//! * [`FnWrite`] -- Called for each bucket of pixel data. Generic over pixel
21//! type `T: PixelType`. Receives only the bucket data, not the full image.
22//!
23//! * [`FnFinish`] -- Called once when the output driver is closed. Does NOT
24//! receive pixel data (matching the C ndspy API). Use [`AccumulatingCallbacks`]
25//! if you need the full accumulated image.
26//!
27//! ## Pixel Types
28//!
29//! The callbacks are generic over pixel type via the [`PixelType`] trait.
30//! Use the corresponding typed driver constant:
31//!
32//! * [`FERRIS_F32`] -- 32-bit float pixels.
33//! * [`FERRIS_U32`]/[`FERRIS_I32`] -- 32-bit integer pixels.
34//! * [`FERRIS_U16`]/[`FERRIS_I16`] -- 16-bit integer pixels.
35//! * [`FERRIS_U8`]/[`FERRIS_I8`] -- 8-bit integer pixels.
36//!
37//! ## Example: Streaming buckets
38//!
39//! ```ignore
40//! use nsi_ffi_wrap as nsi;
41//! use std::sync::{Arc, Mutex};
42//!
43//! let pixels = Arc::new(Mutex::new(Vec::<f32>::new()));
44//! let pixels_write = Arc::clone(&pixels);
45//!
46//! // Write callback receives each bucket as it's rendered.
47//! let write = nsi::output::WriteCallback::<f32>::new(
48//! move |_name, width, _height, x0, x1, y0, y1, fmt, bucket: &[f32]| {
49//! let mut buf = pixels_write.lock().unwrap();
50//! // Accumulate bucket into full image buffer...
51//! nsi::output::Error::None
52//! },
53//! );
54//!
55//! ctx.set_attribute(
56//! "driver",
57//! &[
58//! nsi::string!("drivername", nsi::output::FERRIS_F32),
59//! nsi::string!("imagefilename", "render"),
60//! nsi::callback!("callback.write", write),
61//! ],
62//! );
63//! ```
64//!
65//! ## Example: Using AccumulatingCallbacks
66//!
67//! For the common case where you need the full accumulated image at the end,
68//! use [`AccumulatingCallbacks`]:
69//!
70//! ```ignore
71//! use nsi_ffi_wrap as nsi;
72//!
73//! let (write, finish) = nsi::output::AccumulatingCallbacks::<f32>::new(
74//! |name, width, height, fmt, pixels: Vec<f32>| {
75//! // Called once with the complete accumulated image.
76//! write_exr(&name, width, height, &pixels);
77//! nsi::output::Error::None
78//! },
79//! );
80//!
81//! ctx.set_attribute(
82//! "driver",
83//! &[
84//! nsi::string!("drivername", nsi::output::FERRIS_F32),
85//! nsi::string!("imagefilename", "render"),
86//! nsi::callback!("callback.write", write),
87//! nsi::callback!("callback.finish", finish),
88//! ],
89//! );
90//! ```
91//!
92//! ## Color Profiles
93//!
94//! The pixel color data that the renderer generates is linear and
95//! scene-referred. I.e. relative to whatever units you used to describe
96//! illuminants in your scene.
97//!
98//! Using the
99//! [`"colorprofile"` attribute](https://nsi.readthedocs.io/en/latest/nodes.html?highlight=outputlayer#the-outputlayer-node)
100//! of an [`OutputLayer`](crate::OUTPUT_LAYER) you can ask the
101//! renderer to apply an [Open Color IO](https://opencolorio.org/) (OCIO)
102//! [profile/LUT](https://github.com/colour-science/OpenColorIO-Configs/tree/feature/aces-1.2-config/aces_1.2/luts)
103//! before quantizing (see below).
104//!
105//! Once OCIO has a [Rust wrapper](https://crates.io/crates/opencolorio) you can easily choose to
106//! do these color conversions yourself. In the meantime there is the
107//! [`colorspace`](https://crates.io/crates/colorspace) crate which has some useful profiles built
108//! in, e.g. [ACEScg](https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#ACEScg).
109//!
110//! ```
111//! # use nsi_ffi_wrap as nsi;
112//! # let ctx = nsi::Context::new(None).unwrap();
113//! ctx.create("beauty", nsi::OUTPUT_LAYER, None);
114//! ctx.set_attribute(
115//! "beauty",
116//! &[
117//! // The Ci variable comes from Open Shading Language.
118//! nsi::string!("variablename", "Ci"),
119//! // We want the pixel data 'display-referred' in sRGB and quantized down to 0.0..255.0.
120//! nsi::string!("colorprofile", "/home/luts/linear_to_sRGB.spi1d"),
121//! nsi::string!("scalarformat", "uint8"),
122//! ],
123//! );
124//! ```
125//!
126//! ## Quantization
127//!
128//! Using the [`"scalarformat"`
129//! attribute](https://nsi.readthedocs.io/en/latest/nodes.html?highlight=outputlayer#the-outputlayer-node)
130//! of an [`OutputLayer`](crate::OUTPUT_LAYER) you can ask the
131//! renderer to quantize data down to a suitable range. For example, setting
132//! this to `"uint16"` will get you valid `u16` values from `0.0..65535.0`, but
133//! stored in the `f32`s of the `pixel_data` buffer. The value of `1.0` will map
134//! to `65535.0` and everything above will be clipped. You can convert
135//! such a value straight via `f32 as u16`.
136//!
137//! Unless you asked the renderer to also apply some color profile (see above)
138//! the data is linear. To look good on a screen it needs to be made
139//! display-referred.
140//!
141//! See the `output` example on how to do this with a simple, display-referred
142//! `sRGB` curve.
143use crate::argument::CallbackPtr;
144use parking_lot::Mutex;
145use std::{
146 ffi::CStr,
147 mem::{size_of, take},
148 os::raw::{c_char, c_int, c_void},
149 sync::Arc,
150};
151
152pub mod pixel_format;
153pub use pixel_format::*;
154
155pub mod pixel_type;
156pub use pixel_type::*;
157
158/// Driver name for f32 pixel type.
159pub static FERRIS_F32: &str = "ferris_f32";
160/// Driver name for u32 pixel type.
161pub static FERRIS_U32: &str = "ferris_u32";
162/// Driver name for i32 pixel type.
163pub static FERRIS_I32: &str = "ferris_i32";
164/// Driver name for u16 pixel type.
165pub static FERRIS_U16: &str = "ferris_u16";
166/// Driver name for i16 pixel type.
167pub static FERRIS_I16: &str = "ferris_i16";
168/// Driver name for u8 pixel type.
169pub static FERRIS_U8: &str = "ferris_u8";
170/// Driver name for i8 pixel type.
171pub static FERRIS_I8: &str = "ferris_i8";
172
173/// Legacy driver name - defaults to f32.
174/// Deprecated: Use [`FERRIS_F32`], [`FERRIS_U16`], etc. for type-specific drivers.
175#[deprecated(
176 since = "0.9.0",
177 note = "Use FERRIS_F32, FERRIS_U16, etc. for type-specific drivers"
178)]
179pub static FERRIS: &str = "ferris_f32";
180
181/// An error type the callbacks return to communicate with the
182/// renderer.
183#[repr(u32)]
184#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, num_enum::IntoPrimitive)]
185pub enum Error {
186 /// Everything is dandy.
187 None = ndspy_sys::PtDspyError::None as _,
188 /// We ran out of memory.
189 NoMemory = ndspy_sys::PtDspyError::NoMemory as _,
190 /// We do no support this request.
191 Unsupported = ndspy_sys::PtDspyError::Unsupported as _,
192 BadParameters = ndspy_sys::PtDspyError::BadParams as _,
193 NoResource = ndspy_sys::PtDspyError::NoResource as _,
194 /// Something else went wrong.
195 Undefined = ndspy_sys::PtDspyError::Undefined as _,
196 /// Stop the render.
197 Stop = ndspy_sys::PtDspyError::Stop as _,
198}
199
200impl From<Error> for ndspy_sys::PtDspyError {
201 fn from(item: Error) -> ndspy_sys::PtDspyError {
202 match item {
203 Error::None => ndspy_sys::PtDspyError::None,
204 Error::NoMemory => ndspy_sys::PtDspyError::NoMemory,
205 Error::Unsupported => ndspy_sys::PtDspyError::Unsupported,
206 Error::BadParameters => ndspy_sys::PtDspyError::BadParams,
207 Error::NoResource => ndspy_sys::PtDspyError::NoResource,
208 Error::Undefined => ndspy_sys::PtDspyError::Undefined,
209 Error::Stop => ndspy_sys::PtDspyError::Stop,
210 }
211 }
212}
213
214/// A closure which is called once per
215/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance.
216///
217/// It is passed to ɴsɪ via the `"callback.open"` attribute on that node.
218///
219/// The closure is called once, before the renderer starts sending pixels to the
220/// output driver.
221///
222/// # Arguments
223/// The `pixel_format` parameter is an array of strings that details the
224/// composition of the `f32` data that the renderer will send to the [`FnWrite`]
225/// and/or [`FnFinish`] closures.
226///
227/// # Example
228/// ```
229/// # #[cfg(feature = "output")]
230/// # {
231/// # use nsi_ffi_wrap as nsi;
232/// # use nsi::output::PixelFormat;
233/// # let ctx = nsi::Context::new(None).unwrap();
234/// # ctx.create("display_driver", nsi::OUTPUT_DRIVER, None);
235/// let open = nsi::output::OpenCallback::new(
236/// |name: &str,
237/// width: usize,
238/// height: usize,
239/// pixel_format: &nsi::output::PixelFormat| {
240/// println!(
241/// "Resolution: {}×{}\nPixel Format:\n{:?}",
242/// width, height, pixel_format
243/// );
244/// nsi::output::Error::None
245/// },
246/// );
247/// # }
248/// ```
249pub trait FnOpen<'a>: FnMut(
250 // Filename.
251 &str,
252 // Width.
253 usize,
254 // Height.
255 usize,
256 // Pixel format.
257 &PixelFormat,
258) -> Error
259+ 'a {}
260
261#[doc(hidden)]
262impl<'a, T: FnMut(&str, usize, usize, &PixelFormat) -> Error + 'a> FnOpen<'a>
263 for T
264{
265}
266
267// FIXME once trait aliases are in stable.
268/*
269trait FnOpen<'a> = FnMut(
270 // Filename.
271 &str,
272 // Width.
273 usize,
274 // Height.
275 usize,
276 // Pixel format.
277 &PixelFormat,
278 ) -> Error
279 + 'a
280*/
281
282/// A closure which is called for each bucket of pixels the
283/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance sends
284/// during rendering.
285///
286/// The closure receives ONLY the bucket data, not an accumulated image.
287/// Bucket dimensions are: `(x_max_plus_one - x_min) x (y_max_plus_one - y_min)`.
288/// Data layout is row-major with channels interleaved per pixel.
289///
290/// It is passed to ɴsɪ via the `"callback.write"` attribute on that node.
291///
292/// # Type Parameter
293///
294/// `T` is the pixel scalar type (e.g., `f32`, `u16`, `u8`). It must implement
295/// [`PixelType`]. The driver name must match the type (e.g., `FERRIS_F32` for `f32`).
296///
297/// # Example
298/// ```ignore
299/// let write = nsi::output::WriteCallback::<f32>::new(
300/// |name: &str,
301/// width: usize,
302/// height: usize,
303/// x_min: usize,
304/// x_max_plus_one: usize,
305/// y_min: usize,
306/// y_max_plus_one: usize,
307/// pixel_format: &nsi::output::PixelFormat,
308/// bucket_data: &[f32]| {
309/// // bucket_data contains ONLY this bucket, not the full image
310/// // Upload bucket to GPU texture, etc.
311/// nsi::output::Error::None
312/// },
313/// );
314///
315/// ctx.set_attribute(
316/// "driver",
317/// &[
318/// nsi::string!("drivername", nsi::output::FERRIS_F32),
319/// nsi::callback!("callback.write", write),
320/// ],
321/// );
322/// ```
323pub trait FnWrite<'a, T: PixelType>: Fn(
324 // Filename.
325 &str,
326 // Full image width.
327 usize,
328 // Full image height.
329 usize,
330 // Bucket x_min.
331 usize,
332 // Bucket x_max_plus_one.
333 usize,
334 // Bucket y_min.
335 usize,
336 // Bucket y_max_plus_one.
337 usize,
338 // Pixel format.
339 &PixelFormat,
340 // Bucket pixel data (NOT the full image!).
341 &[T],
342 ) -> Error
343 + Sync
344 + 'a {}
345
346#[doc(hidden)]
347impl<'a, T: PixelType, F> FnWrite<'a, T> for F where
348 F: Fn(
349 &str,
350 usize,
351 usize,
352 usize,
353 usize,
354 usize,
355 usize,
356 &PixelFormat,
357 &[T],
358 ) -> Error
359 + Sync
360 + 'a
361{
362}
363
364/// A closure which is called once per
365/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance when rendering completes.
366///
367/// It is passed to ɴsɪ via the `"callback.finish"` attribute on that node.
368///
369/// **Note:** This closure does NOT receive pixel data. This matches the C ndspy
370/// API behavior where the display driver is responsible for accumulating pixels
371/// if needed. Use [`AccumulatingCallbacks`] if you need the complete image.
372///
373/// # Example
374/// ```ignore
375/// let finish = nsi::output::FinishCallback::new(
376/// |name: String,
377/// width: usize,
378/// height: usize,
379/// pixel_format: nsi::output::PixelFormat| {
380/// println!("Rendering complete: {}x{}", width, height);
381/// nsi::output::Error::None
382/// },
383/// );
384///
385/// ctx.set_attribute(
386/// "driver",
387/// &[
388/// nsi::string!("drivername", nsi::output::FERRIS_F32),
389/// nsi::callback!("callback.finish", finish),
390/// ],
391/// );
392/// ```
393pub trait FnFinish<'a>: FnMut(
394 // Filename.
395 String,
396 // Width.
397 usize,
398 // Height.
399 usize,
400 // Pixel format.
401 PixelFormat,
402) -> Error
403+ 'a {}
404
405#[doc(hidden)]
406impl<'a, F> FnFinish<'a> for F where
407 F: FnMut(String, usize, usize, PixelFormat) -> Error + 'a
408{
409}
410
411enum Query {}
412
413trait FnQuery<'a>: FnMut(Query) -> Error + 'a {}
414impl<'a, T: FnMut(Query) -> Error + 'a> FnQuery<'a> for T {}
415
416// FIXME once trait aliases are in stable.
417/*
418pub trait FnQuery<'a> = dyn FnMut(Query) -> Error + 'a;
419*/
420
421/// Wrapper to pass an [`FnOpen`] closure to an
422/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
423pub struct OpenCallback<'a>(Box<Box<dyn FnOpen<'a>>>);
424
425// A `Box<dyn Trait>` is a fat pointer (data + vtable), which cannot cross
426// the C boundary; one more `Box` makes it thin. That is the whole reason
427// for the double `Box` -- and, correspondingly, for `Box<Box<T>>` being
428// the type `extract_callback` reads back.
429//
430// This used to be a *triple* `Box`, which was compensating for
431// `Callback::as_c_ptr` handing ɴsɪ the pointer where ɴsɪ expects the
432// address of the pointer. The renderer's extra dereference consumed the
433// extra layer. With the marshalling fixed, two layers are correct; see
434// `argument::pointer_marshalling_tests` and `ffi_round_trip_tests`.
435impl<'a> OpenCallback<'a> {
436 pub fn new<F>(fn_open: F) -> Self
437 where
438 F: FnOpen<'a>,
439 {
440 OpenCallback(Box::new(Box::new(fn_open)))
441 }
442}
443
444impl CallbackPtr for OpenCallback<'_> {
445 #[doc(hidden)]
446 fn to_ptr(self) -> *const core::ffi::c_void {
447 Box::into_raw(self.0) as *const _ as _
448 }
449
450 #[doc(hidden)]
451 unsafe fn drop_ptr(ptr: *const core::ffi::c_void) {
452 // The lifetime is erased here; drop glue lives in the vtable, so
453 // reclaiming through any lifetime runs the right destructor.
454 // SAFETY: `ptr` came from `to_ptr` above and is reclaimed once.
455 drop(unsafe { Box::from_raw(ptr as *mut Box<dyn FnOpen<'static>>) });
456 }
457}
458/// Wrapper to pass an [`FnWrite`] closure to an
459/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
460///
461/// # Type Parameter
462///
463/// `T` is the pixel scalar type. Use the matching driver name:
464/// - `WriteCallback::<f32>` with `FERRIS_F32`
465/// - `WriteCallback::<u16>` with `FERRIS_U16`
466/// - etc.
467pub struct WriteCallback<'a, T: PixelType>(Box<Box<dyn FnWrite<'a, T>>>);
468
469impl<'a, T: PixelType> WriteCallback<'a, T> {
470 pub fn new<F>(fn_write: F) -> Self
471 where
472 F: FnWrite<'a, T>,
473 {
474 WriteCallback(Box::new(Box::new(fn_write)))
475 }
476}
477
478impl<T: PixelType> CallbackPtr for WriteCallback<'_, T> {
479 #[doc(hidden)]
480 fn to_ptr(self) -> *const core::ffi::c_void {
481 Box::into_raw(self.0) as *const _ as _
482 }
483
484 #[doc(hidden)]
485 unsafe fn drop_ptr(ptr: *const core::ffi::c_void) {
486 // SAFETY: `ptr` came from `to_ptr` above and is reclaimed once.
487 drop(unsafe {
488 Box::from_raw(ptr as *mut Box<dyn FnWrite<'static, T>>)
489 });
490 }
491}
492
493/// Wrapper to pass an [`FnFinish`] closure to an
494/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
495///
496/// **Note:** `FnFinish` does not receive pixel data. If you need the complete
497/// accumulated image, use [`AccumulatingCallbacks`] instead.
498pub struct FinishCallback<'a>(Box<Box<dyn FnFinish<'a>>>);
499
500impl<'a> FinishCallback<'a> {
501 pub fn new<F>(fn_finish: F) -> Self
502 where
503 F: FnFinish<'a>,
504 {
505 FinishCallback(Box::new(Box::new(fn_finish)))
506 }
507}
508
509impl CallbackPtr for FinishCallback<'_> {
510 #[doc(hidden)]
511 fn to_ptr(self) -> *const core::ffi::c_void {
512 Box::into_raw(self.0) as *const _ as _
513 }
514
515 #[doc(hidden)]
516 unsafe fn drop_ptr(ptr: *const core::ffi::c_void) {
517 // SAFETY: `ptr` came from `to_ptr` above and is reclaimed once.
518 drop(unsafe { Box::from_raw(ptr as *mut Box<dyn FnFinish<'static>>) });
519 }
520}
521
522/// Internal data structure passed through FFI as the image handle.
523/// Generic over pixel type T for zero-cost type handling.
524struct DisplayData<'a, T: PixelType> {
525 name: String,
526 width: usize,
527 height: usize,
528 pixel_format: PixelFormat,
529 // NO pixel_data buffer - we pass buckets directly to callbacks
530 // Non-owning: these point into the renderer's memory (see
531 // `extract_callback`). Dropping them would free memory we do not own.
532 fn_write: Option<*mut Box<dyn FnWrite<'a, T>>>,
533 fn_finish: Option<*mut Box<dyn FnFinish<'a>>>,
534 // FIXME: unused atm.
535 #[allow(dead_code)]
536 fn_query: Option<*mut Box<dyn FnQuery<'a>>>,
537 // PhantomData to ensure T is used
538 _phantom: std::marker::PhantomData<T>,
539}
540
541/// Reads a callback pointer back out of the parameters ndspy hands the
542/// driver.
543///
544/// `UserParameter::value` is a `void**`: it addresses the renderer's own
545/// copy of the pointer we passed to ɴsɪ. So we read the pointer *out of*
546/// that cell and return it without taking ownership -- the cell belongs to
547/// the renderer, and `Box::from_raw`-ing it frees memory we never
548/// allocated (`free(): invalid pointer`).
549fn extract_callback<T: ?Sized>(
550 name: &str,
551 type_: u8,
552 len: usize,
553 parameters: &[ndspy_sys::UserParameter],
554) -> Option<*mut Box<T>> {
555 for p in parameters.iter() {
556 // SAFETY: Parameter names come from NSI API and should be valid C strings
557 if p.name.is_null() {
558 continue;
559 }
560 let p_name = match unsafe { CStr::from_ptr(p.name) }.to_str() {
561 Ok(name) => name,
562 Err(_) => continue,
563 };
564
565 if name == p_name
566 && type_ == p.valueType as _
567 && len == p.valueCount as _
568 {
569 if !p.value.is_null() {
570 // SAFETY: `value` addresses the renderer's copy of the
571 // pointer produced by the callback's `to_ptr`, and the
572 // parameter type has been verified above. We copy that
573 // pointer out; ownership stays with whoever allocated it.
574 return Some(unsafe { *(p.value as *const *mut Box<T>) });
575 } else {
576 // Parameter exists but value is missing - exit quietly.
577 break;
578 }
579 }
580 }
581 None
582}
583
584// Generic trampoline function for the FnOpen callback.
585// Each instantiation (image_open::<f32>, image_open::<u16>, etc.) handles a specific pixel type.
586pub(crate) extern "C" fn image_open<T: PixelType>(
587 image_handle_ptr: *mut ndspy_sys::PtDspyImageHandle,
588 _driver_name: *const c_char,
589 output_filename: *const c_char,
590 width: c_int,
591 height: c_int,
592 parameters_count: c_int,
593 parameters: *const ndspy_sys::UserParameter,
594 format_count: c_int,
595 format: *mut ndspy_sys::PtDspyDevFormat,
596 flag_stuff: *mut ndspy_sys::PtFlagStuff,
597) -> ndspy_sys::PtDspyError {
598 // Catch any panics to prevent unwinding into C code.
599 match std::panic::catch_unwind(|| {
600 if (image_handle_ptr.is_null())
601 || (output_filename.is_null())
602 || format.is_null()
603 || (format_count <= 0)
604 || ((parameters_count > 0) && parameters.is_null())
605 {
606 return Error::BadParameters.into();
607 }
608
609 // SAFETY: We only read from the parameters slice, never modify it.
610 let parameters = unsafe {
611 std::slice::from_raw_parts(parameters, parameters_count as _)
612 };
613
614 let mut display_data = Box::new(DisplayData::<T> {
615 name: {
616 // SAFETY: output_filename is checked for null above and comes from NSI C API
617 let c_str = unsafe { CStr::from_ptr(output_filename) };
618 c_str.to_string_lossy().into_owned()
619 },
620 width: width as _,
621 height: height as _,
622 pixel_format: PixelFormat::default(),
623 // NO pixel_data allocation - we pass buckets directly
624 fn_write: extract_callback::<dyn FnWrite<T>>(
625 "callback.write",
626 b'p',
627 1,
628 parameters,
629 ),
630 fn_finish: extract_callback::<dyn FnFinish>(
631 "callback.finish",
632 b'p',
633 1,
634 parameters,
635 ),
636 fn_query: None,
637 _phantom: std::marker::PhantomData,
638 });
639
640 // SAFETY: format is a valid pointer to format_count elements from NSI C API.
641 let format = unsafe {
642 std::slice::from_raw_parts_mut(format, format_count as _)
643 };
644
645 // Set format to the requested pixel type T
646 format.iter_mut().for_each(|f| f.type_ = T::NDSPY_TYPE);
647
648 display_data.pixel_format = PixelFormat::new(format);
649
650 let error = if let Some(fn_open) =
651 extract_callback::<dyn FnOpen>("callback.open", b'p', 1, parameters)
652 {
653 // SAFETY: `fn_open` is the pointer the caller handed to ɴsɪ via
654 // `callback!`; it stays alive for as long as the node holds the
655 // attribute. We borrow it, we do not own it.
656 let fn_open = unsafe { &mut *fn_open };
657 fn_open(
658 &display_data.name,
659 width as _,
660 height as _,
661 &display_data.pixel_format,
662 )
663 } else {
664 Error::None
665 };
666
667 // SAFETY: image_handle_ptr and flag_stuff are valid pointers from NSI C API
668 unsafe {
669 *image_handle_ptr = Box::into_raw(display_data) as _;
670 // Preserve renderer-provided flags, but clear the empty-bucket request.
671 (*flag_stuff).flags &=
672 !(ndspy_sys::PkDspyFlagsWantsEmptyBuckets as i32);
673 }
674
675 error.into()
676 }) {
677 Ok(result) => result,
678 Err(_) => {
679 // If we panicked, return an error to the renderer
680 Error::Undefined.into()
681 }
682 }
683}
684
685// FIXME: this will be used for a FnProgress callback later.
686#[unsafe(no_mangle)]
687pub(crate) extern "C" fn image_query(
688 _image_handle_ptr: ndspy_sys::PtDspyImageHandle,
689 query_type: ndspy_sys::PtDspyQueryType,
690 data_len: c_int,
691 data: *mut c_void,
692) -> ndspy_sys::PtDspyError {
693 // Catch any panics to prevent unwinding into C code.
694 match std::panic::catch_unwind(|| {
695 match query_type {
696 ndspy_sys::PtDspyQueryType::RenderProgress => {
697 if (data_len as usize)
698 < core::mem::size_of::<ndspy_sys::PtDspyRenderProgressFuncPtr>()
699 || data.is_null()
700 {
701 Error::BadParameters
702 } else {
703 // SAFETY: data is a valid pointer to PtDspyRenderProgressFuncPtr
704 // as specified by the query type and validated by data_len check.
705 unsafe {
706 let func_ptr = data as *mut ndspy_sys::PtDspyRenderProgressFuncPtr;
707 *func_ptr = Some(image_progress);
708 }
709 Error::None
710 }
711 }
712 ndspy_sys::PtDspyQueryType::Progressive => {
713 if (data_len as usize) < size_of::<ndspy_sys::PtDspyProgressiveInfo>()
714 || data.is_null()
715 {
716 Error::BadParameters
717 } else {
718 // SAFETY: data points to PtDspyProgressiveInfo with validated length.
719 unsafe {
720 let info = data as *mut ndspy_sys::PtDspyProgressiveInfo;
721 (*info).acceptProgressive = 1;
722 }
723 Error::None
724 }
725 }
726 ndspy_sys::PtDspyQueryType::Thread => {
727 if (data_len as usize) < size_of::<ndspy_sys::PtDspyThreadInfo>()
728 || data.is_null()
729 {
730 Error::BadParameters
731 } else {
732 // SAFETY: data points to PtDspyThreadInfo with validated length.
733 unsafe {
734 let info = data as *mut ndspy_sys::PtDspyThreadInfo;
735 // Allow multithreaded buckets.
736 (*info).multithread = 1;
737 }
738 Error::None
739 }
740 }
741 ndspy_sys::PtDspyQueryType::Cooked => {
742 if (data_len as usize) < size_of::<ndspy_sys::PtDspyCookedInfo>()
743 || data.is_null()
744 {
745 Error::BadParameters
746 } else {
747 // SAFETY: data points to PtDspyCookedInfo with validated length.
748 unsafe {
749 let info = data as *mut ndspy_sys::PtDspyCookedInfo;
750 // Accept filtered pixel data (cooked=1 = PkDspyCQDefault).
751 (*info).cooked = 1;
752 }
753 Error::None
754 }
755 }
756 // StopQuery asks "should rendering stop?" - return None to continue.
757 ndspy_sys::PtDspyQueryType::Stop => Error::None,
758 ndspy_sys::PtDspyQueryType::Overwrite => {
759 if (data_len as usize) < size_of::<ndspy_sys::PtDspyOverwriteInfo>()
760 || data.is_null()
761 {
762 Error::BadParameters
763 } else {
764 // SAFETY: data points to PtDspyOverwriteInfo with validated length.
765 unsafe {
766 let info = data as *mut ndspy_sys::PtDspyOverwriteInfo;
767 // Allow the renderer to overwrite existing files.
768 (*info).overwrite = 1;
769 }
770 Error::None
771 }
772 }
773 _ => Error::Unsupported,
774 }
775 .into()
776 }) {
777 Ok(result) => result,
778 Err(_) => {
779 // If we panicked, return an error to the renderer
780 Error::Undefined.into()
781 }
782 }
783}
784
785// Generic trampoline function for the FnWrite callback.
786// Passes bucket data directly to callback - NO accumulation, NO memcpy to full buffer.
787pub(crate) extern "C" fn image_write<T: PixelType>(
788 image_handle_ptr: ndspy_sys::PtDspyImageHandle,
789 x_min: c_int,
790 x_max_plus_one: c_int,
791 y_min: c_int,
792 y_max_plus_one: c_int,
793 _entry_size: c_int,
794 pixel_data: *const u8,
795) -> ndspy_sys::PtDspyError {
796 // Catch any panics to prevent unwinding into C code
797 match std::panic::catch_unwind(|| {
798 // SAFETY: image_handle_ptr should be valid as it was created by image_open
799 if image_handle_ptr.is_null() {
800 return Error::BadParameters.into();
801 }
802 // Shared, not exclusive. We answer `multithread = 1`, so the
803 // renderer may be inside this call on another thread; `&mut` here
804 // would be two exclusive references to one `DisplayData`. Every
805 // field read below is just that -- a read.
806 let display_data =
807 unsafe { &*(image_handle_ptr as *const DisplayData<T>) };
808
809 let channels = display_data.pixel_format.channels();
810 let bucket_width = (x_max_plus_one - x_min) as usize;
811 let bucket_height = (y_max_plus_one - y_min) as usize;
812 let bucket_pixel_count = bucket_width * bucket_height * channels;
813
814 // SAFETY: pixel_data comes from the renderer and should be valid
815 if pixel_data.is_null() {
816 return Error::None.into();
817 }
818
819 // Zero-cost slice creation - just pointer reinterpretation, no copy!
820 let bucket_data = unsafe {
821 std::slice::from_raw_parts(
822 pixel_data as *const T,
823 bucket_pixel_count,
824 )
825 };
826
827 // Pass bucket directly to callback - NO memcpy, NO accumulation!
828 if let Some(fn_write) = display_data.fn_write {
829 // Shared, not exclusive: we answer `multithread = 1` to
830 // `PkThreadQuery`, so the renderer may be inside this call on
831 // another thread right now. `FnWrite` is `Fn + Sync` for
832 // exactly that reason -- taking `&mut` here would be two
833 // exclusive references to one closure.
834 // SAFETY: borrowed, renderer-owned; see `extract_callback`.
835 let fn_write = unsafe { &*fn_write };
836 fn_write(
837 &display_data.name,
838 display_data.width,
839 display_data.height,
840 x_min as _,
841 x_max_plus_one as _,
842 y_min as _,
843 y_max_plus_one as _,
844 &display_data.pixel_format,
845 bucket_data, // Just the bucket!
846 )
847 } else {
848 Error::None
849 }
850 .into()
851 }) {
852 Ok(result) => result,
853 Err(_) => {
854 // If we panicked, return an error to the renderer
855 Error::Undefined.into()
856 }
857 }
858}
859
860// Generic trampoline function for the FnFinish callback.
861// FnFinish does NOT receive pixel data - user accumulates if needed.
862pub(crate) extern "C" fn image_close<T: PixelType>(
863 image_handle_ptr: ndspy_sys::PtDspyImageHandle,
864) -> ndspy_sys::PtDspyError {
865 // Catch any panics to prevent unwinding into C code
866 match std::panic::catch_unwind(|| {
867 // SAFETY: image_handle_ptr should be valid as it was created by image_open
868 if image_handle_ptr.is_null() {
869 return Error::BadParameters.into();
870 }
871 let mut display_data =
872 unsafe { Box::from_raw(image_handle_ptr as *mut DisplayData<T>) };
873
874 // FnFinish receives no pixel data - user accumulates if needed
875 let error = if let Some(fn_finish) = display_data.fn_finish {
876 // SAFETY: borrowed, renderer-owned; see `extract_callback`.
877 let fn_finish = unsafe { &mut *fn_finish };
878 fn_finish(
879 std::mem::take(&mut display_data.name),
880 display_data.width,
881 display_data.height,
882 std::mem::take(&mut display_data.pixel_format),
883 )
884 } else {
885 Error::None
886 };
887
888 // The callback fields are non-owning pointers into renderer memory,
889 // so there is nothing to release here. The double-free this block
890 // used to guard against was `extract_callback` claiming ownership
891 // of the renderer's pointer cell; see its documentation.
892
893 error.into()
894 }) {
895 Ok(result) => result,
896 Err(_) => {
897 // If we panicked, return an error to the renderer
898 Error::Undefined.into()
899 }
900 }
901}
902
903#[unsafe(no_mangle)]
904extern "C" fn image_progress(
905 _image_handle_ptr: ndspy_sys::PtDspyImageHandle,
906 _progress: f32,
907) -> ndspy_sys::PtDspyError {
908 // Progress logging disabled to reduce spam
909 Error::None.into()
910}
911
912/// Helper for users who want the complete accumulated image at finish time.
913///
914/// The core API passes buckets directly to callbacks without accumulation.
915/// This helper provides the common use case of receiving the complete image
916/// when rendering finishes.
917///
918/// # Example
919///
920/// ```ignore
921/// use std::sync::{Arc, Mutex};
922///
923/// // Create accumulating callbacks that deliver the full image at finish
924/// let (write, finish) = nsi::output::AccumulatingCallbacks::<f32>::new(
925/// |name, width, height, format, pixels| {
926/// // Called once at end with complete accumulated image
927/// save_image(&name, width, height, &pixels);
928/// nsi::output::Error::None
929/// },
930/// );
931///
932/// ctx.set_attribute(
933/// "driver",
934/// &[
935/// nsi::string!("drivername", nsi::output::FERRIS_F32),
936/// nsi::callback!("callback.write", write),
937/// nsi::callback!("callback.finish", finish),
938/// ],
939/// );
940/// ```
941pub struct AccumulatingCallbacks<T: PixelType> {
942 _phantom: std::marker::PhantomData<T>,
943}
944
945impl<T: PixelType> AccumulatingCallbacks<T> {
946 /// Create a pair of callbacks that accumulate pixels and deliver the
947 /// complete image to the finish callback.
948 ///
949 /// Returns `(WriteCallback, FinishCallback)` where:
950 /// - The write callback accumulates buckets into an internal buffer
951 /// - The finish callback delivers the complete buffer to your closure
952 pub fn new<'a, F>(
953 mut on_finish: F,
954 ) -> (WriteCallback<'a, T>, FinishCallback<'a>)
955 where
956 F: FnMut(String, usize, usize, PixelFormat, Vec<T>) -> Error + 'a,
957 {
958 // Shared state between write and finish callbacks.
959 struct AccumState<T: PixelType> {
960 buffer: Vec<T>,
961 width: usize,
962 height: usize,
963 channels: usize,
964 initialized: bool,
965 }
966
967 let state = Arc::new(Mutex::new(AccumState::<T> {
968 buffer: Vec::new(),
969 width: 0,
970 height: 0,
971 channels: 0,
972 initialized: false,
973 }));
974
975 let write_state = state.clone();
976 let finish_state = state;
977
978 let write = WriteCallback::new(
979 move |_name: &str,
980 width: usize,
981 height: usize,
982 x_min: usize,
983 x_max_plus_one: usize,
984 y_min: usize,
985 y_max_plus_one: usize,
986 format: &PixelFormat,
987 bucket_data: &[T]| {
988 let mut state = write_state.lock();
989
990 // Initialize on first bucket
991 if !state.initialized {
992 state.width = width;
993 state.height = height;
994 state.channels = format.channels();
995 state.buffer =
996 vec![T::default(); width * height * state.channels];
997 state.initialized = true;
998 }
999
1000 // Copy bucket into the full buffer
1001 let bucket_width = x_max_plus_one - x_min;
1002 let channels = state.channels;
1003
1004 for y in y_min..y_max_plus_one {
1005 let src_start = (y - y_min) * bucket_width * channels;
1006 let dst_start = (y * state.width + x_min) * channels;
1007 let row_len = bucket_width * channels;
1008
1009 state.buffer[dst_start..dst_start + row_len]
1010 .copy_from_slice(
1011 &bucket_data[src_start..src_start + row_len],
1012 );
1013 }
1014
1015 Error::None
1016 },
1017 );
1018
1019 let finish = FinishCallback::new(
1020 move |name: String,
1021 width: usize,
1022 height: usize,
1023 format: PixelFormat| {
1024 let mut state = finish_state.lock();
1025 let buffer = take(&mut state.buffer);
1026 // Release the lock before calling the user's callback.
1027 drop(state);
1028
1029 on_finish(name, width, height, format, buffer)
1030 },
1031 );
1032
1033 (write, finish)
1034 }
1035}
1036
1037/// End-to-end shape of a callback's journey through the FFI boundary.
1038///
1039/// The driver trampolines are pure Rust -- they never call out through the
1040/// C API -- so Miri can execute the whole lifecycle, which is what makes
1041/// these a proof rather than a smoke test. Run them with:
1042///
1043/// ```text
1044/// cargo +nightly miri test -p nsi-ffi-wrap --features output --lib -- \
1045/// pointer_marshalling ffi_round_trip
1046/// ```
1047#[cfg(test)]
1048mod ffi_round_trip_tests {
1049 use super::*;
1050 use crate::{Arg, ArgData, Callback, argument::to_c_param_vec};
1051 use nsi_sys::NSIParam;
1052 use std::{
1053 cell::Cell,
1054 ffi::CString,
1055 rc::Rc,
1056 sync::{
1057 Arc,
1058 atomic::{AtomicUsize, Ordering},
1059 },
1060 };
1061
1062 /// One ɴsɪ parameter as it reaches a display driver.
1063 ///
1064 /// Models the two hops between `set_attribute` and `DspyImageOpen`:
1065 ///
1066 /// 1. ɴsɪ copies one `void*` **out of** `NSIParam::data` and keeps its
1067 /// own copy -- `data` addresses the value, it is not the value.
1068 /// 2. ndspy hands the driver the address of *that copy*, so
1069 /// `UserParameter::value` is a `void**` into renderer memory.
1070 ///
1071 /// Verified against lib3delight 2.9.208 with a probe driver.
1072 struct RendererParam {
1073 name: CString,
1074 /// The renderer's own copy of the pointer. Boxed so its address is
1075 /// stable and distinct from ours, exactly like renderer memory.
1076 cell: Box<*const c_void>,
1077 }
1078
1079 impl RendererParam {
1080 fn new(name: &str, param: &NSIParam) -> Self {
1081 // SAFETY: `data` addresses one `NSITypePointer` value.
1082 let value = unsafe { *(param.data as *const *const c_void) };
1083 Self {
1084 name: CString::new(name).unwrap(),
1085 cell: Box::new(value),
1086 }
1087 }
1088
1089 fn as_user_parameter(&self) -> ndspy_sys::UserParameter {
1090 ndspy_sys::UserParameter {
1091 name: self.name.as_ptr(),
1092 valueType: b'p' as _,
1093 valueCount: 1,
1094 value: &*self.cell as *const *const c_void as *const c_void,
1095 nbytes: core::mem::size_of::<*const c_void>() as _,
1096 }
1097 }
1098 }
1099
1100 /// Marshals `args` and returns them as the driver would see them.
1101 fn through_the_renderer(args: &[Arg]) -> Vec<RendererParam> {
1102 let (_, _, params) = to_c_param_vec(Some(args));
1103 args.iter()
1104 .zip(params.iter())
1105 .map(|(arg, param)| RendererParam::new(arg.name.as_str(), param))
1106 .collect()
1107 }
1108
1109 #[test]
1110 fn open_callback_survives_the_round_trip() {
1111 let ran = Rc::new(Cell::new(false));
1112 let flag = Rc::clone(&ran);
1113
1114 let args = [Arg::new(
1115 "callback.open",
1116 ArgData::from(Callback::new(OpenCallback::new(
1117 move |_: &str, _: usize, _: usize, _: &PixelFormat| {
1118 flag.set(true);
1119 Error::None
1120 },
1121 ))),
1122 )];
1123 let seen = through_the_renderer(&args);
1124 let user_params: Vec<_> =
1125 seen.iter().map(RendererParam::as_user_parameter).collect();
1126
1127 let recovered = extract_callback::<dyn FnOpen>(
1128 "callback.open",
1129 b'p',
1130 1,
1131 &user_params,
1132 )
1133 .expect("the callback must be recoverable from the parameters");
1134
1135 {
1136 // Borrow, exactly as the driver trampolines do. Owning this
1137 // would free the renderer's cell.
1138 // SAFETY: `recovered` is the pointer we handed to ɴsɪ above.
1139 let callback = unsafe { &mut *recovered };
1140 assert_eq!(
1141 Error::None,
1142 callback("beauty", 4, 4, &PixelFormat::default())
1143 );
1144 }
1145 assert!(
1146 ran.get(),
1147 "the closure handed to ɴsɪ must be the one invoked"
1148 );
1149
1150 // The round trip must be lossless: what comes back is the very
1151 // pointer that went in, so its original owner can still reclaim it.
1152 // Miri's leak check is the assertion here.
1153 // SAFETY: nothing else owns this allocation.
1154 drop(unsafe { Box::from_raw(recovered) });
1155 }
1156
1157 /// We answer `PkThreadQuery` with `multithread = 1`, so prove we
1158 /// honour it: drive `image_write` from two threads at once on one
1159 /// handle.
1160 ///
1161 /// `PkThreadQuery` is a 3Delight extension (`ndspy.h:167`) -- standard
1162 /// ndspy has no thread negotiation and the renderer serialises calls
1163 /// into the driver. Opting in is what makes `FnWrite: Fn + Sync`
1164 /// necessary: `image_write` takes a *shared* reference to one closure
1165 /// that two threads may be inside simultaneously. Taking `&mut` there
1166 /// -- which this crate did until this test existed -- is two exclusive
1167 /// references to the same `FnMut`.
1168 ///
1169 /// Run under Miri, whose data-race detector is the actual assertion:
1170 ///
1171 /// ```text
1172 /// cargo +nightly miri test -p nsi-ffi-wrap --features output --lib \
1173 /// -- concurrent_buckets
1174 /// ```
1175 #[test]
1176 fn concurrent_buckets_are_honoured_not_just_advertised() {
1177 let mut info = ndspy_sys::PtDspyThreadInfo { multithread: 0 };
1178 let error = image_query(
1179 core::ptr::null_mut(),
1180 ndspy_sys::PtDspyQueryType::Thread,
1181 core::mem::size_of::<ndspy_sys::PtDspyThreadInfo>() as _,
1182 &mut info as *mut _ as *mut c_void,
1183 );
1184 assert_eq!(ndspy_sys::PtDspyError::None as u32, error as u32);
1185 assert_eq!(
1186 1, info.multithread,
1187 "we advertise concurrent buckets; the rest of this test is \
1188 what earns the right to"
1189 );
1190
1191 let seen = Arc::new(AtomicUsize::new(0));
1192 let counted = Arc::clone(&seen);
1193
1194 let args = [Arg::new(
1195 "callback.write",
1196 ArgData::from(Callback::new(WriteCallback::<f32>::new(
1197 move |_: &str,
1198 _: usize,
1199 _: usize,
1200 _: usize,
1201 _: usize,
1202 _: usize,
1203 _: usize,
1204 _: &PixelFormat,
1205 pixels: &[f32]| {
1206 assert!(pixels.iter().all(|p| *p == 0.5));
1207 counted.fetch_add(1, Ordering::SeqCst);
1208 Error::None
1209 },
1210 ))),
1211 )];
1212 let params = through_the_renderer(&args);
1213 let user_params: Vec<_> = params
1214 .iter()
1215 .map(RendererParam::as_user_parameter)
1216 .collect();
1217
1218 let layer_name = CString::new("beauty.000").unwrap();
1219 let mut format = [ndspy_sys::PtDspyDevFormat {
1220 name: layer_name.as_ptr(),
1221 type_: 0,
1222 }];
1223 let mut flags = ndspy_sys::PtFlagStuff { flags: 0 };
1224 let filename = CString::new("render").unwrap();
1225 let driver = CString::new("ferris_f32").unwrap();
1226 let mut handle: ndspy_sys::PtDspyImageHandle = core::ptr::null_mut();
1227
1228 assert_eq!(
1229 ndspy_sys::PtDspyError::None as u32,
1230 image_open::<f32>(
1231 &mut handle,
1232 driver.as_ptr(),
1233 filename.as_ptr(),
1234 4,
1235 4,
1236 user_params.len() as _,
1237 user_params.as_ptr(),
1238 format.len() as _,
1239 format.as_mut_ptr(),
1240 &mut flags,
1241 ) as u32
1242 );
1243
1244 let channels = PixelFormat::new(&format).channels();
1245 let bucket = vec![0.5f32; 2 * 2 * channels];
1246
1247 // The renderer hands different regions to different threads; the
1248 // handle and the closure behind it are shared. Carried in a
1249 // newtype rather than as a `usize`, which would launder away the
1250 // pointer's provenance and blind Miri to exactly what we are
1251 // asking it to check.
1252 struct Shared(ndspy_sys::PtDspyImageHandle);
1253 // SAFETY: this is the renderer's contract under `multithread = 1`
1254 // -- the handle is used from several threads at once.
1255 unsafe impl Sync for Shared {}
1256 let shared = Shared(handle);
1257
1258 std::thread::scope(|scope| {
1259 for y in 0..2 {
1260 let bucket = &bucket;
1261 let shared = &shared;
1262 scope.spawn(move || {
1263 let error = image_write::<f32>(
1264 shared.0,
1265 0,
1266 2,
1267 y * 2,
1268 y * 2 + 2,
1269 core::mem::size_of::<f32>() as _,
1270 bucket.as_ptr() as *const u8,
1271 );
1272 assert_eq!(
1273 ndspy_sys::PtDspyError::None as u32,
1274 error as u32
1275 );
1276 });
1277 }
1278 });
1279
1280 assert_eq!(
1281 2,
1282 seen.load(Ordering::SeqCst),
1283 "both concurrent buckets must reach the callback"
1284 );
1285
1286 assert_eq!(
1287 ndspy_sys::PtDspyError::None as u32,
1288 image_close::<f32>(handle) as u32
1289 );
1290 // SAFETY: reclaim the one callback we handed over.
1291 unsafe { WriteCallback::<f32>::drop_ptr(*params[0].cell) };
1292 }
1293
1294 /// The whole driver lifecycle, through the real trampolines: open, a
1295 /// bucket of pixels, then close. Nothing here calls out through the C
1296 /// API, so Miri executes every step and checks the memory it touches.
1297 #[test]
1298 fn the_driver_lifecycle_is_sound_end_to_end() {
1299 let opened = Rc::new(Cell::new(false));
1300 // Not `Rc<Cell<_>>`: `FnWrite` is `Fn + Sync`, because the
1301 // renderer may call the write path concurrently.
1302 let buckets = Arc::new(AtomicUsize::new(0));
1303 let finished = Rc::new(Cell::new(false));
1304
1305 let open_flag = Rc::clone(&opened);
1306 let write_count = Arc::clone(&buckets);
1307 let finish_flag = Rc::clone(&finished);
1308
1309 let args = [
1310 Arg::new(
1311 "callback.open",
1312 ArgData::from(Callback::new(OpenCallback::new(
1313 move |_: &str, w: usize, h: usize, _: &PixelFormat| {
1314 assert_eq!((4, 4), (w, h));
1315 open_flag.set(true);
1316 Error::None
1317 },
1318 ))),
1319 ),
1320 Arg::new(
1321 "callback.write",
1322 ArgData::from(Callback::new(WriteCallback::<f32>::new(
1323 move |_: &str,
1324 _: usize,
1325 _: usize,
1326 x_min: usize,
1327 x_max: usize,
1328 y_min: usize,
1329 y_max: usize,
1330 format: &PixelFormat,
1331 pixels: &[f32]| {
1332 // The bucket must be exactly the region announced,
1333 // one f32 per channel per pixel.
1334 assert_eq!(
1335 (x_max - x_min)
1336 * (y_max - y_min)
1337 * format.channels(),
1338 pixels.len()
1339 );
1340 assert!(pixels.iter().all(|p| *p == 0.5));
1341 write_count.fetch_add(1, Ordering::SeqCst);
1342 Error::None
1343 },
1344 ))),
1345 ),
1346 Arg::new(
1347 "callback.finish",
1348 ArgData::from(Callback::new(FinishCallback::new(
1349 move |_: String, _: usize, _: usize, _: PixelFormat| {
1350 finish_flag.set(true);
1351 Error::None
1352 },
1353 ))),
1354 ),
1355 ];
1356 let seen = through_the_renderer(&args);
1357 let user_params: Vec<_> =
1358 seen.iter().map(RendererParam::as_user_parameter).collect();
1359
1360 // One single-channel layer, named the way ndspy names them.
1361 let layer_name = CString::new("beauty.000").unwrap();
1362 let mut format = [ndspy_sys::PtDspyDevFormat {
1363 name: layer_name.as_ptr(),
1364 type_: 0,
1365 }];
1366 let mut flags = ndspy_sys::PtFlagStuff { flags: 0 };
1367 let filename = CString::new("render").unwrap();
1368 let driver = CString::new("ferris_f32").unwrap();
1369 let mut handle: ndspy_sys::PtDspyImageHandle = core::ptr::null_mut();
1370
1371 let error = image_open::<f32>(
1372 &mut handle,
1373 driver.as_ptr(),
1374 filename.as_ptr(),
1375 4,
1376 4,
1377 user_params.len() as _,
1378 user_params.as_ptr(),
1379 format.len() as _,
1380 format.as_mut_ptr(),
1381 &mut flags,
1382 );
1383 assert_eq!(
1384 ndspy_sys::PtDspyError::None as u32,
1385 error as u32,
1386 "image_open must accept the parameters"
1387 );
1388 assert!(opened.get(), "the open callback must have run");
1389 assert!(!handle.is_null(), "image_open must hand back a handle");
1390
1391 // One 2x2 bucket, sized from the format the driver settled on.
1392 // Getting this wrong is a buffer overread, which is precisely the
1393 // kind of thing running this under Miri is meant to catch.
1394 let channels = PixelFormat::new(&format).channels();
1395 let bucket = vec![0.5f32; 2 * 2 * channels];
1396 let error = image_write::<f32>(
1397 handle,
1398 0,
1399 2,
1400 0,
1401 2,
1402 core::mem::size_of::<f32>() as _,
1403 bucket.as_ptr() as *const u8,
1404 );
1405 assert_eq!(ndspy_sys::PtDspyError::None as u32, error as u32);
1406 assert_eq!(
1407 1,
1408 buckets.load(Ordering::SeqCst),
1409 "the write callback must have run once"
1410 );
1411
1412 let error = image_close::<f32>(handle);
1413 assert_eq!(ndspy_sys::PtDspyError::None as u32, error as u32);
1414 assert!(finished.get(), "the finish callback must have run");
1415
1416 // Reclaim what we handed over -- the context does this in real use.
1417 // Miri's leak check verifies we get all of it back, and that
1418 // `image_close` did not free the renderer's memory on the way.
1419 for param in &seen {
1420 // SAFETY: each cell holds the pointer we produced above, and
1421 // each is reclaimed exactly once.
1422 unsafe {
1423 let ptr = *param.cell;
1424 if param.name.as_c_str() == c"callback.open" {
1425 OpenCallback::drop_ptr(ptr);
1426 } else if param.name.as_c_str() == c"callback.write" {
1427 WriteCallback::<f32>::drop_ptr(ptr);
1428 } else {
1429 FinishCallback::drop_ptr(ptr);
1430 }
1431 }
1432 }
1433 }
1434}