Migration guide¶
This page documents the migration steps for upgrading between major versions of MuJoCo-rs. For a full list of changes, see the Changelog.
Migrating to 5.0.0¶
Version 5.0.0 updates MuJoCo to 3.9.0, redesigns model-editing element deletion, migrates several rendering APIs to the MjrContext error type, and hardens model-editing setters against out-of-range object types.
MuJoCo upgrade¶
MuJoCo-rs 5.0.0 links against MuJoCo 3.9.0. Download the matching release and update your library path. See Installation for details.
SpecItem::element_pointer pointer type changed (potentially breaking)¶
SpecItem::element_pointer
now returns *const mjsElement (was *mut mjsElement) and is no longer unsafe. This only
affects code that called the model-editing trait methods directly (e.g. to drive MuJoCo’s C
model-editing API). When a mutable pointer is required, use
SpecItem::element_mut_pointer
(also no longer unsafe). Any unsafe block wrapping these calls is now redundant and can be
removed.
Before (4.x):
let ptr: *mut mjsElement = unsafe { item.element_pointer() };
After (5.0.0):
// read-only C calls take an immutable pointer:
let ptr: *const mjsElement = item.element_pointer();
// when a mutable pointer is required:
let ptr: *mut mjsElement = item.element_mut_pointer();
Deprecated implementation of removing model-editing elements¶
Due to the SpecItem::delete method relying on undefined behavior, the said method is now deprecated. It’s replacement — MjSpec::delete_element — now forces the user to drop all existing references of MjSpec and its subsequent elements (geoms, joints, etc.), providing some inconvenience compared to the previous implementation.
Before (4.x):
let mut spec = MjSpec::new();
let body = spec.world_body_mut().add_body(); // &mut MjsBody
unsafe { body.delete().unwrap() } ;
After (5.0.0):
let mut spec = MjSpec::new();
let body = spec.world_body_mut().add_body(); // &mut MjsBody
let body_ptr = body.element_mut_pointer();
unsafe { spec.delete_element(body_ptr).unwrap() }
MjsTendon::wrap / wrap_mut no longer return Option¶
MjsTendon’s wrap and wrap_mut now return &MjsWrap / &mut MjsWrap (previously
Option<&MjsWrap> / Option<&mut MjsWrap>) and panic if the index is out of bounds. The old
signature documented None on an out-of-range index, but the underlying C mjs_getWrap aborts
the process for such an index and never returns null, so the None arm was unreachable. Drop the
.unwrap() on existing calls and ensure the index is in range (< wrap_num()), or use the new
fallible try_wrap / try_wrap_mut.
Before (4.x):
let wrap = tendon.wrap(i).unwrap();
After (5.0.0):
let wrap = tendon.wrap(i);
Deprecated fallible tendon-wrap methods¶
MjsTendon’s try_wrap_site, try_wrap_geom, try_wrap_joint, and try_wrap_pulley
are now deprecated. Their MjEditError::AllocationFailed arm is unreachable: on an allocation
failure MuJoCo aborts the process (or, under a non-default error configuration, writes through the
null pointer before returning), so the failure cannot be recovered soundly. Switch to the panicking
wrap_site / wrap_geom / wrap_joint / wrap_pulley, which return &mut MjsWrap
directly.
These methods may be undeprecated in the future if MuJoCo’s upstream C++ code is changed to return null recoverably.
Before (4.x):
let wrap = tendon.try_wrap_geom("geom", "sidesite")?;
After (5.0.0):
let wrap = tendon.wrap_geom("geom", "sidesite");
MjrContext::upload_texture parameter type changed¶
MjrContext::upload_texture
now accepts texture_id: usize instead of texid: u32. Update call sites to cast or
convert the argument accordingly.
Before (4.x):
context.upload_texture(&model, 0u32);
After:
context.upload_texture(&model, 0usize);
MjrContext::upload_texture returns Result¶
MjrContext::upload_texture
now returns Result<(), MjrContextError> instead of ().
Handle or propagate the result at each call site.
Before (4.x):
context.upload_texture(&model, id);
After:
context.upload_texture(&model, id)?;
MjrContext::add_aux / set_aux error type changed¶
MjrContext::add_aux and
MjrContext::set_aux
now return Result<(), MjrContextError> instead of Result<(), MjSceneError>.
The MjSceneError::InvalidAuxBufferIndex variant has been removed; use
MjrContextError::IndexOutOfBounds instead.
Before (4.x):
match context.add_aux(index, width, height, samples) {
Err(MjSceneError::InvalidAuxBufferIndex { index }) => { /* … */ }
Ok(()) => { /* … */ }
}
After:
match context.add_aux(index, width, height, samples) {
Err(MjrContextError::IndexOutOfBounds { id, len }) => { /* … */ }
Ok(()) => { /* … */ }
}
MjrContext::read_pixels error type changed¶
MjrContext::read_pixels
now returns Result<…, MjrContextError> instead of Result<…, MjSceneError>.
MjSceneError::InvalidViewport and MjSceneError::BufferTooSmall have been removed;
use MjrContextError::InvalidViewport and MjrContextError::BufferTooSmall instead.
Before (4.x):
match context.read_pixels(Some(&mut rgb_buf), None, &viewport) {
Err(MjSceneError::InvalidViewport { .. }) => { /* … */ }
Err(MjSceneError::BufferTooSmall { .. }) => { /* … */ }
Ok(()) => { /* … */ }
}
After:
match context.read_pixels(Some(&mut rgb_buf), None, &viewport) {
Err(MjrContextError::InvalidViewport { .. }) => { /* … */ }
Err(MjrContextError::BufferTooSmall { .. }) => { /* … */ }
Ok(()) => { /* … */ }
}
MjModelError::InvalidIndex removed¶
MjModelError::InvalidIndex(usize, usize) has been removed. Use
IndexOutOfBounds
with named fields instead.
Before (4.x):
match model.try_max_contacts(geom1, geom2, None) {
Err(MjModelError::InvalidIndex(id, len)) => { /* … */ }
Ok(count) => { /* … */ }
}
After:
match model.try_max_contacts(geom1, geom2, None) {
Err(MjModelError::IndexOutOfBounds { id, len }) => { /* … */ }
Ok(count) => { /* … */ }
}
MjsTuple::set_objtype now takes &[MjtObj] and is fallible¶
MjsTuple::set_objtype previously accepted &[i32] and returned (). It now takes
&[MjtObj] and returns Result<(), MjEditError>: out-of-range object types are rejected with
MjEditError::InvalidParameter.
Before (4.x):
tuple.set_objtype(&[MjtObj::mjOBJ_BODY as i32, MjtObj::mjOBJ_GEOM as i32]);
After:
tuple.set_objtype(&[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_GEOM])?;
MjsSensor::set_objtype / set_reftype are now fallible¶
MjsSensor::set_objtype and set_reftype previously returned (); they now return
Result<(), MjEditError>, rejecting out-of-range object types with MjEditError::InvalidParameter.
The builder counterparts with_objtype / with_reftype keep their signature but panic on a
rejected value.
Before (4.x):
sensor.set_objtype(MjtObj::mjOBJ_SITE);
sensor.set_reftype(MjtObj::mjOBJ_BODY);
After:
sensor.set_objtype(MjtObj::mjOBJ_SITE)?;
sensor.set_reftype(MjtObj::mjOBJ_BODY)?;
MjsNumeric::set_size is now fallible¶
MjsNumeric::set_size previously returned (); it now returns Result<(), MjEditError>,
rejecting a negative size with MjEditError::InvalidParameter (a negative size triggers an
out-of-bounds write in the model compiler).
Before (4.x):
numeric.set_size(8);
After:
numeric.set_size(8)?;
Some index/size vector setters are now unsafe¶
MjsFlex::set_elemtexcoord,
MjsSkin::set_face, MjsMesh::set_userfacetexcoord and MjsMesh::set_userfacenormal are now
unsafe fn. Each writes a
value the model compiler or renderer later trusts as an unchecked index/count/length, and the
correct constraint is cross-field, so it cannot be validated from the setter. Wrap calls in
unsafe after ensuring the obligation in each method’s # Safety section (e.g. for
set_face, every index is in 0..nvert and the length is a multiple of 3; for
set_userfacenormal, every index is in 0..N, where N is the set_usernormal slice
length divided by 3).
Before (4.x):
skin.set_face(&faces);
After:
// SAFETY: every index is < nvert and faces.len() is a multiple of 3.
unsafe { skin.set_face(&faces); }
MjData::add_contact is now an unsafe fn¶
add_contact wraps mj_addContact, an advanced entry point that stores the caller-supplied
MjContact verbatim. MuJoCo later uses the stored contact without validation, so a malformed
contact can cause undefined behavior. The method is now unsafe; the caller must ensure the
contact is valid for the model. The signature is otherwise unchanged, so existing call sites only
need an unsafe block.
Before (4.x):
data.add_contact(&contact)?;
After:
// SAFETY: `contact` is a valid contact for this model.
unsafe { data.add_contact(&contact)? };
MjData::reset_debug is now an unsafe fn¶
reset_debug wraps mj_resetDataDebug, which fills every buffer-resident array with raw
debug_value bytes. Arrays that MuJoCo does not re-initialize afterwards keep those bytes, and
some safe accessors expose them as types with validity invariants (bvh_active as &[bool],
body_awake as a fieldless enum slice), so reading them can be undefined behavior. The method
is now unsafe; the caller must not read such accessors before the next reset unless
debug_value produces valid bit patterns for them. The signature is otherwise unchanged, so
existing call sites only need an unsafe block.
Before (4.x):
data.reset_debug(7);
After:
// SAFETY: no invariant-carrying accessor (bvh_active, body_awake) is read
// before the next reset().
unsafe { data.reset_debug(7) };
MjData::history_mut is now an unsafe fn¶
history_mut exposes the whole history buffer for mutation. Each slot stores a cursor in
buf[1] that mj_readSensor / mj_readCtrl trust as an array index without a bound check,
so corrupting it from safe code can cause an out-of-bounds read. The method is now unsafe; the
caller must keep every slot’s cursor valid. The signature is otherwise unchanged, so existing call
sites only need an unsafe block. The immutable history accessor stays safe.
Before (4.x):
let buf = data.history_mut();
After:
// SAFETY: every history slot's cursor (buf[1]) is kept valid.
let buf = unsafe { data.history_mut() };
MjData::ray / ray_mesh now take &mut self¶
ray, ray_mesh, and try_ray_mesh now take &mut self (previously &self). Their
bounding-volume traversal (mju_rayTree) writes data.bvh_active when the model’s bvactive
visualization flag is set, even though the underlying MuJoCo C functions are declared
const mjData*. The &self signature therefore let safe code drive shared MjData mutation
(a data race under MjData<M>: Sync, or mutation of a live &[bool] obtained from the
bvh_active accessor). Callers that held a shared &MjData now need an exclusive borrow.
ray_flex and ray_hfield are unaffected; multi_ray already took &mut self.
Before (4.x):
let data = model.make_data();
let (geomid, dist) = data.ray(&pnt, &vec, None, false, None, None);
After:
let mut data = model.make_data();
let (geomid, dist) = data.ray(&pnt, &vec, None, false, None, None);
MjvScene::find_selection now takes &mut MjData¶
MjvScene’s find_selection now takes data: &mut MjData<M> (previously &MjData<M>).
It calls mjv_select -> mj_ray, whose bounding-volume traversal writes data.bvh_active
(the same const mjData*-but-mutating path as ray above), so a shared borrow was unsound.
Pass an exclusive borrow instead. (MjvScene::update already took &mut MjData.)
Before (4.x):
let data = model.make_data();
let sel = scene.find_selection(&data, &opt, aspect, relx, rely);
After:
let mut data = model.make_data();
let sel = scene.find_selection(&mut data, &opt, aspect, relx, rely);
MjSpec is no longer Sync¶
MjSpec is now Send but no longer Sync. clone / try_clone make a faithful,
independent copy, but the underlying C++ copy constructor is not strictly const on the source
(it rewrites transient, normally-empty per-actuator keyframe-cache maps), so two threads cloning a
shared &MjSpec concurrently is a data race. MjSpec still moves between threads (Send);
only shared cross-thread access is removed. Code that placed a &MjSpec in a Sync-requiring
context must transfer ownership (or clone per thread) instead.
Before (4.x):
// relied on &MjSpec being shareable across threads
std::thread::scope(|s| {
s.spawn(|| { let _ = spec.clone(); });
s.spawn(|| { let _ = spec.clone(); });
});
After:
// move an owned MjSpec into each thread (clone up front if needed)
let spec2 = spec.clone();
std::thread::scope(|s| {
s.spawn(move || { let _ = spec; });
s.spawn(move || { let _ = spec2; });
});
Mjs* element handles are no longer Send / Sync¶
The Mjs* element handles (MjsBody, MjsGeom, …) and MjsDefault previously carried a
blanket unsafe impl Send/Sync. Each is only a raw pointer into one shared mjSpec /
mjCModel arena, so moving or sharing a handle across threads was unsound. They are now
!Send + !Sync. Code that sent a handle to another thread will no longer compile — move the
owning MjSpec instead (it stays Send; it is now !Sync, see above) and derive the
per-thread handles from it on the thread that uses them.
Before (4.x):
// relied on Mjs* handles being Send: a handle was moved into another thread
let geom = body.add_geom();
std::thread::scope(|s| {
s.spawn(move || { let _ = geom.set_name("g"); });
});
After:
// move the owning MjSpec; derive handles on the thread that uses them
std::thread::scope(|s| {
s.spawn(move || {
let mut spec = spec;
let geom = spec.world_body_mut().add_geom();
let _ = geom.set_name("g");
});
});
Migrating to 4.0.0¶
Version 4.0.0 updates MuJoCo to 3.8.0 and follows the upstream API changes in
mj_geomDistance and the model-editing specification structs.
MuJoCo upgrade¶
MuJoCo-rs 4.0.0 links against MuJoCo 3.8.0. Download the matching release and update your library path. See Installation for details.
MjData::geom_distance() now requires &mut self¶
MuJoCo 3.7.0 changed mj_geomDistance to mutate mjData internally. As a
result, both
MjData::geom_distance
and
MjData::try_geom_distance
now require mutable access to MjData.
Before (3.x):
let data = MjData::new(&model);
let dist = data.geom_distance(geom1, geom2, dist_max, None);
After (4.0.0):
let mut data = MjData::new(&model);
let dist = data.geom_distance(geom1, geom2, dist_max, None);
MjsJoint and MjsTendon stiffness/damping now use coefficient arrays¶
MuJoCo 3.7.0 replaced the scalar stiffness and damping fields in the
editing API with polynomial coefficients. MuJoCo-rs mirrors that change:
MjsJoint and MjsTendon now use coefficient arrays for stiffness and
damping. Update call sites to pass coefficient arrays instead of scalar
values.
Before (3.x):
joint.with_stiffness(10.0)
.with_damping(0.5);
tendon.with_stiffness(20.0)
.with_damping(0.2);
After (4.0.0):
joint.with_stiffness([10.0, 0.0, 0.0])
.with_damping([0.5, 0.0, 0.0]);
tendon.with_stiffness([20.0, 0.0, 0.0])
.with_damping([0.2, 0.0, 0.0]);
MjsFlex::vertcollide was removed¶
MuJoCo 3.7.0 removed the upstream mjsFlex::vertcollide field. The
corresponding MjsFlex::vertcollide getter/setter methods are therefore no
longer available in MuJoCo-rs 4.0.0.
Before (3.x):
flex.set_vertcollide(true);
After (4.0.0):
// Remove vertcollide access -- the upstream field no longer exists.
let _ = flex;
MjData::model_mut is now unsafe¶
To prevent a false sense of correctness, method MjData::model_mut is now marked unsafe to prevent users from direct swapping models to incompatible ones.
Before (3.x):
data.model_mut().opt_mut().gravity[2] = half_gravity;
*data.model_mut() = new_model;
After (4.0.0):
// For field modification (safe accessors):
data.model_opt_mut().gravity[2] = half_gravity;
// For full model replacement:
let mut old_model = data.swap_model(new_model);
MjViewer::add_ui_callback closure now receives Box<MjModel>¶
The passive model stored inside the viewer changed from Arc<MjModel> to
Box<MjModel>. The closure passed to
MjViewer::add_ui_callback
now receives &mut MjData<Box<MjModel>>.
Before (3.x):
viewer.add_ui_callback(|ctx, data: &mut MjData<Arc<MjModel>>| { ... });
After (4.0.0):
viewer.add_ui_callback(|ctx, data: &mut MjData<Box<MjModel>>| { ... });
Type annotations in closure parameters must be updated if present. Closures without an explicit type annotation require no change.
Migrating to 3.0.0¶
Version 3.0.0 updates MuJoCo to 3.6.0, introduces typed error enums, tightens safety requirements, and removes deprecated APIs.
MuJoCo upgrade¶
MuJoCo-rs 3.0.0 links against MuJoCo 3.6.0. Download the matching release and update your library path. See Installation for details.
Default features changed¶
The viewer, viewer-ui, renderer, and renderer-winit-fallback
features are no longer enabled by default. If your project relies on the
viewer or renderer, add the features explicitly in Cargo.toml:
[dependencies]
mujoco-rs = { version = "3", features = ["viewer-ui", "renderer-winit-fallback"] }
Error handling¶
The most significant change in 3.0.0 is replacing io::Error and bare return
types with typed error enums.
New error types¶
Six new types in error (all #[non_exhaustive],
re-exported from the prelude):
MjDataError — MjData operations.
MjSceneError — MjvScene and rendering.
MjEditError — MjSpec and model editing.
MjModelError — MjModel load/save/state.
MjVfsError — virtual file system.
GlInitError — OpenGL context init (requires
viewerorrenderer-winit-fallbackfeature).
RendererError and MjViewerError gained new variants.
Updating error handling code¶
Replace io::ErrorKind matches with the specific error enum variants.
Before (2.x):
use std::io;
match model.save_last_xml("out.xml") {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
eprintln!("Invalid path: {e}");
}
Err(e) => eprintln!("Save failed: {e}"),
}
After (3.0.0):
use mujoco_rs::prelude::*;
match model.save_last_xml("out.xml") {
Ok(()) => {}
Err(MjModelError::SaveFailed(msg)) => eprintln!("Save failed: {msg}"),
Err(e) => eprintln!("Other error: {e}"),
}
The table below maps the breaking error-type changes:
Type / methods |
2.x error type |
3.0.0 error type |
|---|---|---|
MjModel: |
|
|
MjVfs: |
|
|
MjSpec: |
|
|
MjData: |
|
|
MjRenderer:
|
|
|
MjViewer: |
|
|
MjRenderer::rgb / MjRenderer::depth no longer return Result¶
rgb and
depth previously returned
io::Result (2.x) / Result<_, RendererError> (intermediate 3.0 dev builds).
They now panic on error and return the image directly.
Use try_rgb / try_depth for fallible alternatives.
Before (2.x):
let pixels = renderer.rgb::<W, H>()?;
let depth = renderer.depth::<W, H>()?;
After (3.0.0):
// Panicking (most callers)
let pixels = renderer.rgb::<W, H>();
let depth = renderer.depth::<W, H>();
// Fallible
let pixels = renderer.try_rgb::<W, H>()?;
let depth = renderer.try_depth::<W, H>()?;
Newly Result-returning methods¶
- MjData methods now returning
Result<_, MjDataError>: constraint_update,print,print_formatted.- MjvScene / MjvGeom / MjrContext methods now returning
Result<_, MjSceneError>: set_label,add_aux,set_aux.
MjvScene: create_geom now panics on failure (was bare type in 2.x).
Use try_create_geom for a fallible alternative.
Before:
let geom = scene.create_geom(MjtGeom::mjGEOM_BOX, None, None, None, None);
After (panicking):
let geom = scene.create_geom(MjtGeom::mjGEOM_BOX, None, None, None, None);
After (fallible):
let geom = scene.try_create_geom(MjtGeom::mjGEOM_BOX, None, None, None, None)?;
MjSpec: add_default now panics on failure (was Result in 2.x).
Use try_add_default for a fallible alternative.
Before:
let def = spec.add_default("my_class", None)?;
After (panicking):
let def = spec.add_default("my_class", None);
After (fallible):
let def = spec.try_add_default("my_class", None)?;
- MjRenderer methods now returning
Result: set_font_scalereturnsResult<(), RendererError>;with_font_scalereturnsResult<Self, RendererError>.
Before:
renderer.set_font_scale(MjtFontScale::mjFONTSCALE_150);
let renderer = renderer.with_font_scale(MjtFontScale::mjFONTSCALE_150);
After:
renderer.set_font_scale(MjtFontScale::mjFONTSCALE_150)?;
let renderer = renderer.with_font_scale(MjtFontScale::mjFONTSCALE_150)?;
Model-editing API changes¶
set_name
now returns Result<(), MjEditError> instead of ().
with_name
still returns &mut Self but now panics on duplicate names.
SpecItem is now a sealed
trait. External implementations are no longer permitted – remove any
impl SpecItem for MyType blocks from downstream code.
MjsOrientation::switch_quat no longer has a generic type parameter. Remove
turbofish syntax and call it directly.
Before (2.x):
item.set_name("arm_joint");
item.with_name("arm_joint");
orientation.switch_quat::<[f64; 4]>();
After (3.0.0):
item.set_name("arm_joint")?;
item.with_name("arm_joint");
orientation.switch_quat();
MjModel::clone()¶
MjModel now implements the standard Clone trait. The previous clone()
returning Option<MjModel> has been removed.
Before (2.x):
let model_copy = model.clone().expect("clone failed");
After (3.0.0):
let model_copy = model.clone(); // panics on failure
let model_copy = model.try_clone()?; // fallible
MjModel::save() split¶
MjModel::save(filename: Option<&str>, buffer: Option<&mut [u8]>) has been
replaced by two dedicated methods:
MjModel::save_to_file — saves to a binary MJB file.
MjModel::save_to_buffer — saves to a memory buffer. Returns
MjModelError::BufferTooSmallif the buffer is too small.
Before (2.x):
model.save(Some("model.mjb"), None);
let mut buffer = vec![0u8; model.size() as usize];
model.save(None, Some(&mut buffer));
After (3.0.0):
model.save_to_file("model.mjb")?;
let mut buffer = vec![0u8; model.size()];
model.save_to_buffer(&mut buffer)?;
Mutable accessor restrictions¶
*_mut() methods on MjModel, MjData, and MjvScene array fields are now
unsafe fn where unrestricted writes can corrupt MuJoCo’s internal state.
Two categories of fields are affected.
Structural invariants¶
Topology, address, and engine-computed arrays that must not be changed at runtime.
MjModel: 199 fields — every address array (
*adr,*num), topology index (*bodyid,*jntid, …), and physics-invariant.MjData: 43 fields —
contact,efc_id,efc_J_*,efc_AR_*,efc_island,iefc_id,iefc_J_*,tree_island,dof_island,tendon_efcadr,island_*,map_*,iM_*,ten_wrapadr,ten_wrapnum,moment_*,body_awake_ind,parent_awake_ind,dof_awake_ind.MjvScene: 12 fields —
flexedge,geoms,flexedgeadr,flexedgenum,flexvertadr,flexvertnum,flexfaceadr,flexfacenum,flexfaceused,skinfacenum,skinvertadr,skinvertnum.
Before (2.x):
model.body_parentid_mut()[0] = new_parent;
After (3.0.0):
// SAFETY: caller keeps all companion topology fields consistent.
unsafe { model.body_parentid_mut()[0] = new_parent; }
Companion-index fields¶
Type/mode fields whose values control which array a companion index
(*id, *adr) indexes into. Writing an inconsistent value causes
out-of-bounds access inside MuJoCo.
MjModel:
jnt_type,actuator_trntype,actuator_dyntype,eq_type,eq_objtype,wrap_type,wrap_prm,sensor_type,sensor_objtype,sensor_reftype,skin_matid,tendon_matid,tendon_treeid,body_plugin,actuator_plugin,geom_plugin,sensor_plugin.MjData:
efc_type,iefc_type,tree_asleep,wrap_obj.
Before (2.x):
model.jnt_type_mut()[i] = MjtJoint::mjJNT_BALL;
After (3.0.0):
// SAFETY: companion fields (jnt_qposadr, jnt_dofadr, jnt_bodyid) are also
// updated to a state consistent with mjJNT_BALL.
unsafe { model.jnt_type_mut()[i] = MjtJoint::mjJNT_BALL; }
Per-object ViewMut types expose these fields as
PointerViewUnsafeMut struct fields.
Reading is safe; mutation requires
as_mut_slice
inside unsafe:
View type |
Fields requiring |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Before (2.x):
view.r#type[0] = MjtJoint::mjJNT_BALL;
After (3.0.0):
// SAFETY: companion fields are also updated consistently.
unsafe { view.r#type.as_mut_slice()[0] = MjtJoint::mjJNT_BALL; }
Null-terminated string buffer fields¶
Concatenated c_char arrays where each object’s name or attribute is null-terminated.
Overwriting a '\0' byte allows MuJoCo’s C string functions (and CStr::from_ptr
inside wrappers such as id_to_name)
to scan past the buffer boundary, causing undefined behavior.
MjModel:
names,plugin_attr,text_data,paths.
Before (2.x):
// Write a replacement name directly (unsafe in 3.0.0).
let buf = model.names_mut();
buf[0] = b'X' as c_char;
After (3.0.0):
// SAFETY: all '\0' terminators within the buffer are preserved; no byte
// at offset nnames-1 (the final terminator) is overwritten.
let buf = unsafe { model.names_mut() };
buf[0] = b'X' as c_char;
MjData::print() and MjModel::print()¶
Both print and print_formatted now accept AsRef<Path> for the filename
and return Result with the appropriate error type.
Before (2.x):
data.print("data.txt");
model.print("model.txt")?;
After (3.0.0):
data.print("data.txt")?;
model.print("model.txt")?;
Type changes¶
MjModel:
size()returnsusize(wasi32).MjModel:
state_size()returnsusize(wasi32).MjModel:
name_to_id()returnsOption<usize>(wasi32;-1is nowNone).MjCameraModelView/MjCameraModelViewMut:projection(MjtProjection) replaces the old booleanorthographicfield.MjModel:
tuple_objtype()returns&[MjtObj](was&[i32]).MjModel:
id_to_name:idtakesusize(wasi32).MjData:
maxuse_threadstack()returns&[MjtSize; mjMAXTHREAD](was&[MjtSize]).MjData:
jac,jac_body,jac_body_com,jac_subtree_com,jac_geom,jac_site,angmom_mat,object_velocity,object_acceleration,geom_distance,local_to_global: index parameters now takeusize(wasi32). Addas usizeat call sites.MjData:
rayandmulti_ray:bodyexcludechanged fromi32(-1= no exclusion) toOption<usize>(None= no exclusion). Replace-1withNoneandbody_idwithSome(body_id as usize).MjData:
ray()returns(Option<usize>, MjtNum)(was(i32, MjtNum)).Nonemeans no intersection (previously-1).MjData:
try_multi_ray()returnsResult<(Vec<Option<usize>>, Vec<MjtNum>), ...>(was(Vec<i32>, Vec<MjtNum>)).multi_ray()panics on error. EachNoneelement means no intersection for that ray (previously-1).MjsTendon:
limitedandactfrclimitedare nowMjtLimitedtri-state (wasbool).MjvCamera:
new_fixed,new_tracking,track,fixnow takeusize(wasu32). Removeas u32casts at call sites.TryFrom<i32> for MjtCameranow usesMjSceneErroras its error type (was()). ReplaceErr(())matches withErr(MjSceneError::InvalidCameraType(_)).SceneSelection:
body_id,geom_id,flex_id,skin_idare nowOption<usize>(wasi32). Replacesel.body_id >= 0withsel.body_id.is_some()orif let Some(id) = sel.body_id.MjData:
runge_kutta()now takesn: u32(wasi32). Replacedata.runge_kutta(n)withdata.runge_kutta(n as u32)at call sites. The function also now panics ifn < 1(previously passed negative or zero values silently to MuJoCo C).MjsTexture::set_data now requires
T: bytemuck::NoUninit(addbytemuckto your dependencies if you call this method with a custom type, and derive or implementNoUninitfor it).MjTendonDataInfo:J_rownnz,J_rowadr, andJ_colindare no longer exposed. These fields moved tomjModelin MuJoCo 3.6.0 and are now accessible viaMjTendonModelInfo(i.e.model.tendon()).// Before (2.x) -- accessed through data let view = data.tendon(0).view(&data); let nnz = view.J_rownnz[0]; // After (3.0.0) -- accessed through model let view = model.tendon(0).view(&model); let nnz = view.J_rownnz[0];
MjCameraModelView::projection replaces orthographic¶
MuJoCo 3.6.0 replaced the old cam_orthographic flag with the
cam_projection enum. The Rust camera model views mirror that upstream
change, so code that previously read view.orthographic[0] must now read
view.projection[0] and compare it against MjtProjection.
Before (2.x):
let view = model.camera("main").unwrap().view(&model);
let is_ortho = view.orthographic[0];
After (3.0.0):
let view = model.camera("main").unwrap().view(&model);
let is_ortho = view.projection[0] == MjtProjection::mjPROJ_ORTHOGRAPHIC;
MjData::reset_keyframe()¶
MjData::reset_keyframe
now takes key: usize (was i32) and returns Result<(), MjDataError>
instead of (). Out-of-range keys that previously silently fell back to a
plain reset now return Err(MjDataError::IndexOutOfBounds).
Before (2.x):
data.reset_keyframe(0); // silently ignored if key >= nkey
After (3.0.0):
data.reset_keyframe(0)?; // returns Err if key >= nkey
Methods now return Result instead of panicking¶
The following methods previously returned () and panicked on invalid input.
They now return Result directly.
2.x call |
3.0.0 call |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Append .unwrap(), .expect(...) or ? at call sites.
Ray-casting parameter changes¶
MjData::ray gained a new
normal_out parameter. The bodyexclude parameter changed from i32 (-1
= no exclusion) to Option<usize> (None = no exclusion). The return type
changed from (i32, MjtNum) to (Option<usize>, MjtNum); the geom id is
None when the ray misses all geometry (previously -1).
Before (2.x):
let (geom_id, dist) = data.ray(&pnt, &vec, None, true, -1);
if geom_id == -1 { /* miss */ }
After (3.0.0):
let (geom_id, dist) = data.ray(&pnt, &vec, None, true, None, None);
if geom_id.is_none() { /* miss */ }
Similarly, MjData::multi_ray
gained normals_out, now panics on invalid input (use try_multi_ray for a
fallible alternative), and its geom-id vector changed from
Vec<i32> to Vec<Option<usize>>. Pass None for normals_out.
mju_ray_geom also gained
normal_out: Option<&mut [MjtNum; 3]>.
Before (2.x):
let dist = mju_ray_geom(&pos, &mat, &size, &pnt, &vec, geomtype);
After (3.0.0):
let dist = mju_ray_geom(&pos, &mat, &size, &pnt, &vec, geomtype, None);
MjData::jac_subtree_com() parameter change¶
The jacp: bool parameter has been removed (the Jacobian is always computed).
Before (2.x):
let jac = data.jac_subtree_com(true, body_id);
After (3.0.0):
let jac = data.jac_subtree_com(body_id);
MjvPerturb::update_local_pos¶
MjvPerturb::update_local_pos
now takes selection_xyz by reference (&[MjtNum; 3]) instead of by value.
Before (2.x):
perturb.update_local_pos(xyz, &data);
After (3.0.0):
perturb.update_local_pos(&xyz, &data);
MjvPerturb::move_¶
MjvPerturb::move_
no longer takes a separate model parameter (the model is obtained from
data.model()), and data changed from &mut MjData<M> to &MjData<M>.
Before (2.x):
perturb.move_(&model, &mut data, action, dx, dy, &scene);
After (3.0.0):
perturb.move_(&data, action, dx, dy, &scene);
MjvPerturb::start¶
MjvPerturb::start
no longer takes a separate model parameter (the model is obtained from
data.model()), and scene changed from &MjvScene<M> to &MjvScene.
Before (2.x):
perturb.start(type_, &model, &mut data, &scene);
After (3.0.0):
perturb.start(type_, &mut data, &scene);
MjvPerturb::apply¶
MjvPerturb::apply
no longer takes a separate model parameter (the model is obtained from
data.model()).
Before (2.x):
perturb.apply(&model, &mut data);
After (3.0.0):
perturb.apply(&mut data);
find_selection() return type¶
MjvScene’s find_selection now returns a
SceneSelection named
struct instead of a 5-tuple.
Before (2.x):
let (body_id, geom_id, flex_id, skin_id, point) = scene.find_selection(&data, ...);
After (3.0.0):
let sel = scene.find_selection(&data, ...);
println!("{:?} {:?} {:?} {:?} {:?}", sel.body_id, sel.geom_id, sel.flex_id, sel.skin_id, sel.point);
Core Send/Sync bound tightening¶
MjData<M> now requires M: Send / M: Sync.
MjvScene is no longer generic and derives Send + Sync unconditionally.
MjViewerCpp::launch_passive now requires M: Send + Sync.
If you were using Rc<MjModel> in threaded code, switch to Arc<MjModel>.
New unsafe requirements¶
MjrContext::new
is now unsafe fn. A valid OpenGL context must be current on the calling thread.
In most cases you will not call this directly (MjRenderer and MjViewer call
it internally). If you construct MjrContext manually:
Before (2.x):
let context = MjrContext::new(&model);
After (3.0.0):
// SAFETY: a valid GL context has been made current above.
let context = unsafe { MjrContext::new(&model) };
MjData::set_state is now unsafe and returns Result¶
set_state is now unsafe and returns Result<(), MjDataError> directly.
When spec includes mjSTATE_EQ_ACTIVE, MuJoCo writes raw f64 bytes into
the eq_active byte array without booleanization, making a subsequent call to
eq_active() undefined behavior. Re-validate by calling mj_forward /
mj_step before reading eq_active().
Before (2.x):
data.set_state(&saved, MjtState::mjSTATE_FULLPHYSICS as u32);
After (3.0.0):
// SAFETY: state captured via state(); bools are valid (0 or 1).
unsafe { data.set_state(&saved, MjtState::mjSTATE_FULLPHYSICS as u32) }?;
API renames¶
Type |
Old name |
New name |
|---|---|---|
|
|
|
|
|
sync() deprecated¶
MjRenderer::sync is deprecated.
Use MjRenderer::sync_data
followed by MjRenderer::render
instead. sync_data updates the scene without rendering, giving you the
opportunity to insert custom logic (e.g. user-scene geoms) between the sync and
render steps.
Before (2.x):
renderer.sync(&mut data);
After (3.0.0):
renderer.sync_data(&mut data)?;
renderer.render()?;
MjRenderer is no longer generic¶
MjRenderer and
MjRendererBuilder are no longer generic
over M. Remove the <M> type annotation from all usage sites.
The Clone bound on M has also been dropped; any
M: Deref<Target = MjModel> is now accepted.
Before (2.x):
let mut renderer: MjRenderer<Arc<MjModel>> = MjRenderer::builder()
.build(model.clone())
.expect("failed to initialize renderer");
renderer.sync(&mut data);
After (3.0.0):
let mut renderer: MjRenderer = MjRenderer::builder()
.build(model.clone())
.expect("failed to initialize renderer");
renderer.sync_data(&mut data).unwrap();
renderer.render().unwrap();
In practice the type annotation is rarely needed, so in most cases only the
MjRenderer<Arc<MjModel>> annotation at the variable declaration (or in a
struct field) needs to be updated.
MjvScene is no longer generic¶
MjvScene is no longer generic over M. Remove the <M> type annotation
from all usage sites.
Before (2.x):
let scene: MjvScene<Arc<MjModel>> = MjvScene::new(model.clone(), 1000);
scene.update(&mut data, &opt, &perturb, &mut cam);
After (3.0.0):
let mut scene: MjvScene = MjvScene::new(model.clone(), 1000);
scene.update(&mut data, &opt, &perturb, &mut cam);
Additional details:
MjvScene::update,MjvScene::update_with_catmask, andMjvScene::find_selectionretain<M: Deref<Target = MjModel>>as method-level generics; call sites are unchanged.MjvPerturb::start/move_andMjvCamera::move_now take&MjvScenewithout a type parameter.vis_common::sync_geomsis now non-generic.MjvScene derives
Send + Syncunconditionally (no bound onMrequired).
C++ viewer changes¶
cpp_viewer::MjViewerCpp is no longer generic;
the type parameter M has been removed from the struct.
MjViewerCpp::launch_passive
now requires M: Send + Sync. Switch to Arc<MjModel> if using Rc<MjModel>.
MjViewerCpp::render is now
unsafe fn, must be called from the main thread, no longer accepts
update_timer, and returns Result<(), &'static str>.
MjViewerCpp::__raw() has been removed (no replacement).
Before (2.x):
viewer.render(true);
After (3.0.0):
// SAFETY: called from the main thread.
unsafe { viewer.render().unwrap() };
MjViewerCpp::launch_passive() is now unsafe¶
Callers must ensure the model and data remain alive and at a stable address.
Before (2.x):
let viewer = MjViewerCpp::launch_passive(&model, &data, 100);
After (3.0.0):
// SAFETY: model and data kept alive and at a stable address.
let viewer = unsafe { MjViewerCpp::launch_passive(&model, &data, 100) };
MjsTendon::wrap / wrap_mut no longer return Option¶
MjsTendon::wrap and
MjsTendon::wrap_mut now return
&MjsWrap / &mut MjsWrap instead of Option<&MjsWrap> / Option<&mut MjsWrap>, and
panic when the index is out of bounds. Drop the .unwrap() and make sure the index is
< wrap_num(), or use the new fallible try_wrap / try_wrap_mut.
Before:
let wrap = tendon.wrap(1).unwrap();
After:
let wrap = tendon.wrap(1);
// or, fallibly:
let wrap = tendon.try_wrap(1)?;
Removed deprecated methods¶
Removed |
Replacement |
|---|---|
|
|
|
|
Type aliases |
Use the |
|
|
|
|
|
|
|
|
|
|
MjvFigure |
|