Skip to main content

mujoco_rs/
cpp_viewer.rs

1//! Wrapper around MuJoCo's original C++ viewer (also named Simulate).
2//!
3//! This module exposes [`MjViewerCpp`], which requires static linking against a patched MuJoCo
4//! build. It is only available when the `cpp-viewer` Cargo feature is enabled.
5//! For most use cases, the Rust-native [`crate::viewer::MjViewer`] is recommended instead.
6use crate::mujoco_c::*;
7use std::ffi::CString;
8use std::ops::Deref;
9
10use crate::wrappers::mj_visualization::*;
11use crate::wrappers::mj_model::MjModel;
12use crate::wrappers::mj_data::MjData;
13
14#[repr(C)]
15struct mujoco_Simulate { _unused: [u8; 0] }
16
17unsafe extern "C" {
18    fn mujoco_cSimulate_create(
19        cam: *mut mjvCamera,
20        opt: *mut mjvOption,
21        pert: *mut mjvPerturb,
22        user_scn: *mut mjvScene,
23    ) -> *mut mujoco_Simulate;
24    fn mujoco_cSimulate_RenderInit(sim: *mut mujoco_Simulate);
25    fn mujoco_cSimulate_Load(sim: *mut mujoco_Simulate, m: *mut mjModel_, d: *mut mjData_, displayed_filename: *const std::os::raw::c_char);
26    fn mujoco_cSimulate_RenderStep(sim: *mut mujoco_Simulate) -> std::os::raw::c_int;
27    fn mujoco_cSimulate_Sync(sim: *mut mujoco_Simulate, state_only: std::os::raw::c_int);
28    fn mujoco_cSimulate_ExitRequest(sim: *mut mujoco_Simulate);
29    fn mujoco_cSimulate_destroy(sim: *mut mujoco_Simulate);
30}
31
32
33/// Wrapper around the C++ implementation of MuJoCo viewer.
34/// If you don't need the side UI, we recommend you use the Rust-native viewer [`crate::viewer::MjViewer`] instead.
35///
36/// # Safety
37/// Calls to [`MjViewerCpp::render`] must be done only on the **main** thread!
38/// For convenience [`MjViewerCpp`] implements both `Send` and `Sync`, however that is meant only for
39/// syncing the viewer.
40///
41/// [`MjViewerCpp::launch_passive`] keeps internal pointers to mjModel and mjData.
42/// The caller must ensure both remain alive and at a fixed address for the viewer's lifetime.
43/// See [`MjViewerCpp::launch_passive`] for the full safety contract.
44#[derive(Debug)]
45pub struct MjViewerCpp {
46    sim: *mut mujoco_Simulate,
47    running: bool,
48
49    user_scn: Box<MjvScene>,
50    _cam: Box<MjvCamera>,
51    _opt: Box<MjvOption>,
52    _pert: Box<MjvPerturb>,
53}
54
55impl MjViewerCpp {
56    /// Returns whether the viewer window is still open.
57    pub fn running(&self) -> bool {
58        self.running
59    }
60
61    /// Returns a mutable reference to the user scene for drawing custom visual-only geoms.
62    pub fn user_scn_mut(&mut self) -> &mut MjvScene {
63        &mut self.user_scn
64    }
65
66    /// Launches a wrapper around MuJoCo's C++ viewer. The `max_user_geom` parameter
67    /// defines how much space will be allocated for additional, user-defined visual-only geoms.
68    /// It can thus be set to 0 if no additional geoms will be drawn by the user.
69    /// Unlike the Rust-native viewer ([`crate::viewer::MjViewer`]), this also accepts a `data` parameter.
70    /// Additionally, this just returns a [`MjViewerCpp`] instance directly, without result
71    /// as the initialization may fail internally in C++ anyway, which we have no way of checking.
72    ///
73    /// # Safety
74    /// The caller must ensure that both `model` and `data` remain alive and at a stable memory
75    /// address for the entire lifetime of the returned [`MjViewerCpp`]. Dropping or moving the
76    /// underlying [`MjModel`] or [`MjData`] while the viewer is alive is undefined behavior.
77    /// Calls to [`MjViewerCpp::render`] must be done only on the **main** thread.
78    ///
79    /// # Panics
80    /// Panics if `mujoco_cSimulate_create` returns a null pointer, or if the load thread panics.
81    pub unsafe fn launch_passive<M: Deref<Target = MjModel> + Clone + Send + Sync>(model: M, data: &MjData<M>, max_user_geom: usize) -> Self {
82        // Allocate on the heap as the data must not be moved due to C++ bindings
83        let mut cam = Box::new(MjvCamera::default());
84        let mut opt: Box<MjvOption> = Box::new(MjvOption::default());
85        let mut pert = Box::new(MjvPerturb::default());
86        let mut user_scn = Box::new(MjvScene::new(model.clone(), max_user_geom));
87
88        // SAFETY: all pointer arguments are valid (heap-allocated above); the caller guarantees
89        // model and data remain alive at stable addresses for the viewer's lifetime.
90        let sim = unsafe { mujoco_cSimulate_create(&mut *cam, &mut *opt, &mut *pert, user_scn.ffi_mut()) };
91        assert!(!sim.is_null(), "mujoco_cSimulate_create returned a null pointer");
92        let sim_usize = sim as usize;
93
94        let model_usize = model.as_raw_ptr() as usize;
95        let data_usize = data.as_raw_ptr() as usize;
96
97        unsafe { mujoco_cSimulate_RenderInit(sim) };
98
99        // Load on another thread, since the viewer internally blocks until loaded.
100        // This is intentional and is the intended way of using the C++ viewer.
101        let load_thread = std::thread::spawn(move || {
102            let sim = sim_usize as *mut mujoco_Simulate;
103            let m = model_usize as *mut mjModel_;
104            let d = data_usize as *mut mjData_;
105            let c_filename = CString::new("file.xml").unwrap();
106            // SAFETY: sim, m, and d are valid pointers kept alive by the caller's contract
107            // (model and data at stable addresses for the viewer's lifetime). c_filename is
108            // a valid null-terminated C string for the duration of this call.
109            unsafe { mujoco_cSimulate_Load(sim, m, d, c_filename.as_ptr()) };
110        });
111
112        while !load_thread.is_finished() {
113            let running = unsafe { mujoco_cSimulate_RenderStep(sim) };
114            if running == 0 {
115                // Window closed during model load; stop rendering.
116                break;
117            }
118        }
119        load_thread.join().unwrap();
120
121        Self {sim, running: true, user_scn, _cam: cam, _opt: opt, _pert: pert}
122    }
123
124    /// Renders the simulation.
125    ///
126    /// # Errors
127    /// Returns `Err` when called after the viewer has already been closed.
128    /// The call that detects the close event still returns `Ok(())` and flips
129    /// the internal running state to false.
130    ///
131    /// # Safety
132    /// Must be called from the **main thread**. GLFW requires main-thread access; calling
133    /// from any other thread causes undefined behaviour.
134    pub unsafe fn render(&mut self) -> Result<(), &'static str> {
135        if !self.running {
136            return Err("render called after viewer has been closed!");
137        }
138        // SAFETY: self.sim is a valid non-null pointer (asserted on construction and kept alive
139        // while the viewer is running); the caller guarantees this is the main thread.
140        unsafe { self.running = mujoco_cSimulate_RenderStep(self.sim) == 1; }
141        Ok(())
142    }
143
144    /// Syncs the simulation state with the viewer.
145    pub fn sync(&mut self) {
146        if !self.running {
147            return;
148        }
149        // SAFETY: self.sim is a valid non-null pointer kept alive for the viewer's lifetime.
150        unsafe {
151            mujoco_cSimulate_Sync(self.sim, 0);
152        }
153    }
154}
155
156/// Requests viewer exit and destroys the underlying C++ simulation handle.
157impl Drop for MjViewerCpp {
158    fn drop(&mut self) {
159        // SAFETY: self.sim is a valid non-null pointer; ExitRequest signals the C++ side to
160        // shut down, and destroy frees the allocation. Called at most once (in Drop).
161        unsafe {
162            mujoco_cSimulate_ExitRequest(self.sim);
163            mujoco_cSimulate_destroy(self.sim);
164        }
165    }
166}
167
168/// # Safety
169/// Rendering must only be performed on the main thread. `Send` is provided so
170/// the viewer handle can be moved to the main thread after construction.
171unsafe impl Send for MjViewerCpp {}
172/// # Safety
173/// The viewer is safe to share across threads for syncing, but rendering must
174/// only be done on the main thread. See [`MjViewerCpp`] for the full contract.
175unsafe impl Sync for MjViewerCpp {}