Skip to content

Beta: camera streaming, shared memory, and the beta protocol - #5

Open
jonathanembleyriches wants to merge 20 commits into
mainfrom
feat/urlab-beta
Open

Beta: camera streaming, shared memory, and the beta protocol#5
jonathanembleyriches wants to merge 20 commits into
mainfrom
feat/urlab-beta

Conversation

@jonathanembleyriches

Copy link
Copy Markdown
Contributor

Summary

  • Camera streaming redesigned: async in every step mode, an inline sync capture reachable from the typed step API, and server-side latency emulation via runtime.set_camera_delay.
  • Shared memory alongside ZMQ for state, frames and RPC.
  • External puppet sources, model-drift detection, a render-farm pool, and MuJoCo aligned with the URLab beta runtime.

Motivation

Camera frames used to ride in the step reply, which bloated it and stalled high-rate puppet stepping. Serving them from async streams decouples camera rate from step rate. The rest is the client catching up to the plugin's beta protocol.

Notable fixes

  • The dashboard claims control when it connects. A step request carries ctrl and the server refuses ctrl from a client holding no claim, so the Step button answered not_control_owner and nothing moved.
  • The dashboard stays responsive while connecting.connect() ran on the UI thread, and with a camera in the scene its last act starts a broadcast and opens a SUB socket per camera — seconds of work with the window frozen.
  • Realistic per-op timeouts for set_camera_streaming, list_actors, find_actors, get_actor_bounds, actor_hierarchy, snapshot. These walk the whole world on the game thread and outgrew the 5 s default.
  • <general> actuators are accepted on the wire. It is MJCF's own actuator element and the one every other kind is shorthand for; mjlab models author it directly, and 29 of them came back typeless.
  • Model elements match by the prefix the model actually uses, not the spawn actor id — the empty-articulation / inert-robot case.
  • The golden test derives its reference from stock MuJoCo instead of comparing against a checked-in recording, which could only ever be as correct as the day it was captured.

Test evidence

offline: 261 passed, 66 skipped
live: 65 passed, 1 skipped (against a running editor, ZMQ + SHM, all three step modes)

The one skip is honest: the golden arm uses <position> actuators, so it has no UE-side controller for the PD-gain test to exercise.

Compatibility

Requires the plugin beta (URLab-Sim/UnrealRoboticsLab), which this branch's protocol tracks.

Supersedes

Closes the earlier external-puppet and MuJoCo-alignment PRs; their commits are carried here with authorship intact.

jonathanembleyrichesand others added 20 commits August 11, 2026 14:07
…XML model
An unloadable MJB (cross-version) now falls back to building the local
model from the handshake's mjcf_compiled + vfs_assets instead of leaving
art.joints/art.actuators silently empty. Removes the local_model=False
bypasses from the dashboard and integration conftest.
…ulation
Thin wrapper over the new UE set_camera_delay op: per-camera delay_s / jitter_s
/ clock / seed plus capture-rate knobs (on_state_change, max_fps). Frames arrive
pre-delayed over the existing stream, so no client-side buffering.
The SHM RPC region (req/rep.shm + kernel events) lives on a static
session, but the client re-derived it from the per-PIE camera/state
stream dir. Requests landed in a directory UE's RPC worker never
serviced, so every SHM step stalled its full timeout (~5s) then fell
back to ZMQ. Read the advertised shm_rpc block (paths + event names) and
wire the RPC region separately from the stream dir. SHM step RTT drops
from ~5000ms to ~1.5ms.
- client: transport=auto, SHM handshake contract, camera delay tracking, camera_frame_cond
- transports: ZMQ state-loop reconnect, SHM wrong_transport/retry-safe ops, resolve_endpoint
- articulation: zmq_endpoint/zmq_topic properties
- farm: URLabPool discovery/leasing, launcher, model upload
- policy: state_stream, auto_grasp, lerobot_runner stubs
- cleanup: remove urlab_tools (ROS now in-plugin), drop ros2 extra, update deps
…r to an OS-assigned port
The step_ok reply renamed per_articulation -> arts and entities -> scene;
six tests still hand-built replies with the old keys. Requests still use
per_articulation, so those assertions are unchanged.
MockStepServer used a fixed 55900+ counter band, which sits inside the
Windows dynamic port range and intermittently collided with an outbound
socket in the suite.
attach_puppet_simulation binds an external MjModel before the URLab
scene exists — the level is loaded, the MJCF imported and the actor
spawned afterwards — so the two models can diverge before the first
push, and a puppet step is just a raw qpos/qvel/ctrl vector with no way
to notice.
Every handshake carrying an MJB is now compared against the attached
model. Dimension mismatches always raise, since a push transmits exactly
nq/nv/nu floats and no reading of differing counts is benign. Name and
joint-ordering differences follow puppet_drift_check ('error' | 'warn' |
'off'), defaulting to warn because a scene may legitimately carry bodies
the source model never had. sim.start absorbs a fresh handshake once PIE
is up, so the check covers the post-import state, not just connect.
… API
Puppet mode pauses the server's camera publishers (the step server owns
cadence there), so `camera_query="latest"/"fresh"` wait on streams that
never tick and no typed call could produce a frame. The server's inline
path renders from the state the step just applied and waits on that
capture's readback serial, which is the stronger correspondence
guarantee -- but `include_cameras` was never forwarded onto the wire by
either step path, so it was unreachable except by hand-building the RPC.
Add `camera_query="sync"` to request it. The reply decode already
existed; only the request side was missing.
Two fixes in that decode, both of which the sync path now exercises:
- It searched per-articulation cameras only, so a scene-level camera --
a worldbody <camera> in the imported MJCF, which is what an external
simulation exports -- had its pixels silently discarded. Use the same
lookup as `get_camera`, which also covers `global_cameras`.
- It sized the buffer from the view's registered resolution and dropped
the frame on a mismatch. Prefer the payload's own width/height, which
are what the bytes were actually sized by.
The plugin builds MuJoCo from source at 3.11.1 and 3.11.0 is the newest
PyPI release, so the client pins one patch behind on purpose: the
handshake compares major.minor, and a patch-level skew falls back to the
compiled-XML model instead of the version-locked MJB.
mujoco-warp moves with it, or mjlab's import-time enum lookup fails and
the runner is silently absent. Asserted in tests so the two pins cannot
drift apart unnoticed.
The six-way sweep gated SHM on Linux and said the Windows kernel-event
path was covered by separate platform runs. There are none, so half the
matrix never ran on the platform most of the development happens on.
The client implements POSIX futex and Windows events alike; gate on that.
The server refuses actuator writes from a client holding no claim, which
`claim_control` was added for and the policy runners already do. The
integration fixtures never caught up, so twelve live tests failed on
`not_control_owner` rather than on anything they were testing.
An articulation's `prefix` is its ActorId, which the server reports so
topics and control-ownership keys stay stable across spawns. The compiled
model names its elements from the UE object name instead, and the server
sends that as `actor_name`. Matching model names against `prefix` found
nothing, so an articulation came back with no actuators, joints or
sensors -- an inert robot with no error anywhere.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jonathanembleyriches@StuartShanks