nsi_ffi_wrap/output/mod.rs
1#![cfg_attr(feature = "nightly", doc(cfg(feature = "output")))]
2// The triple-Box (`Box<Box<Box<dyn Fn…>>>`) pattern below is required for
3// crossing the C-FFI boundary: the outer Box turns the trait object into a
4// thin pointer, the middle Box is captured by ndspy as `void*`, and the
5// inner Box is what the user originally allocated. Clippy doesn't recognise
6// the FFI shape and flags it as `redundant_allocation`. Same for the `new`
7// constructor that returns a `*mut`-shaped state for ndspy to keep — it's
8// not idiomatic Rust but it IS the C-API contract.
9#![allow(clippy::redundant_allocation)]
10#![allow(clippy::new_ret_no_self)]
11//! Output driver callbacks.
12//!
13//! This module provides type-safe, generic callback support for streaming pixel
14//! data during and after renders. Callbacks are generic over pixel type (`f32`,
15//! `u16`, `u8`, etc.) with zero runtime cost via monomorphization.
16//!
17//! ## Callback Types
18//!
19//! * [`FnOpen`] -- Called once when the output driver is opened by the renderer.
20//!
21//! * [`FnWrite`] -- Called for each bucket of pixel data. Generic over pixel
22//! type `T: PixelType`. Receives only the bucket data, not the full image.
23//!
24//! * [`FnFinish`] -- Called once when the output driver is closed. Does NOT
25//! receive pixel data (matching the C ndspy API). Use [`AccumulatingCallbacks`]
26//! if you need the full accumulated image.
27//!
28//! ## Pixel Types
29//!
30//! The callbacks are generic over pixel type via the [`PixelType`] trait.
31//! Use the corresponding typed driver constant:
32//!
33//! * [`FERRIS_F32`] -- 32-bit float pixels.
34//! * [`FERRIS_U32`] / [`FERRIS_I32`] -- 32-bit integer pixels.
35//! * [`FERRIS_U16`] / [`FERRIS_I16`] -- 16-bit integer pixels.
36//! * [`FERRIS_U8`] / [`FERRIS_I8`] -- 8-bit integer pixels.
37//!
38//! ## Example: Streaming buckets
39//!
40//! ```ignore
41//! use nsi_ffi_wrap as nsi;
42//! use std::sync::{Arc, Mutex};
43//!
44//! let pixels = Arc::new(Mutex::new(Vec::<f32>::new()));
45//! let pixels_write = Arc::clone(&pixels);
46//!
47//! // Write callback receives each bucket as it's rendered.
48//! let write = nsi::output::WriteCallback::<f32>::new(
49//! move |_name, width, _height, x0, x1, y0, y1, fmt, bucket: &[f32]| {
50//! let mut buf = pixels_write.lock().unwrap();
51//! // Accumulate bucket into full image buffer...
52//! nsi::output::Error::None
53//! },
54//! );
55//!
56//! ctx.set_attribute(
57//! "driver",
58//! &[
59//! nsi::string!("drivername", nsi::output::FERRIS_F32),
60//! nsi::string!("imagefilename", "render"),
61//! nsi::callback!("callback.write", write),
62//! ],
63//! );
64//! ```
65//!
66//! ## Example: Using AccumulatingCallbacks
67//!
68//! For the common case where you need the full accumulated image at the end,
69//! use [`AccumulatingCallbacks`]:
70//!
71//! ```ignore
72//! use nsi_ffi_wrap as nsi;
73//!
74//! let (write, finish) = nsi::output::AccumulatingCallbacks::<f32>::new(
75//! |name, width, height, fmt, pixels: Vec<f32>| {
76//! // Called once with the complete accumulated image.
77//! write_exr(&name, width, height, &pixels);
78//! nsi::output::Error::None
79//! },
80//! );
81//!
82//! ctx.set_attribute(
83//! "driver",
84//! &[
85//! nsi::string!("drivername", nsi::output::FERRIS_F32),
86//! nsi::string!("imagefilename", "render"),
87//! nsi::callback!("callback.write", write),
88//! nsi::callback!("callback.finish", finish),
89//! ],
90//! );
91//! ```
92//!
93//! ## Color Profiles
94//!
95//! The pixel color data that the renderer generates is linear and
96//! scene-referred. I.e. relative to whatever units you used to describe
97//! illuminants in your scene.
98//!
99//! Using the
100//! [`"colorprofile"` attribute](https://nsi.readthedocs.io/en/latest/nodes.html?highlight=outputlayer#the-outputlayer-node)
101//! of an [`OutputLayer`](crate::OUTPUT_LAYER) you can ask the
102//! renderer to apply an [Open Color IO](https://opencolorio.org/) (OCIO)
103//! [profile/LUT](https://github.com/colour-science/OpenColorIO-Configs/tree/feature/aces-1.2-config/aces_1.2/luts)
104//! before quantizing (see below).
105//!
106//! Once OCIO has a [Rust wrapper](https://crates.io/crates/opencolorio) you can easily choose to
107//! do these color conversions yourself. In the meantime there is the
108//! [`colorspace`](https://crates.io/crates/colorspace) crate which has some useful profiles built
109//! in, e.g. [ACEScg](https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#ACEScg).
110//!
111//! ```
112//! # use nsi_ffi_wrap as nsi;
113//! # let ctx = nsi::Context::new(None).unwrap();
114//! ctx.create("beauty", nsi::OUTPUT_LAYER, None);
115//! ctx.set_attribute(
116//! "beauty",
117//! &[
118//! // The Ci variable comes from Open Shading Language.
119//! nsi::string!("variablename", "Ci"),
120//! // We want the pixel data 'display-referred' in sRGB and quantized down to 0.0..255.0.
121//! nsi::string!("colorprofile", "/home/luts/linear_to_sRGB.spi1d"),
122//! nsi::string!("scalarformat", "uint8"),
123//! ],
124//! );
125//! ```
126//!
127//! ## Quantization
128//!
129//! Using the [`"scalarformat"`
130//! attribute](https://nsi.readthedocs.io/en/latest/nodes.html?highlight=outputlayer#the-outputlayer-node)
131//! of an [`OutputLayer`](crate::OUTPUT_LAYER) you can ask the
132//! renderer to quantize data down to a suitable range. For example, setting
133//! this to `"uint16"` will get you valid `u16` values from `0.0..65535.0`, but
134//! stored in the `f32`s of the `pixel_data` buffer. The value of `1.0` will map
135//! to `65535.0` and everything above will be clipped. You can convert
136//! such a value straight via `f32 as u16`.
137//!
138//! Unless you asked the renderer to also apply some color profile (see above)
139//! the data is linear. To look good on a screen it needs to be made
140//! display-referred.
141//!
142//! See the `output` example on how to do this with a simple, display-referred
143//! `sRGB` curve.
144use crate::argument::CallbackPtr;
145use std::{
146 ffi::CStr,
147 mem::size_of,
148 os::raw::{c_char, c_int, c_void},
149};
150
151pub mod pixel_format;
152pub use pixel_format::*;
153
154pub mod pixel_type;
155pub use pixel_type::*;
156
157/// Driver name for f32 pixel type.
158pub static FERRIS_F32: &str = "ferris_f32";
159/// Driver name for u32 pixel type.
160pub static FERRIS_U32: &str = "ferris_u32";
161/// Driver name for i32 pixel type.
162pub static FERRIS_I32: &str = "ferris_i32";
163/// Driver name for u16 pixel type.
164pub static FERRIS_U16: &str = "ferris_u16";
165/// Driver name for i16 pixel type.
166pub static FERRIS_I16: &str = "ferris_i16";
167/// Driver name for u8 pixel type.
168pub static FERRIS_U8: &str = "ferris_u8";
169/// Driver name for i8 pixel type.
170pub static FERRIS_I8: &str = "ferris_i8";
171
172/// Legacy driver name - defaults to f32.
173/// Deprecated: Use [`FERRIS_F32`], [`FERRIS_U16`], etc. for type-specific drivers.
174#[deprecated(
175 since = "0.9.0",
176 note = "Use FERRIS_F32, FERRIS_U16, etc. for type-specific drivers"
177)]
178pub static FERRIS: &str = "ferris_f32";
179
180/// An error type the callbacks return to communicate with the
181/// renderer.
182#[repr(u32)]
183#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, num_enum::IntoPrimitive)]
184pub enum Error {
185 /// Everything is dandy.
186 None = ndspy_sys::PtDspyError::None as _,
187 /// We ran out of memory.
188 NoMemory = ndspy_sys::PtDspyError::NoMemory as _,
189 /// We do no support this request.
190 Unsupported = ndspy_sys::PtDspyError::Unsupported as _,
191 BadParameters = ndspy_sys::PtDspyError::BadParams as _,
192 NoResource = ndspy_sys::PtDspyError::NoResource as _,
193 /// Something else went wrong.
194 Undefined = ndspy_sys::PtDspyError::Undefined as _,
195 /// Stop the render.
196 Stop = ndspy_sys::PtDspyError::Stop as _,
197}
198
199impl From<Error> for ndspy_sys::PtDspyError {
200 fn from(item: Error) -> ndspy_sys::PtDspyError {
201 match item {
202 Error::None => ndspy_sys::PtDspyError::None,
203 Error::NoMemory => ndspy_sys::PtDspyError::NoMemory,
204 Error::Unsupported => ndspy_sys::PtDspyError::Unsupported,
205 Error::BadParameters => ndspy_sys::PtDspyError::BadParams,
206 Error::NoResource => ndspy_sys::PtDspyError::NoResource,
207 Error::Undefined => ndspy_sys::PtDspyError::Undefined,
208 Error::Stop => ndspy_sys::PtDspyError::Stop,
209 }
210 }
211}
212
213/// A closure which is called once per
214/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance.
215///
216/// It is passed to ɴsɪ via the `"callback.open"` attribute on that node.
217///
218/// The closure is called once, before the renderer starts sending pixels to the
219/// output driver.
220///
221/// # Arguments
222/// The `pixel_format` parameter is an array of strings that details the
223/// composition of the `f32` data that the renderer will send to the [`FnWrite`]
224/// and/or [`FnFinish`] closures.
225///
226/// # Example
227/// ```
228/// # #[cfg(feature = "output")]
229/// # {
230/// # use nsi_ffi_wrap as nsi;
231/// # use nsi::output::PixelFormat;
232/// # let ctx = nsi::Context::new(None).unwrap();
233/// # ctx.create("display_driver", nsi::OUTPUT_DRIVER, None);
234/// let open = nsi::output::OpenCallback::new(
235/// |name: &str,
236/// width: usize,
237/// height: usize,
238/// pixel_format: &nsi::output::PixelFormat| {
239/// println!(
240/// "Resolution: {}×{}\nPixel Format:\n{:?}",
241/// width, height, pixel_format
242/// );
243/// nsi::output::Error::None
244/// },
245/// );
246/// # }
247/// ```
248pub trait FnOpen<'a>: FnMut(
249 // Filename.
250 &str,
251 // Width.
252 usize,
253 // Height.
254 usize,
255 // Pixel format.
256 &PixelFormat,
257) -> Error
258+ 'a {}
259
260#[doc(hidden)]
261impl<'a, T: FnMut(&str, usize, usize, &PixelFormat) -> Error + 'a> FnOpen<'a>
262 for T
263{
264}
265
266// FIXME once trait aliases are in stable.
267/*
268trait FnOpen<'a> = FnMut(
269 // Filename.
270 &str,
271 // Width.
272 usize,
273 // Height.
274 usize,
275 // Pixel format.
276 &PixelFormat,
277 ) -> Error
278 + 'a
279*/
280
281/// A closure which is called for each bucket of pixels the
282/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance sends
283/// during rendering.
284///
285/// The closure receives ONLY the bucket data, not an accumulated image.
286/// Bucket dimensions are: `(x_max_plus_one - x_min) x (y_max_plus_one - y_min)`.
287/// Data layout is row-major with channels interleaved per pixel.
288///
289/// It is passed to ɴsɪ via the `"callback.write"` attribute on that node.
290///
291/// # Type Parameter
292///
293/// `T` is the pixel scalar type (e.g., `f32`, `u16`, `u8`). It must implement
294/// [`PixelType`]. The driver name must match the type (e.g., `FERRIS_F32` for `f32`).
295///
296/// # Example
297/// ```ignore
298/// let write = nsi::output::WriteCallback::<f32>::new(
299/// |name: &str,
300/// width: usize,
301/// height: usize,
302/// x_min: usize,
303/// x_max_plus_one: usize,
304/// y_min: usize,
305/// y_max_plus_one: usize,
306/// pixel_format: &nsi::output::PixelFormat,
307/// bucket_data: &[f32]| {
308/// // bucket_data contains ONLY this bucket, not the full image
309/// // Upload bucket to GPU texture, etc.
310/// nsi::output::Error::None
311/// },
312/// );
313///
314/// ctx.set_attribute(
315/// "driver",
316/// &[
317/// nsi::string!("drivername", nsi::output::FERRIS_F32),
318/// nsi::callback!("callback.write", write),
319/// ],
320/// );
321/// ```
322pub trait FnWrite<'a, T: PixelType>: FnMut(
323 // Filename.
324 &str,
325 // Full image width.
326 usize,
327 // Full image height.
328 usize,
329 // Bucket x_min.
330 usize,
331 // Bucket x_max_plus_one.
332 usize,
333 // Bucket y_min.
334 usize,
335 // Bucket y_max_plus_one.
336 usize,
337 // Pixel format.
338 &PixelFormat,
339 // Bucket pixel data (NOT the full image!).
340 &[T],
341 ) -> Error
342 + 'a {}
343
344#[doc(hidden)]
345impl<'a, T: PixelType, F> FnWrite<'a, T> for F where
346 F: FnMut(
347 &str,
348 usize,
349 usize,
350 usize,
351 usize,
352 usize,
353 usize,
354 &PixelFormat,
355 &[T],
356 ) -> Error
357 + 'a
358{
359}
360
361/// A closure which is called once per
362/// [`OutputDriver`](crate::OUTPUT_DRIVER) instance when rendering completes.
363///
364/// It is passed to ɴsɪ via the `"callback.finish"` attribute on that node.
365///
366/// **Note:** This closure does NOT receive pixel data. This matches the C ndspy
367/// API behavior where the display driver is responsible for accumulating pixels
368/// if needed. Use [`AccumulatingCallbacks`] if you need the complete image.
369///
370/// # Example
371/// ```ignore
372/// let finish = nsi::output::FinishCallback::new(
373/// |name: String,
374/// width: usize,
375/// height: usize,
376/// pixel_format: nsi::output::PixelFormat| {
377/// println!("Rendering complete: {}x{}", width, height);
378/// nsi::output::Error::None
379/// },
380/// );
381///
382/// ctx.set_attribute(
383/// "driver",
384/// &[
385/// nsi::string!("drivername", nsi::output::FERRIS_F32),
386/// nsi::callback!("callback.finish", finish),
387/// ],
388/// );
389/// ```
390pub trait FnFinish<'a>: FnMut(
391 // Filename.
392 String,
393 // Width.
394 usize,
395 // Height.
396 usize,
397 // Pixel format.
398 PixelFormat,
399) -> Error
400+ 'a {}
401
402#[doc(hidden)]
403impl<'a, F> FnFinish<'a> for F where
404 F: FnMut(String, usize, usize, PixelFormat) -> Error + 'a
405{
406}
407
408enum Query {}
409
410trait FnQuery<'a>: FnMut(Query) -> Error + 'a {}
411impl<'a, T: FnMut(Query) -> Error + 'a> FnQuery<'a> for T {}
412
413// FIXME once trait aliases are in stable.
414/*
415pub trait FnQuery<'a> = dyn FnMut(Query) -> Error + 'a;
416*/
417
418/// Wrapper to pass an [`FnOpen`] closure to an
419/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
420pub struct OpenCallback<'a>(Box<Box<Box<dyn FnOpen<'a>>>>);
421
422// Why do we need a triple Box here? Why does a Box<Box<T>> not suffice?
423// This is a known pattern for passing closures through FFI boundaries.
424// The issue is related to fat pointers (trait objects):
425// - Box<dyn Trait> is a fat pointer (16 bytes: data ptr + vtable ptr)
426// - Box<Box<dyn Trait>> is a thin pointer (8 bytes) to the fat pointer
427// - Box<Box<Box<dyn Trait>>> is a thin pointer to a thin pointer
428//
429// When casting through *const c_void, type information is lost.
430// With double Box, reconstructing the fat pointer fails (segfault).
431// Triple Box ensures we always deal with thin pointers at the FFI boundary.
432impl<'a> OpenCallback<'a> {
433 pub fn new<F>(fn_open: F) -> Self
434 where
435 F: FnOpen<'a>,
436 {
437 OpenCallback(Box::new(Box::new(Box::new(fn_open))))
438 }
439}
440
441impl CallbackPtr for OpenCallback<'_> {
442 #[doc(hidden)]
443 fn to_ptr(self) -> *const core::ffi::c_void {
444 Box::into_raw(self.0) as *const _ as _
445 }
446}
447/// Wrapper to pass an [`FnWrite`] closure to an
448/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
449///
450/// # Type Parameter
451///
452/// `T` is the pixel scalar type. Use the matching driver name:
453/// - `WriteCallback::<f32>` with `FERRIS_F32`
454/// - `WriteCallback::<u16>` with `FERRIS_U16`
455/// - etc.
456pub struct WriteCallback<'a, T: PixelType>(Box<Box<Box<dyn FnWrite<'a, T>>>>);
457
458impl<'a, T: PixelType> WriteCallback<'a, T> {
459 pub fn new<F>(fn_write: F) -> Self
460 where
461 F: FnWrite<'a, T>,
462 {
463 WriteCallback(Box::new(Box::new(Box::new(fn_write))))
464 }
465}
466
467impl<T: PixelType> CallbackPtr for WriteCallback<'_, T> {
468 #[doc(hidden)]
469 fn to_ptr(self) -> *const core::ffi::c_void {
470 Box::into_raw(self.0) as *const _ as _
471 }
472}
473
474/// Wrapper to pass an [`FnFinish`] closure to an
475/// [`OutputDriver`](crate::OUTPUT_DRIVER) node.
476///
477/// **Note:** `FnFinish` does not receive pixel data. If you need the complete
478/// accumulated image, use [`AccumulatingCallbacks`] instead.
479pub struct FinishCallback<'a>(Box<Box<Box<dyn FnFinish<'a>>>>);
480
481impl<'a> FinishCallback<'a> {
482 pub fn new<F>(fn_finish: F) -> Self
483 where
484 F: FnFinish<'a>,
485 {
486 FinishCallback(Box::new(Box::new(Box::new(fn_finish))))
487 }
488}
489
490impl CallbackPtr for FinishCallback<'_> {
491 #[doc(hidden)]
492 fn to_ptr(self) -> *const core::ffi::c_void {
493 Box::into_raw(self.0) as *const _ as _
494 }
495}
496
497/// Internal data structure passed through FFI as the image handle.
498/// Generic over pixel type T for zero-cost type handling.
499struct DisplayData<'a, T: PixelType> {
500 name: String,
501 width: usize,
502 height: usize,
503 pixel_format: PixelFormat,
504 // NO pixel_data buffer - we pass buckets directly to callbacks
505 fn_write: Option<Box<Box<Box<dyn FnWrite<'a, T>>>>>,
506 fn_finish: Option<Box<Box<Box<dyn FnFinish<'a>>>>>,
507 // FIXME: unused atm.
508 fn_query: Option<Box<Box<Box<dyn FnQuery<'a>>>>>,
509 // PhantomData to ensure T is used
510 _phantom: std::marker::PhantomData<T>,
511}
512
513fn extract_callback<T: ?Sized>(
514 name: &str,
515 type_: u8,
516 len: usize,
517 parameters: &[ndspy_sys::UserParameter],
518) -> Option<Box<Box<Box<T>>>> {
519 for p in parameters.iter() {
520 // SAFETY: Parameter names come from NSI API and should be valid C strings
521 if p.name.is_null() {
522 continue;
523 }
524 let p_name = match unsafe { CStr::from_ptr(p.name) }.to_str() {
525 Ok(name) => name,
526 Err(_) => continue,
527 };
528
529 if name == p_name
530 && type_ == p.valueType as _
531 && len == p.valueCount as _
532 {
533 if !p.value.is_null() {
534 // SAFETY: p.value was created by Box::into_raw in the callback's to_ptr method
535 // The type cast is valid because we verified the parameter type matches
536 return Some(unsafe {
537 Box::from_raw(p.value as *mut Box<Box<T>>)
538 });
539 } else {
540 // Parameter exists but value is missing - exit quietly.
541 break;
542 }
543 }
544 }
545 None
546}
547
548// Generic trampoline function for the FnOpen callback.
549// Each instantiation (image_open::<f32>, image_open::<u16>, etc.) handles a specific pixel type.
550pub(crate) extern "C" fn image_open<T: PixelType>(
551 image_handle_ptr: *mut ndspy_sys::PtDspyImageHandle,
552 _driver_name: *const c_char,
553 output_filename: *const c_char,
554 width: c_int,
555 height: c_int,
556 parameters_count: c_int,
557 parameters: *const ndspy_sys::UserParameter,
558 format_count: c_int,
559 format: *mut ndspy_sys::PtDspyDevFormat,
560 flag_stuff: *mut ndspy_sys::PtFlagStuff,
561) -> ndspy_sys::PtDspyError {
562 // Catch any panics to prevent unwinding into C code.
563 match std::panic::catch_unwind(|| {
564 if (image_handle_ptr.is_null())
565 || (output_filename.is_null())
566 || format.is_null()
567 || (format_count <= 0)
568 || ((parameters_count > 0) && parameters.is_null())
569 {
570 return Error::BadParameters.into();
571 }
572
573 // SAFETY: We only read from the parameters slice, never modify it.
574 let parameters = unsafe {
575 std::slice::from_raw_parts(parameters, parameters_count as _)
576 };
577
578 let mut display_data = Box::new(DisplayData::<T> {
579 name: {
580 // SAFETY: output_filename is checked for null above and comes from NSI C API
581 let c_str = unsafe { CStr::from_ptr(output_filename) };
582 c_str.to_string_lossy().into_owned()
583 },
584 width: width as _,
585 height: height as _,
586 pixel_format: PixelFormat::default(),
587 // NO pixel_data allocation - we pass buckets directly
588 fn_write: extract_callback::<dyn FnWrite<T>>(
589 "callback.write",
590 b'p',
591 1,
592 parameters,
593 ),
594 fn_finish: extract_callback::<dyn FnFinish>(
595 "callback.finish",
596 b'p',
597 1,
598 parameters,
599 ),
600 fn_query: None,
601 _phantom: std::marker::PhantomData,
602 });
603
604 // SAFETY: format is a valid pointer to format_count elements from NSI C API.
605 let format = unsafe {
606 std::slice::from_raw_parts_mut(format, format_count as _)
607 };
608
609 // Set format to the requested pixel type T
610 format.iter_mut().for_each(|f| f.type_ = T::NDSPY_TYPE);
611
612 display_data.pixel_format = PixelFormat::new(format);
613
614 let error = if let Some(mut fn_open) =
615 extract_callback::<dyn FnOpen>("callback.open", b'p', 1, parameters)
616 {
617 let error = fn_open(
618 &display_data.name,
619 width as _,
620 height as _,
621 &display_data.pixel_format,
622 );
623 // wtf?
624 Box::leak(fn_open);
625
626 error
627 } else {
628 Error::None
629 };
630
631 // SAFETY: image_handle_ptr and flag_stuff are valid pointers from NSI C API
632 unsafe {
633 *image_handle_ptr = Box::into_raw(display_data) as _;
634 // Preserve renderer-provided flags, but clear the empty-bucket request.
635 (*flag_stuff).flags &=
636 !(ndspy_sys::PkDspyFlagsWantsEmptyBuckets as i32);
637 }
638
639 error.into()
640 }) {
641 Ok(result) => result,
642 Err(_) => {
643 // If we panicked, return an error to the renderer
644 Error::Undefined.into()
645 }
646 }
647}
648
649// FIXME: this will be used for a FnProgress callback later.
650#[unsafe(no_mangle)]
651pub(crate) extern "C" fn image_query(
652 _image_handle_ptr: ndspy_sys::PtDspyImageHandle,
653 query_type: ndspy_sys::PtDspyQueryType,
654 data_len: c_int,
655 data: *mut c_void,
656) -> ndspy_sys::PtDspyError {
657 // Catch any panics to prevent unwinding into C code.
658 match std::panic::catch_unwind(|| {
659 match query_type {
660 ndspy_sys::PtDspyQueryType::RenderProgress => {
661 if (data_len as usize)
662 < core::mem::size_of::<ndspy_sys::PtDspyRenderProgressFuncPtr>()
663 || data.is_null()
664 {
665 Error::BadParameters
666 } else {
667 // SAFETY: data is a valid pointer to PtDspyRenderProgressFuncPtr
668 // as specified by the query type and validated by data_len check.
669 unsafe {
670 let func_ptr = data as *mut ndspy_sys::PtDspyRenderProgressFuncPtr;
671 *func_ptr = Some(image_progress);
672 }
673 Error::None
674 }
675 }
676 ndspy_sys::PtDspyQueryType::Progressive => {
677 if (data_len as usize) < size_of::<ndspy_sys::PtDspyProgressiveInfo>()
678 || data.is_null()
679 {
680 Error::BadParameters
681 } else {
682 // SAFETY: data points to PtDspyProgressiveInfo with validated length.
683 unsafe {
684 let info = data as *mut ndspy_sys::PtDspyProgressiveInfo;
685 (*info).acceptProgressive = 1;
686 }
687 Error::None
688 }
689 }
690 ndspy_sys::PtDspyQueryType::Thread => {
691 if (data_len as usize) < size_of::<ndspy_sys::PtDspyThreadInfo>()
692 || data.is_null()
693 {
694 Error::BadParameters
695 } else {
696 // SAFETY: data points to PtDspyThreadInfo with validated length.
697 unsafe {
698 let info = data as *mut ndspy_sys::PtDspyThreadInfo;
699 // Allow multithreaded buckets.
700 (*info).multithread = 1;
701 }
702 Error::None
703 }
704 }
705 ndspy_sys::PtDspyQueryType::Cooked => {
706 if (data_len as usize) < size_of::<ndspy_sys::PtDspyCookedInfo>()
707 || data.is_null()
708 {
709 Error::BadParameters
710 } else {
711 // SAFETY: data points to PtDspyCookedInfo with validated length.
712 unsafe {
713 let info = data as *mut ndspy_sys::PtDspyCookedInfo;
714 // Accept filtered pixel data (cooked=1 = PkDspyCQDefault).
715 (*info).cooked = 1;
716 }
717 Error::None
718 }
719 }
720 // StopQuery asks "should rendering stop?" - return None to continue.
721 ndspy_sys::PtDspyQueryType::Stop => Error::None,
722 ndspy_sys::PtDspyQueryType::Overwrite => {
723 if (data_len as usize) < size_of::<ndspy_sys::PtDspyOverwriteInfo>()
724 || data.is_null()
725 {
726 Error::BadParameters
727 } else {
728 // SAFETY: data points to PtDspyOverwriteInfo with validated length.
729 unsafe {
730 let info = data as *mut ndspy_sys::PtDspyOverwriteInfo;
731 // Allow the renderer to overwrite existing files.
732 (*info).overwrite = 1;
733 }
734 Error::None
735 }
736 }
737 _ => Error::Unsupported,
738 }
739 .into()
740 }) {
741 Ok(result) => result,
742 Err(_) => {
743 // If we panicked, return an error to the renderer
744 Error::Undefined.into()
745 }
746 }
747}
748
749// Generic trampoline function for the FnWrite callback.
750// Passes bucket data directly to callback - NO accumulation, NO memcpy to full buffer.
751pub(crate) extern "C" fn image_write<T: PixelType>(
752 image_handle_ptr: ndspy_sys::PtDspyImageHandle,
753 x_min: c_int,
754 x_max_plus_one: c_int,
755 y_min: c_int,
756 y_max_plus_one: c_int,
757 _entry_size: c_int,
758 pixel_data: *const u8,
759) -> ndspy_sys::PtDspyError {
760 // Catch any panics to prevent unwinding into C code
761 match std::panic::catch_unwind(|| {
762 // SAFETY: image_handle_ptr should be valid as it was created by image_open
763 if image_handle_ptr.is_null() {
764 return Error::BadParameters.into();
765 }
766 let display_data =
767 unsafe { &mut *(image_handle_ptr as *mut DisplayData<T>) };
768
769 let channels = display_data.pixel_format.channels();
770 let bucket_width = (x_max_plus_one - x_min) as usize;
771 let bucket_height = (y_max_plus_one - y_min) as usize;
772 let bucket_pixel_count = bucket_width * bucket_height * channels;
773
774 // SAFETY: pixel_data comes from the renderer and should be valid
775 if pixel_data.is_null() {
776 return Error::None.into();
777 }
778
779 // Zero-cost slice creation - just pointer reinterpretation, no copy!
780 let bucket_data = unsafe {
781 std::slice::from_raw_parts(
782 pixel_data as *const T,
783 bucket_pixel_count,
784 )
785 };
786
787 // Pass bucket directly to callback - NO memcpy, NO accumulation!
788 if let Some(ref mut fn_write) = display_data.fn_write {
789 fn_write(
790 &display_data.name,
791 display_data.width,
792 display_data.height,
793 x_min as _,
794 x_max_plus_one as _,
795 y_min as _,
796 y_max_plus_one as _,
797 &display_data.pixel_format,
798 bucket_data, // Just the bucket!
799 )
800 } else {
801 Error::None
802 }
803 .into()
804 }) {
805 Ok(result) => result,
806 Err(_) => {
807 // If we panicked, return an error to the renderer
808 Error::Undefined.into()
809 }
810 }
811}
812
813// Generic trampoline function for the FnFinish callback.
814// FnFinish does NOT receive pixel data - user accumulates if needed.
815pub(crate) extern "C" fn image_close<T: PixelType>(
816 image_handle_ptr: ndspy_sys::PtDspyImageHandle,
817) -> ndspy_sys::PtDspyError {
818 // Catch any panics to prevent unwinding into C code
819 match std::panic::catch_unwind(|| {
820 // SAFETY: image_handle_ptr should be valid as it was created by image_open
821 if image_handle_ptr.is_null() {
822 return Error::BadParameters.into();
823 }
824 let mut display_data =
825 unsafe { Box::from_raw(image_handle_ptr as *mut DisplayData<T>) };
826
827 // FnFinish receives no pixel data - user accumulates if needed
828 let error = if let Some(ref mut fn_finish) = display_data.fn_finish {
829 fn_finish(
830 std::mem::take(&mut display_data.name),
831 display_data.width,
832 display_data.height,
833 std::mem::take(&mut display_data.pixel_format),
834 )
835 } else {
836 Error::None
837 };
838
839 // SAFETY: The callbacks were passed to us via FFI from Box::into_raw.
840 // They should be dropped when DisplayData is dropped, but this causes
841 // a double-free. This suggests the callbacks are being freed elsewhere,
842 // possibly by the renderer. For now, we leak them to prevent crashes.
843 // TODO: Investigate why double-free occurs and fix properly.
844 if let Some(fn_write) = display_data.fn_write.take() {
845 Box::leak(fn_write);
846 }
847 if let Some(fn_query) = display_data.fn_query.take() {
848 Box::leak(fn_query);
849 }
850 if let Some(fn_finish) = display_data.fn_finish.take() {
851 Box::leak(fn_finish);
852 }
853
854 error.into()
855 }) {
856 Ok(result) => result,
857 Err(_) => {
858 // If we panicked, return an error to the renderer
859 Error::Undefined.into()
860 }
861 }
862}
863
864#[unsafe(no_mangle)]
865extern "C" fn image_progress(
866 _image_handle_ptr: ndspy_sys::PtDspyImageHandle,
867 _progress: f32,
868) -> ndspy_sys::PtDspyError {
869 // Progress logging disabled to reduce spam
870 Error::None.into()
871}
872
873/// Helper for users who want the complete accumulated image at finish time.
874///
875/// The core API passes buckets directly to callbacks without accumulation.
876/// This helper provides the common use case of receiving the complete image
877/// when rendering finishes.
878///
879/// # Example
880///
881/// ```ignore
882/// use std::sync::{Arc, Mutex};
883///
884/// // Create accumulating callbacks that deliver the full image at finish
885/// let (write, finish) = nsi::output::AccumulatingCallbacks::<f32>::new(
886/// |name, width, height, format, pixels| {
887/// // Called once at end with complete accumulated image
888/// save_image(&name, width, height, &pixels);
889/// nsi::output::Error::None
890/// },
891/// );
892///
893/// ctx.set_attribute(
894/// "driver",
895/// &[
896/// nsi::string!("drivername", nsi::output::FERRIS_F32),
897/// nsi::callback!("callback.write", write),
898/// nsi::callback!("callback.finish", finish),
899/// ],
900/// );
901/// ```
902pub struct AccumulatingCallbacks<T: PixelType> {
903 _phantom: std::marker::PhantomData<T>,
904}
905
906impl<T: PixelType> AccumulatingCallbacks<T> {
907 /// Create a pair of callbacks that accumulate pixels and deliver the
908 /// complete image to the finish callback.
909 ///
910 /// Returns `(WriteCallback, FinishCallback)` where:
911 /// - The write callback accumulates buckets into an internal buffer
912 /// - The finish callback delivers the complete buffer to your closure
913 pub fn new<'a, F>(
914 mut on_finish: F,
915 ) -> (WriteCallback<'a, T>, FinishCallback<'a>)
916 where
917 F: FnMut(String, usize, usize, PixelFormat, Vec<T>) -> Error + 'a,
918 {
919 use std::sync::{Arc, Mutex};
920
921 // Shared state between write and finish callbacks
922 struct AccumState<T: PixelType> {
923 buffer: Vec<T>,
924 width: usize,
925 height: usize,
926 channels: usize,
927 initialized: bool,
928 }
929
930 let state = Arc::new(Mutex::new(AccumState::<T> {
931 buffer: Vec::new(),
932 width: 0,
933 height: 0,
934 channels: 0,
935 initialized: false,
936 }));
937
938 let write_state = state.clone();
939 let finish_state = state;
940
941 let write = WriteCallback::new(
942 move |_name: &str,
943 width: usize,
944 height: usize,
945 x_min: usize,
946 x_max_plus_one: usize,
947 y_min: usize,
948 y_max_plus_one: usize,
949 format: &PixelFormat,
950 bucket_data: &[T]| {
951 let mut state = write_state.lock().unwrap();
952
953 // Initialize on first bucket
954 if !state.initialized {
955 state.width = width;
956 state.height = height;
957 state.channels = format.channels();
958 state.buffer =
959 vec![T::default(); width * height * state.channels];
960 state.initialized = true;
961 }
962
963 // Copy bucket into the full buffer
964 let bucket_width = x_max_plus_one - x_min;
965 let channels = state.channels;
966
967 for y in y_min..y_max_plus_one {
968 let src_start = (y - y_min) * bucket_width * channels;
969 let dst_start = (y * state.width + x_min) * channels;
970 let row_len = bucket_width * channels;
971
972 state.buffer[dst_start..dst_start + row_len]
973 .copy_from_slice(
974 &bucket_data[src_start..src_start + row_len],
975 );
976 }
977
978 Error::None
979 },
980 );
981
982 let finish = FinishCallback::new(
983 move |name: String,
984 width: usize,
985 height: usize,
986 format: PixelFormat| {
987 let mut state = finish_state.lock().unwrap();
988 let buffer = std::mem::take(&mut state.buffer);
989 drop(state); // Release lock before calling user callback
990
991 on_finish(name, width, height, format, buffer)
992 },
993 );
994
995 (write, finish)
996 }
997}