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