google-deepmind / google-deepmind/mujoco
mj_recompile preserves actuator controls by actuator index instead of control-block identity
- Dominant language
- C++
- Stars
- 15.2k
- Forks
- 1.8k
- Avg merge
- 10d 16h
- Merged PRs (30d)
- 25
Description
### Intro
Hi!
I am testing state continuity for current variable-input actuators. `mj_recompile` stores actuator control values using the actuator ordinal rather than each actuator's control-block address and width.
### My setup
- MuJoCo source build from `main` at `10124d5d9dca411ec3c8988aa1e3b619103d71bb`
- C API, double precision
- Ubuntu 24.04.5 LTS, Linux 7.0.0-28-generic x86_64
- Normal GCC 11.5.0 build and GCC 13.3.0 AddressSanitizer build
### What's happening? What did you expect?
I expected controls for a surviving actuator to remain associated with that actuator and all of its input slots across a recompile. Instead, `SaveState` and `RestoreState` use `ctrl[i]` where `i` is the actuator index, although current actuators have `ctrladr_` and `ctrlnum_` control blocks.
The attached package contains three manifestations of this one root error:
1. With scalar controls `[11, 22]`, deleting the first actuator makes the retained actuator receive `11` and produce force `11`; its own prior command was `22`.
2. A no-op recompile of a three-input PID changes its control block from `[0, 0, 5]` to `[0, 0, 0]`, changing force from `5` to `0`.
3. A zero-input DC motor followed by a scalar motor has `nactuator=2` and `nu=1`. The loop reads `ctrl[1]`; AddressSanitizer reports a `use-after-poison` read at `src/user/user_model.cc:4103`.
Representative output:
```text
result=0 nu=1 retained-ctrl=11.0 retained-joint-force=11.0
result=0 nu=3 ctrl=0.0,0.0,0.0 force-before=5.0 force-after=0.0
```
Relevant source:
- https://github.com/google-deepmind/mujoco/blob/10124d5d9dca411ec3c8988aa1e3b619103d71bb/src/user/user_model.cc#L4097-L4104
- https://github.com/google-deepmind/mujoco/blob/10124d5d9dca411ec3c8988aa1e3b619103d71bb/src/user/user_model.cc#L4150-L4158
- https://github.com/google-deepmind/mujoco/blob/10124d5d9dca411ec3c8988aa1e3b619103d71bb/src/user/user_model.cc#L3852-L3861
### Steps for reproduction
## Steps for reproduction
1. Build the commit above normally and with AddressSanitizer.
2. Compile the three programs in `poc/` using `poc/README.md`.
3. Run `delete_first.cc` and observe command migration.
4. Run `multi_input.cc` and observe loss of the PID feed-forward slot on a no-op recompile.
5. Run `zero_input.cc` against the ASan build and observe the out-of-bounds control read.
Reproduction package:
[recompile-control-block-indexing-poc.zip](https://github.com/user-attachments/files/32260457/recompile-control-block-indexing-poc.zip)
### Minimal model for reproduction
Each program embeds a complete asset-free MJCF. The variants are kept separate so every output has a single causal mutation and a clear control.
### Code required for reproduction
See `poc/delete_first.cc`, `poc/multi_input.cc`, and `poc/zero_input.cc`. I have not proposed a production patch because the cache needs a variable-width per-actuator representation and compatibility policy.
### Confirmations
- [x] I searched the [latest documentation](https://mujoco.readthedocs.io/en/latest/overview.html) thoroughly before posting.
- [x] I searched previous [Issues](https://github.com/google-deepmind/mujoco/issues) and [Discussions](https://github.com/google-deepmind/mujoco/discussions), I am certain this has not been raised before.
Contributor guide
Research direction
Start with SaveState and RestoreState in src/user/user_model.cc at the cited lines, then inspect the control-block handling around lines 3852-3861. Build and run the three reproductions in poc/delete_first.cc, poc/multi_input.cc, and poc/zero_input.cc using poc/README.md. Done means surviving actuators retain their complete control blocks across recompilation and the zero-input case performs no invalid control read.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- robotics
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100