diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c654214e7..625f0e1f6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -215,6 +215,12 @@ jobs:
with:
path: src/unilab/assets/robots
key: ${{ runner.os }}-robot-assets-v1-${{ hashFiles('src/unilab/assets/hub.py') }}
+ - name: Keep git-tracked assets at the checked-out commit
+ # The robots cache persists HF-downloaded meshes across runs, but it
+ # also snapshots git-tracked scene XMLs and would silently roll them
+ # back to the cached revision. Restore tracked files from the index;
+ # untracked downloads stay untouched.
+ run: git restore --worktree -- src/unilab/assets/robots
- name: Pull robot assets
run: uv run --no-sync unilab-pull-assets --robot all
- name: Test with coverage
diff --git a/docs/sphinx/source/en/5-reference/5-support_matrix.md b/docs/sphinx/source/en/5-reference/5-support_matrix.md
index a9307537e..8fe789240 100644
--- a/docs/sphinx/source/en/5-reference/5-support_matrix.md
+++ b/docs/sphinx/source/en/5-reference/5-support_matrix.md
@@ -24,7 +24,9 @@ in English. Do not infer support beyond the evidence grade shown below.
- `mujoco`: `--render-mode auto` exports `play_video.mp4`; `--render-mode
viser` serves the rollout in a browser-based viser viewer.
- `motrix`: `--render-mode auto` opens an interactive renderer window; it does
- not record a video and is not bound by `play_steps`.
+ not record a video and is not bound by `play_steps`. `--render-mode viser`
+ routes to the browser-based viser viewer with per-env MuJoCo playback
+ models driven by physics-state snapshots.
- `mjwarp`: supports explicit, finite-step `record` by default, rendered offline
through the task owner's MuJoCo visual model; `--render-mode interactive`
routes to the MuJoCo interactive viewer (mjwarp runs the physics while
diff --git a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md
index ffd1018ad..a00cc9ac8 100644
--- a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md
+++ b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md
@@ -20,7 +20,7 @@
- `mujoco`: `--render-mode auto` 会导出 `play_video.mp4`;`--render-mode viser`
通过基于浏览器的 viser viewer 展示回放
-- `motrix`: `--render-mode auto` 会打开交互式 renderer 窗口,不录制视频,不受 `play_steps` 限制
+- `motrix`: `--render-mode auto` 会打开交互式 renderer 窗口,不录制视频,不受 `play_steps` 限制;`--render-mode viser` 路由到浏览器 viser viewer(物理快照驱动按 env 的 MuJoCo playback model)
- `mjwarp`: 默认仅支持显式、有限步数的 `record`,通过 task owner 的 MuJoCo visual model 离线录制;`--render-mode interactive` 路由到 MuJoCo 交互 viewer(mjwarp 跑物理、MuJoCo 渲染 env[0],强制单 env);`--render-mode viser` 路由到浏览器 viser viewer(按 env 使用 MuJoCo playback model);不支持 `auto` 或 native renderer
- `isaacsim`: `auto` 在有 display 时选择 Kit viewer,否则选择 headless RGB camera;当前真实主机仍有 RTX renderer 初始化 blocker,支持等级保持 `Configured`
- `--render-mode record`: MuJoCo、mjwarp、Motrix 和 IsaacSim 都只录制视频
diff --git a/pyproject.rocm.toml b/pyproject.rocm.toml
index 0128cabcb..9eaffbf0f 100644
--- a/pyproject.rocm.toml
+++ b/pyproject.rocm.toml
@@ -23,7 +23,7 @@ classifiers = [
requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
- "unisim-core>=1.7.3",
+ "unisim-core>=1.7.8",
"torch==2.11.0",
"triton-rocm==3.6.0 ; sys_platform == 'linux' and platform_machine == 'x86_64'",
"gymnasium",
@@ -69,7 +69,7 @@ mujoco = [
"mujoco~=3.11.0",
"mjbatch-uni~=0.2.1",
]
-motrix = ["unisim-core[motrix]>=1.7.3"]
+motrix = ["unisim-core[motrix]>=1.7.8"]
uni_rl = ["unilab-rl==1.3.2"]
[dependency-groups]
diff --git a/pyproject.toml b/pyproject.toml
index f05644878..45e59e5ca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,7 @@ classifiers = [
requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
- "unisim-core>=1.7.5",
+ "unisim-core>=1.7.8",
"numba>=0.67",
"prettytable>=3.10",
# A range (not an exact pin) lets ROCm users substitute a ROCm torch
@@ -108,13 +108,13 @@ newton = [
"pyglet>=2.1.6,<3",
"imgui-bundle>=1.92.0",
]
-motrix = ["unisim-core[motrix]>=1.7.5"]
+motrix = ["unisim-core[motrix]>=1.7.8"]
genesis = ["genesis-world==1.3.3"]
uni_rl = ["unilab-rl==1.3.2"]
# Wheels are CPython 3.12/3.13 Linux x86_64 only; the marker keeps uv lock
# resolvable for the other required-environments.
superdex = [
- "unisim-core[superdex]>=1.7.5 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
+ "unisim-core[superdex]>=1.7.8 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
]
[dependency-groups]
diff --git a/src/unilab/assets/robots/stewart/scene.xml b/src/unilab/assets/robots/stewart/scene.xml
index a3a8b7f41..9e5b1e699 100644
--- a/src/unilab/assets/robots/stewart/scene.xml
+++ b/src/unilab/assets/robots/stewart/scene.xml
@@ -62,8 +62,8 @@
-
-
+
+
@@ -82,8 +82,8 @@
-
-
+
+
@@ -102,8 +102,8 @@
-
-
+
+
@@ -122,8 +122,8 @@
-
-
+
+
@@ -142,8 +142,8 @@
-
-
+
+
@@ -162,8 +162,8 @@
-
-
+
+
diff --git a/src/unilab/cli.py b/src/unilab/cli.py
index a7d156e53..abf9b1ec3 100644
--- a/src/unilab/cli.py
+++ b/src/unilab/cli.py
@@ -40,7 +40,7 @@
# playback contract the viser viewer renders from (they expose native
# renderers instead). Supporting viser there requires upstream unisim
# capability work, so these sims fail closed with an actionable message.
-VISER_UNSUPPORTED_SIMS = frozenset({"motrix", "genesis", "isaacgym", "isaacsim"})
+VISER_UNSUPPORTED_SIMS = frozenset({"genesis", "isaacgym", "isaacsim"})
RESERVED_OVERRIDE_KEYS = {
"algo",
"task",
@@ -229,7 +229,11 @@ def _needs_motrix_renderer(mode: str, sim: str, overrides: Sequence[str]) -> boo
if sim != "motrix":
return False
play_render_mode = _override_value(overrides, "training.play_render_mode")
- if play_render_mode is not None and play_render_mode.strip().lower() in {"none", "record"}:
+ if play_render_mode is not None and play_render_mode.strip().lower() in {
+ "none",
+ "record",
+ "viser",
+ }:
return False
if mode == "eval":
return True
diff --git a/tests/base/test_backend_pre_step_control.py b/tests/base/test_backend_pre_step_control.py
index c53ea20bb..e7637ca70 100644
--- a/tests/base/test_backend_pre_step_control.py
+++ b/tests/base/test_backend_pre_step_control.py
@@ -299,6 +299,8 @@ def _fake_motrix_backend(pre_step_control_fn=None):
actuator_ctrls=np.zeros((1, 2), dtype=np.float32),
sensor_value=0.0,
)
+ backend._sim_dt = 0.002
+ backend._time_view = np.zeros((1,), dtype=np.float32)
backend._refresh_link_pose_cache = lambda: None
return backend
diff --git a/tests/base/test_motrix_backend_options.py b/tests/base/test_motrix_backend_options.py
index 61111aafa..934e7db06 100644
--- a/tests/base/test_motrix_backend_options.py
+++ b/tests/base/test_motrix_backend_options.py
@@ -247,6 +247,16 @@ def _install_fake_motrix(monkeypatch, tmp_path):
"_materialize_motrix_scene_with_sensor_names",
lambda **kwargs: (fake_model, ()),
)
+ # These tests exercise override caches and terrain plumbing with a fake
+ # native model; unisim-core 1.7.6 validates the whole-MJCF joint order
+ # against the real source file at construction (covered upstream), which
+ # has no meaning for the fake model and only reads "source.xml" from disk.
+ monkeypatch.setattr(
+ mod.MotrixBackend,
+ "_validate_non_portable_joint_order",
+ lambda self, model_file: None,
+ raising=False,
+ )
return mod, fake_model
diff --git a/tests/test_cli.py b/tests/test_cli.py
index b87389149..473b7c5aa 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -785,18 +785,74 @@ def test_macos_mujoco_viser_eval_uses_current_python(
assert command[0] == sys.executable
-def test_eval_viser_render_mode_rejected_for_motrix(
+def test_eval_viser_render_mode_routes_to_play_viser_for_motrix(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
_make_minimal_checkout(tmp_path)
+ (tmp_path / "scripts" / "play_viser.py").write_text("", encoding="utf-8")
_pretend_motrix_is_installed(monkeypatch)
+ # The viser viewer renders MuJoCo playback models, so the CLI also
+ # requires the mujoco package regardless of the physics backend.
+ monkeypatch.setattr(
+ cli,
+ "find_spec",
+ lambda name: ModuleSpec(name, loader=None) if name in {"motrixsim", "mujoco"} else None,
+ )
+
+ command = cli.build_command(
+ mode="eval",
+ algo="ppo",
+ task="go2_joystick_flat",
+ sim="motrix",
+ overrides=[],
+ load_run="-1",
+ render_mode="viser",
+ root=tmp_path,
+ )
+
+ assert command[1] == str(tmp_path / "scripts" / "play_viser.py")
+ assert "--sim" in command and "motrix" in command
+
+
+def test_train_viser_render_mode_routes_to_train_script_for_motrix(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ _make_minimal_checkout(tmp_path)
+ _pretend_motrix_is_installed(monkeypatch)
+ monkeypatch.setattr(
+ cli,
+ "find_spec",
+ lambda name: ModuleSpec(name, loader=None) if name in {"motrixsim", "mujoco"} else None,
+ )
+
+ command = cli.build_command(
+ mode="train",
+ algo="ppo",
+ task="go2_joystick_flat",
+ sim="motrix",
+ overrides=[],
+ render_mode="viser",
+ root=tmp_path,
+ )
+
+ assert command[1] == str(tmp_path / "scripts" / "train_rsl_rl.py")
+ assert "training.play_render_mode=viser" in command
+
+
+def test_eval_viser_render_mode_rejected_for_genesis(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ owner_dir = tmp_path / "conf" / "ppo" / "task" / "go2_joystick_flat"
+ owner_dir.mkdir(parents=True)
+ (owner_dir / "genesis.yaml").write_text("training:\n sim_backend: genesis\n", encoding="utf-8")
+ _pretend_genesis_runtime(monkeypatch, available=True)
with pytest.raises(SystemExit, match="viser"):
cli.build_command(
mode="eval",
algo="ppo",
task="go2_joystick_flat",
- sim="motrix",
+ sim="genesis",
overrides=[],
load_run="-1",
render_mode="viser",
@@ -804,18 +860,20 @@ def test_eval_viser_render_mode_rejected_for_motrix(
)
-def test_train_viser_render_mode_rejected_for_motrix(
+def test_train_viser_render_mode_rejected_for_genesis(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- _make_minimal_checkout(tmp_path)
- _pretend_motrix_is_installed(monkeypatch)
+ owner_dir = tmp_path / "conf" / "ppo" / "task" / "go2_joystick_flat"
+ owner_dir.mkdir(parents=True)
+ (owner_dir / "genesis.yaml").write_text("training:\n sim_backend: genesis\n", encoding="utf-8")
+ _pretend_genesis_runtime(monkeypatch, available=True)
with pytest.raises(SystemExit, match="viser"):
cli.build_command(
mode="train",
algo="ppo",
task="go2_joystick_flat",
- sim="motrix",
+ sim="genesis",
overrides=[],
render_mode="viser",
root=tmp_path,
diff --git a/uv.lock b/uv.lock
index abee3ce21..276a7ed37 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5238,9 +5238,9 @@ requires-dist = [
{ name = "trimesh", specifier = ">=3.21.7" },
{ name = "typing-extensions" },
{ name = "unilab-rl", marker = "extra == 'uni-rl'", specifier = "==1.3.2" },
- { name = "unisim-core", specifier = ">=1.7.5" },
- { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.5" },
- { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.5" },
+ { name = "unisim-core", specifier = ">=1.7.8" },
+ { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.8" },
+ { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.8" },
{ name = "viser", specifier = ">=1.0.26" },
{ name = "wandb" },
{ name = "warp-lang", marker = "extra == 'mjwarp'", specifier = "==1.16.0" },
@@ -5284,13 +5284,13 @@ wheels = [
[[package]]
name = "unisim-core"
-version = "1.7.5"
+version = "1.7.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ea/2c/8255a60c0260a4dc3b41aa60d9910b417a9ae11a1ef3de2b682252680390/unisim_core-1.7.5.tar.gz", hash = "sha256:7613702bf757a6253691bbcd5ab7ae38bf2ce2b8466cf5e6b0947c59466fbc11", size = 526190, upload-time = "2026-09-23T12:31:03.655Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/7f/9b13d12cf57250b704b9e43f0caba5646db0479124aa880393cff1c54273/unisim_core-1.7.8.tar.gz", hash = "sha256:d550e7052c85762778aa7e365fac748099b8b749a12fc6216947fd4ba98a3add", size = 532853, upload-time = "2026-09-24T10:02:44.662Z" }
[package.optional-dependencies]
motrix = [