Commit Graph

5340 Commits

Author SHA1 Message Date
Yuval Tassa d6dc966c04 Support planar reflections on physically based materials.
PiperOrigin-RevId: 961670546
Change-Id: Ifa69b8251c5f3fae6a76427f13df9c688370c741
2026-08-09 02:03:28 -07:00
Copybara-Service cb4259e4e4 Merge pull request #3465 from shi-eric:shi-eric/mjx-warp-cache-ffi-callables
PiperOrigin-RevId: 961136122
Change-Id: Ia468812e0557bb4efb7866f7130dab5d1cea673f
2026-08-07 14:59:07 -07:00
Yuval Tassa 9c765e7ba1 Fix formatting in the changelog.
PiperOrigin-RevId: 961105326
Change-Id: I52e522e16fb8ad8b8a96d32e3de70608363f3805
2026-08-07 13:55:10 -07:00
Copybara-Service e144090273 Merge pull request #3466 from smallquail:flex-implicit-contact
PiperOrigin-RevId: 960804180
Change-Id: Ic3eb203ca0d9cf19c9043b46127db342678d94f9
2026-08-07 02:36:26 -07:00
Yuval Tassa dca295dde2 Composite Filament planar reflections as Fresnel-weighted specular.
PiperOrigin-RevId: 960554436
Change-Id: Ife6f5c760993ad185e9421cd7b3ce7fd06fffc90
2026-08-06 16:07:30 -07:00
Baruch Tabanpour 5e3464f475 Fix #3435. Add token to ensure sequential calls for mjx-warp refit and render.
PiperOrigin-RevId: 960553589
Change-Id: I76caca5a82dd7f96b39e51c5b67b7382ebd1726d
2026-08-06 16:05:47 -07:00
Eric Shi ab4fe36121 Cache MJX-Warp FFI callables
Equivalent JAX retraces rebuilt flattened wrapper functions, causing
Warp to register a new FFI callable for every trace.

Reuse callables with matching structural configurations while keeping
distinct shim functions isolated. This keeps Warp's registry and graph
cache effective without changing callback lifetime.

Document the structural key and lock scope so future changes preserve
the cache's intended boundaries.

Signed-off-by: Eric Shi <ershi@nvidia.com>
2026-08-06 19:49:45 +00:00
Taylor Howell a1d772c9ad Import google-deepmind/mujoco_warp from GitHub.
PiperOrigin-RevId: 960411406
Change-Id: I687b06586881d1915bab1bf8ebab6473e6ef29db
2026-08-06 11:24:34 -07:00
Yuval Tassa b3f8f91ec5 Include the environment in Filament planar reflections.
PiperOrigin-RevId: 960321393
Change-Id: Ia5855f9906ae91dd656d9f9ca233c0f379a49831
2026-08-06 08:37:24 -07:00
Kyle Bayes 35342f403f Check if geoms are separated in GJK after an early exit. Fix #3383.
PiperOrigin-RevId: 960316577
Change-Id: I328322e0b1bd14089defb26e9dc35a264642b4d6
2026-08-06 08:27:05 -07:00
Alessio 2a3554c8a3 Integrate passive flex contact implicitly
Contact of a flex with `passive` collisions enabled was applied as an
explicit spring of fixed stiffness 1e4, which the timestep bounds: any
stiffness worth having oscillates faster than the step can resolve, so
the force was too soft to keep sheets apart and interpenetration was
routine.

Carry its curvature in the effective metric M + K instead, alongside the
flex's own stretch and bending stiffness. The contact block k*J^T*J is
appended to the per-vertex candidate list already assembled for the flex
stencils, so it costs additional entries in an existing matrix rather
than a new one, and the accompanying shift -h*K*v is what damps the
stiff modes. At a 2 ms timestep this holds roughly 50x the stiffness an
explicit force of the same step could.

With the timestep no longer setting the bound, the stiffness is chosen
as a natural frequency scaled by the participating vertex mass rather
than left at a fixed 1e4, so one value suits models of any scale.

Passive handling is scoped to contacts whose every dof is a flex vertex
carried by the metric: flex against flex, flex against itself, and flex
against static geometry, which contributes no dofs of its own. For those
the Hessian is assembled in full. Contact with a body that can move
would have that body's dofs dropped from it, and is left on the
constraint solver.

The feature now requires an integrator whose constraint solve runs in
that metric, and is rejected with an error otherwise.

Add model/flex/drape.xml as the example model, replacing sphere_passive,
whose contacts no longer demonstrated the feature.
2026-08-06 15:38:05 +01:00
Sam Haves 39e4458806 Update USD support in MuJoCo to Newton USD schemas v0.4.0.
See changelog for more info.

PiperOrigin-RevId: 960284190
Change-Id: I89a84b1efbc22c89ee99db7436851ef27a325176
2026-08-06 07:17:39 -07:00
Haroon Qureshi 63b779042c Split Renderer class into explicit "classic" and "filament" classes.
PiperOrigin-RevId: 960272089
Change-Id: I9494472b7b8e838a75beaf0cd68e5ce4b42a80a4
2026-08-06 06:52:37 -07:00
Kyle Bayes 7f305abd71 Add minor improvements to x_k norm in GJK.
PiperOrigin-RevId: 960266053
Change-Id: I0e774617cd8ed7a189caee97f6139f6fd206d39e
2026-08-06 06:36:45 -07:00
Sam Haves 6fe04aa8d3 Remove custom mujoco.texture format.
PiperOrigin-RevId: 960257562
Change-Id: I318c84d82576d405f0a5d8510acd5bab5f08c9af
2026-08-06 06:13:54 -07:00
Matija Kecman 84950fa371 MuJoCo Web Viewer: Implement parallel chunked model downloading and in-place model reloading.
This change introduces parallel chunked downloading for large model files (.mjb) directly into WASM linear memory, enables model loading without full page reloads and reduces memory overhead.

Key changes:
- Implement a chunked model endpoint in the Python web server to support range requests.
- Add parallel chunked fetching in the frontend with retry logic and a single-fetch fallback.
- Increase initial WASM memory to 3 GB to accommodate large models and prevent heap fragmentation.
- Support in-place model reloading in the C++ client, including texture cache invalidation when the Filament context is recreated.
- Display a model download progress bar and model parsing/loading banner to the UI.
- Fixes model drag and drop (caused by typo in sessionId, corrected to session_id).

PiperOrigin-RevId: 960249236
Change-Id: Icac89e6a4ca099882aaf9b111744c1b6ab0cc6c1
2026-08-06 05:51:43 -07:00
Yuval Tassa 7fd2061f5c Change default light softness from 0 to 0.2.
PiperOrigin-RevId: 960218397
Change-Id: I58f211f5a275f8f19254424facd02c14eaa8c668
2026-08-06 04:28:17 -07:00
Yuval Tassa ed13bf5647 Mocap bodies are the weld root of their own kinematic subtree.
PiperOrigin-RevId: 960212286
Change-Id: Ibeff129c3110576c8846c76ba6ab756f23c0fa2e
2026-08-06 04:10:34 -07:00
Yuval Tassa 0accc5b3c7 Emit dm_control's schema.xml from mjcf.schema
PiperOrigin-RevId: 959934262
Change-Id: I75b96a0fbad2aa4acefbd1ea650ad08205c78576
2026-08-05 16:31:15 -07:00
Sam Haves 596b6f433d Add args field to mjResource for decoder and encoder arguments.
Add an optional `const char* args` field to `mjResource` to allow passing
resource arguments/hints (such as requested channel count or encoding options)
to resource decoders and encoders.

PiperOrigin-RevId: 959774058
Change-Id: Icc41a2bb3895fef24f98c5fa9a77cdad092a6bc7
2026-08-05 11:31:35 -07:00
Kyle Bayes 83e621d771 Implement mesh extrema in a 3x3x3 grid corresponding to each feature of a unit cube. These are used as seeds for a better initial point in mesh hill climbing with up to a 2x speedup in mjc_Convex.
PiperOrigin-RevId: 959645299
Change-Id: I032ae534704e0cc440ddb7691fd21a29a359f521
2026-08-05 07:22:48 -07:00
Alessio Quaglino 1362a8bded Move BVH active visualization flag reset after early return check in MuJoCo collision driver.
This avoids unnecessarily resetting the bvh_active flags when the collision driver returns early due to disabled constraints, disabled contacts, or having fewer than two bodies/flexes.

PiperOrigin-RevId: 959531283
Change-Id: I85caf054b06e3be1018d710f9ab762627fc3356a
2026-08-05 02:41:46 -07:00
Yuval Tassa f9a00bd5b5 Add light softness: spotlight edge softness for physically-based rendering.
The new spotlight attribute softness (real in [0, 1], default 0) is the
fraction of the cone, measured inward from the cutoff, over which
intensity falls to zero. It is used by physically-based lighting models;
the Phong model's corresponding knob remains exponent.

The filament renderer previously hardcoded the inner cone angle to 0,
making the entire beam penumbra: the shader attenuates by the squared
smoothstep ((cos(theta) - cos(outer)) / (cos(inner) - cos(outer)))^2, so
a cutoff-25 spot delivered its rated candela only exactly on-axis and
about a third of it averaged over the light pool, with the deficit
shrinking as the cutoff widens. The inner angle is now
(1 - softness) * cutoff, so at the default the light delivers its full
intensity everywhere inside the cone and illuminance follows E = I/d^2
independent of the cutoff. Setting softness to 1 reproduces the previous
appearance exactly (verified bit-identical), which is the migration path
for models tuned against the old behavior.

The filament light type also changes from FOCUSED_SPOT to SPOT. With
intensity given in candela and the cone set at build time the two types
produce identical output (FOCUSED_SPOT's power-conserving rescale only
applies when the cone changes after the intensity is set), but SPOT
guarantees that candela never rescales with cone angle should the cone
ever become runtime-editable.

Verified with headless renders under a linear tone mapper against an
equal-candela point light at cutoffs 25/45/80: softness 0 gives
spot/point luminance ratio 1.000 at all sampled angles inside the cone;
softness 0.2 is flat over the inner 80% of the cone; softness 1 matches
the previous renderer with zero linear-pixel difference. XML round-trip
and the [0, 1] compile-time check verified. Introspect and wasm bindings
regenerated.

PiperOrigin-RevId: 959334706
Change-Id: I0f0729781899880de1729ea9b3d8c055d715a025
2026-08-04 18:11:48 -07:00
Yuval Tassa 92261d9095 Fix test breakage under float32
PiperOrigin-RevId: 959249928
Change-Id: Ibcf7dbe41e184be0c23a8d14af54ea6bde5b8de3
2026-08-04 15:04:09 -07:00
Alessio Quaglino 63c7175ed5 Adjust rotEPS for single precision in mju_mat2Rot.
This change defines rotEPS as 1e-6f when mjUSESINGLE is defined, and 1e-9 otherwise. With the larger epsilon for single precision, the algorithm converges in fewer iterations, allowing the test assertion for maximum iterations to be simplified to a constant 150.

PiperOrigin-RevId: 959070496
Change-Id: I4f6fae056cd71955d3921c01a9929af2cdd81fac
2026-08-04 09:38:28 -07:00
Matija Kecman b3ef7a8c2f Simplify MuJoCo Studio viewer selection and unify model flags.
This change removes the ViewerMode enum and instead infers whether to launch the Web Viewer or Native Viewer based on the graphics mode (gfx) setting. Specifically, setting gfx to "web" or "webgl" will now launch the Web Viewer (in future "webgpu" would also launch the Web Viewer).

Additionally, this unifies command-line flags across studio scripts and samples by replacing the --mjcf flag with --model (with positional argument fallback) and standardizing absl flags usage. --model is a better name since formats like .mjz and .mjb are also supported by this argument.

PiperOrigin-RevId: 959058940
Change-Id: If4e0ace664ddc6e45cb681d168679148e8c4901d
2026-08-04 09:14:07 -07:00
Haroon Qureshi 0a10860257 Classic rendering mode works with wayland now.
PiperOrigin-RevId: 959036941
Change-Id: I31231c644e8a535918e4ace5d44f84b9b7da78bc
2026-08-04 08:29:04 -07:00
Yuval Tassa 9553926158 Save flex node coordinates in the new nodecoord attribute.
Interpolated flexes with pinned nodes could not be reloaded after saving:
pinned nodes share their parent body, and their positions within it lived
only in mjsFlex.node, which had no MJCF attribute. On reload the pinned
nodes collapsed onto the parent body origin, degenerating the trilinear
interpolation grid ("flex grid rotation R0 is not orthonormal"). This
made model/flex/strain.xml and gripper_trilinear.xml fail to round-trip.

Add flex/nodecoord, real(3*nnode), the node analog of flex/vertex: local
node coordinates within the corresponding body frames. The reader picks
it up from the regenerated schema tables; the writer emits it with the
precision-aware WriteVector, since VectorToString ignores the XML
precision setting and truncating node coordinates to 6 digits while body
positions carry 17 fails the R0 orthonormality check at full precision.

Add a WritesPinnedFlexNodes round-trip regression test, and remove the
two write-read sweep exclusions documenting this bug. The removed
substring filter "strain" was also matching core_constraint, silently
excluding that entire testdata directory from the sweep; its ~40 models
are now covered and pass.

PiperOrigin-RevId: 959025281
Change-Id: I2fed28c01491c5a8431e813102a423d12b659911
2026-08-04 08:04:27 -07:00
Yuval Tassa 574b6bd6bf More schema-related cleanup.
- Declare every nonzero default; make the defaults cross-check total.
- Skip default-valued attributes in the hand-written writer paths.
- Fix type facts on hand-read elements, found by the dm_control diff.

PiperOrigin-RevId: 958999733
Change-Id: I3064ccc6ae1f049c20f273abc234cd02990a8b7e
2026-08-04 07:05:41 -07:00
Copybara-Service 10793e539e Merge pull request #3451 from smallquail:flex-geometric-stiffness
PiperOrigin-RevId: 958995742
Change-Id: Ibb60c3112b6dbc2373a5cd98df0477d639a5a001
2026-08-04 06:56:37 -07:00
Yuval Tassa 3d1d133a48 In the compile sample, copy the error message before deleting the spec that owns it.
PiperOrigin-RevId: 958970071
Change-Id: I1e8e367f26fde4778747020ab61f26105d70d690
2026-08-04 05:44:59 -07:00
Yuval Tassa 1e3c8f3f91 Add XSD schema.
Fixes #6, #3237, #3410

PiperOrigin-RevId: 958922477
Change-Id: Ia68e10be06d7c880b6a095edcc79572ee8b6f50e
2026-08-04 03:32:24 -07:00
Yuval Tassa 2544c34997 Clean up new schema code.
- Declare the full child lists of the body-alias elements.
- Verify read-table coverage: every generated row array must be consumed.
- Fix stale attribute facts on hand-read elements.

PiperOrigin-RevId: 958685667
Change-Id: I4a914f3136a5078eb8ca24aa4e162d55afafd923
2026-08-03 18:15:39 -07:00
Taylor Howell f848b185d3 update mjx make_data and put_data docstrings for njmax
PiperOrigin-RevId: 958526560
Change-Id: Iaeff307e4d09c9193134fc29689aeedfdcaa9d53
2026-08-03 12:49:29 -07:00
Kyle Bayes 0bfa4e984c Fix latent bug in multiccd so that we can never write more contacts than what the buffer space allows.
PiperOrigin-RevId: 958505449
Change-Id: I77cb1142405df5107be46ca0dbef6effc690028d
2026-08-03 12:07:00 -07:00
Taylor Howell 382af63dc3 update mjx for naccdmax
PiperOrigin-RevId: 958479874
Change-Id: If0f48f2f372955f2948cb286966f5fce52c3f0ba
2026-08-03 11:20:34 -07:00
Saran Tunyasuvunakool b0c3486249 Tweak URL printout in terminal when launching the web viewer.
PiperOrigin-RevId: 958422471
Change-Id: I2d637c615015915eb01b65bb0bad22749886a372
2026-08-03 09:41:25 -07:00
Yuval Tassa d33f297b56 Zero out stale lambda coordinate in S3D in GJK.
PiperOrigin-RevId: 958414972
Change-Id: Ia21f8959a990c18b9d67959be5b2adeb1d3fa1dd
2026-08-03 09:25:46 -07:00
Copybara-Service eb049fb287 Merge pull request #3450 from teerthsharma:topo/hullgraph-inverse-map
PiperOrigin-RevId: 958414730
Change-Id: I4a7f8fdde77513a3267e07e46a13144592e73ff0
2026-08-03 09:24:33 -07:00
Yuval Tassa 8a643f4e2b Format some more files in user/
PiperOrigin-RevId: 958383817
Change-Id: I17fcdbf32967ec5a5c4d2552823ef5100faaea3b
2026-08-03 08:17:01 -07:00
Alessio 5497994763 Add the geometric term to the flex stretch stiffness
K_stretch was the Gauss-Newton Hessian of the stretch force, not its Jacobian.
With elongation e_a = L_a^2 - L0_a^2 and force f = -sum_ab M_ab e_a grad(e_b)/2,

  K = 2 sum_ab M_ab (s_a d_a)(s_b d_b)^T + sum_a Me_a (Laplacian_a (x) I3)

and only the first term was there. The second is proportional to the edge
tension Me_a = sum_b M_ab e_b, so it vanishes at rest and grows with strain:
the operator was first-order correct and no more. Finite-differencing it
against -d(qfrc_passive)/dq on a mesh dilated by 5% gives 7.8% of the force
scale; with the term it is exact to roundoff.

Add only the tensile part. The geometric block is Me_a*[[I,-I],[-I,I]] over the
edge's two vertices, which is positive semi-definite exactly when Me_a >= 0; a
compressed edge would make K indefinite, and both consumers -- the CG
constraint solver and the PCG in mjd_effSolve -- require SPD. The clamp is
structural, so no eigendecomposition is needed, and it is confined to the
operator: mj_flexPassiveStretch keeps the full Me_a, so no force changes.

Both the matrix-free operator and the CSR assembly the effective metric builds
from are updated, since they must agree.

This changes how flexes with elastic2d="stretch" integrate under the implicit
integrators and the effective metric -- bag.xml moves, poncho.xml is
bit-identical because bending energy is quadratic and has no geometric term.
The interpolated-flex path still uses its Gauss-Newton approximation, which
FlexInterpDerivativesDeformed asserts.
2026-08-03 16:16:23 +01:00
Alessio Quaglino 7927e908a3 Fix flex bending frame transformation when parent body is rotated.
PiperOrigin-RevId: 958362895
Change-Id: I0f6ba12a441bd7daa54292bfe2c70f0d836b008d
2026-08-03 07:28:52 -07:00
Copybara-Service 2c8d7b565d Merge pull request #3252 from devshahofficial:devshahofficial/studio-filament-source-build
PiperOrigin-RevId: 958356099
Change-Id: If4e68babdc6c68d7dd05ced7197a1869db4a0625
2026-08-03 07:12:38 -07:00
teerthsharma 747d36da91 Remap convex hull graph ids with an inverse table
Signed-off-by: Teerth Sharma <teerths57@gmail.com>
2026-08-03 18:08:09 +05:30
Yuval Tassa b475bb6f36 Move schema related generated files into their own directory
PiperOrigin-RevId: 958296165
Change-Id: I48cacc72c7df5994f5f816489ba069a5813845a1
2026-08-03 04:23:56 -07:00
Yuval Tassa 5e99ca6cb8 Table-driven attribute writing: rebase the writer on the shared rows.
The writer consumes the same generated rows as the reader.
mjXWriter::WriteAttrTable drives the mechanical attributes of an
element from its mjXAttr rows: each bound field is compared against
the class default at the same offset -- the default object is the same
struct type, so the rows carry no comparison values -- and attributes
equal to their default are skipped. A null default object means the
element has no defaults, and every defined value is written.
Ranged-arity rows write with trailing-default trimming, which the
reader makes round-trip exact by refilling from the same default.
Call sites upcast to the private mjs base (the friend declarations
permit it; mjCMesh gains the friendship its siblings had); the
comparison object is def->X().spec, a freshly-defaulted struct for
the sections, or zero-initialized for size, whose spec defaults (-1,
auto) are resolved by compilation.

Converted: pair, geom, site, joint, camera, light, material, the
equality family, both tendon types (the fixed rows are the spatial
rows without appearance attributes -- exactly the tag difference), the
actuator, flex with its three sub-elements, mesh, skin, option, the
six visual sub-sections, statistic and size. The remnants keep names,
files, resolved reference strings (the mjC classes null their private
base's string pointers; resolved names live behind accessors), and the
writing=custom policies the schema declares: compile directives never
saved (fromto, springdamper, fitscale), type-dependent lengths and
attributes (sizes, joint pos/axis/limited, shellinertia), and
alternatives (mass/density, fovy-versus-intrinsics, the plugin-gated
gain/bias family). Compiler keeps its write-if-nonzero policy;
keyframe keeps its model-sized vectors.

Saved files are canonical: attributes follow schema declaration order
with remnants trailing, and sections follow the schema's dependency
order (statistic before visual, deformable before the contact and
equality sections that name flexes, tendon before the equality
constraints that name tendons, custom demoted to the data tail).
Uniform behavior fixes fall out: default-equal positionals are
dropped, dynprm is trimmed like every other ranged vector, and mesh
material -- read into the spec but never written -- now survives
save/load round trips. Changelog entries ride along.

Verified: full suite, doc_test, and the two-tier A/B harness --
saved XML reorders attributes, and every corpus model reloads to a
byte-identical binary.
PiperOrigin-RevId: 958255003
Change-Id: I5fe7346014450db88b2f3f8680a8f616f7d31266
2026-08-03 02:27:22 -07:00
Yuval Tassa 760477f406 Add .clang-format to user/, format user_composite
PiperOrigin-RevId: 958225464
Change-Id: Iad7eb52dbecf7585f98375c90863ca707c494590
2026-08-03 01:22:00 -07:00
Yuval Tassa 4278c7b0cd Table-driven attribute reading: rebase the reader on generated rows.
Every mechanical attribute read in MJCF now derives from mjcf.schema.
generate_read_table.py emits typed mjXAttr rows (mjcf_read_table.inc,
doc_test-gated) binding each attribute to its spec struct field; field
offsets are offsetof() expressions, so binding mistakes are compile
errors, and the field's C type -- parsed from the headers -- selects the
row kind, so mjtNum-versus-double is decided by the struct, not the
schema. mjXReader::ReadAttrTable is the generic loop; its static core
also serves the section parsers and records XML-authored fields via
mjs_setAuthored for attach conflict resolution. Row kinds cover
strings, string lists, numeric scalars and vectors (exact and ranged),
enums (int- and byte-width), bitwise flag sets, bools, unbounded typed
vectors, fixed char arrays, and identity constants declared by 'set'.
The rows are inline variables, and carry the writing=custom flag,
because the writer will share them.

The keyword maps the rows reference are generated too: the ~48
hand-written mjMap tables become mjcf_map.h, one map and size constant
per enum as C++17 inline variables, retiring the hand-maintained
extern block in xml_base.h. Map names follow the schema enum names
(fluid->fluidshape, TFAuto->FalseTrueAuto, FAuto->FalseAuto,
joint->jointtype, geom->geomtype, jac->jacobian); all maps are
key-order- and value-identical to the hand tables they replace, and
bool_map is hand-emitted (the bool type is built in, not a schema
enum).

The OneX() parsers reduce to genuine irregulars, schema-marked as
reading=custom: orientation alternatives, file attributes (VFS and
asset-dir context), the actuator shorthand remappings and per-type
input maps, springlength's one-value copy, mesh builtin construction,
hfield elevation, texture cube files, flexcomp seeding, the memory
suffix parse, and the flag bit families. All 41 sensors that are pure
identity-plus-references -- including the frame family and insidesite
-- dispatch through a generated tag table; frame-sensor
objtype/reftype vocabulary tightens from the full mju_str2Type
namespace to the documented body/xbody/geom/site/camera subset, so an
invalid keyword now fails at parse time instead of compile time. The
equality family and both tendon types read shared group rows; the
twelve actuator shorthands share the general rows, with per-tag
legality enforced by the schema check. Sections bind non-mjs structs,
the visual sub-sections reaching their anonymous sub-structs through
member paths declared by an element-level field= facet.

Latent irregularities surfaced by the migration and preserved via
schema declarations or remnants: key's name is set even when absent,
eulerseq and gridlayout are fixed char arrays (chars[n], arity in
characters), gridlayout's length-must-match-gridsize stays a
value-conditional remnant, and constructor-style elements (tendon
wraps, asset model, replicate, attach) are annotated as such -- their
attributes are arguments, not field writes.

Two coherence tests guard the schema against the C sources: every
schema enum constant must be a member of the C enum it claims, and
every C member must be a keyword, a count sentinel, or a documented
exemption; and generate_default_table.py emits one row per defaulted
attribute (mjcf_default_table.inc), compared by SchemaDefaultsTest
against a freshly-constructed spec -- the schema cannot disagree with
the C default-constructors without failing the suite.

Verified: doc_test regenerates and diffs every artifact; the full
suite; and an A/B harness compiling the model corpus against the
pre-migration reader -- saved XML and binary models are byte-identical.
PiperOrigin-RevId: 958075724
Change-Id: I9715fe4deeb438eec988fd5084d74ba8b466b10b
2026-08-02 17:24:45 -07:00
Yuval Tassa 790f8fac30 Generate the MJCF grammar table and enforce its constraints.
The hand-written MJCF[] table in xml_native_reader.cc is replaced by
mjcf_table.inc, emitted from mjcf.schema by generate_mjcf_table.py and
checked for freshness by doc_test. nMJCF is now self-sizing. The two
tables are identical as trees of (tag, cardinality, attribute-set);
within-row attribute order changes where the schema factors shared
groups and projects default-context rows, and top-level rows follow
the schema's dependency order -- neither affects validation, which is
set-based, nor XMLschema.rst, whose generator orders sections itself
(regenerated here, reading the .inc instead of the reader source).

The schema's constraint declarations become enforcement: the emitter
writes a companion MJCF_constraints[] array (row-indexed into MJCF[]),
and mjXSchema::Check evaluates each element's constraints after its
attribute check, with uniform messages derived from the declaration:
"at most one of 'fovy', 'sensorsize' can be specified", "attributes
'reftype', 'refname' must be specified together", and so on.
Multi-attribute bundles render as ('site1', 'site2').

Fifteen hand-written co-occurrence checks across fourteen elements are
deleted -- connect/weld semantics mixing and completeness, the actuator
transmission mutex, camera fovy/sensorsize, light directional/type,
inertial fullinertia-versus-orientation, rangefinder and the distance
family, contact's matching criteria, user-sensor pairing, the frame
family's reftype/refname, size memory exclusivities, mesh builtin
exclusions, and attach body/frame (newly declared). Tests assert the
uniform messages.

Two findings along the way: sensorsize-requires-resolution is a
value-level compiler rule (positive resolution), not a presence rule --
a presence constraint would be wrong and is not declared; and Size()'s
nstack/njmax range checks tested the spec value before assignment, so
they never validated the parsed value -- now they do.

Verified by compiling all 81 models in the model/ corpus.

PiperOrigin-RevId: 958064622
Change-Id: I802cf5c0aee08a62926e36a281320ff9e34c0668
2026-08-02 16:30:02 -07:00
Yuval Tassa 3f8db4c17a Add mjcf.schema and its declarative schema language.
The complete MJCF surface in one hand-maintained file: 144 elements, 8
shared attribute groups, 47 enums, 1,497 typed attributes with
defaults, the presence-constraint inventory previously visible only as
hand-written reader checks, and the save policies previously visible
only as hand-written writer logic.

The language is a small IDL: elements bound to their mjSpec structs,
typed attributes with arities and defaults, enum keyword sets with C
bindings, reusable and variant groups, explicit name/reference
namespaces (id<ns>/ref<ns>, following dm_control's
identifier/reference model), child cardinalities, presence constraints
(exclusive/together/requires/oneof over attribute bundles), bitwise
flag sets, identity constants (set field = CONST), fixed char arrays
(chars[n], arity counting characters), numeric range facets, and two
escape hatches: reading=custom (no typed binding is generated; both
reading and saving are hand-written) and writing=custom (the binding
drives the reader, the save policy is hand-written).
doc/generate/mjcf_schema.py is the dependency-free parser and semantic
validator; errors report file:line; 55 unit tests. The language is
documented by the cheat-sheet legend at the top of the schema file.

The schema was bootstrapped by extraction from the sources of record --
the MJCF[] table, the mjMap keyword tables, the ~660 ReadAttr*/MapValue
call sites, mjspec.h struct fields, and the default-constructors in
user_init.c and engine_init.c -- then hand-curated. Same-tag elements
that differ by context are distinct declarations carrying an xml=
facet; worldbody, frame and replicate carry alias=body, mirroring
mjXSchema::NameMatch. The top-level order is by dependency, what a
saved file should read like: front matter, declarations before use,
the tree, the sections that reference it, the data tail.
PiperOrigin-RevId: 958060695
Change-Id: Ie10fd9f0ef202a3626f4d635d02c8731a4d287df
2026-08-02 16:10:14 -07:00