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,

// 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

mujoco_rs::get_mujoco_version()

mujoco_rs::mujoco_version()

vfs.add_from_file(Some(dir), file)

vfs.add_file_from(dir, file)

vfs.add_from_file(None, file)

vfs.add_file(file)

renderer.sync(&mut data)

renderer.sync_data(&mut data)?; renderer.render()?

MjvFigure::new()

MjvFigure::new_boxed()

data.contacts()

data.contact()

data.get_state(spec)

data.state(spec)

skin.set_vertid(&ids)

skin.append_vertid(&ids)

skin.set_vertweight(&weights)

skin.append_vertweight(&weights)

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_eye(&mut mat, n)

mju_eye(&mut mat)

mju_symmetrize(&mut res, &mat, n)

mju_symmetrize(&mut res, &mat)

mju_sqr_mat_td(&mut res, &mat, diag, nr, nc)

mju_sqr_mat_td(&mut res, &mat, diag, nr)

mju_sym2dense(&mut res, &mat, n, &rownnz, &rowadr, &colind)

mju_sym2dense(&mut res, &mat, &rownnz, &rowadr, &colind)

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

forward or step2

efc_state, efc_force, efc_b, iefc_state, iefc_force, iefc_aref, iacc, iacc_smooth, ifrc_smooth, ifrc_constraint

step1, fwd_velocity or later

efc_vel, efc_aref

fwd_position or later, with a sparse Jacobian

efc_j_rownnz, efc_j_rowadr, efc_j_rowsuper, efc_j_colind

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):

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: from_xml, from_xml_vfs, from_xml_string, from_buffer, save_last_xml, print, print_formatted

io::Error / NulError

MjModelError

MjVfs: add_from_buffer, delete_file

io::Error

MjVfsError

MjSpec: from_xml, from_xml_vfs, from_xml_string, compile, save_xml, save_xml_string, add_default (now try_add_default)

io::Error

MjEditError

MjData: add_contact

io::Error

MjDataError

MjRenderer: save_rgb, save_depth, save_depth_raw

io::Error

RendererError

MjViewer: render

()

Result<(), MjViewerError>

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_scale returns Result<(), RendererError>; with_font_scale returns Result<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:

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 unsafe

MjJointModelViewMut

r#type

MjActuatorModelViewMut

trntype, dyntype

MjEqualityModelViewMut

r#type, objtype

MjSensorModelViewMut

r#type, objtype, reftype

MjSkinModelViewMut

matid

MjTendonModelViewMut

matid, treeid

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() returns usize (was i32).

  • MjModel: state_size() returns usize (was i32).

  • MjModel: name_to_id() returns Option<usize> (was i32; -1 is now None).

  • MjCameraModelView / MjCameraModelViewMut: projection (MjtProjection) replaces the old boolean orthographic field.

  • MjModel: tuple_objtype() returns &[MjtObj] (was &[i32]).

  • MjModel: id_to_name: id takes usize (was i32).

  • 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 take usize (was i32). Add as usize at call sites.

  • MjData: ray and multi_ray: bodyexclude changed from i32 (-1 = no exclusion) to Option<usize> (None = no exclusion). Replace -1 with None and body_id with Some(body_id as usize).

  • MjData: ray() returns (Option<usize>, MjtNum) (was (i32, MjtNum)). None means no intersection (previously -1).

  • MjData: try_multi_ray() returns Result<(Vec<Option<usize>>, Vec<MjtNum>), ...> (was (Vec<i32>, Vec<MjtNum>)). multi_ray() panics on error. Each None element means no intersection for that ray (previously -1).

  • MjsTendon: limited and actfrclimited are now MjtLimited tri-state (was bool).

  • MjvCamera: new_fixed, new_tracking, track, fix now take usize (was u32). Remove as u32 casts at call sites.

  • TryFrom<i32> for MjtCamera now uses MjSceneError as its error type (was ()). Replace Err(()) matches with Err(MjSceneError::InvalidCameraType(_)).

  • SceneSelection: body_id, geom_id, flex_id, skin_id are now Option<usize> (was i32). Replace sel.body_id >= 0 with sel.body_id.is_some() or if let Some(id) = sel.body_id.

  • MjData: runge_kutta() now takes n: u32 (was i32). Replace data.runge_kutta(n) with data.runge_kutta(n as u32) at call sites. The function also now panics if n < 1 (previously passed negative or zero values silently to MuJoCo C).

  • MjsTexture::set_data now requires T: bytemuck::NoUninit (add bytemuck to your dependencies if you call this method with a custom type, and derive or implement NoUninit for it).

  • MjTendonDataInfo: J_rownnz, J_rowadr, and J_colind are no longer exposed. These fields moved to mjModel in MuJoCo 3.6.0 and are now accessible via MjTendonModelInfo (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

data.reset_keyframe(0)

data.reset_keyframe(0)?

data.set_state(&s, spec)

unsafe { data.set_state(&s, spec)? }

data.copy_visual_to(&mut dst)

data.copy_visual_to(&mut dst)?

data.copy_to(&mut dst)

data.copy_to(&mut dst)?

ctx.read_pixels(rgb, depth, &vp)

ctx.read_pixels(rgb, depth, &vp)?

fig.push(idx, x, y)

fig.push(idx, x, y)?

fig.set_at(idx, pt, x, y)

fig.set_at(idx, pt, x, y)?

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

MjModel

get_totalmass()

totalmass()

MjrContext

mjr_set_buffer()

set_buffer()

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, and MjvScene::find_selection retain <M: Deref<Target = MjModel>> as method-level generics; call sites are unchanged.

  • MjvPerturb::start / move_ and MjvCamera::move_ now take &MjvScene without a type parameter.

  • vis_common::sync_geoms is now non-generic.

  • MjvScene derives Send + Sync unconditionally (no bound on M required).

MjViewer, MjViewerBuilder, ViewerSharedState are no longer generic

MjViewer, MjViewerBuilder, and ViewerSharedState are no longer generic over M. Remove the <M> type parameter from all usage sites.

Before (2.x):

let viewer: MjViewer<Arc<MjModel>> = MjViewer::builder()
    .build_passive(model.clone())
    .expect("failed to start viewer");

viewer.add_ui_callback(|ctx, data: &mut MjData<Arc<MjModel>>| { ... });

After (3.0.0):

let mut viewer: MjViewer = MjViewer::builder()
    .build_passive(model.clone())
    .expect("failed to start viewer");

viewer.add_ui_callback(|ctx, data: &mut MjData<Arc<MjModel>>| { ... });

Additional details:

  • sync_data, sync_data_full, and build_passive retain <M: Deref<Target = MjModel>> as method-level generics; call sites are unchanged.

  • The closure passed to MjViewer::add_ui_callback now receives &mut MjData<Arc<MjModel>> instead of &mut MjData<M>. If your closure used a different M, update the type annotation to &mut MjData<Arc<MjModel>>.

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

MjData::warning_stats

MjData::warning

MjData::timer_stats

MjData::timer

Type aliases MjJointInfo, MjJointView, MjJointViewMut, MjGeomInfo, MjGeomView, MjGeomViewMut, MjActuatorInfo, MjActuatorView, MjActuatorViewMut

Use the MjJointData*, MjGeomData*, MjActuatorData* types.

MjJointDataViewMut::reset

MjJointDataViewMut::zero

MjModel::name2id

MjModel::name_to_id

MjvCamera::new

MjvCamera::new_free, new_fixed, new_tracking, or new_user

MjViewer::user_scene, user_scene_mut, user_scn, user_scn_mut

ViewerSharedState::user_scene / user_scene_mut via MjViewer::state

MjViewer::sync

MjViewer::sync_data then MjViewer::render

MjvFigure figure()

draw()

get_mujoco_version

~~mujoco_rs::mujoco_version

MjData::get_state

MjData::state

MjvFigure::new

MjvFigure::new_boxed