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