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 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):
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 behaviour. 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) };
Removed deprecated methods¶
Removed |
Replacement |
|---|---|
|
|
|
|
Type aliases |
Use the |
|
|
|
|
|
|
|
|
|
|
MjvFigure |
|