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