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 7.0.0¶
Tendon tree count is unsafe to mutate¶
treenum is now marked as unsafe to mutate (wrong values can cause out-of-bound reads). Consequently,
the
treenumfield of MjTendonModelViewMut is now PointerViewUnsafeMut;MjModel::tendon_treenum_mut is now an
unsafe fn.
// Before
model.tendon_treenum_mut()[0] = 1;
model.tendon("t").unwrap().view_mut(&mut model).treenum[0] = 1;
// After
// SAFETY: tendon 0 spans exactly one tree.
unsafe { model.tendon_treenum_mut()[0] = 1; }
let mut view = model.tendon("t").unwrap().view_mut(&mut model);
// SAFETY: tendon "t" spans exactly one tree.
unsafe { view.treenum.as_mut_slice()[0] = 1; }
Plugin ids in model views are unsafe to mutate¶
The plugin field of MjActuatorModelViewMut and
MjBodyModelViewMut is now
PointerViewUnsafeMut.
// Before
model.actuator("a").unwrap().view_mut(&mut model).plugin[0] = -1;
// After
let mut view = model.actuator("a").unwrap().view_mut(&mut model);
// SAFETY: actuator "a" is not bound to a plugin instance.
unsafe { view.plugin.as_mut_slice()[0] = -1; }
Equality data, flex stiffness and flex bending are unsafe to mutate¶
MjModel::eq_data_mut,
MjModel::flex_stiffness_mut and
MjModel::flex_bending_mut are now unsafe fn.
The data field of MjEqualityModelViewMut is now
PointerViewUnsafeMut.
// Before
model.eq_data_mut()[0][0] = 0.5;
model.equality("eq").unwrap().view_mut(&mut model).data[0] = 0.5;
// After
// SAFETY: equality 0 is not a flex strain equality.
unsafe { model.eq_data_mut()[0][0] = 0.5; }
let mut view = model.equality("eq").unwrap().view_mut(&mut model);
// SAFETY: equality "eq" is not a flex strain equality.
unsafe { view.data.as_mut_slice()[0] = 0.5; }
Actuator bias type and parameters, body simple flag and flex flags are unsafe to mutate¶
MjModel::actuator_biastype_mut,
MjModel::actuator_dynprm_mut,
MjModel::actuator_gainprm_mut,
MjModel::body_simple_mut,
MjModel::flex_rigid_mut and
MjModel::flex_centered_mut are now
unsafe fn. The biastype, dynprm and gainprm fields of
MjActuatorModelViewMut and the simple field of
MjBodyModelViewMut are now
PointerViewUnsafeMut.
// Before
model.actuator_biastype_mut()[0] = MjtBias::mjBIAS_NONE;
model.body_simple_mut()[1] = 0;
let mut view = model.actuator("a").unwrap().view_mut(&mut model);
view.gainprm[0] = 2.0;
// After
// SAFETY: mjBIAS_NONE reads no actuator state, and 0 selects the general mass matrix path.
unsafe {
model.actuator_biastype_mut()[0] = MjtBias::mjBIAS_NONE;
model.body_simple_mut()[1] = 0;
}
let mut view = model.actuator("a").unwrap().view_mut(&mut model);
// SAFETY: actuator "a" is not a PID or a DC motor actuator, so gainprm does not set its number
// of activation slots.
unsafe {
view.gainprm.as_mut_slice()[0] = 2.0;
}
Tuple object types are unsafe to mutate¶
MjModel::tuple_objtype_mut is now an unsafe fn.
The objtype field of MjTupleModelViewMut is now
PointerViewUnsafeMut.
// Before
model.tuple_objtype_mut()[0] = MjtObj::mjOBJ_SITE;
model.tuple("t").unwrap().view_mut(&mut model).objtype[0] = MjtObj::mjOBJ_SITE;
// After
// SAFETY: tuple_objid[0] is a valid site id.
unsafe { model.tuple_objtype_mut()[0] = MjtObjBase::mjOBJ_SITE; }
let mut view = model.tuple("t").unwrap().view_mut(&mut model);
// SAFETY: the first object id of tuple "t" is a valid site id.
unsafe { view.objtype.as_mut_slice()[0] = MjtObjBase::mjOBJ_SITE; }
Height field sizes are unsafe to mutate¶
MjModel::hfield_size_mut is now an unsafe fn.
The size field of MjHfieldModelViewMut is now
PointerViewUnsafeMut.
// Before
model.hfield_size_mut()[0][0] = 2.0;
model.hfield("h").unwrap().view_mut(&mut model).size[0] = 2.0;
// After
// SAFETY: every size is positive and finite.
unsafe { model.hfield_size_mut()[0][0] = 2.0; }
let mut view = model.hfield("h").unwrap().view_mut(&mut model);
// SAFETY: the size is positive and finite.
unsafe { view.size.as_mut_slice()[0] = 2.0; }
Site types and asset paths are unsafe to mutate¶
MjModel::site_type_mut and
MjModel::paths_mut are now unsafe fn.
The r#type field of MjSiteModelViewMut is now
PointerViewUnsafeMut.
// Before
model.site_type_mut()[0] = MjtGeom::mjGEOM_BOX;
model.site("s").unwrap().view_mut(&mut model).r#type[0] = MjtGeom::mjGEOM_BOX;
// After
// SAFETY: a box site needs no mesh data.
unsafe { model.site_type_mut()[0] = MjtGeomBase::mjGEOM_BOX; }
let mut view = model.site("s").unwrap().view_mut(&mut model);
// SAFETY: a box site needs no mesh data.
unsafe { view.r#type.as_mut_slice()[0] = MjtGeomBase::mjGEOM_BOX; }
zero of a mutable view skips the unsafe fields¶
The zero method of a mutable view (e.g.
MjActuatorModelViewMut::zero) no longer
zeroes the fields that are now PointerViewUnsafeMut (above). Write these fields through the
unsafe view if you need them zeroed.
// Before
let mut view = model.actuator("a").unwrap().view_mut(&mut model);
view.zero();
// After
let mut view = model.actuator("a").unwrap().view_mut(&mut model);
view.zero();
// SAFETY: zero gain parameters are valid for the actuator type of "a".
unsafe { view.gainprm.as_mut_slice().fill(0.0); }
More constraint arrays of MjData are unsafe to read¶
MjData::efc_diag_a,
MjData::efc_kbip,
MjData::efc_d,
MjData::efc_r,
MjData::iefc_d,
MjData::iefc_r,
MjData::efc_y,
MjData::efc_ar,
MjData::efm_c and
MjData::efm_l are now
unsafe fn, and so are their _mut variants. MuJoCo does not zero these arrays, so read them
only after the pipeline stage
that computes them. efc_y and efc_ar stay uninitialized under some solver settings.
// Before
data.step1();
let impedance = data.efc_kbip()[0][2];
// After
data.forward();
// SAFETY: forward built the constraints, so efc_kbip holds computed values.
let impedance = unsafe { data.efc_kbip() }[0][2];
Viewer, renderer and scene constructors take &MjModel¶
MjViewer::launch_passive,
MjViewerBuilder::build_passive,
MjRenderer::new,
MjRendererBuilder::build and
MjvScene::new take the model as
&MjModel (MjModel) instead of a generic model type.
// Before
let model = Arc::new(MjModel::from_xml_string(XML).unwrap());
let viewer = MjViewer::launch_passive(model.clone(), 0)?;
let renderer = MjRenderer::builder().build(model.clone())?;
let scene = MjvScene::new(model.clone(), 100);
// After
let model = Arc::new(MjModel::from_xml_string(XML).unwrap());
let viewer = MjViewer::launch_passive(&model, 0)?;
let renderer = MjRenderer::builder().build(&model)?;
let scene = MjvScene::new(&model, 100);
The C++ viewer owns the simulation data¶
MjViewerCpp::launch_passive takes the
MjData by value and no longer takes a model parameter. The model type of the data must
implement ModelTypeMut. Step and sync
the data through MjViewerCpp::state.
MjViewerCpp::render is no longer unsafe.
Replace user_scn_mut with
MjCppViewerSharedState::user_geoms_mut and
MjViewerCpp::sync_user_geoms.
The former method is unsafe due to the ability of users to write invalid IDs into
MjvGeom.
MjViewerCpp no longer implements Send and Sync. To step the simulation in another
thread, send a clone of the shared state to that thread and keep the viewer on the thread that
created it.
// Before
let model = MjModel::from_xml_string(XML).unwrap();
let mut data = MjData::new(&model);
let mut viewer = unsafe { MjViewerCpp::launch_passive(&model, &data, 100) };
while viewer.running() {
viewer.sync();
unsafe { viewer.render().unwrap() };
data.step();
}
// After
use mujoco_rs::util::LockUnpoison;
let model = Box::new(MjModel::from_xml_string(XML).unwrap());
let mut viewer = unsafe { MjViewerCpp::launch_passive(MjData::new(model), 100) };
let state = viewer.state().clone();
while viewer.running() {
{
let mut lock = state.lock_unpoison();
// SAFETY: the data is only stepped, never replaced.
unsafe { lock.data_mut() }.step();
lock.sync();
}
viewer.render().unwrap();
}
MjData::read_ctrl returns a slice¶
MjData::read_ctrl and
MjData::try_read_ctrl return a
Cow<[MjtNum]> (MjtNum) with one value per
control of the actuator. To avoid heap allocation, use
MjData::read_ctrl_fixed or
MjData::read_ctrl_into.
// Before
let value: MjtNum = data.read_ctrl(0, time, 1);
// After
let value: MjtNum = data.read_ctrl(0, time, 1)[0];
// or
let [value] = data.read_ctrl_fixed::<1>(0, time, 1);
Removed append_cubefiles and append_textures¶
Removed append_cubefiles and append_textures from
MjsTexture and
MjsMaterial, respectively.
cubefiles and textures require a fixed number of elements.
Use MjsTexture::set_cubefile and
MjsMaterial::set_texture, respectively,
to set the elements.
// Before
texture.append_cubefiles("right.png");
material.append_textures("wood");
// After
texture.set_cubefile(MjtCubeFace::Right, "right.png");
material.set_texture(MjtTextureRoleBase::mjTEXROLE_RGB, "wood");
Removed the frame and body adders of MjsFrame¶
Removed add_frame, try_add_frame, add_body, try_add_body and
add_body_with_class from MjsFrame. Add elements to bodies instead and assign them into
a frame using
FrameChild::with_frame or
FrameChild::set_frame.
Nested frames can also be created using MjsBody::add_frame_in.
// Before
let outer = world.add_frame().with_name("outer");
outer.add_frame().with_name("inner");
outer.add_body().with_name("ball");
outer.add_body_with_class("cls").unwrap();
// After
world.add_frame().with_name("outer");
world.add_frame_in("outer").unwrap().with_name("inner");
world.add_body().with_name("ball").with_frame("outer").unwrap();
world.add_body_with_class("cls").unwrap().with_frame("outer").unwrap();
The other element adders of MjsFrame are deprecated. Replace them in the same way:
// Before
frame.add_geom();
frame.add_site_with_class("cls").unwrap();
// After
body.add_geom().with_frame("outer").unwrap();
body.add_site_with_class("cls").unwrap().with_frame("outer").unwrap();
We are aware that the new API may be less clean, however, the change was necessary due to a high chance of possible accidental aliasing (two mutable references with same address) problems being created. Notably, deprecated-only methods are technically safe, however, they internally do the same thing as the new API. We deprecated them (slated for removal) for consistency reasons. We feel it would be annoying trying to figure out how to add bodies and frames through the new API if the old API was still meant to be used for other items.
MjsDefault is no longer a SpecItem¶
MjsDefault no longer implements SpecItem, thus a default class cannot be renamed. Renaming a default class caused undefined behavior.
It now implements only the new supertrait SpecElement, the latter holding SpecElement::element_pointer, SpecElement::element_mut_pointer and SpecElement::name.
Code that previously relied on (or imported explicitly) SpecItem directly should
also add an import of SpecElement, in addition to updating the type bounds in generics.
// Before
use mujoco_rs::wrappers::SpecItem;
fn print_name<T: SpecItem>(item: &T) { println!("{}", item.name()); }
let class = spec.add_default("a", None);
class.set_name("b").unwrap();
print_name(class);
// After
use mujoco_rs::wrappers::{SpecElement, SpecItem};
fn print_name<T: SpecElement>(item: &T) { println!("{}", item.name()); }
let class = spec.add_default("b", None);
print_name(class);
Default-class members have field accessors only¶
The member accessors of MjsDefault now return
handles marked with InDefault, for example
&mut MjsJoint<InDefault> (MjsJoint). These handles have no methods of
SpecElement or of the traits built on it
(SpecItem, SpecObject, FrameChild,
Attach,
UserValued).
The marker is needed to hide all the (trait) methods that aren’t actually supported and would, prior to the change, cause a segmentation fault.
// Before
let joint: &mut MjsJoint = spec.add_default("cls", None).joint_mut();
joint.set_armature(0.1);
// After
let joint: &mut MjsJoint<InDefault> = spec.add_default("cls", None).joint_mut();
joint.set_armature(0.1);
MjData::contact_force panics on an invalid ID (runtime-breaking)¶
MjData::contact_force now panics when
contact_id >= ncon. Use
MjData::try_contact_force to handle an
invalid ID.
// Before
let force = data.contact_force(contact_id); // [0.0; 6] when contact_id >= ncon
// After
let force = data.try_contact_force(contact_id).unwrap_or([0.0; 6]);
MjvScene refuses more models (runtime-breaking)¶
MjvScene::is_compatible_with_model now refuses more models, and MjvScene::update panics on such a model. If you reuse one scene for several models, create a new scene when the check fails.
// Before
scene.update(&mut data, &opt, &pert, &mut camera);
// After
if !scene.is_compatible_with_model(data.model()) {
scene = MjvScene::new(data.model(), max_geom);
}
scene.update(&mut data, &opt, &pert, &mut camera);
Incompatible-model errors now carry layouts¶
MjDataError::IncompatibleModel and
MjModelError::IncompatibleModel now hold the
MjModelLayout of both models. MjvScene::signature
and model_signature of the Info types are removed. Read the signature from the model.
// Before
if let Err(MjDataError::IncompatibleModel { source, destination }) = data.try_swap_model(other) {
println!("{source:#X} vs {destination:#X}");
}
let signature = scene.signature();
let signature = body_info.model_signature();
// After
if let Err(MjDataError::IncompatibleModel { source, destination }) = data.try_swap_model(other) {
println!("{source:?} vs {destination:?}");
}
let signature = model.signature();
Unsupported-object-type errors now carry the object type¶
MjDataError::UnsupportedObjectType now holds an
MjtObjBase instead of an i32.
// Before
if let Err(MjDataError::UnsupportedObjectType(raw)) = data.try_object_velocity(MjtObj::mjOBJ_JOINT, 0, false) {
println!("object type {raw}");
}
// After
if let Err(MjDataError::UnsupportedObjectType(obj_type)) =
data.try_object_velocity(MjtObjBase::mjOBJ_JOINT, 0, false)
{
println!("object type {obj_type:?}");
}
A model swap refuses more models¶
MjData::swap_model and MjData::try_swap_model now refuse a model that differs in a size or in an integer table that no safe accessor writes, apart from the names, the paths, the buffer size, the mesh faces and hulls, the bounding volume and octree nodes and the values derived from floats. If you know that the new model is still safe for the data, use MjData::swap_model_unchecked. Otherwise, create a new MjData.
// Before
let old = data.swap_model(edited);
// After
// SAFETY: `edited` only changes a geom material, so every size and every address table agrees.
let old = unsafe { data.swap_model_unchecked(edited) };
Real geom types, object types and texture roles use base enums¶
Model and specification fields and functions that accept only real geom types, real object types or real texture roles now use MjtGeomBase, MjtObjBase and MjtTextureRoleBase, respectively. The object type setters of MjsSensor and MjsTuple are no longer fallible.
// Before
spec.world_body_mut().add_geom().with_type(MjtGeom::mjGEOM_PLANE);
sensor.set_objtype(MjtObj::mjOBJ_SITE)?;
tuple.set_objtype(&[MjtObj::mjOBJ_BODY, MjtObj::mjOBJ_GEOM])?;
material.set_texture(MjtTextureRole::mjTEXROLE_RGB, "wood");
let id = model.name_to_id(MjtObj::mjOBJ_BODY, "ball");
let name = model.id_to_name(model.sensor_objtype()[0], 0);
// After
spec.world_body_mut().add_geom().with_type(MjtGeomBase::mjGEOM_PLANE);
sensor.set_objtype(MjtObjBase::mjOBJ_SITE);
tuple.set_objtype(&[MjtObjBase::mjOBJ_BODY, MjtObjBase::mjOBJ_GEOM]);
material.set_texture(MjtTextureRoleBase::mjTEXROLE_RGB, "wood");
let id = model.name_to_id(MjtObjBase::mjOBJ_BODY, "ball");
let name = model.id_to_name(model.sensor_objtype()[0], 0);
Migrating to 6.0.0¶
MuJoCo upgrade¶
This release links against MuJoCo 3.12.0. Download the matching release and update your library path. See Installation for details.
The FFI bindings follow the upstream API. This page gives no entry for an accessor that only mirrors a renamed, a removed or a resized MuJoCo field, and none for a shifted enum discriminant. For these changes, see MuJoCo’s changelog for this release: https://mujoco.readthedocs.io/en/3.12.0/changelog.html
Minimum Rust version¶
This release needs Rust 1.95, up from 1.88. Run rustup update stable before you build.
Mutable body iteration¶
MjSpec lost body_iter_mut, and MjsBody’s body_iter_mut lost its recurse
argument. Every item a mutable iterator yields borrows its parent for the same lifetime, so all of
them stay live together. A body owns a subtree, so both forms could hand out a body and that
body’s own descendant at once, and writing through the second handle broke the uniqueness that
&mut promises. Walk the tree instead: a child borrows its parent, so the two are never live
together.
// Before
for body in spec.body_iter_mut() {
body.set_gravcomp(1.0);
}
// After: the world body is the root of the walk. The prelude does not carry `MjsBody`.
// use mujoco_rs::wrappers::mj_editing::MjsBody;
fn walk(body: &mut MjsBody) {
body.set_gravcomp(1.0);
for child in body.body_iter_mut() {
walk(child);
}
}
walk(spec.world_body_mut());
// Before
for body in world.body_iter_mut(true) { /* ... */ }
// After: `body_iter_mut` yields the direct children, so recurse by hand.
for body in world.body_iter_mut() { /* ... */ }
Read-only iteration is unchanged. MjSpec::body_iter still walks every body flat, and
MjsBody::body_iter still takes recurse, so a loop that only reads needs no edit beyond
dropping _mut.
Items deprecated in 3.0.0 are removed¶
Each one has carried a deprecation warning since 3.0.0. Switch to the replacement.
Before |
After |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MjRenderer::sync unwrapped both failures; sync_data and
MjRenderer::render return a Result each.
Redundant dimension parameters removed¶
Four utility wrappers took a dimension that the slices they already take fix. Drop the argument; the wrapper derives it and still panics when the slice lengths disagree.
Before |
After |
|---|---|
|
|
|
|
|
|
|
|
mju_sqr_mat_td keeps nr, because a zero
nc leaves nr free.
SignatureMismatch is renamed to IncompatibleModel¶
The variant is renamed in MjDataError, MjModelError, MjViewerError and RendererError. It now also fires for two models that share a signature but need different buffer sizes.
Before:
match data.copy_to(&mut destination) {
Err(MjDataError::SignatureMismatch { source, destination }) => { /* ... */ }
other => other.unwrap(),
}
After:
match data.copy_to(&mut destination) {
Err(MjDataError::IncompatibleModel { source, destination }) => { /* ... */ }
other => other.unwrap(),
}
A test of your own that compared signatures directly should call MjModel::is_compatible_with_model, or MjvScene::is_compatible_with_model for a scene.
Before:
if model.signature() == other.signature() {
data.swap_model(other);
}
After:
if model.is_compatible_with_model(&other) {
data.swap_model(other);
}
MjvCamera::move_ no longer takes a scene¶
The scene parameter was dropped from MjvCamera move_.
Before:
camera.move_(MjtMouse::mjMOUSE_ZOOM, &model, 0.0, -1.0, renderer.scene());
After:
camera.move_(MjtMouse::mjMOUSE_ZOOM, &model, 0.0, -1.0);
MIMO-aware actuator dimensions¶
Actuators now have separate control and output dimensions.
Before:
for id in 0..model.nu() as usize {
let trntype = model.actuator_trntype()[id];
let gear = model.actuator_gear()[id];
let range = model.actuator_ctrlrange()[id];
}
After:
for id in 0..model.nactuator() as usize {
let out = model.actuator_outadr()[id] as usize;
let ctrl = model.actuator_ctrladr()[id] as usize;
let trntype = model.actuator_trntype()[id]; // actuator-indexed
let gear = model.actuator_gear()[out]; // output-indexed
let range = model.actuator_ctrlrange()[ctrl]; // control-indexed
}
The MjData control-history methods follow the same rule: read_ctrl, try_read_ctrl and
init_ctrl_history now take an actuator index bounded by nactuator, not a control index
bounded by nu.
Stricter MjrContext accessor types¶
The MjrContext flags gl_initialized, window_available, window_stereo and
window_doublebuffer now read as bool, and font_scale and read_depth_map now read
as MjtFontScale and MjtDepthMap, instead of i32.
Before:
if context.gl_initialized() != 0 && context.window_doublebuffer() != 0 {
let scale: i32 = context.font_scale();
let depth: i32 = context.read_depth_map();
}
After:
if context.gl_initialized() && context.window_doublebuffer() {
let scale: MjtFontScale = context.font_scale();
let depth: MjtDepthMap = context.read_depth_map();
}
MjrContext::set_buffer takes MjtFramebuffer¶
set_buffer now takes MjtFramebuffer.
Before:
context.set_buffer(MjtFramebuffer::mjFB_OFFSCREEN as i32);
After:
context.set_buffer(MjtFramebuffer::mjFB_OFFSCREEN);
Unsafe mutation of MjModel actuator_gaintype¶
actuator_gaintype_mut is now an unsafe fn; the actuator views are affected the same way,
so a write through a view needs as_mut_slice inside unsafe. A wrong gain type makes the
engine write more force outputs than the actuator owns.
Before:
model.actuator_gaintype_mut()[id] = MjtGain::mjGAIN_AFFINE;
view.gaintype[0] = MjtGain::mjGAIN_AFFINE;
After:
// SAFETY: mjGAIN_AFFINE writes exactly one force output, which this actuator owns.
unsafe { model.actuator_gaintype_mut()[id] = MjtGain::mjGAIN_AFFINE };
unsafe { view.gaintype.as_mut_slice()[0] = MjtGain::mjGAIN_AFFINE };
Unsafe mutation of three more MjModel tables¶
tex_type_mut, sensor_intprm_mut and flex_elemlayer_mut are now unsafe fn, for the
same reason as actuator_gaintype_mut above: C sizes a buffer or indexes an array from the
value of each one.
Before:
model.tex_type_mut()[id] = MjtTexture::mjTEXTURE_CUBE;
model.sensor_intprm_mut()[id][0] = 1;
model.flex_elemlayer_mut()[id] = 0;
After:
// SAFETY: the texture data behind `id` holds the six faces that a cube map reads.
unsafe { model.tex_type_mut()[id] = MjtTexture::mjTEXTURE_CUBE };
// SAFETY: 1 selects an object that this sensor's stage can read.
unsafe { model.sensor_intprm_mut()[id][0] = 1 };
// SAFETY: 0 keeps the element on the surface, so the face count does not grow.
unsafe { model.flex_elemlayer_mut()[id] = 0 };
Drawing an MjvFigure is unsafe¶
draw is now an unsafe fn. Every linepnt entry must stay within 0..=mjMAXLINEPNT,
and the bound is a caller precondition. push refuses to grow a plot that is already at
capacity, so a figure filled only through the line-editing methods already satisfies it; a direct
write to the public linepnt field is what needs the care.
Before:
figure.draw(viewport, &context);
After:
// SAFETY: every linepnt entry stays within 0..=mjMAXLINEPNT, as only push wrote them.
unsafe { figure.draw(viewport, &context) };
Reading an MjData arena array is unsafe¶
MuJoCo allocates the constraint arrays in the mjData arena and never zeroes it, so an array is
uninitialized until the pipeline stage that computes it has run. Sixteen accessors and their
_mut counterparts are now unsafe fn, grouped by the stage each one needs:
Run first |
Accessors |
|---|---|
|
|
|
|
|
|
Two groups need extra care. MuJoCo fills the four Jacobian index arrays only when opt.jacobian
resolves to sparse, so a dense model leaves them uninitialized even after a full forward. The
four island dof arrays stay uninitialized after forward as well when opt.solver is PGS,
because only the CG and Newton island paths gather into them.
Before:
data.step1();
let state = data.efc_state(); // allocated, but the solver has not run
After:
data.forward();
// SAFETY: forward ran the constraint solver, so efc_state holds computed values.
let state = unsafe { data.efc_state() };
if state[0] == MjtConstraintState::mjCNSTRSTATE_QUADRATIC {
// ...
}
Unsafe geom creation on MjvScene¶
create_geom and try_create_geom are now unsafe fn. The caller keeps matid and
texid below the counts of the model that the rendering context was created for, and objid
below the scene’s flex or skin count. All three ids start at -1, which means no material, no
texture and no object.
Before:
let geom = scene.create_geom(MjtGeom::mjGEOM_BOX, None, None, None, None);
After:
// SAFETY: the geom keeps the ids that create_geom sets to none.
let geom = unsafe { scene.create_geom(MjtGeom::mjGEOM_BOX, None, None, None, None) };
Renamed DcMotorConfig input field¶
The public field DcMotorConfig::input_mode is now ctrlspec and the builder
with_input_mode is now with_ctrlspec. The value is a bitmask of MjtCtrlInput values
instead of a single mode selector.
Before:
let config = DcMotorConfig::default().with_input_mode(0);
After:
let config = DcMotorConfig::default()
.with_ctrlspec(MjtCtrlInput::mjINPUT_VOLTAGE as i32);
Relaxed MjData body_awake view mutability¶
The body views now match the body_awake array accessor, so a write through a view no longer
needs as_mut_slice inside unsafe.
Before:
unsafe { view.awake.as_mut_slice()[0] = MjtSleepState::mjS_AWAKE };
After:
view.awake[0] = MjtSleepState::mjS_AWAKE;
SpecItem::default returns an Option¶
MuJoCo’s mjs_getDefault returns null for an element added without a default class. The method
now returns Option<&MjsDefault>.
Before:
let class = body.default();
After:
let class = body.default().unwrap();
Deprecated fallible allocation methods¶
MjModel’s try_clone, MjData’s try_clone, MjSpec’s try_new, try_add_frame
on MjsBody and MjsFrame, and every macro-generated try_add_* element constructor on
MjSpec, MjsBody and MjsFrame are now deprecated. Their AllocationFailed arm is
unreachable. try_add_default and try_add_flexcomp keep their Result. Switch to
clone, new and the panicking add_* methods.
Before (5.x):
let mut spec = MjSpec::try_new()?;
let body = spec.world_body_mut().try_add_body()?;
let data_copy = data.try_clone()?;
After (6.0.0):
let mut spec = MjSpec::new();
let body = spec.world_body_mut().add_body();
let data_copy = data.clone();
egui 0.36 and the new UI callback signature¶
The 3D viewer builds against egui 0.36, so the closure passed to
MjViewer::add_ui_callback receives a
&mut egui::Ui instead of a
&egui::Context. The second parameter
changed from the locked passive MjData to the &Arc<Mutex<ViewerSharedState>>
(ViewerSharedState), so the closure decides when to take
the lock.
MjViewer::add_ui_callback_detached was removed.
Before (5.x):
viewer.add_ui_callback(|ctx, data| {
egui::SidePanel::right("custom_panel").show(ctx, |ui| {
ui.heading("Custom Panel");
if ui.button("Zero the controls").clicked() {
data.ctrl_mut().fill(0.0);
}
});
});
viewer.add_ui_callback_detached(|ctx| {
egui::TopBottomPanel::top("custom_top_panel").show(ctx, |ui| {
ui.label("Custom Top Bar");
});
});
After (6.0.0):
viewer.add_ui_callback(|ui, state| {
egui::Panel::right("custom_panel").show(ui, |ui| {
ui.heading("Custom Panel");
ui.label(format!("running: {}", state.lock().unwrap().running()));
});
});
viewer.add_ui_callback(|ui, _| {
egui::Panel::top("custom_top_panel").show(ui, |ui| {
ui.label("Custom Top Bar");
});
});
Port the widgets themselves against egui’s changelog: https://github.com/emilk/egui/blob/0.36.1/CHANGELOG.md
Model handles are bound by ModelType¶
The model-handle generic M is bound by
ModelType instead of
Deref<Target = MjModel>, and the model-mutating methods by
ModelTypeMut instead of
DerefMut<Target = MjModel>. Both markers hold the guarantee that the handle always derefs to
the same model, which the wrappers rely on but Deref alone does not give.
&MjModel, &mut MjModel, Box<MjModel>, Rc<MjModel>, Arc<MjModel>,
Ref<MjModel>, RefMut<MjModel>, MutexGuard<MjModel>, RwLockReadGuard<MjModel> and
RwLockWriteGuard<MjModel> implement ModelType. The five that deref mutably
(&mut MjModel, Box<MjModel>, RefMut<MjModel>, MutexGuard<MjModel> and
RwLockWriteGuard<MjModel>) also implement ModelTypeMut. Rename the bound in your own
generic code, and claim the marker for a hand-written handle.
Before:
use std::ops::Deref;
use mujoco_rs::prelude::*;
fn step_all<M: Deref<Target = MjModel>>(datas: &mut [MjData<M>]) {
for data in datas {
data.step();
}
}
struct MyHandle(Box<MjModel>);
impl Deref for MyHandle {
type Target = MjModel;
fn deref(&self) -> &MjModel { &self.0 }
}
After:
use mujoco_rs::wrappers::mj_model::traits::ModelType;
use std::ops::Deref;
use mujoco_rs::prelude::*;
fn step_all<M: ModelType>(datas: &mut [MjData<M>]) {
for data in datas {
data.step();
}
}
struct MyHandle(Box<MjModel>);
impl Deref for MyHandle {
type Target = MjModel;
fn deref(&self) -> &MjModel { &self.0 }
}
// SAFETY: deref returns the model that the handle owns, for every call.
unsafe impl ModelType for MyHandle {}
Two model view fields became read-only¶
MuJoCo reads sensor_intprm as an output-length bitfield and tex_type as the texture upload
kind, and it bounds neither, so both now sit in the read-only list of their view.
use mujoco_rs::prelude::*;
use mujoco_rs::wrappers::mj_model::MjtTexture;
fn set_kind(model: &mut MjModel) {
let info = model.texture("tex").unwrap();
// Before
// info.view_mut(model).r#type[0] = MjtTexture::mjTEXTURE_CUBE;
// After
// SAFETY: the caller keeps the kind consistent with the texture's own width and height.
unsafe { info.view_mut(model).r#type.as_mut_slice()[0] = MjtTexture::mjTEXTURE_CUBE };
}
Element handles are opaque types¶
An Mjs* element handle is no longer an alias for its mujoco_c struct. It is an opaque,
zero-sized type that stands at the address of that struct, so code that stays inside the wrapper
API needs no edit, with one exception: MjSpec::with_compiler is gone, because a by-value
builder cannot take an opaque handle.
use mujoco_rs::prelude::*;
fn build() -> MjSpec {
let mut spec = MjSpec::new();
// Before
// let spec = spec.with_compiler(compiler);
// After
spec.compiler_mut().set_degree(false);
spec
}
Two things change. Naming the FFI struct where the handle belongs no longer compiles, and passing
a handle straight to a mujoco_c function no longer coerces. Call ffi for the FFI struct,
and unsafe fn ffi_mut for writing.
use mujoco_rs::prelude::*;
use mujoco_rs::mujoco_c::mjs_sensorDim;
fn dimension(spec: &MjSpec) -> i32 {
let sensor = spec.sensor("s").unwrap();
// Before
// unsafe { mjs_sensorDim(sensor) }
// After
// SAFETY: the borrow of the sensor keeps it alive for the call.
unsafe { mjs_sensorDim(sensor.ffi()) }
}
std::mem::swap on two handles used to exchange the contents of two elements. It now
exchanges nothing.
MjSpec is no longer Send¶
A model that attaches another model, through <asset><model file=.../></asset>, keeps the child
specification shared between a spec and every clone of it, behind a reference count that MuJoCo
does not synchronise. Keep the specification on one thread and move the compiled MjModel, which
stays Send and Sync.
use mujoco_rs::prelude::*;
fn build(path: &str) -> MjModel {
let mut spec = MjSpec::from_xml(path).unwrap();
// Before
// std::thread::spawn(move || spec.compile().unwrap()).join().unwrap()
// After
let model = spec.compile().unwrap();
std::thread::spawn(move || model).join().unwrap()
}
MjsWrap::side_site_mut is removed¶
Read the name through the shared side_site and take the mutable borrow from the specification.
use mujoco_rs::prelude::*;
fn widen(spec: &mut MjSpec) {
// Before
// let site = spec.tendon_mut("t").unwrap().wrap_mut(0).side_site_mut().unwrap();
// site.with_size([0.1, 0.1, 0.1]);
// After
let name = spec.tendon("t").unwrap().wrap(0).side_site().unwrap().name().to_owned();
spec.site_mut(&name).unwrap().with_size([0.1, 0.1, 0.1]);
}
MjsPlugin no longer stands for the plugin that an element embeds¶
A body, geom, mesh, actuator and sensor embed a reference that names a plugin instance and carries
no element of its own. The plugin and plugin_mut accessors return MjsPluginReference,
which implements no SpecItem method. Reach the instance itself through MjSpec::plugin.
use mujoco_rs::wrappers::mj_editing::MjsPluginReference;
use mujoco_rs::prelude::*;
fn configure(spec: &mut MjSpec) {
let geom = spec.world_body_mut().add_geom();
// Before
// let plugin: &mut MjsPlugin = geom.plugin_mut();
// After
let plugin: &mut MjsPluginReference = geom.plugin_mut();
plugin.set_name("instance");
plugin.set_active(true);
}
delete moved from SpecItem to SpecObject¶
SpecObject::delete keeps the shape of
the removed SpecItem::delete, so a call through the element handle needs no edit. Import
SpecObject, which mujoco_rs::prelude also
exports.
MjSpec::delete_element is deprecated. Delete through the handle instead.
use mujoco_rs::prelude::*;
fn prune(spec: &mut MjSpec) {
unsafe { spec.geom_mut("g0").unwrap().delete() }.unwrap();
}
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 |
|