Skip to main content

nsi_ffi_wrap/
context.rs

1//! An ɴsɪ context.
2
3// Needed for the example dode to build.
4extern crate self as nsi;
5#[allow(unused_imports)]
6use crate::HashMap;
7use crate::*;
8use parking_lot::Mutex;
9use std::{
10    ffi::{CStr, c_char},
11    marker::PhantomData,
12    ops::Drop,
13    os::raw::{c_int, c_void},
14};
15use triomphe::Arc;
16use ustr::{Ustr, ustr};
17
18/// The actual context and a marker to hold on to callbacks
19/// (closures)/references passed via [`set_attribute()`] or the like.
20///
21/// We wrap this in an [`Arc`] in [`Context`] to make sure drop() is only
22/// called when the last clone ceases existing.
23struct InnerContext<'a> {
24    context: NSIContext,
25    /// The renderer this context is talking to.
26    ///
27    /// **Held here rather than looked up.** The renderer used to be a
28    /// process-wide static, so a process had one for its whole life.
29    /// Carrying it means two contexts in one process can be two
30    /// different renderers, which is what `"renderer"` promises -- and
31    /// it means the library cannot be dropped while a context still
32    /// needs it to run `NSIEnd`.
33    api: std::sync::Arc<dyn FfiApi>,
34    /// The callbacks this context was handed, keyed by the node handle and
35    /// attribute name they were set on.
36    ///
37    /// A callback is a closure that has been turned into a raw pointer for
38    /// the renderer, so nothing else can free it. The context is the right
39    /// owner: it outlives every callback set on it, and its `Drop` runs
40    /// `NSIEnd`, after which the renderer can no longer reach them.
41    callbacks: Mutex<HashMap<(Ustr, Ustr), CallbackOwned>>,
42    /// Callbacks displaced by a later `set_attribute` on the same key.
43    ///
44    /// Not freed on the spot: a render may still be running with the old
45    /// pointer. They are reclaimed at the next `Stop`/`Wait` -- when no
46    /// render is in flight -- or when the context drops.
47    retired: Mutex<Vec<CallbackOwned>>,
48    // _marker needs to be invariant in 'a.
49    // See "Making a struct outlive a parameter given to a method of
50    // that struct": https://stackoverflow.com/questions/62374326/.
51    _marker: PhantomData<*mut &'a ()>,
52}
53
54/// The `"renderer"` argument's value, if the caller gave one.
55///
56/// Only a string is meaningful: a renderer is named, not measured. An
57/// argument by that name carrying anything else is ignored here, and
58/// then dropped from what is forwarded like any other -- so a caller
59/// who passes the wrong type gets the default renderer rather than a
60/// renderer that receives an argument it cannot read.
61fn requested_renderer(args: &ArgSlice<'_, '_>) -> Option<std::string::String> {
62    args.iter()
63        .find(|arg| crate::backend::RENDERER == arg.name.as_str())
64        .and_then(|arg| match &arg.data {
65            ArgData::String(value) => value.as_str(),
66            _ => None,
67        })
68        .map(|name| name.to_string())
69}
70
71/// A callback pointer the context owns, with the function that frees it.
72#[derive(Debug)]
73struct CallbackOwned {
74    data: *const c_void,
75    drop_fn: unsafe fn(*const c_void),
76}
77
78impl Drop for CallbackOwned {
79    fn drop(&mut self) {
80        // SAFETY: `data` came from the matching `CallbackPtr::to_ptr`, is
81        // reclaimed exactly once (this type is neither `Copy` nor `Clone`),
82        // and by construction the renderer can no longer reach it.
83        unsafe { (self.drop_fn)(self.data) }
84    }
85}
86
87// The context id alone identifies a context; the callback tables are
88// interior mutability and say nothing about identity.
89// By hand, because `dyn FfiApi` is not `Debug` and giving it that
90// bound would constrain every implementation for the sake of a
91// derive.
92impl<'a> core::fmt::Debug for InnerContext<'a> {
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        f.debug_struct("InnerContext")
95            .field("context", &self.context)
96            .field("callbacks", &self.callbacks)
97            .field("retired", &self.retired)
98            .finish_non_exhaustive()
99    }
100}
101
102impl<'a> PartialEq for InnerContext<'a> {
103    fn eq(&self, other: &Self) -> bool {
104        self.context == other.context
105    }
106}
107impl<'a> Eq for InnerContext<'a> {}
108impl<'a> core::hash::Hash for InnerContext<'a> {
109    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
110        self.context.hash(state);
111    }
112}
113
114impl<'a> InnerContext<'a> {
115    fn new(context: NSIContext, api: std::sync::Arc<dyn FfiApi>) -> Self {
116        Self {
117            context,
118            api,
119            callbacks: Mutex::new(HashMap::new()),
120            retired: Mutex::new(Vec::new()),
121            _marker: PhantomData,
122        }
123    }
124
125    /// Takes ownership of every callback in `args`, keyed by `handle` and
126    /// the argument name. Anything it displaces is retired, not freed.
127    fn own_callbacks(&self, handle: &str, args: Option<&ArgSlice<'_, 'a>>) {
128        let Some(args) = args else { return };
129
130        let handle = ustr(handle);
131        let mut owned = self.callbacks.lock();
132
133        args.iter()
134            .filter_map(|arg| match &arg.data {
135                ArgData::Callback(callback) => Some((arg.name, callback)),
136                _ => None,
137            })
138            .for_each(|(name, callback)| {
139                if let Some(displaced) = owned.insert(
140                    (handle, name),
141                    CallbackOwned {
142                        data: callback.data,
143                        drop_fn: callback.drop_fn,
144                    },
145                ) {
146                    self.retired.lock().push(displaced);
147                }
148            });
149    }
150
151    /// Frees callbacks displaced by a later `set_attribute`.
152    ///
153    /// Only call this where no render can be using them -- after `Stop` or
154    /// `Wait` returns, or once `NSIEnd` has.
155    fn reclaim_retired(&self) {
156        self.retired.lock().clear();
157    }
158}
159
160// Guaranteed by the C API.
161unsafe impl<'a> Send for InnerContext<'a> {}
162unsafe impl<'a> Sync for InnerContext<'a> {}
163
164impl<'a> Drop for InnerContext<'a> {
165    #[inline]
166    fn drop(&mut self) {
167        // Order matters: the renderer may still reach a callback until
168        // `NSIEnd` returns, so free nothing before it does.
169        self.api.NSIEnd(self.context);
170        crate::forget(self.context);
171        self.callbacks.lock().clear();
172        self.retired.lock().clear();
173    }
174}
175
176/// # An ɴꜱɪ Context.
177///
178/// A `Context` is used to describe a scene to the renderer and request images
179/// to be rendered from it.
180///
181/// ## Safety
182/// A `Context` may be used in multiple threads at once.
183///
184/// ## Lifetime
185/// A `Context` can be used without worrying about its lifetime until you want
186/// to store it somewhere, e.g. in a struct.
187///
188/// The reason `Context` has an explicit lifetime is so that it can take
189/// [`Reference`]s and [`Callback`]s (closures). These must be valid until the
190/// context is dropped and this guarantee requires explicit lifetimes. When you
191/// use a context directly this is not an issue but when you want to reference
192/// it somewhere the same rules as with all references apply.
193///
194/// ## Memory & ownership: callbacks and references
195///
196/// A [`Callback`] is a closure turned into a raw pointer for the renderer,
197/// so it has to outlive the call that passes it. The **`Context` owns it**
198/// from that moment and frees it for you. There is nothing to manage, but
199/// two consequences are worth knowing:
200///
201/// * **A callback keeps its captures alive.** An output-driver write
202///   callback typically captures an `Arc` to your pixel buffer, so that
203///   buffer lives as long as the callback does. `Arc::try_unwrap` on
204///   something a live callback captured will therefore fail -- read it out
205///   under its lock instead.
206/// * **Re-setting an attribute retires the callback it displaces**, rather
207///   than freeing it on the spot: a render may still be running with the
208///   old pointer. Retired callbacks are freed at the next
209///   [`Action::Stop`] or [`Action::Wait`], and unconditionally when the
210///   context drops. So a long-lived context whose callbacks are swapped
211///   repeatedly -- switching output modes, say -- reclaims them each time
212///   it stops, and in the worst case holds one displaced set until then.
213///
214/// Everything is released when the `Context` drops, after `NSIEnd`, which
215/// is the first moment the renderer can no longer reach any of it.
216///
217/// A [`Reference`] is different: it points at data *you* own (the
218/// [`StableDeref`] bound means a `Box` or `Arc`), so the context never
219/// frees it and you must keep it alive for as long as the context.
220///
221/// ## Let every thread that owns a `Context` finish before the process ends
222///
223/// `NSIEnd` runs in [`Drop`]. Two ways to skip it, both of which leave the
224/// renderer mid-shutdown while libc tears the process down -- a `SIGSEGV`
225/// *after* a completely clean render:
226///
227/// 1. [`std::process::exit`] (or `abort`) runs no destructors at all, so a
228///    live `Context` never reaches `NSIEnd`. Return from `main` normally --
229///    with [`std::process::ExitCode`] if you need a specific status.
230/// 2. Not joining a thread that owns a `Context`. Signalling completion
231///    over a channel is not enough: the thread still has to drop the
232///    `Context` afterwards, and that is where `NSIEnd` happens. Keep the
233///    [`JoinHandle`](std::thread::JoinHandle) and join it.
234///
235/// `NSIEnd` itself is synchronous -- once it returns, 3Delight has no
236/// threads of its own left running. See `examples/concurrent_interactive`,
237/// which joins its render threads and needs no other precaution.
238///
239/// ## Further Reading
240/// See the [ɴꜱɪ documentation on context
241/// handling](https://nsi.readthedocs.io/en/latest/c-api.html#context-handling).
242#[derive(Clone, Debug, Hash, PartialEq, Eq)]
243pub struct Context<'a>(Arc<InnerContext<'a>>);
244
245unsafe impl<'a> Send for Context<'a> {}
246unsafe impl<'a> Sync for Context<'a> {}
247
248impl<'a> From<Context<'a>> for NSIContext {
249    #[inline]
250    fn from(context: Context<'a>) -> Self {
251        context.0.context
252    }
253}
254
255impl<'a> From<NSIContext> for Context<'a> {
256    /// **The default renderer**, because a bare handle does not say
257    /// which one made it. Adopting a handle from a renderer that is
258    /// not the default is not expressible here; make the [`Context`]
259    /// with [`Context::new`] and a `"renderer"` argument instead.
260    ///
261    /// # Panics
262    ///
263    /// If no renderer can be loaded at all. Every other path returns
264    /// an error, but this conversion has nowhere to put one.
265    #[inline]
266    fn from(context: NSIContext) -> Self {
267        let api = crate::renderer_of(context)
268            .unwrap_or_else(|error| panic!("{error}"));
269        Self(Arc::new(InnerContext::new(context, api)))
270    }
271}
272
273impl<'a> Context<'a> {
274    /// Creates an ɴsɪ context.
275    ///
276    /// Contexts may be used in multiple threads at once.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// # use nsi_ffi_wrap as nsi;
282    /// // Create rendering context that dumps to stdout.
283    /// let ctx =
284    ///     nsi::Context::new(Some(&[nsi::string!("streamfilename", "stdout")]))
285    ///         .expect("Could not create ɴsɪ context.");
286    /// ```
287    /// # Choosing a renderer
288    ///
289    /// ɴsɪ is an interface and more than one renderer implements it.
290    /// A `"renderer"` argument says which:
291    ///
292    /// ```no_run
293    /// # use nsi_ffi_wrap as nsi;
294    /// let ctx = nsi::Context::new(Some(&[nsi::string!("renderer", "moonray")]));
295    /// ```
296    ///
297    /// A name this crate knows is looked for where that renderer
298    /// installs; anything else is taken as a library to load, so an
299    /// implementation released after this crate still works, as does
300    /// a path to a build of your own. Without the argument,
301    /// `$NSI_RENDERER` decides, and without that, 3Delight.
302    /// [`backend`](crate::backend) has the whole rule.
303    ///
304    /// The argument is **consumed here and never forwarded**: it tells
305    /// this crate which library to open, and no renderer would know
306    /// what to do with it.
307    ///
308    /// # Error
309    /// If this method fails for some reason, it returns [`None`].
310    #[inline]
311    pub fn new(args: Option<&ArgSlice<'_, 'a>>) -> Option<Self> {
312        // Read before anything is turned into C parameters, and
313        // dropped from what is, further down.
314        //
315        // Dropped *there* rather than by filtering `args` into a new
316        // slice, which is what a first version did. Both are correct:
317        // the filtered `Vec` was a local that outlived the `NSIBegin`
318        // call, and a renderer copies what it is handed during the
319        // call -- `nsi-intermediate`'s `OwnedData` owns every payload,
320        // and the one thing it does not copy, a `HostPointer`, it
321        // never dereferences. This way simply does not clone the
322        // caller's arguments, which allocates nothing and leaves no
323        // second `Arg` holding a duplicate of a callback pointer that
324        // stands for ownership of a leaked box.
325        let renderer = args.and_then(requested_renderer);
326
327        let api = match crate::renderer(renderer.as_deref()) {
328            Ok(api) => api,
329            Err(error) => {
330                // The one failure this crate cannot render through, so
331                // it is worth a line even though the signature can
332                // only say `None`.
333                eprintln!("nsi: {error}");
334                return None;
335            }
336        };
337
338        let (_, _, mut args_out) = to_c_param_vec(args);
339
340        // **Not forwarded.** `"renderer"` told this crate which
341        // library to open; no renderer declares it. `Ustr` interns, so
342        // equal names are one pointer and this compares addresses.
343        if renderer.is_some() {
344            let directive = ustr(crate::backend::RENDERER).as_char_ptr();
345            args_out.retain(|param| !std::ptr::eq(param.name, directive));
346        }
347
348        let fn_pointer: nsi_sys::NSIErrorHandler = Some(
349            error_handler
350                as extern "C" fn(*mut c_void, c_int, c_int, *const c_char),
351        );
352
353        if let Some(args) = args
354            && let Some(arg) =
355                args.iter().find(|arg| ustr("errorhandler") == arg.name)
356        {
357            // `as_c_ptr` already yields the address of the pointer, which
358            // is what ɴsɪ reads a `Pointer` value out of. Taking its address
359            // again here would be one indirection too many.
360            args_out.push(nsi_sys::NSIParam {
361                name: ustr("errorhandler").as_char_ptr(),
362                data: &fn_pointer as *const _ as _,
363                type_: NSIType::Pointer as _,
364                arraylength: 0,
365                count: 1,
366                flags: 0,
367            });
368            args_out.push(nsi_sys::NSIParam {
369                name: ustr("errorhandlerdata").as_char_ptr(),
370                data: arg.data.as_c_ptr(),
371                type_: NSIType::Pointer as _,
372                arraylength: 1,
373                count: 1,
374                flags: 0,
375            });
376        }
377
378        let context = api.NSIBegin(args_out.len() as _, args_out.as_ptr());
379
380        if 0 == context {
381            None
382        } else {
383            crate::remember(context, &api);
384            let inner = InnerContext::new(context, api);
385            // The error handler is a callback like any other; the context
386            // owns it under a reserved key, since it belongs to no node.
387            inner.own_callbacks(".context", args);
388            Some(Self(Arc::new(inner)))
389        }
390    }
391
392    /// Creates a new node.
393    ///
394    /// # Arguments
395    ///
396    /// * `handle` -- A node handle. This string will uniquely identify the node
397    ///   in the scene.
398    ///
399    ///   If the supplied handle matches an existing node, the function does
400    ///   nothing if all other parameters match the call which created that
401    ///   node. Otherwise, it emits an error. Note that handles need only be
402    ///   unique within a given [`Context`]. It is ok to reuse the same
403    ///   handle inside different [`Context`]s.
404    ///
405    /// * `node_type` -- The type of node to create. The crate has `&str`
406    ///   constants for all [`node`]s that are in the official NSI
407    ///   specification. As this parameter is just a string you can instance
408    ///   other node types that a particular implementation may provide and
409    ///   which are not part of the official specification.
410    ///
411    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
412    ///   *There are no optional arguments defined as of now*.
413    ///
414    /// ```
415    /// # use nsi_ffi_wrap as nsi;
416    /// // Create a context to send the scene to.
417    /// let ctx = nsi::Context::new(None).unwrap();
418    ///
419    /// // Create an infinte plane.
420    /// ctx.create("ground", nsi::PLANE, None);
421    /// ```
422    #[inline]
423    pub fn create(
424        &self,
425        handle: &str,
426        node_type: &str,
427        args: Option<&ArgSlice<'_, 'a>>,
428    ) {
429        let handle = HandleString::from(handle);
430        let node_type = ustr(node_type);
431        let (args_len, args_ptr, _args_out) = to_c_param_vec(args);
432
433        self.0.api.NSICreate(
434            self.0.context,
435            handle.as_char_ptr(),
436            node_type.as_char_ptr(),
437            args_len,
438            args_ptr,
439        );
440    }
441
442    /// This function deletes a node from the scene. All connections to and from
443    /// the node are also deleted.
444    ///
445    /// Note that it is not possible to delete the `.root` or the `.global`
446    /// node.
447    ///
448    /// # Arguments
449    ///
450    /// * `handle` -- A handle to a node previously created with
451    ///   [`create()`](Context::create()).
452    ///
453    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
454    ///
455    /// # Optional Arguments
456    ///
457    /// * `"recursive"` ([`I32`]) -- Specifies whether deletion is
458    ///   recursive. By default, only the specified node is deleted. If a value
459    ///   of `1` is given, then nodes which connect to the specified node are
460    ///   recursively removed. Unless they meet one of the following conditions:
461    ///   * They also have connections which do not eventually lead to the
462    ///     specified node.
463    ///   * Their connection to the node to be deleted was created with a
464    ///     `strength` greater than `0`.
465    ///
466    ///   This allows, for example, deletion of an entire shader network in a
467    ///   single call.
468    #[inline]
469    pub fn delete(&self, handle: &str, args: Option<&ArgSlice<'_, 'a>>) {
470        let handle = HandleString::from(handle);
471        let (args_len, args_ptr, _args_out) = to_c_param_vec(args);
472
473        self.0.api.NSIDelete(
474            self.0.context,
475            handle.as_char_ptr(),
476            args_len,
477            args_ptr,
478        );
479    }
480
481    /// This functions sets attributes on a previously node.
482    /// All optional arguments of the function become attributes of
483    /// the node.
484    ///
485    /// On a [`shader`](`node::SHADER`), this function is used to set the
486    /// implicitly defined shader arguments.
487    ///
488    /// Setting an attribute using this function replaces any value
489    /// previously set by [`set_attribute()`](Context::set_attribute()) or
490    /// [`set_attribute_at_time()`](Context::set_attribute_at_time()).
491    ///
492    /// To reset an attribute to its default value, use
493    /// [`delete_attribute()`](Context::delete_attribute()).
494    ///
495    /// # Arguments
496    ///
497    /// * `handle` -- A handle to a node previously created with
498    ///   [`create()`](Context::create()).
499    ///
500    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
501    #[inline]
502    pub fn set_attribute(&self, handle: &str, args: &ArgSlice<'_, 'a>) {
503        self.0.own_callbacks(handle, Some(args));
504
505        let handle = HandleString::from(handle);
506        let (args_len, args_ptr, _args_out) = to_c_param_vec(Some(args));
507
508        self.0.api.NSISetAttribute(
509            self.0.context,
510            handle.as_char_ptr(),
511            args_len,
512            args_ptr,
513        );
514    }
515
516    /// This function sets time-varying attributes (i.e. motion blurred).
517    ///
518    /// The `time` argument specifies at which time the attribute is being
519    /// defined.
520    ///
521    /// It is not required to set time-varying attributes in any
522    /// particular order. In most uses, attributes that are motion blurred must
523    /// have the same specification throughout the time range.
524    ///
525    /// A notable  exception is the `P` attribute on [`particles`
526    /// node](`node::PARTICLES`) which can be of different size for each
527    /// time step because of appearing or disappearing particles. Setting an
528    /// attribute using this function replaces any value previously set by
529    /// [`set_attribute()`](Context::set_attribute()).
530    ///
531    /// # Arguments
532    ///
533    /// * `handle` -- A handle to a node previously created with
534    ///   [`create()`](Context::create()).
535    ///
536    /// * `time` -- The time at which to set the value.
537    ///
538    /// * `args` -- A [`slice`](std::slice) of optional [`Arg`] arguments.
539    #[inline]
540    pub fn set_attribute_at_time(
541        &self,
542        handle: &str,
543        time: f64,
544        args: &ArgSlice<'_, 'a>,
545    ) {
546        self.0.own_callbacks(handle, Some(args));
547
548        let handle = HandleString::from(handle);
549        let (args_len, args_ptr, _args_out) = to_c_param_vec(Some(args));
550
551        self.0.api.NSISetAttributeAtTime(
552            self.0.context,
553            handle.as_char_ptr(),
554            time,
555            args_len,
556            args_ptr,
557        );
558    }
559
560    /// This function deletes any attribute with a name which matches
561    /// the `name` argument on the specified object. There is no way to
562    /// delete an attribute only for a specific time value.
563    ///
564    /// Deleting an attribute resets it to its default value.
565    ///
566    /// For example, after deleting the `transformationmatrix` attribute
567    /// on a [`transform` node](`node::TRANSFORM`), the transform will be an
568    /// identity. Deleting a previously set attribute on a [`shader`
569    /// node](`node::SHADER`) will default to whatever is declared inside
570    /// the shader.
571    ///
572    /// # Arguments
573    ///
574    /// * `handle` -- A handle to a node previously created with
575    ///   [`create()`](Context::create()).
576    ///
577    /// * `name` -- The name of the attribute to be deleted/reset.
578    #[inline]
579    pub fn delete_attribute(&self, handle: &str, name: &str) {
580        let handle = HandleString::from(handle);
581        let name = ustr(name);
582
583        self.0.api.NSIDeleteAttribute(
584            self.0.context,
585            handle.as_char_ptr(),
586            name.as_char_ptr(),
587        );
588    }
589
590    /// Create a connection between two elements.
591    ///
592    /// It is not an error to create a connection which already exists
593    /// or to remove a connection which does not exist but the nodes
594    /// on which the connection is performed must exist.
595    ///
596    /// # Arguments
597    ///
598    /// * `from` -- The handle of the node from which the connection is made.
599    ///
600    /// * `from_attribute` -- The name of the attribute from which the connection is
601    ///   made.
602    ///
603    ///   If this is `None` or `Some("")` the `from` node itself will be
604    ///   connected.
605    ///
606    /// * `to` -- The handle of the node to which the connection is made.
607    ///
608    /// * `to_attribute` -- The name of the attribute to which the connection is
609    ///   made. If this is an empty string then the connection is made to the
610    ///   node instead of to a specific attribute of the node.
611    ///
612    /// # Optional Arguments
613    ///
614    /// * `"value"` -- This can be used to change the value of a node's
615    ///   attribute in some contexts. Refer to guidelines on inter-object
616    ///   visibility for more information about the utility of this parameter.
617    ///
618    /// * `"priority"` ([`I32`]) -- When connecting attribute nodes,
619    ///   indicates in which order the nodes should be considered when
620    ///   evaluating the value of an attribute.
621    ///
622    /// * `"strength"` ([`I32`]) -- A connection with a `strength` greater
623    ///   than `0` will *block* the progression of a recursive
624    ///   [`delete()`](Context::delete()).
625    #[inline]
626    pub fn connect(
627        &self,
628        from: &str,
629        from_attribute: Option<&str>,
630        to: &str,
631        to_attribute: &str,
632        args: Option<&ArgSlice<'_, 'a>>,
633    ) {
634        let from = HandleString::from(from);
635        let from_attribute = ustr(from_attribute.unwrap_or(""));
636        let to = HandleString::from(to);
637        let to_attribute = ustr(to_attribute);
638        let (args_len, args_ptr, _args_out) = to_c_param_vec(args);
639
640        self.0.api.NSIConnect(
641            self.0.context,
642            from.as_char_ptr(),
643            from_attribute.as_char_ptr(),
644            to.as_char_ptr(),
645            to_attribute.as_char_ptr(),
646            args_len,
647            args_ptr,
648        );
649    }
650
651    /// This function removes a connection between two elements.
652    ///
653    /// The handle for either node may be the special value
654    /// [`.all`](crate::node::ALL). This will remove all connections which
655    /// match the other three arguments.
656    ///
657    /// # Examples
658    ///
659    /// ```
660    /// # use nsi_ffi_wrap as nsi;
661    /// # // Create a rendering context.
662    /// # let ctx = nsi::Context::new(None).unwrap();
663    /// // Disconnect everything from the scene's root.
664    /// ctx.disconnect(nsi::ALL, None, ".root", "");
665    /// ```
666    #[inline]
667    pub fn disconnect(
668        &self,
669        from: &str,
670        from_attribute: Option<&str>,
671        to: &str,
672        to_attribute: &str,
673    ) {
674        let from = HandleString::from(from);
675        let from_attribute = ustr(from_attribute.unwrap_or(""));
676        let to = HandleString::from(to);
677        let to_attribute = ustr(to_attribute);
678
679        self.0.api.NSIDisconnect(
680            self.0.context,
681            from.as_char_ptr(),
682            from_attribute.as_char_ptr(),
683            to.as_char_ptr(),
684            to_attribute.as_char_ptr(),
685        );
686    }
687
688    /// This function includes a block of interface calls from an external
689    /// source into the current scene. It blends together the concepts of a
690    /// file include, commonly known as an *archive*, with that of
691    /// procedural include which is traditionally a compiled executable. Both
692    /// are the same idea expressed in a different language.
693    ///
694    /// Note that for delayed procedural evaluation you should use a
695    /// `Procedural` node.
696    ///
697    /// The ɴꜱɪ adds a third option which sits in-between -- [Lua
698    /// scripts](https://nsi.readthedocs.io/en/latest/lua-api.html). They are more powerful than a
699    /// simple included file yet they are also easier to generate as they do not
700    /// require compilation.
701    ///
702    /// For example, it is realistic to export a whole new script for every
703    /// frame of an animation. It could also be done for every character in
704    /// a frame. This gives great flexibility in how components of a scene
705    /// are put together.
706    ///
707    /// The ability to load ɴꜱɪ commands from memory is also provided.
708    ///
709    /// # Optional Arguments
710    ///
711    /// * `"type"` ([`String`]) -- The type of file which will generate the
712    ///   interface calls. This can be one of:
713    ///   * `"apistream"` -- Read in an ɴꜱɪ stream. This requires either
714    ///     `"filename"` or `"buffer"`/`"size"` arguments to be specified too.
715    ///
716    ///   * `"lua"` -- Execute a Lua script, either from file or inline. See also
717    ///     [how to evaluate a Lua script](https://nsi.readthedocs.io/en/latest/lua-api.html#luaapi-evaluation).
718    ///
719    ///   * `"dynamiclibrary"` -- Execute native compiled code in a loadable library. See
720    ///     [dynamic library procedurals](https://nsi.readthedocs.io/en/latest/procedurals.html#section-procedurals)
721    ///     for an implementation example in C.
722    ///
723    /// * `"filename"` ([`String`]) -- The name of the file which contains the
724    ///   interface calls to include.
725    ///
726    /// * `"script"` ([`String`]) -- A valid Lua script to execute when `"type"`
727    ///   is set to `"lua"`.
728    ///
729    /// * `"buffer"` ([`String`]) -- A memory block that contain ɴꜱɪ commands to
730    ///   execute.
731    ///
732    /// * `"backgroundload"` ([`I32`]) -- If this is nonzero, the object may
733    ///   be loaded in a separate thread, at some later time. This requires that
734    ///   further interface calls not directly reference objects defined in the
735    ///   included file. The only guarantee is that the file will be loaded
736    ///   before rendering begins.
737    #[inline]
738    pub fn evaluate(&self, args: &ArgSlice<'_, 'a>) {
739        let (args_len, args_ptr, _args_out) = to_c_param_vec(Some(args));
740
741        self.0.api.NSIEvaluate(self.0.context, args_len, args_ptr);
742    }
743
744    /// This function is the only control function of the API.
745    ///
746    /// It is responsible of starting, suspending and stopping the render. It
747    /// also allows for synchronizing the render with interactive calls that
748    /// might have been issued.
749    ///
750    /// Note that this call will block if [`Action::Wait`] is selected.
751    ///
752    /// # Arguments
753    ///
754    /// * `action` -- Specifies the render [`Action`] to be performed on the
755    ///   scene.
756    ///
757    /// # Optional Arguments
758    ///
759    /// * `"progressive"` ([`I32`]) -- If set to `1`, render the image in a
760    ///   progressive fashion.
761    ///
762    /// * `"interactive"` ([`I32`]) -- If set to `1`, the renderer will
763    ///   accept commands to edit scene’s state while rendering. The difference
764    ///   with a normal render is that the render task will not exit even if
765    ///   rendering is finished. Interactive renders are by definition
766    ///   progressive.
767    ///
768    /// * `"callback"` ([`FnStatus`]) -- A closure that will be called be when
769    ///   the status of the render changes.
770    ///
771    ///   # Example
772    ///
773    ///   ```
774    ///   # use nsi_ffi_wrap as nsi;
775    ///   # let ctx = nsi::Context::new(None).unwrap();
776    ///   let status_callback = nsi::StatusCallback::new(
777    ///       |_ctx: &nsi::Context, status: nsi::RenderStatus| {
778    ///           println!("Status: {:?}", status);
779    ///         },
780    ///      );
781    ///
782    ///   /// The renderer will abort because we didn't define an output driver.
783    ///   /// So our status_callback() above will receive RenderStatus::Aborted.
784    ///   ctx.render_control(
785    ///       nsi::Action::Start,
786    ///       Some(&[
787    ///           nsi::i32!("interactive", true as _),
788    ///           nsi::callback!("callback", status_callback),
789    ///       ]),
790    ///   );
791    ///
792    ///   // Block until the renderer is really done.
793    ///   ctx.render_control(nsi::Action::Wait, None);
794    ///   ```
795    #[inline]
796    pub fn render_control(
797        &self,
798        action: Action,
799        args: Option<&ArgSlice<'_, 'a>>,
800    ) {
801        let (_, _, mut args_out) = to_c_param_vec(args);
802
803        let fn_pointer: nsi_sys::NSIRenderStopped =
804            Some(render_status as extern "C" fn(*mut c_void, c_int, c_int));
805
806        args_out.push(nsi_sys::NSIParam {
807            name: ustr("action").as_char_ptr(),
808            data: &ustr(action.as_str()).as_char_ptr() as *const _ as _,
809            type_: NSIType::String as _,
810            arraylength: 0,
811            count: 1,
812            flags: 0,
813        });
814
815        if let Some(args) = args
816            && let Some(arg) =
817                args.iter().find(|arg| ustr("callback") == arg.name)
818        {
819            // See the `errorhandlerdata` note in `Context::new`.
820            args_out.push(nsi_sys::NSIParam {
821                name: ustr("stoppedcallback").as_char_ptr(),
822                data: &fn_pointer as *const _ as _,
823                type_: NSIType::Pointer as _,
824                arraylength: 0,
825                count: 1,
826                flags: 0,
827            });
828            args_out.push(nsi_sys::NSIParam {
829                name: ustr("stoppedcallbackdata").as_char_ptr(),
830                data: arg.data.as_c_ptr(),
831                type_: NSIType::Pointer as _,
832                arraylength: 1,
833                count: 1,
834                flags: 0,
835            });
836        }
837
838        self.0.own_callbacks(".context", args);
839
840        self.0.api.NSIRenderControl(
841            self.0.context,
842            args_out.len() as _,
843            args_out.as_ptr(),
844        );
845
846        // `Stop` and `Wait` return only once the render is done, so any
847        // callback displaced by an earlier `set_attribute` is now
848        // unreachable and can be freed.
849        if matches!(action, Action::Stop | Action::Wait) {
850            self.0.reclaim_retired();
851        }
852    }
853}
854
855// Action is re-exported from nsi-trait via lib.rs
856
857/// The status of a *interactive* render session.
858#[repr(i32)]
859#[derive(Debug, Copy, Clone, PartialEq, Eq, num_enum::FromPrimitive)]
860pub enum RenderStatus {
861    #[num_enum(default)]
862    Completed = nsi_sys::NSIStoppingStatus::RenderCompleted as _,
863    Aborted = nsi_sys::NSIStoppingStatus::RenderAborted as _,
864    Synchronized = nsi_sys::NSIStoppingStatus::RenderSynchronized as _,
865    Restarted = nsi_sys::NSIStoppingStatus::RenderRestarted as _,
866}
867
868/// A closure which is called to inform about the status of an ongoing render.
869///
870/// It is passed to ɴsɪ via [`render_control()`](Context::render_control())’s
871/// `"callback"` argument.
872///
873/// # Examples
874///
875/// ```
876/// # use nsi_ffi_wrap as nsi;
877/// # let ctx = nsi::Context::new(None).unwrap();
878/// let status_callback = nsi::context::StatusCallback::new(
879///     |_: &nsi::context::Context, status: nsi::context::RenderStatus| {
880///         println!("Status: {:?}", status);
881///     },
882/// );
883///
884/// ctx.render_control(
885///     nsi::Action::Start,
886///     Some(&[nsi::callback!("callback", status_callback)]),
887/// );
888/// ```
889pub trait FnStatus<'a>: Fn(
890    // The [`Context`] for which this closure was called.
891    &Context,
892    // Status of interactive render session.
893    RenderStatus,
894)
895+ 'a {}
896
897#[doc(hidden)]
898impl<
899    'a,
900    T: Fn(&Context, RenderStatus)
901        + 'a
902        + for<'r, 's> Fn(&'r context::Context<'s>, RenderStatus),
903> FnStatus<'a> for T
904{
905}
906
907// FIXME once trait aliases are in stable.
908/*
909trait FnStatus<'a> = FnMut(
910    // Status of interactive render session.
911    status: RenderStatus
912    )
913    + 'a
914*/
915
916/// Wrapper to pass a [`FnStatus`] closure to a [`Context`].
917pub struct StatusCallback<'a>(Box<Box<dyn FnStatus<'a>>>);
918
919unsafe impl Send for StatusCallback<'static> {}
920unsafe impl Sync for StatusCallback<'static> {}
921
922impl<'a> StatusCallback<'a> {
923    pub fn new<F>(fn_status: F) -> Self
924    where
925        F: FnStatus<'a>,
926    {
927        StatusCallback(Box::new(Box::new(fn_status)))
928    }
929}
930
931impl CallbackPtr for StatusCallback<'_> {
932    #[doc(hidden)]
933    fn to_ptr(self) -> *const core::ffi::c_void {
934        Box::into_raw(self.0) as *const _ as _
935    }
936
937    #[doc(hidden)]
938    unsafe fn drop_ptr(ptr: *const core::ffi::c_void) {
939        // SAFETY: `ptr` came from `to_ptr` above and is reclaimed once.
940        drop(unsafe { Box::from_raw(ptr as *mut Box<dyn FnStatus<'static>>) });
941    }
942}
943
944// Trampoline function for the FnStatus callback.
945#[unsafe(no_mangle)]
946pub(crate) extern "C" fn render_status(
947    payload: *mut c_void,
948    context: nsi_sys::NSIContext,
949    status: c_int,
950) {
951    // Catch any panics to prevent unwinding into C code.
952    let _ = std::panic::catch_unwind(|| {
953        if !payload.is_null() {
954            // SAFETY: The payload originates from `StatusCallback::to_ptr`, which
955            // leaks a `Box<dyn FnStatus>`. We only borrow it here and never take
956            // ownership, so the pointer stays valid for the lifetime of the
957            // context.
958            let fn_status = unsafe { &*(payload as *mut Box<dyn FnStatus>) };
959            // The renderer that made this context, so a closure
960            // calling back into it reaches the right one. See
961            // `crate::renderer_of`.
962            let Ok(api) = crate::renderer_of(context) else {
963                return;
964            };
965            let ctx = Context(Arc::new(InnerContext::new(context, api)));
966
967            fn_status(&ctx, status.into());
968
969            // We must not call drop() on this context.
970            // This is safe as Context doesn't allocate and this one is on
971            // the stack anyway.
972            std::mem::forget(ctx);
973        }
974    });
975    // If we panic, we can't do much -- just return silently.
976}
977
978/// A closure which is called to inform about the errors during scene defintion
979/// or a render.
980///
981/// It is passed to ɴsɪ via [`new()`](Context::new())’s `"errorhandler"`
982/// argument.
983///
984/// # Examples
985///
986/// ```
987/// # use nsi_ffi_wrap as nsi;
988/// use log::{Level, debug, error, info, trace, warn};
989///
990/// let error_handler = nsi::ErrorCallback::new(
991///     |level: Level, message_id: i32, message: &str| match level {
992///         Level::Error => error!("[{}] {}", message_id, message),
993///         Level::Warn => warn!("[{}] {}", message_id, message),
994///         Level::Info => info!("[{}] {}", message_id, message),
995///         Level::Debug => debug!("[{}] {}", message_id, message),
996///         Level::Trace => trace!("[{}] {}", message_id, message),
997///     },
998/// );
999///
1000/// let ctx = nsi::Context::new(Some(&[nsi::callback!(
1001///     "errorhander",
1002///     error_handler
1003/// )]))
1004/// .unwrap();
1005///
1006/// // Do something with ctx ...
1007/// ```
1008pub trait FnError<'a>: Fn(
1009    // The error level.
1010    log::Level,
1011    // The message id.
1012    i32,
1013    // The message.
1014    &str,
1015)
1016+ 'a {}
1017
1018#[doc(hidden)]
1019impl<
1020    'a,
1021    T: Fn(log::Level, i32, &str) + 'a + for<'r> Fn(log::Level, i32, &'r str),
1022> FnError<'a> for T
1023{
1024}
1025
1026/// Wrapper to pass a [`FnError`] closure to a [`Context`].
1027pub struct ErrorCallback<'a>(Box<Box<dyn FnError<'a>>>);
1028
1029unsafe impl Send for ErrorCallback<'static> {}
1030unsafe impl Sync for ErrorCallback<'static> {}
1031
1032impl<'a> ErrorCallback<'a> {
1033    pub fn new<F>(fn_error: F) -> Self
1034    where
1035        F: FnError<'a>,
1036    {
1037        ErrorCallback(Box::new(Box::new(fn_error)))
1038    }
1039}
1040
1041impl CallbackPtr for ErrorCallback<'_> {
1042    #[doc(hidden)]
1043    fn to_ptr(self) -> *const core::ffi::c_void {
1044        Box::into_raw(self.0) as *const _ as _
1045    }
1046
1047    #[doc(hidden)]
1048    unsafe fn drop_ptr(ptr: *const core::ffi::c_void) {
1049        // SAFETY: `ptr` came from `to_ptr` above and is reclaimed once.
1050        drop(unsafe { Box::from_raw(ptr as *mut Box<dyn FnError<'static>>) });
1051    }
1052}
1053
1054// Trampoline function for the FnError callback.
1055#[unsafe(no_mangle)]
1056pub(crate) extern "C" fn error_handler(
1057    payload: *mut c_void,
1058    level: c_int,
1059    code: c_int,
1060    message: *const c_char,
1061) {
1062    // Catch any panics to prevent unwinding into C code.
1063    let _ = std::panic::catch_unwind(|| {
1064        if !payload.is_null() {
1065            // SAFETY: The payload originates from `ErrorCallback::to_ptr`, which
1066            // leaks a `Box<dyn FnError>`. We only borrow it here and never take
1067            // ownership, so the pointer stays valid for the lifetime of the
1068            // context.
1069            let fn_error = unsafe { &*(payload as *mut Box<dyn FnError>) };
1070
1071            // SAFETY: message pointer comes from NSI C API and should be valid.
1072            // We validate it is not null and handle UTF-8 errors gracefully.
1073            let message = if message.is_null() {
1074                "<null message>"
1075            } else {
1076                let c_str = unsafe { CStr::from_ptr(message as _) };
1077                match c_str.to_str() {
1078                    Ok(s) => s,
1079                    Err(_) => {
1080                        // If the string is not valid UTF-8, use lossy conversion
1081                        // This prevents crashes while still conveying the message
1082                        &c_str.to_string_lossy()
1083                    }
1084                }
1085            };
1086
1087            let level = match NSIErrorLevel::from(level) {
1088                NSIErrorLevel::Message => log::Level::Trace,
1089                NSIErrorLevel::Info => log::Level::Info,
1090                NSIErrorLevel::Warning => log::Level::Warn,
1091                NSIErrorLevel::Error => log::Level::Error,
1092            };
1093
1094            fn_error(level, code as _, message);
1095        }
1096    });
1097    // If we panic, we cannot do much - just return silently.
1098}