Skip to main content

hapi_rs/
session.rs

1//! Session is responsible for communicating with HAPI
2//!
3//! The Engine [promises](https://www.sidefx.com/docs/hengine/_h_a_p_i__sessions.html#HAPI_Sessions_Multithreading)
4//! to be thread-safe when accessing a single `Session` from multiple threads.
5//! `hapi-rs` relies on this promise and the [Session] struct holds only an `Arc` pointer to the session,
6//! and *does not* protect the session with Mutex, although there is a [`ReentrantMutex`]
7//! private member which is used internally in a few cases where API calls must be sequential.
8//!
9//! When the last instance of the `Session` is about to get dropped, it'll be cleaned up
10//! (if [`SessionOptions::cleanup`] was set) and automatically closed.
11//!
12//! The Engine process (pipe, socket, or shared memory) can be auto-terminated as well if told so when starting
13//! the server. See [`crate::server::start_engine_server`] together with the transport helpers
14//! [`crate::server::connect_to_pipe_server`], [`crate::server::connect_to_socket_server`], and
15//! [`crate::server::connect_to_memory_server`].
16//!
17//! Helper constructors terminate the server by default. This is useful for quick one-off jobs.
18//!
19use log::{debug, error};
20use parking_lot::ReentrantMutex;
21use std::fmt::Debug;
22use std::path::PathBuf;
23use std::{ffi::CString, path::Path, sync::Arc};
24
25pub use crate::{
26    asset::AssetLibrary,
27    errors::*,
28    ffi::{
29        CompositorOptions, CookOptions, ImageFileFormat, SessionInfo, SessionSyncInfo,
30        ThriftServerOptions, TimelineOptions, Viewport, enums::*,
31    },
32    node::{HoudiniNode, ManagerNode, ManagerType, NodeHandle, NodeType, Transform},
33    parameter::Parameter,
34    server::ServerOptions,
35    stringhandle::StringArray,
36};
37
38// A result of HAPI_GetStatus with HAPI_STATUS_COOK_STATE
39pub type SessionState = State;
40pub type LicenseType = raw::License;
41
42use crate::cop::CopImageDescription;
43use crate::ffi::ImageInfo;
44use crate::stringhandle::StringHandle;
45use crate::{ffi::raw, utils};
46
47/// Builder struct for [`Session::node_builder`] API
48pub struct NodeBuilder<'s> {
49    session: &'s Session,
50    name: String,
51    label: Option<String>,
52    parent: Option<NodeHandle>,
53    cook: bool,
54}
55
56impl NodeBuilder<'_> {
57    /// Give new node a label
58    #[must_use]
59    pub fn with_label(mut self, label: impl Into<String>) -> Self {
60        self.label = Some(label.into());
61        self
62    }
63
64    /// Create new node as child of a parent node.
65    #[must_use]
66    pub fn with_parent<H: AsRef<NodeHandle>>(mut self, parent: H) -> Self {
67        self.parent.replace(*parent.as_ref());
68        self
69    }
70
71    /// Cook node after creation.
72    #[must_use]
73    pub fn cook(mut self, cook: bool) -> Self {
74        self.cook = cook;
75        self
76    }
77
78    /// Consume the builder and create the node
79    pub fn create(self) -> Result<HoudiniNode> {
80        let NodeBuilder {
81            session,
82            name,
83            label,
84            parent,
85            cook,
86        } = self;
87        session.create_node_with(&name, parent, label.as_deref(), cook)
88    }
89}
90
91impl PartialEq for raw::HAPI_Session {
92    fn eq(&self, other: &Self) -> bool {
93        self.type_ == other.type_ && self.id == other.id
94    }
95}
96
97/// Trait bound for [`Session::get_server_var()`] and [`Session::set_server_var()`]
98pub trait EnvVariable {
99    type Type: ?Sized + ToOwned + Debug;
100    fn get_value(session: &Session, key: impl AsRef<str>)
101    -> Result<<Self::Type as ToOwned>::Owned>;
102    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()>;
103}
104
105impl EnvVariable for str {
106    type Type = str;
107
108    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<String> {
109        let key = CString::new(key.as_ref())?;
110        let handle = crate::ffi::get_server_env_str(session, &key)?;
111        crate::stringhandle::get_string(handle, session)
112    }
113
114    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
115        let key = CString::new(key.as_ref())?;
116        let val = CString::new(val)?;
117        crate::ffi::set_server_env_str(session, &key, &val)
118    }
119}
120
121impl EnvVariable for Path {
122    type Type = Self;
123
124    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<PathBuf> {
125        let key = CString::new(key.as_ref())?;
126        crate::stringhandle::get_string(crate::ffi::get_server_env_str(session, &key)?, session)
127            .map(PathBuf::from)
128    }
129
130    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
131        let key = CString::new(key.as_ref())?;
132        let val = utils::path_to_cstring(val)?;
133        crate::ffi::set_server_env_str(session, &key, &val)
134    }
135}
136
137impl EnvVariable for i32 {
138    type Type = Self;
139
140    fn get_value(session: &Session, key: impl AsRef<str>) -> Result<Self::Type> {
141        let key = CString::new(key.as_ref())?;
142        crate::ffi::get_server_env_int(session, &key)
143    }
144
145    fn set_value(session: &Session, key: impl AsRef<str>, val: &Self::Type) -> Result<()> {
146        let key = CString::new(key.as_ref())?;
147        crate::ffi::set_server_env_int(session, &key, *val)
148    }
149}
150
151/// Result of async cook operation [`Session::cook`]
152#[derive(Debug, Clone, Eq, PartialEq)]
153pub enum CookResult {
154    Succeeded,
155    /// Some nodes cooked with errors
156    CookErrors(String),
157    /// One or more nodes could not cook - should abort cooking
158    FatalErrors(String),
159}
160
161impl CookResult {
162    /// Convenient method for cook result message if any
163    #[must_use]
164    pub fn message(&self) -> Option<&str> {
165        match self {
166            Self::Succeeded => None,
167            Self::CookErrors(msg) | Self::FatalErrors(msg) => Some(msg.as_str()),
168        }
169    }
170}
171
172/// By which means the session communicates with the server.
173#[derive(Debug)]
174pub(crate) struct SessionInner {
175    pub(crate) handle: raw::HAPI_Session,
176    pub(crate) options: SessionOptions,
177    // Server options are only available for Thrift servers.
178    pub(crate) server_options: Option<ServerOptions>,
179    pub(crate) lock: ReentrantMutex<()>,
180    pub(crate) server_pid: Option<u32>,
181}
182
183/// Session represents a unique connection to the Engine instance and all API calls require a valid session.
184/// It implements [`Clone`] and is [`Send`] and [`Sync`]
185#[derive(Debug, Clone)]
186pub struct Session {
187    pub(crate) inner: Arc<SessionInner>,
188}
189
190impl PartialEq for Session {
191    fn eq(&self, other: &Self) -> bool {
192        self.inner.handle.id == other.inner.handle.id
193            && self.inner.handle.type_ == other.inner.handle.type_
194    }
195}
196
197#[derive(Debug)]
198pub struct UninitializedSession {
199    pub(crate) session_handle: raw::HAPI_Session,
200    pub(crate) server_options: Option<ServerOptions>,
201    pub(crate) server_pid: Option<u32>,
202}
203
204impl UninitializedSession {
205    pub fn initialize(self, session_options: SessionOptions) -> Result<Session> {
206        debug!("Initializing session");
207        crate::ffi::initialize_session(self.session_handle, &session_options)
208            .map(|()| Session {
209                inner: Arc::new(SessionInner {
210                    handle: self.session_handle,
211                    options: session_options,
212                    lock: ReentrantMutex::new(()),
213                    server_options: self.server_options,
214                    server_pid: self.server_pid,
215                }),
216            })
217            .with_context(|| "Calling initialize_session")
218    }
219}
220
221impl Session {
222    /// Return [`SessionType`] current session is initialized with.
223    #[must_use]
224    pub fn session_type(&self) -> SessionType {
225        self.inner.handle.type_
226    }
227
228    /// Return enum with extra connection data such as pipe file or socket.
229    #[must_use]
230    pub fn server_pid(&self) -> Option<u32> {
231        self.inner.server_pid
232    }
233
234    #[inline]
235    pub(crate) fn ptr(&self) -> *const raw::HAPI_Session {
236        &raw const self.inner.handle
237    }
238
239    /// Set environment variable on the server. This is set AFTER the server has started.
240    /// For variables set before the server starts, use [`ServerOptions::with_env_variables`].
241    pub fn set_server_var<T: EnvVariable + ?Sized>(
242        &self,
243        key: &str,
244        value: &T::Type,
245    ) -> Result<()> {
246        debug_assert!(self.is_valid());
247        debug!("Setting server variable {key}={value:?}");
248        T::set_value(self, key, value)
249    }
250
251    /// Get environment variable from the server
252    pub fn get_server_var<T: EnvVariable + ?Sized>(
253        &self,
254        key: &str,
255    ) -> Result<<T::Type as ToOwned>::Owned> {
256        debug_assert!(self.is_valid());
257        debug!("Querying server variable {key}");
258        T::get_value(self, key)
259    }
260
261    /// Retrieve all server variables as contiguous string array.
262    /// Iterating over the array will yield strings like "TEST=177".
263    pub fn get_server_variables(&self) -> Result<StringArray> {
264        debug_assert!(self.is_valid());
265        debug!("Querying all server variables");
266        let count = crate::ffi::get_server_env_var_count(self)?;
267        let handles = crate::ffi::get_server_env_var_list(self, count)?;
268        crate::stringhandle::get_string_array(&handles, self).context("Calling get_string_array")
269    }
270
271    /// Retrieve string data given a handle.
272    pub fn get_string(&self, handle: StringHandle) -> Result<String> {
273        crate::stringhandle::get_string(handle, self)
274    }
275
276    /// Retrieve multiple strings in batch mode.
277    pub fn get_string_batch(&self, handles: &[StringHandle]) -> Result<StringArray> {
278        crate::stringhandle::get_string_array(handles, self)
279    }
280
281    /// Push a custom string to the server and return a handle to it.
282    pub fn set_custom_string(&self, string: impl AsRef<str>) -> Result<StringHandle> {
283        debug_assert!(self.is_valid());
284        debug!("Setting custom string: {}", string.as_ref());
285        let string = CString::new(string.as_ref())?;
286        crate::ffi::set_custom_string(self, &string)
287    }
288
289    /// Remove a custom string from the server.
290    pub fn remove_custom_string(&self, handle: StringHandle) -> Result<()> {
291        debug_assert!(self.is_valid());
292        debug!("Removing custom string: {handle:?}");
293        crate::ffi::remove_custom_string(self, handle)
294    }
295
296    /// Consumes and cleanups up the session. Session becomes invalid after this call
297    pub fn cleanup(self) -> Result<()> {
298        debug!("Cleaning up session");
299        debug_assert!(self.is_valid());
300        crate::ffi::cleanup_session(&self)
301    }
302
303    /// Create an input geometry node which can accept modifications
304    pub fn create_input_node(
305        &self,
306        name: &str,
307        parent: Option<NodeHandle>,
308    ) -> Result<crate::geometry::Geometry> {
309        debug!("Creating input node: {name}");
310        debug_assert!(self.is_valid());
311        let name = CString::new(name)?;
312        let id = crate::ffi::create_input_node(self, &name, parent)?;
313        let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
314        crate::ffi::cook_node(&node, None).with_context(|| "Cooking input node")?;
315        let info =
316            crate::geometry::GeoInfo::from_node(&node).with_context(|| "Getting geometry info")?;
317        Ok(crate::geometry::Geometry { node, info })
318    }
319
320    /// Create an input geometry node with [`PartType`] set to `Curve`
321    pub fn create_input_curve_node(
322        &self,
323        name: &str,
324        parent: Option<NodeHandle>,
325    ) -> Result<crate::geometry::Geometry> {
326        debug!("Creating input curve node: {name}");
327        debug_assert!(self.is_valid());
328        let name = CString::new(name)?;
329        let id = crate::ffi::create_input_curve_node(self, &name, parent)?;
330        let node = HoudiniNode::new(self.clone(), NodeHandle(id), None)?;
331        let info = crate::geometry::GeoInfo::from_node(&node)?;
332        Ok(crate::geometry::Geometry { node, info })
333    }
334
335    /// Create a node. `name` must start with a network category, e.g, "Object/geo", "Sop/box",
336    /// in operator namespace was used, the full name may look like this: `namespace::Object/mynode`
337    /// If you need more creating options, see the [`Session::node_builder`] API.
338    /// New node will *not* be cooked.
339    pub fn create_node(&self, name: impl AsRef<str>) -> Result<HoudiniNode> {
340        self.create_node_with(name.as_ref(), None, None, false)
341    }
342
343    /// A builder pattern for creating a node with more options.
344    pub fn node_builder(&self, node_name: impl Into<String>) -> NodeBuilder<'_> {
345        NodeBuilder {
346            session: self,
347            name: node_name.into(),
348            label: None,
349            parent: None,
350            cook: false,
351        }
352    }
353
354    // Internal function for creating nodes
355    pub(crate) fn create_node_with<P>(
356        &self,
357        name: &str,
358        parent: P,
359        label: Option<&str>,
360        cook: bool,
361    ) -> Result<HoudiniNode>
362    where
363        P: Into<Option<NodeHandle>>,
364    {
365        let parent = parent.into();
366        debug!("Creating node instance for op: {name}, with parent: {parent:?}");
367        debug_assert!(self.is_valid());
368        debug_assert!(
369            parent.is_some() || name.contains('/'),
370            "Node name must be fully qualified if parent node is not specified"
371        );
372        debug_assert!(
373            !(parent.is_some() && name.contains('/')),
374            "Cannot use fully qualified node name with parent node"
375        );
376        let name = CString::new(name)?;
377        let label = label.map(CString::new).transpose()?;
378        let node_id = crate::ffi::create_node(&name, label.as_deref(), self, parent, cook)?;
379        if self.inner.options.threaded {
380            // In async cooking mode, cook() always returns a CookResult::Success, we need to check CookResult for errors
381            if let CookResult::FatalErrors(message) = self.cook()? {
382                return Err(HapiError::Hapi {
383                    result_code: HapiResultCode(HapiResult::Failure),
384                    server_message: Some(message),
385                    contexts: Vec::new(),
386                });
387            }
388        }
389        HoudiniNode::new(self.clone(), NodeHandle(node_id), None)
390    }
391
392    /// Delete the node from the session. See also [`HoudiniNode::delete`]
393    pub fn delete_node<H: Into<NodeHandle>>(&self, node: H) -> Result<()> {
394        let node = node.into();
395        debug!(
396            "Deleting node {}",
397            node.path(self)
398                .unwrap_or_else(|_| "Could not get path".to_owned())
399        );
400        crate::ffi::delete_node(node, self)
401    }
402
403    /// Find a node given an absolute path. To find a child node, pass the `parent` node
404    /// or use [`HoudiniNode::find_child_node`]
405    pub fn get_node_from_path(
406        &self,
407        path: impl AsRef<str>,
408        parent: impl Into<Option<NodeHandle>>,
409    ) -> Result<Option<HoudiniNode>> {
410        debug_assert!(self.is_valid());
411        debug!("Searching node at path: {}", path.as_ref());
412        let path = CString::new(path.as_ref())?;
413        match crate::ffi::get_node_from_path(self, parent.into(), &path) {
414            Ok(handle) => Ok(NodeHandle(handle).to_node(self).ok()),
415            Err(HapiError::Hapi { result_code, .. })
416                if matches!(result_code.0, HapiResult::InvalidArgument) =>
417            {
418                Ok(None)
419            }
420            Err(e) => Err(e),
421        }
422    }
423
424    /// Find a parameter by path, absolute or relative to a start node.
425    pub fn find_parameter_from_path(
426        &self,
427        path: impl AsRef<str>,
428        start: impl Into<Option<NodeHandle>>,
429    ) -> Result<Option<Parameter>> {
430        debug_assert!(self.is_valid());
431        debug!("Searching parameter at path: {}", path.as_ref());
432        let Some((path, parm)) = path.as_ref().rsplit_once('/') else {
433            return Ok(None);
434        };
435        let Some(node) = self.get_node_from_path(path, start)? else {
436            debug!("Node {path} not found");
437            return Ok(None);
438        };
439        Ok(node.parameter(parm).ok())
440    }
441
442    /// Returns a manager (root) node such as OBJ, TOP, CHOP, etc
443    pub fn get_manager_node(&self, manager: ManagerType) -> Result<ManagerNode> {
444        debug_assert!(self.is_valid());
445        debug!("Getting Manager node of type: {manager:?}");
446        let node_type = NodeType::from(manager);
447        let handle = crate::ffi::get_manager_node(self, node_type)?;
448        Ok(ManagerNode {
449            session: self.clone(),
450            handle: NodeHandle(handle),
451            node_type: manager,
452        })
453    }
454
455    /// Return a list of transforms for all object nodes under a given parent node.
456    pub fn get_composed_object_transform(
457        &self,
458        parent: impl AsRef<NodeHandle>,
459        rst_order: RSTOrder,
460    ) -> Result<Vec<Transform>> {
461        debug_assert!(self.is_valid());
462        crate::ffi::get_composed_object_transforms(self, *parent.as_ref(), rst_order)
463            .map(|transforms| transforms.into_iter().map(Transform).collect())
464    }
465
466    /// Save current session to hip file
467    pub fn save_hip(&self, path: impl AsRef<Path>, lock_nodes: bool) -> Result<()> {
468        debug!("Saving hip file: {}", path.as_ref().display());
469        debug_assert!(self.is_valid());
470        let path = utils::path_to_cstring(path)?;
471        crate::ffi::save_hip(self, &path, lock_nodes)
472    }
473
474    /// Load a hip file into current session
475    pub fn load_hip(&self, path: impl AsRef<Path>, cook: bool) -> Result<()> {
476        debug!("Loading hip file: {}", path.as_ref().display());
477        debug_assert!(self.is_valid());
478        let path = utils::path_to_cstring(path)?;
479        crate::ffi::load_hip(self, &path, cook)
480    }
481
482    /// Merge a hip file into current session
483    pub fn merge_hip(&self, name: &str, cook: bool) -> Result<i32> {
484        debug!("Merging hip file: {name}");
485        debug_assert!(self.is_valid());
486        let name = CString::new(name)?;
487        crate::ffi::merge_hip(self, &name, cook)
488    }
489
490    /// Get node ids created by merging [`Session::merge_hip`] a hip file.
491    pub fn get_hip_file_nodes(&self, hip_id: i32) -> Result<Vec<NodeHandle>> {
492        crate::ffi::get_hipfile_node_ids(self, hip_id)
493            .map(|handles| handles.into_iter().map(NodeHandle).collect())
494    }
495
496    /// Load an HDA file into current session
497    pub fn load_asset_file(&self, file: impl AsRef<Path>) -> Result<AssetLibrary> {
498        debug_assert!(self.is_valid());
499        AssetLibrary::from_file(self.clone(), file)
500    }
501
502    /// Returns a list of loaded asset libraries including Houdini's default.
503    pub fn get_loaded_asset_libraries(&self) -> Result<Vec<AssetLibrary>> {
504        debug_assert!(self.is_valid());
505
506        crate::ffi::get_asset_library_ids(self)?
507            .into_iter()
508            .map(|library_id| {
509                crate::ffi::get_asset_library_file_path(self, library_id).map(|path| AssetLibrary {
510                    lib_id: library_id,
511                    session: self.clone(),
512                    file: Some(PathBuf::from(path)),
513                })
514            })
515            .collect()
516    }
517
518    /// Interrupt session cooking
519    pub fn interrupt(&self) -> Result<()> {
520        debug_assert!(self.is_valid());
521        debug!("Interrupting session cooking");
522        crate::ffi::interrupt(self)
523    }
524
525    // Uncertain if this API makes sense.
526    #[doc(hidden)]
527    #[allow(unused)]
528    pub(crate) fn get_call_result_status(&self) -> Result<HapiResult> {
529        debug_assert!(self.is_valid());
530        let status = crate::ffi::get_status_code(self, StatusType::CallResult)?;
531        Ok(unsafe { std::mem::transmute::<i32, HapiResult>(status) })
532    }
533
534    /// Get session state when the server is in threaded mode.
535    pub fn get_cook_state_status(&self) -> Result<SessionState> {
536        debug_assert!(self.is_valid());
537        crate::ffi::get_cook_state_status(self)
538    }
539
540    /// Is session currently cooking. In non-threaded mode always returns false
541    pub fn is_cooking(&self) -> Result<bool> {
542        debug_assert!(self.is_valid());
543        Ok(matches!(
544            self.get_cook_state_status()?,
545            SessionState::Cooking
546        ))
547    }
548
549    /// Explicit check if the session is valid. Many APIs do this check in the debug build.
550    #[inline]
551    #[must_use]
552    pub fn is_valid(&self) -> bool {
553        crate::ffi::is_session_valid(self)
554    }
555
556    /// Get the status message given a type and verbosity
557    pub fn get_status_string(
558        &self,
559        status: StatusType,
560        verbosity: StatusVerbosity,
561    ) -> Result<String> {
562        debug_assert!(self.is_valid());
563        crate::ffi::get_status_string(self, status, verbosity)
564    }
565
566    /// Get session cook result status as string
567    pub fn get_cook_result_string(&self, verbosity: StatusVerbosity) -> Result<String> {
568        debug_assert!(self.is_valid());
569        self.get_status_string(StatusType::CookResult, verbosity)
570    }
571
572    /// How many nodes need to cook
573    pub fn cooking_total_count(&self) -> Result<i32> {
574        debug_assert!(self.is_valid());
575        crate::ffi::get_cooking_total_count(self)
576    }
577
578    /// How many nodes have already cooked
579    pub fn cooking_current_count(&self) -> Result<i32> {
580        debug_assert!(self.is_valid());
581        crate::ffi::get_cooking_current_count(self)
582    }
583
584    /// In threaded mode wait for Session finishes cooking. In single-thread mode, immediately return
585    /// See [Documentation](https://www.sidefx.com/docs/hengine/_h_a_p_i__sessions.html)
586    pub fn cook(&self) -> Result<CookResult> {
587        debug_assert!(self.is_valid());
588        debug!("Cooking session..");
589        if self.inner.options.threaded {
590            loop {
591                match self.get_cook_state_status()? {
592                    SessionState::Ready => break Ok(CookResult::Succeeded),
593                    SessionState::ReadyWithFatalErrors => {
594                        self.interrupt()?;
595                        let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
596                        break Ok(CookResult::FatalErrors(err));
597                    }
598                    SessionState::ReadyWithCookErrors => {
599                        let err = self.get_cook_result_string(StatusVerbosity::Errors)?;
600                        break Ok(CookResult::CookErrors(err));
601                    }
602                    // Continue polling
603                    _ => {}
604                }
605            }
606        } else {
607            // In single threaded mode, the cook happens inside of HAPI_CookNode(),
608            // and HAPI_GetStatus() will immediately return HAPI_STATE_READY.
609            Ok(CookResult::Succeeded)
610        }
611    }
612
613    /// Retrieve connection error if could not connect to engine instance
614    pub fn get_connection_error(&self, clear: bool) -> Result<String> {
615        debug_assert!(self.is_valid());
616        crate::ffi::get_connection_error(clear)
617    }
618
619    /// Get Houdini time
620    pub fn get_time(&self) -> Result<f64> {
621        debug_assert!(self.is_valid());
622        crate::ffi::get_time(self)
623    }
624
625    /// Set Houdini time
626    pub fn set_time(&self, time: f64) -> Result<()> {
627        debug_assert!(self.is_valid());
628        crate::ffi::set_time(self, time)
629    }
630
631    /// Lock the internal reentrant mutex. Should not be used in general, but may be useful
632    /// in certain situations when a series of API calls must be done in sequence
633    pub fn lock(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
634        self.inner.lock.lock()
635    }
636
637    /// Set Houdini timeline options
638    pub fn set_timeline_options(&self, options: &TimelineOptions) -> Result<()> {
639        debug_assert!(self.is_valid());
640        crate::ffi::set_timeline_options(self, &options.0)
641    }
642
643    /// Get Houdini timeline options
644    pub fn get_timeline_options(&self) -> Result<TimelineOptions> {
645        debug_assert!(self.is_valid());
646        crate::ffi::get_timeline_options(self).map(TimelineOptions)
647    }
648
649    /// Set session to use Houdini time
650    pub fn set_use_houdini_time(&self, do_use: bool) -> Result<()> {
651        debug_assert!(self.is_valid());
652        crate::ffi::set_use_houdini_time(self, do_use)
653    }
654
655    /// Check if session uses Houdini time
656    pub fn get_use_houdini_time(&self) -> Result<bool> {
657        debug_assert!(self.is_valid());
658        crate::ffi::get_use_houdini_time(self)
659    }
660
661    /// Get the viewport(camera) position
662    pub fn get_viewport(&self) -> Result<Viewport> {
663        debug_assert!(self.is_valid());
664        crate::ffi::get_viewport(self).map(Viewport)
665    }
666
667    /// Set the viewport(camera) position
668    pub fn set_viewport(&self, viewport: &Viewport) -> Result<()> {
669        debug_assert!(self.is_valid());
670        crate::ffi::set_viewport(self, viewport)
671    }
672
673    /// Set session sync mode on/off
674    pub fn set_sync(&self, enable: bool) -> Result<()> {
675        debug_assert!(self.is_valid());
676        crate::ffi::set_session_sync(self, enable)
677    }
678    /// Get session sync info
679    pub fn get_sync_info(&self) -> Result<SessionSyncInfo> {
680        debug_assert!(self.is_valid());
681        crate::ffi::get_session_sync_info(self).map(SessionSyncInfo)
682    }
683
684    /// Set session sync info
685    pub fn set_sync_info(&self, info: &SessionSyncInfo) -> Result<()> {
686        debug_assert!(self.is_valid());
687        crate::ffi::set_session_sync_info(self, &info.0)
688    }
689
690    /// Get license type used by this session
691    pub fn get_license_type(&self) -> Result<LicenseType> {
692        debug_assert!(self.is_valid());
693        crate::ffi::session_get_license_type(self)
694    }
695
696    /// Render a COP node to an image file
697    pub fn render_cop_to_image(
698        &self,
699        cop_node: impl Into<NodeHandle>,
700        output_name: Option<&str>,
701        image_planes: impl AsRef<str>,
702        out_image: impl AsRef<Path>,
703    ) -> Result<String> {
704        let cop_node = cop_node.into();
705        let out_image = out_image.as_ref();
706        debug!("Start rendering COP to image file {}", out_image.display());
707        debug_assert!(cop_node.is_valid(self)?);
708        if let Some(output_name) = output_name {
709            let output_name = CString::new(output_name)?;
710            crate::ffi::render_cop_output_to_image(self, cop_node, &output_name)?;
711        } else {
712            crate::ffi::render_cop_to_image(self, cop_node)?;
713        }
714        crate::material::extract_image_to_file(self, cop_node, image_planes, out_image)
715    }
716
717    /// Loads some raw image data into a COP node.
718    /// TODO: Figure out which node the data is actually ends up in as the API doesn't say.
719    pub fn create_cop_image(
720        &self,
721        description: CopImageDescription,
722        parent_node: Option<NodeHandle>,
723    ) -> Result<()> {
724        crate::ffi::create_cop_image(
725            self,
726            parent_node,
727            description.width,
728            description.height,
729            description.packing,
730            description.flip_x,
731            description.flip_y,
732            description.image_data,
733        )
734    }
735
736    // TODO: consider removing this in favour of ['Material'] helper struct which has this API
737    pub fn render_texture_to_image(
738        &self,
739        node: impl Into<NodeHandle>,
740        parm_name: &str,
741    ) -> Result<()> {
742        debug_assert!(self.is_valid());
743        let name = CString::new(parm_name)?;
744        let node = node.into();
745        let id = crate::ffi::get_parm_id_from_name(&name, node, self)?;
746        crate::ffi::render_texture_to_image(self, node, crate::parameter::ParmHandle(id))
747    }
748
749    // TODO: consider removing this in favour of ['Material'] helper struct which has this API
750    pub fn extract_image_to_file(
751        &self,
752        node: impl Into<NodeHandle>,
753        image_planes: &str,
754        path: impl AsRef<Path>,
755    ) -> Result<String> {
756        crate::material::extract_image_to_file(self, node.into(), image_planes, path)
757    }
758
759    // TODO: consider removing this in favour of ['Material'] helper struct which has this API
760    pub fn extract_image_to_memory(
761        &self,
762        node: impl Into<NodeHandle>,
763        buffer: &mut Vec<u8>,
764        image_planes: impl AsRef<str>,
765        format: impl AsRef<str>,
766    ) -> Result<()> {
767        debug_assert!(self.is_valid());
768        crate::material::extract_image_to_memory(self, node.into(), buffer, image_planes, format)
769    }
770
771    // TODO: consider removing this in favour of ['Material'] helper struct which has this API
772    pub fn get_image_info(&self, node: impl Into<NodeHandle>) -> Result<ImageInfo> {
773        debug_assert!(self.is_valid());
774        crate::ffi::get_image_info(self, node.into()).map(ImageInfo)
775    }
776
777    /// Render a COP node to a memory buffer
778    // TODO: consider removing this in favour of ['Material'] helper struct which has this API
779    pub fn render_cop_to_memory(
780        &self,
781        cop_node: impl Into<NodeHandle>,
782        buffer: &mut Vec<u8>,
783        image_planes: impl AsRef<str>,
784        format: impl AsRef<str>,
785    ) -> Result<()> {
786        debug!("Start rendering COP to memory.");
787        let cop_node = cop_node.into();
788        debug_assert!(cop_node.is_valid(self)?);
789        crate::ffi::render_cop_to_image(self, cop_node)?;
790        crate::material::extract_image_to_memory(self, cop_node, buffer, image_planes, format)
791    }
792
793    pub fn get_supported_image_formats(&self) -> Result<Vec<ImageFileFormat<'_>>> {
794        debug_assert!(self.is_valid());
795        crate::ffi::get_supported_image_file_formats(self).map(|v| {
796            v.into_iter()
797                .map(|inner| ImageFileFormat(inner, self.into()))
798                .collect()
799        })
800    }
801
802    pub fn get_active_cache_names(&self) -> Result<StringArray> {
803        debug_assert!(self.is_valid());
804        crate::ffi::get_active_cache_names(self)
805    }
806
807    pub fn get_cache_property_value(
808        &self,
809        cache_name: &str,
810        property: CacheProperty,
811    ) -> Result<i32> {
812        let cache_name = CString::new(cache_name)?;
813        crate::ffi::get_cache_property(self, &cache_name, property)
814    }
815
816    pub fn set_cache_property_value(
817        &self,
818        cache_name: &str,
819        property: CacheProperty,
820        value: i32,
821    ) -> Result<()> {
822        let cache_name = CString::new(cache_name)?;
823        crate::ffi::set_cache_property(self, &cache_name, property, value)
824    }
825
826    pub fn python_thread_interpreter_lock(&self, lock: bool) -> Result<()> {
827        debug_assert!(self.is_valid());
828        crate::ffi::python_thread_interpreter_lock(self, lock)
829    }
830    pub fn get_compositor_options(&self) -> Result<CompositorOptions> {
831        crate::ffi::get_compositor_options(self).map(CompositorOptions)
832    }
833
834    pub fn set_compositor_options(&self, options: &CompositorOptions) -> Result<()> {
835        crate::ffi::set_compositor_options(self, &options.0)
836    }
837
838    pub fn get_preset_names(&self, bytes: &[u8]) -> Result<Vec<String>> {
839        debug_assert!(self.is_valid());
840        let mut handles = vec![];
841        for handle in crate::ffi::get_preset_names(self, bytes)? {
842            let v = crate::stringhandle::get_string(handle, self)?;
843            handles.push(v);
844        }
845        Ok(handles)
846    }
847
848    pub fn start_performance_monitor_profile(&self, title: &str) -> Result<i32> {
849        let title = CString::new(title)?;
850        crate::ffi::start_performance_monitor_profile(self, &title)
851    }
852
853    pub fn stop_performance_monitor_profile(
854        &self,
855        profile_id: i32,
856        output_file: &str,
857    ) -> Result<()> {
858        let output_file = CString::new(output_file)?;
859        crate::ffi::stop_performance_monitor_profile(self, profile_id, &output_file)
860    }
861
862    #[cfg(feature = "async-cooking")]
863    pub fn get_job_status(&self, job_id: i32) -> Result<JobStatus> {
864        crate::ffi::get_job_status(self, job_id)
865    }
866}
867
868impl Drop for Session {
869    fn drop(&mut self) {
870        if Arc::strong_count(&self.inner) == 1 {
871            debug!("Dropping session pid: {:?}", self.server_pid());
872            if self.is_valid() {
873                if self.inner.options.cleanup
874                    && let Err(e) = crate::ffi::cleanup_session(self)
875                {
876                    error!("Session cleanup failed in Drop: {e}");
877                }
878                if let Err(e) = crate::ffi::shutdown_session(self) {
879                    error!("Could not shutdown session in Drop: {e}");
880                }
881                if let Err(e) = crate::ffi::close_session(self) {
882                    error!("Closing session failed in Drop: {e}");
883                }
884            } else {
885                // The server should automatically delete the pipe file when closed successfully,
886                // but we could try a cleanup just in case.
887                debug!("Session was invalid in Drop!");
888                if let Some(server_options) = &self.inner.server_options
889                    && let crate::server::ThriftTransport::Pipe(transport) =
890                        &server_options.thrift_transport
891                {
892                    let _ = std::fs::remove_file(&transport.pipe_path);
893                }
894            }
895        }
896    }
897}
898
899/// Session options passed to session create functions like [`crate::server::connect_to_pipe_server`]
900#[derive(Default, Clone, Debug)]
901pub struct SessionOptions {
902    /// Session cook options
903    pub cook_opt: CookOptions,
904    /// Create a Threaded server connection
905    pub threaded: bool,
906    /// Cleanup session upon close
907    pub cleanup: bool,
908    pub env_files: Option<CString>,
909    pub otl_path: Option<CString>,
910    pub dso_path: Option<CString>,
911    pub img_dso_path: Option<CString>,
912    pub aud_dso_path: Option<CString>,
913}
914
915impl SessionOptions {
916    /// A list of Houdini environment files the Engine will load from.
917    ///
918    /// # Panics
919    /// Panics if the joined path list contains an interior null byte.
920    #[must_use]
921    pub fn houdini_env_files<I>(mut self, files: I) -> Self
922    where
923        I: IntoIterator,
924        I::Item: AsRef<str>,
925    {
926        let paths = utils::join_paths(files);
927        self.env_files
928            .replace(CString::new(paths).expect("Zero byte"));
929        self
930    }
931
932    /// Add search paths for the Engine to find HDAs.
933    ///
934    /// # Panics
935    /// Panics if the joined path list contains an interior null byte.
936    #[must_use]
937    pub fn otl_search_paths<I>(mut self, paths: I) -> Self
938    where
939        I: IntoIterator,
940        I::Item: AsRef<str>,
941    {
942        let paths = utils::join_paths(paths);
943        self.otl_path
944            .replace(CString::new(paths).expect("Zero byte"));
945        self
946    }
947
948    /// Add search paths for the Engine to find DSO plugins.
949    ///
950    /// # Panics
951    /// Panics if the joined path list contains an interior null byte.
952    #[must_use]
953    pub fn dso_search_paths<P>(mut self, paths: P) -> Self
954    where
955        P: IntoIterator,
956        P::Item: AsRef<str>,
957    {
958        let paths = utils::join_paths(paths);
959        self.dso_path
960            .replace(CString::new(paths).expect("Zero byte"));
961        self
962    }
963
964    /// Add search paths for the Engine to find image plugins.
965    ///
966    /// # Panics
967    /// Panics if the joined path list contains an interior null byte.
968    #[must_use]
969    pub fn image_search_paths<P>(mut self, paths: P) -> Self
970    where
971        P: IntoIterator,
972        P::Item: AsRef<str>,
973    {
974        let paths = utils::join_paths(paths);
975        self.img_dso_path
976            .replace(CString::new(paths).expect("Zero byte"));
977        self
978    }
979
980    /// Add search paths for the Engine to find audio files.
981    ///
982    /// # Panics
983    /// Panics if the joined path list contains an interior null byte.
984    #[must_use]
985    pub fn audio_search_paths<P>(mut self, paths: P) -> Self
986    where
987        P: IntoIterator,
988        P::Item: AsRef<str>,
989    {
990        let paths = utils::join_paths(paths);
991        self.aud_dso_path
992            .replace(CString::new(paths).expect("Zero byte"));
993        self
994    }
995
996    /// Pass session [`CookOptions`]
997    #[must_use]
998    pub fn cook_options(mut self, options: CookOptions) -> Self {
999        self.cook_opt = options;
1000        self
1001    }
1002
1003    /// Makes the server operate in threaded mode. See the official docs for more info.
1004    #[must_use]
1005    pub fn threaded(mut self, threaded: bool) -> Self {
1006        self.threaded = threaded;
1007        self
1008    }
1009
1010    /// Set whether to cleanup the session upon close
1011    #[must_use]
1012    pub fn cleanup(mut self, cleanup: bool) -> Self {
1013        self.cleanup = cleanup;
1014        self
1015    }
1016}
1017
1018/// Create an in-process session.
1019/// Usefull for quick testing and debugging. Session crash will crash the main process.
1020/// For production use, use [`new_thrift_session`] instead.
1021pub fn new_in_process_session(options: Option<SessionOptions>) -> Result<Session> {
1022    debug!("Creating new in-process session");
1023    let session_options = options.unwrap_or_default();
1024    let session_info = SessionInfo::default();
1025    let handle = crate::ffi::create_inprocess_session(&session_info.0)?;
1026    let session = UninitializedSession {
1027        session_handle: handle,
1028        server_options: None,
1029        server_pid: Some(std::process::id()),
1030    }
1031    .initialize(session_options)?;
1032    Ok(session)
1033}
1034
1035/// Start a Thrift server and initialize a session with it.
1036pub fn new_thrift_session(
1037    session_options: SessionOptions,
1038    server_options: ServerOptions,
1039) -> Result<Session> {
1040    match server_options.thrift_transport {
1041        crate::server::ThriftTransport::SharedMemory(_) => {
1042            let pid = crate::server::start_engine_server(&server_options)?;
1043            crate::server::connect_to_memory_server(server_options, Some(pid))
1044                .context("Could not connect to shared memory server")?
1045                .initialize(session_options)
1046        }
1047        crate::server::ThriftTransport::Pipe(_) => {
1048            let pid = crate::server::start_engine_server(&server_options)?;
1049            crate::server::connect_to_pipe_server(server_options, Some(pid))
1050                .context("Could not connect to pipe server")?
1051                .initialize(session_options)
1052        }
1053        crate::server::ThriftTransport::Socket(_) => {
1054            let pid = crate::server::start_engine_server(&server_options)?;
1055            crate::server::connect_to_socket_server(server_options, Some(pid))
1056                .context("Could not connect to socket server")?
1057                .initialize(session_options)
1058                .context("Could not connect to socket server")
1059        }
1060    }
1061}
1062
1063/// Shortcut for creating a simple Thrift session with good defaults.
1064pub fn simple_session() -> Result<Session> {
1065    new_thrift_session(
1066        SessionOptions::default(),
1067        ServerOptions::shared_memory_with_defaults(),
1068    )
1069}