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
This commit is contained in:
Yuval Tassa
2026-08-02 17:24:11 -07:00
committed by Copybara-Service
parent 790f8fac30
commit 4278c7b0cd
14 changed files with 3131 additions and 1668 deletions
+14 -14
View File
@@ -392,7 +392,7 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
// defaults and regular
if (joint->type != def->Joint().type) {
WriteAttrTxt(elem, "type", FindValue(joint_map, joint_sz, joint->type));
WriteAttrTxt(elem, "type", FindValue(jointtype_map, jointtype_sz, joint->type));
}
WriteAttrInt(elem, "group", joint->group, def->Joint().group);
WriteAttr(elem, "ref", 1, &joint->ref, &zero);
@@ -410,11 +410,11 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def,
WriteAttr(elem, "stiffness", nstiff, joint->stiffness, def->Joint().stiffness);
}
if (joint->type != mjJNT_FREE) {
WriteAttrKey(elem, "limited", TFAuto_map, 3, joint->limited, def->Joint().limited);
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, joint->limited, def->Joint().limited);
}
WriteAttr(elem, "range", 2, joint->range, def->Joint().range);
if (joint->type != mjJNT_FREE && joint->type != mjJNT_BALL) {
WriteAttrKey(elem, "actuatorfrclimited", TFAuto_map, 3, joint->actfrclimited,
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, joint->actfrclimited,
def->Joint().actfrclimited);
}
WriteAttrKey(elem, "actuatorgravcomp", bool_map, 2, joint->actgravcomp, def->Joint().actgravcomp);
@@ -485,7 +485,7 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
}
// defaults and regular
WriteAttrKey(elem, "type", geom_map, mjNGEOMTYPES, geom->type, def->Geom().type);
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, geom->type, def->Geom().type);
WriteAttrInt(elem, "contype", geom->contype, def->Geom().contype);
WriteAttrInt(elem, "conaffinity", geom->conaffinity, def->Geom().conaffinity);
WriteAttrInt(elem, "condim", geom->condim, def->Geom().condim);
@@ -500,10 +500,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri
WriteAttr(elem, "surfacevel", 6, geom->surfacevel, def->Geom().surfacevel, true);
WriteAttr(elem, "adhesion", 1, &geom->adhesion, &def->Geom().adhesion);
WriteAttrKey(elem, "fluidshape",
fluid_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid);
fluidshape_map, 2, geom->fluid_ellipsoid, def->Geom().fluid_ellipsoid);
WriteAttr(elem, "fluidcoef", 5, geom->fluid_coefs, def->Geom().fluid_coefs);
if (geom->type != mjGEOM_MESH) {
WriteAttrKey(elem, "shellinertia", meshtype_map, 2, geom->typeinertia,
WriteAttrKey(elem, "shellinertia", bool_map, 2, geom->typeinertia,
def->Geom().typeinertia);
}
if (mjuu_defined(geom->mass)) {
@@ -558,7 +558,7 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri
// defaults and regular
WriteAttrInt(elem, "group", site->group, def->Site().group);
WriteAttrKey(elem, "type", geom_map, mjNGEOMTYPES, site->type, def->Site().type);
WriteAttrKey(elem, "type", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type);
if (site->get_material() != def->Site().get_material()) {
WriteAttrTxt(elem, "material", site->get_material());
}
@@ -772,8 +772,8 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def
true);
WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction,
true);
WriteAttrKey(elem, "limited", TFAuto_map, 3, tendon->limited, def->Tendon().limited);
WriteAttrKey(elem, "actuatorfrclimited", TFAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited);
WriteAttrKey(elem, "limited", FalseTrueAuto_map, 3, tendon->limited, def->Tendon().limited);
WriteAttrKey(elem, "actuatorfrclimited", FalseTrueAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited);
WriteAttr(elem, "range", 2, tendon->range, def->Tendon().range);
WriteAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, def->Tendon().actfrcrange);
WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin);
@@ -863,11 +863,11 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe
WriteAttrInt(elem, "nsample", actuator->nsample, def->Actuator().nsample);
WriteAttrKey(elem, "interp", interp_map, interp_sz, actuator->interp, def->Actuator().interp);
WriteAttr(elem, "delay", 1, &actuator->delay, &def->Actuator().delay);
WriteAttrKey(elem, "ctrllimited", TFAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited);
WriteAttrKey(elem, "ctrllimited", FalseTrueAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited);
WriteAttr(elem, "ctrlrange", 2, actuator->ctrlrange, def->Actuator().ctrlrange);
WriteAttrKey(elem, "forcelimited", TFAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited);
WriteAttrKey(elem, "forcelimited", FalseTrueAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited);
WriteAttr(elem, "forcerange", 2, actuator->forcerange, def->Actuator().forcerange);
WriteAttrKey(elem, "actlimited", TFAuto_map, 3, actuator->actlimited, def->Actuator().actlimited);
WriteAttrKey(elem, "actlimited", FalseTrueAuto_map, 3, actuator->actlimited, def->Actuator().actlimited);
WriteAttr(elem, "actrange", 2, actuator->actrange, def->Actuator().actrange);
WriteAttr(elem, "lengthrange", 2, actuator->lengthrange, def->Actuator().lengthrange);
WriteAttr(elem, "gear", 6, actuator->gear, def->Actuator().gear);
@@ -1086,7 +1086,7 @@ void mjXWriter::Option(XMLElement* root) {
model->option.integrator, opt.integrator);
WriteAttrKey(section, "cone", cone_map, cone_sz,
model->option.cone, opt.cone);
WriteAttrKey(section, "jacobian", jac_map, jac_sz,
WriteAttrKey(section, "jacobian", jacobian_map, jacobian_sz,
model->option.jacobian, opt.jacobian);
WriteAttrKey(section, "solver", solver_map, solver_sz,
model->option.solver, opt.solver);
@@ -1743,7 +1743,7 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
}
// simple optimization
WriteAttrKey(elem, "simple", FAuto_map, 2, body->simple, 1);
WriteAttrKey(elem, "simple", FalseAuto_map, 2, body->simple, 1);
// userdata
WriteVector(elem, "user", body->get_userdata());