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 {}