Skip to main content

mujoco_rs/wrappers/
mj_rendering.rs

1//! Definitions related to rendering.
2use crate::{array_slice_dyn, getter_setter, mujoco_c::*};
3use crate::error::MjrContextError;
4
5use super::mj_model::{MjModel, MjtTexture, MjtTextureRole};
6
7use std::ffi::CString;
8use std::ptr;
9
10/* Types */
11
12/// These are the possible grid positions for text overlays. They are used as an argument to the function
13/// `mjr_overlay`.
14pub type MjtGridPos = mjtGridPos;
15
16/// These are the possible framebuffers. They are used as an argument to the function `mjr_setBuffer`.
17pub type MjtFramebuffer = mjtFramebuffer;
18
19/// These are the depth mapping options. They are used as a value for the `readPixelDepth` attribute of the
20/// `mjrContext` struct, to control how the depth returned by `mjr_readPixels` is mapped from
21/// `znear` to `zfar`.
22pub type MjtDepthMap = mjtDepthMap;
23
24/// These are the possible font sizes. The fonts are predefined bitmaps stored in the dynamic library at three different
25/// sizes.
26pub type MjtFontScale = mjtFontScale;
27
28/// These are the possible font types.
29pub type MjtFont = mjtFont;
30/**********************************************************************************************************************/
31
32
33/***********************************************************************************************************************
34** MjrRectangle
35***********************************************************************************************************************/
36/// Axis-aligned rectangle (bottom-left corner + dimensions) used for off-screen and on-screen viewports.
37pub type MjrRectangle = mjrRect;
38impl MjrRectangle {
39    /// Creates a new rectangle defined by its bottom-left corner (`left`, `bottom`) and
40    /// its `width` and `height` in pixels.
41    pub const fn new(left: i32, bottom: i32, width: i32, height: i32) -> Self {
42        Self {
43            left,
44            bottom,
45            width,
46            height,
47        }
48    }
49}
50
51impl PartialEq for MjrRectangle {
52    fn eq(&self, other: &Self) -> bool {
53        self.left == other.left && self.bottom == other.bottom
54            && self.width == other.width && self.height == other.height
55    }
56}
57impl Eq for MjrRectangle {}
58
59#[allow(clippy::derivable_impls)]  // MjrRectangle is a type alias of a foreign type; derive is not applicable
60impl Default for MjrRectangle {
61    fn default() -> Self {
62        Self {
63            left: 0,
64            bottom: 0,
65            width: 0,
66            height: 0,
67        }
68    }
69}
70
71
72/***********************************************************************************************************************
73** MjrContext
74***********************************************************************************************************************/
75/// Wraps `mjrContext`, the MuJoCo rendering context.
76///
77/// # Thread safety
78/// `MjrContext` is `!Send` and `!Sync`. It must remain on the thread that owns the active
79/// OpenGL context for its entire lifetime, because the underlying GL resources (textures,
80/// renderbuffers, framebuffers) are bound to that GL context and thread. In particular:
81///
82/// - `new()` must be called while a valid GL context is current on the calling thread.
83/// - All method calls, including `drop`, must happen on that same thread while the GL
84///   context is still current. Dropping `MjrContext` on any other thread, or after the GL
85///   context has been released, causes undefined behaviour.
86#[derive(Debug)]
87pub struct MjrContext {
88    ffi: Box<mjrContext>
89}
90
91impl MjrContext {
92    /// Creates and initializes a new rendering context for `model`.
93    /// The font scale defaults to 100 %.
94    ///
95    /// # Safety
96    /// A valid OpenGL context must exist and be current in the calling thread before calling
97    /// this function. Calling without an active GL context causes MuJoCo to abort the process.
98    /// The same GL context must also remain current when this `MjrContext` is dropped, and must
99    /// remain on the same thread for the lifetime of this value.
100    pub unsafe fn new(model: &MjModel) -> Self {
101        // SAFETY: caller guarantees a valid GL context is current (documented above).
102        // Box::new_uninit is fully initialized by mjr_defaultContext + mjr_makeContext
103        // before assume_init.
104        unsafe {
105            let mut c = Box::new_uninit();
106            mjr_defaultContext(c.as_mut_ptr());
107            mjr_makeContext(model.ffi(), c.as_mut_ptr(), MjtFontScale::mjFONTSCALE_100 as i32);
108            Self {ffi: c.assume_init()}
109        }
110    }
111
112    /// Set OpenGL framebuffer for rendering to mjFB_OFFSCREEN.
113    pub fn offscreen(&mut self) -> &mut Self {
114        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
115        unsafe {
116            mjr_setBuffer(MjtFramebuffer::mjFB_OFFSCREEN as i32, self.ffi.as_mut());
117        }
118        self
119    }
120
121    /// Set OpenGL framebuffer for rendering to mjFB_WINDOW.
122    pub fn window(&mut self) -> &mut Self {
123        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
124        unsafe {
125            mjr_setBuffer(MjtFramebuffer::mjFB_WINDOW as i32, self.ffi.as_mut());
126        }
127        self
128    }
129
130    /// Change font of existing context.
131    pub fn change_font(&mut self, fontscale: MjtFontScale) {
132        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
133        unsafe { mjr_changeFont(fontscale as i32, self.ffi_mut()) }
134    }
135
136    /// Add Aux buffer with given index to context; free previous Aux buffer.
137    /// # Errors
138    /// Returns [`MjrContextError::IndexOutOfBounds`] when `index >= mjNAUX` (10).
139    pub fn add_aux(&mut self, index: usize, width: u32, height: u32, samples: usize) -> Result<(), MjrContextError> {
140        if index >= mjNAUX as usize {
141            return Err(MjrContextError::IndexOutOfBounds { id: index, len: mjNAUX as usize });
142        }
143        // SAFETY: index is bounds-checked above; self.ffi is valid.
144        unsafe { mjr_addAux(index as i32, width as i32, height as i32, samples as i32, self.ffi_mut()); }
145        Ok(())
146    }
147
148    /// Resize offscreen buffers.
149    pub fn resize_offscreen(&mut self, width: u32, height: u32) {
150        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
151        unsafe { mjr_resizeOffscreen(width as i32, height as i32, self.ffi_mut()); }
152    }
153
154    /// Re-upload texture to GPU, overwriting previous upload if any.
155    ///
156    /// # Errors
157    /// Returns [`MjrContextError::IndexOutOfBounds`] if `texture_id >= model.ntex()`.
158    pub fn upload_texture(&self, model: &MjModel, texture_id: usize) -> Result<(), MjrContextError> {
159        self.upload_x(model, texture_id, model.ntex() as usize, mjr_uploadTexture)
160    }
161
162    /// Re-upload mesh to GPU, overwriting previous upload if any.
163    ///
164    /// # Errors
165    /// Returns [`MjrContextError::IndexOutOfBounds`] if `mesh_id >= model.nmesh()`.
166    pub fn upload_mesh(&self, model: &MjModel, mesh_id: usize) -> Result<(), MjrContextError> {
167        self.upload_x(model, mesh_id, model.nmesh() as usize, mjr_uploadMesh)
168    }
169
170    /// Re-upload heightfield to GPU, overwriting previous upload if any.
171    ///
172    /// # Errors
173    /// Returns [`MjrContextError::IndexOutOfBounds`] if `hfield_id >= model.nhfield()`.
174    pub fn upload_hfield(&self, model: &MjModel, hfield_id: usize) -> Result<(), MjrContextError> {
175        self.upload_x(model, hfield_id, model.nhfield() as usize, mjr_uploadHField)
176    }
177
178    /// Make the context's buffer current again.
179    pub fn restore_buffer(&mut self) {
180        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
181        unsafe { mjr_restoreBuffer(self.ffi_mut()); }
182    }
183
184    /// Sets the active OpenGL framebuffer to the given raw `framebuffer` id.
185    /// Prefer [`MjrContext::offscreen`] or [`MjrContext::window`] for the common cases.
186    pub fn set_buffer(&mut self, framebuffer: i32) {
187        // SAFETY: self.ffi is a valid, fully initialized mjrContext.
188        unsafe { mjr_setBuffer(framebuffer, self.ffi_mut()); }
189    }
190
191    /// Read pixels from current OpenGL framebuffer to client buffer.
192    /// The `rgb` array is of size `[width * height * 3]`, while `depth` is of size `[width * height]`.
193    ///
194    /// # Errors
195    /// Returns [`MjrContextError::InvalidViewport`] if the viewport has negative
196    /// dimensions, or [`MjrContextError::BufferTooSmall`] if `rgb` or `depth`
197    /// buffers are too small.
198    pub fn read_pixels(
199        &self,
200        rgb: Option<&mut [u8]>,
201        depth: Option<&mut [f32]>,
202        viewport: &MjrRectangle,
203    ) -> Result<(), MjrContextError> {
204        if viewport.width < 0 || viewport.height < 0 {
205            return Err(MjrContextError::InvalidViewport {
206                width: viewport.width,
207                height: viewport.height,
208            });
209        }
210        let size = viewport.width as usize * viewport.height as usize;
211        if let Some(buf) = rgb.as_ref() {
212            let needed = size * 3;
213            if buf.len() < needed {
214                return Err(MjrContextError::BufferTooSmall {
215                    name: "rgb",
216                    got: buf.len(),
217                    needed,
218                });
219            }
220        }
221        if let Some(buf) = depth.as_ref()
222            && buf.len() < size
223        {
224            return Err(MjrContextError::BufferTooSmall {
225                name: "depth",
226                got: buf.len(),
227                needed: size,
228            });
229        }
230
231        // SAFETY: viewport dimensions are validated above; buffer sizes are checked;
232        // null is passed for None options. self.ffi is a valid context.
233        unsafe {
234            mjr_readPixels(
235                rgb.map_or(ptr::null_mut(), |x| x.as_mut_ptr()),
236                depth.map_or(ptr::null_mut(), |x| x.as_mut_ptr()),
237                *viewport, self.ffi()
238            )
239        }
240        Ok(())
241    }
242
243    /// Set Aux buffer for custom OpenGL rendering (call restoreBuffer when done).
244    /// # Errors
245    /// Returns [`MjrContextError::IndexOutOfBounds`] when `index >= mjNAUX` (10).
246    pub fn set_aux(&mut self, index: usize) -> Result<(), MjrContextError> {
247        if index >= mjNAUX as usize {
248            return Err(MjrContextError::IndexOutOfBounds { id: index, len: mjNAUX as usize });
249        }
250        // SAFETY: index is bounds-checked above; self.ffi is valid.
251        unsafe { mjr_setAux(index as i32, self.ffi_mut()); }
252        Ok(())
253    }
254
255    /// Draws a text overlay. The optional `overlay2` parameter displays additional overlay, next to `overlay`.
256    /// # Panics
257    /// When the `overlay` or `overlay2` contain '\0' characters, a panic occurs.
258    pub fn overlay(&mut self, font: MjtFont, gridpos: MjtGridPos, viewport: MjrRectangle, overlay: &str, overlay2: Option<&str>) {
259        let c_overlay = CString::new(overlay).unwrap();
260        let c_overlay2 = overlay2.map(|x| CString::new(x).unwrap());
261
262        // SAFETY: CString pointers are valid for the duration of the call;
263        // null is passed for None overlay2. self.ffi is a valid context.
264        unsafe { mjr_overlay(
265            font as i32, gridpos as i32, viewport,
266            c_overlay.as_ptr(),
267            c_overlay2.as_ref().map_or(std::ptr::null(), |x| x.as_ptr()),
268            self.ffi()
269        ); }
270    }
271
272    /// Reference to the wrapped FFI struct.
273    pub fn ffi(&self) -> &mjrContext {
274        &self.ffi
275    }
276
277    /// Mutable reference to the wrapped FFI struct.
278    ///
279    /// # Safety
280    /// Modifying the underlying FFI struct directly can break the invariants
281    /// upheld by the `mujoco-rs` wrappers and cause undefined behavior.
282    pub unsafe fn ffi_mut(&mut self) -> &mut mjrContext {
283        &mut self.ffi
284    }
285
286    /// Common implementation of GPU upload methods. Specific item upload is made
287    /// by giving the corresponding `mjr_uploadX` to `upload_fn`.
288    fn upload_x(
289        &self, model: &MjModel, item_id: usize, n_items: usize,
290        upload_fn: unsafe extern "C" fn (m: *const mjModel, con: *const mjrContext, id: ::std::ffi::c_int)
291    ) -> Result<(), MjrContextError>
292    {
293        if item_id >= n_items {
294            return Err(MjrContextError::IndexOutOfBounds { id: item_id, len: n_items });
295        }
296        // SAFETY: item_id is bounds-checked above; model and context are valid.
297        unsafe { upload_fn(model.ffi(), self.ffi(), item_id as i32); }
298        Ok(())
299    }
300}
301
302/// Array slices.
303impl MjrContext {
304    array_slice_dyn! {
305        (mut = unsafe) textureType: as_ptr as_mut_ptr &[MjtTexture [force]; "type of texture"; ffi().ntexture],
306        (mut = unsafe) skinvertVBO: &[u32; "skin vertex position VBOs"; ffi().nskin],
307        (mut = unsafe) skinnormalVBO: &[u32; "skin vertex normal VBOs"; ffi().nskin],
308        (mut = unsafe) skintexcoordVBO: &[u32; "skin vertex texture coordinate VBOs"; ffi().nskin],
309        (mut = unsafe) skinfaceVBO: &[u32; "skin face index VBOs"; ffi().nskin]
310    }
311}
312
313impl MjrContext {
314    getter_setter! {get, [
315        [ffi] lineWidth: f32; "line width for wireframe rendering.";
316        [ffi] shadowClip: f32; "clipping radius for directional lights.";
317        [ffi] shadowScale: f32; "fraction of light cutoff for spot lights.";
318        [ffi] fogStart: f32; "fog start = stat.extent * vis.map.fogstart.";
319        [ffi] fogEnd: f32; "fog end = stat.extent * vis.map.fogend.";
320        [ffi] shadowSize: i32; "size of shadow map texture.";
321        [ffi] offWidth: i32; "width of offscreen buffer.";
322        [ffi] offHeight: i32; "height of offscreen buffer.";
323        [ffi] offSamples: i32; "number of offscreen buffer multisamples.";
324        [ffi] fontScale: i32; "font scale.";
325        [ffi] offFBO: u32; "offscreen framebuffer object.";
326        [ffi] offFBO_r: u32; "offscreen framebuffer for resolving multisamples.";
327        [ffi] offColor: u32; "offscreen color buffer.";
328        [ffi] offColor_r: u32; "offscreen color buffer for resolving multisamples.";
329        [ffi] offDepthStencil: u32; "offscreen depth and stencil buffer.";
330        [ffi] offDepthStencil_r: u32; "offscreen depth and stencil buffer for multisamples.";
331        [ffi] shadowFBO: u32; "shadow map framebuffer object.";
332        [ffi] shadowTex: u32; "shadow map texture.";
333        [ffi] ntexture: i32; "number of allocated textures.";
334        [ffi] basePlane: u32; "all planes from model.";
335        [ffi] baseMesh: u32; "all meshes from model.";
336        [ffi] baseHField: u32; "all height fields from model.";
337        [ffi] baseBuiltin: u32; "all builtin geoms, with quality from model.";
338        [ffi] baseFontNormal: u32; "normal font.";
339        [ffi] baseFontShadow: u32; "shadow font.";
340        [ffi] baseFontBig: u32; "big font.";
341        [ffi] rangePlane: i32; "all planes from model.";
342        [ffi] rangeMesh: i32; "all meshes from model.";
343        [ffi] rangeHField: i32; "all hfields from model.";
344        [ffi] rangeBuiltin: i32; "all builtin geoms, with quality from model.";
345        [ffi] rangeFont: i32; "all characters in font.";
346        [ffi] nskin: i32; "number of skins.";
347        [ffi] charHeight: i32; "character heights: normal and shadow.";
348        [ffi] charHeightBig: i32; "character heights: big.";
349        [ffi] glInitialized: i32; "is OpenGL initialized.";
350        [ffi] windowAvailable: i32; "is default/window framebuffer available.";
351        [ffi] windowSamples: i32; "number of samples for default/window framebuffer.";
352        [ffi] windowStereo: i32; "is stereo available for default/window framebuffer.";
353        [ffi] windowDoublebuffer: i32; "is default/window framebuffer double buffered.";
354        [ffi] currentBuffer: i32; "currently active framebuffer: mjFB_WINDOW or mjFB_OFFSCREEN.";
355        [ffi] readPixelFormat: i32; "default color pixel format for mjr_readPixels.";
356        [ffi] readDepthMap: i32; "depth mapping: mjDEPTH_ZERONEAR or mjDEPTH_ZEROFAR.";
357    ]}
358
359    getter_setter! {get, [
360        [ffi] (allow_mut = false) fogRGBA: &[f32; 4]; "fog rgba.";
361        [ffi] (allow_mut = false) auxWidth: &[i32; mjNAUX as usize]; "auxiliary buffer width.";
362        [ffi] (allow_mut = false) auxHeight: &[i32; mjNAUX as usize]; "auxiliary buffer height.";
363        [ffi] (allow_mut = false) auxSamples: &[i32; mjNAUX as usize]; "auxiliary buffer multisamples.";
364        [ffi] (allow_mut = false) auxFBO: &[u32; mjNAUX as usize]; "auxiliary framebuffer object.";
365        [ffi] (allow_mut = false) auxFBO_r: &[u32; mjNAUX as usize]; "auxiliary framebuffer object for resolving.";
366        [ffi] (allow_mut = false) auxColor: &[u32; mjNAUX as usize]; "auxiliary color buffer.";
367        [ffi] (allow_mut = false) auxColor_r: &[u32; mjNAUX as usize]; "auxiliary color buffer for resolving.";
368        [ffi] (allow_mut = false) mat_texid: &[i32; (mjMAXMATERIAL * MjtTextureRole::mjNTEXROLE as u32) as usize]; "material texture ids (-1: no texture).";
369        [ffi] (allow_mut = false) mat_texuniform: &[i32; mjMAXMATERIAL as usize]; "uniform cube mapping.";
370        [ffi] (allow_mut = false) mat_texrepeat: &[f32; (mjMAXMATERIAL * 2) as usize]; "texture repetition for 2d mapping.";
371        [ffi] (allow_mut = false) texture: &[u32; mjMAXTEXTURE as usize]; "texture names.";
372        [ffi] (allow_mut = false) charWidth: &[i32; 127]; "character widths: normal and shadow.";
373        [ffi] (allow_mut = false) charWidthBig: &[i32; 127]; "character widths: big.";
374    ]}
375}
376
377impl Drop for MjrContext {
378    fn drop(&mut self) {
379        // SAFETY: self.ffi was fully initialized in new() and has not been freed.
380        unsafe {
381            mjr_freeContext(self.ffi.as_mut());
382        }
383    }
384}