From 5e99ca6cb86e08393cf63692a6fb96c7e24f0040 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 3 Aug 2026 02:26:45 -0700 Subject: [PATCH] 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 --- doc/changelog.rst | 9 + python/mujoco/specs_test.py | 5 +- src/user/user_objects.h | 1 + src/xml/xml_native_writer.cc | 523 +++++++++-------------------- src/xml/xml_native_writer.h | 10 + src/xml/xml_util.cc | 4 + test/engine/engine_plugin_test.cc | 4 +- test/xml/xml_native_writer_test.cc | 38 ++- 8 files changed, 209 insertions(+), 385 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index cbd6c416..e42fc33c 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -5,6 +5,15 @@ Changelog Upcoming version (not yet released) ----------------------------------- +General +^^^^^^^ + +- The MJCF grammar is now defined in a single source of truth schema file, + `src/xml/mjcf.schema `__. The parser's + grammar table, presence constraints, keyword maps, typed attribute bindings and save policies are generated from it + and gated by tests, as are the schema's enum keywords and declared defaults against the C headers and + default-constructors. + Actuation ^^^^^^^^^ - Added the :ref:`pid` actuator: a PID controller with real position and velocity setpoint inputs, diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 5c107ba3..dc3ffa07 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -20,7 +20,6 @@ import math import os import textwrap import typing -import warnings import zipfile # pylint: disable=unused-import from absl import flags @@ -61,7 +60,7 @@ class SpecsTest(absltest.TestCase): """ spec = mujoco.MjSpec.from_string(xml) - model = spec.compile() + spec.compile() self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0) self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0) @@ -152,7 +151,7 @@ class SpecsTest(absltest.TestCase): - + diff --git a/src/user/user_objects.h b/src/user/user_objects.h index d64da8fe..341cc980 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1138,6 +1138,7 @@ class mjCMesh_ : public mjCBase { class mjCMesh: public mjCMesh_, private mjsMesh { friend class mjCModel; + friend class mjXWriter; public: explicit mjCMesh(mjCModel* = nullptr, mjCDef* = nullptr); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index d65d697c..e2c23b5e 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -38,6 +38,9 @@ #include "xml/xml_util.h" #include "tinyxml2.h" +// typed attribute rows, generated from mjcf.schema; shared with the reader +#include "mjcf_read_table.inc" + namespace { using std::string; @@ -133,14 +136,12 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // common attributes WriteAttrTxt(elem, "name", flex->name); - WriteAttr(elem, "radius", 1, &flex->radius, &defflex.radius); + WriteAttrTable(elem, static_cast(flex), + static_cast(&defflex), kFlexAttrs, + kFlexAttrsN); if (flex->get_material() != defflex.get_material()) { WriteAttrTxt(elem, "material", flex->get_material()); } - WriteAttr(elem, "rgba", 4, flex->rgba, defflex.rgba); - WriteAttrKey(elem, "flatskin", bool_map, 2, flex->flatskin, defflex.flatskin); - WriteAttrInt(elem, "dim", flex->dim, defflex.dim); - WriteAttrInt(elem, "group", flex->group, defflex.group); WriteAttr(elem, "cellcount", 3, flex->spec.cellcount, defflex.spec.cellcount); if (flex->spec.order != defflex.spec.order) { string dof_str = "full"; @@ -177,20 +178,9 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // contact subelement XMLElement* cont = InsertEnd(elem, "contact"); - WriteAttrInt(cont, "contype", flex->contype, defflex.contype); - WriteAttrInt(cont, "conaffinity", flex->conaffinity, defflex.conaffinity); - WriteAttrInt(cont, "condim", flex->condim, defflex.condim); - WriteAttrInt(cont, "priority", flex->priority, defflex.priority); - WriteAttr(cont, "friction", 3, flex->friction, defflex.friction); - WriteAttr(cont, "solmix", 1, &flex->solmix, &defflex.solmix); - WriteAttr(cont, "solref", mjNREF, flex->solref, defflex.solref); - WriteAttr(cont, "solimp", mjNIMP, flex->solimp, defflex.solimp); - WriteAttr(cont, "margin", 1, &flex->margin, &defflex.margin); - WriteAttr(cont, "gap", 1, &flex->gap, &defflex.gap); - WriteAttrKey(cont, "internal", bool_map, 2, flex->internal, defflex.internal); - WriteAttrKey(cont, "selfcollide", flexself_map, 5, flex->selfcollide, defflex.selfcollide); - WriteAttrInt(cont, "activelayers", flex->activelayers, defflex.activelayers); - WriteAttrKey(cont, "passive", bool_map, 2, flex->passive, defflex.passive); + WriteAttrTable(cont, static_cast(flex), + static_cast(&defflex), kFlexcomp_contactAttrs, + kFlexcomp_contactAttrsN); // remove contact is no attributes if (!cont->FirstAttribute()) { @@ -199,16 +189,15 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { // elasticity subelement XMLElement* elastic = InsertEnd(elem, "elasticity"); - WriteAttr(elastic, "young", 1, &flex->young, &defflex.young); - WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson); - WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness); - WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping); - WriteAttrKey(elastic, "elastic2d", elastic2d_map, 4, flex->elastic2d, defflex.elastic2d); + WriteAttrTable(elastic, static_cast(flex), + static_cast(&defflex), kElasticityAttrs, + kElasticityAttrsN); // edge subelement XMLElement* edge = InsertEnd(elem, "edge"); - WriteAttr(edge, "stiffness", 1, &flex->edgestiffness, &defflex.edgestiffness); - WriteAttr(edge, "damping", 1, &flex->edgedamping, &defflex.edgedamping); + WriteAttrTable(edge, static_cast(flex), + static_cast(&defflex), kFlex_edgeAttrs, + kFlex_edgeAttrsN); // remove edge if no attributes if (!edge->FirstAttribute()) { @@ -257,14 +246,11 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) { } // defaults and regular - if (mesh->Inertia() != def->Mesh().Inertia()) { - WriteAttrTxt(elem, "inertia", FindValue(meshinertia_map, 4, mesh->Inertia())); + WriteAttrTable(elem, static_cast(mesh), &def->Mesh().spec, + kMeshAttrs, kMeshAttrsN); + if (mesh->Material() != def->Mesh().Material()) { + WriteAttrTxt(elem, "material", mesh->Material()); } - WriteAttr(elem, "refpos", 3, mesh->Refpos(), def->Mesh().Refpos()); - WriteAttr(elem, "refquat", 4, mesh->Refquat(), def->Mesh().Refquat()); - WriteAttr(elem, "scale", 3, mesh->Scale(), def->Mesh().Scale()); - WriteAttrKey(elem, "smoothnormal", bool_map, 2, mesh->SmoothNormal(), - def->Mesh().SmoothNormal()); } @@ -272,16 +258,16 @@ void mjXWriter::OneMesh(XMLElement* elem, const mjCMesh* mesh, mjCDef* def) { // write skin void mjXWriter::OneSkin(XMLElement* elem, const mjCSkin* skin) { string text; - mjCDef mydef; - float zero = 0; + mjCSkin defskin; // write attributes WriteAttrTxt(elem, "name", skin->name); WriteAttrTxt(elem, "file", skin->File()); WriteAttrTxt(elem, "material", skin->get_material()); WriteAttrInt(elem, "group", skin->group, 0); - WriteAttr(elem, "rgba", 4, skin->rgba, mydef.Geom().rgba); - WriteAttr(elem, "inflate", 1, &skin->inflate, &zero); + WriteAttrTable(elem, static_cast(skin), + static_cast(&defskin), kSkinAttrs, + kSkinAttrsN); // write data if no file if (skin->File().empty()) { @@ -358,15 +344,8 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe } } - WriteAttrKey(elem, "texuniform", bool_map, 2, material->texuniform, def->Material().texuniform); - WriteAttr(elem, "texrepeat", 2, material->texrepeat, def->Material().texrepeat); - WriteAttr(elem, "emission", 1, &material->emission, &def->Material().emission); - WriteAttr(elem, "specular", 1, &material->specular, &def->Material().specular); - WriteAttr(elem, "shininess", 1, &material->shininess, &def->Material().shininess); - WriteAttr(elem, "reflectance", 1, &material->reflectance, &def->Material().reflectance); - WriteAttr(elem, "metallic", 1, &material->metallic, &def->Material().metallic); - WriteAttr(elem, "roughness", 1, &material->roughness, &def->Material().roughness); - WriteAttr(elem, "rgba", 4, material->rgba, def->Material().rgba); + WriteAttrTable(elem, static_cast(material), + &def->Material().spec, kMaterialAttrs, kMaterialAttrsN); } @@ -374,8 +353,6 @@ void mjXWriter::OneMaterial(XMLElement* elem, const mjCMaterial* material, mjCDe // write joint void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def, string_view classname) { - double zero = 0; - // regular if (!writingdefaults) { WriteAttrTxt(elem, "name", joint->name); @@ -391,43 +368,15 @@ void mjXWriter::OneJoint(XMLElement* elem, const mjCJoint* joint, mjCDef* def, } // defaults and regular - if (joint->type != def->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); - WriteAttr(elem, "springref", 1, &joint->springref, &zero); - WriteAttr(elem, "solreflimit", mjNREF, joint->solref_limit, def->Joint().solref_limit, true); - WriteAttr(elem, "solimplimit", mjNIMP, joint->solimp_limit, def->Joint().solimp_limit, true); - WriteAttr(elem, "solreffriction", mjNREF, joint->solref_friction, def->Joint().solref_friction, - true); - WriteAttr(elem, "solimpfriction", mjNIMP, joint->solimp_friction, def->Joint().solimp_friction, - true); - { - int nstiff = 1+mjNPOLY; - while (nstiff > 1 && joint->stiffness[nstiff-1] == 0 - && def->Joint().stiffness[nstiff-1] == 0) nstiff--; - WriteAttr(elem, "stiffness", nstiff, joint->stiffness, def->Joint().stiffness); - } + WriteAttrTable(elem, static_cast(joint), &def->Joint().spec, + kJointAttrs, kJointAttrsN); if (joint->type != mjJNT_FREE) { 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", FalseTrueAuto_map, 3, joint->actfrclimited, def->Joint().actfrclimited); } - WriteAttrKey(elem, "actuatorgravcomp", bool_map, 2, joint->actgravcomp, def->Joint().actgravcomp); - WriteAttr(elem, "actuatorfrcrange", 2, joint->actfrcrange, def->Joint().actfrcrange); - WriteAttr(elem, "margin", 1, &joint->margin, &def->Joint().margin); - WriteAttr(elem, "armature", 1, &joint->armature, &def->Joint().armature); - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && joint->damping[ndamp-1] == 0 - && def->Joint().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, joint->damping, def->Joint().damping); - } - WriteAttr(elem, "frictionloss", 1, &joint->frictionloss, &def->Joint().frictionloss); // userdata if (writingdefaults) { @@ -485,23 +434,10 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri } // defaults and regular - 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); - WriteAttrInt(elem, "group", geom->group, def->Geom().group); - WriteAttrInt(elem, "priority", geom->priority, def->Geom().priority); - WriteAttr(elem, "friction", 3, geom->friction, def->Geom().friction, true); - WriteAttr(elem, "solmix", 1, &geom->solmix, &def->Geom().solmix); - WriteAttr(elem, "solref", mjNREF, geom->solref, def->Geom().solref, true); - WriteAttr(elem, "solimp", mjNIMP, geom->solimp, def->Geom().solimp, true); - WriteAttr(elem, "margin", 1, &geom->margin, &def->Geom().margin); - WriteAttr(elem, "gap", 1, &geom->gap, &def->Geom().gap); - WriteAttr(elem, "surfacevel", 6, geom->surfacevel, def->Geom().surfacevel, true); - WriteAttr(elem, "adhesion", 1, &geom->adhesion, &def->Geom().adhesion); + WriteAttrTable(elem, static_cast(geom), &def->Geom().spec, + kGeomAttrs, kGeomAttrsN); WriteAttrKey(elem, "fluidshape", 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", bool_map, 2, geom->typeinertia, def->Geom().typeinertia); @@ -514,7 +450,6 @@ void mjXWriter::OneGeom(XMLElement* elem, const mjCGeom* geom, mjCDef* def, stri if (geom->get_material() != def->Geom().get_material()) { WriteAttrTxt(elem, "material", geom->get_material()); } - WriteAttr(elem, "rgba", 4, geom->rgba, def->Geom().rgba); // hfield and mesh attributes if (geom->type == mjGEOM_HFIELD) { @@ -547,7 +482,6 @@ void mjXWriter::OneSite(XMLElement* elem, const mjCSite* site, mjCDef* def, stri if (classname != site->classname && site->classname != "main") { WriteAttrTxt(elem, "class", site->classname); } - WriteAttr(elem, "pos", 3, site->pos); WriteAttr(elem, "quat", 4, site->quat, unitq); if (mjGEOMINFO[site->type]) { WriteAttr(elem, "size", mjGEOMINFO[site->type], site->size, def->Site().size); @@ -557,12 +491,11 @@ 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", geomtype_map, mjNGEOMTYPES, site->type, def->Site().type); + WriteAttrTable(elem, static_cast(site), &def->Site().spec, + kSiteAttrs, kSiteAttrsN); if (site->get_material() != def->Site().get_material()) { WriteAttrTxt(elem, "material", site->get_material()); } - WriteAttr(elem, "rgba", 4, site->rgba, def->Site().rgba); // userdata if (writingdefaults) { @@ -584,28 +517,12 @@ void mjXWriter::OneCamera(XMLElement* elem, const mjCCamera* camera, mjCDef* def WriteAttrTxt(elem, "class", camera->classname); } WriteAttrTxt(elem, "target", camera->get_targetbody()); - WriteAttr(elem, "pos", 3, camera->pos); WriteAttr(elem, "quat", 4, camera->quat, unitq); } // defaults and regular - WriteAttr(elem, "ipd", 1, &camera->ipd, &def->Camera().ipd); - WriteAttrKey(elem, "mode", camlight_map, camlight_sz, camera->mode, def->Camera().mode); - WriteAttr(elem, "resolution", 2, camera->resolution, def->Camera().resolution); - - // write output attribute if different from default - if (camera->output != def->Camera().output) { - int data[mjNCAMOUT]; - int ndata = 0; - for (int i = 0; i < mjNCAMOUT; i++) { - if (camera->output & camout_map[i].value) { - data[ndata++] = camout_map[i].value; - } - } - WriteAttrKeys(elem, "output", camout_map, camout_sz, data, ndata, 0); - } - - WriteAttrKey(elem, "projection", projection_map, projection_sz, camera->proj, def->Camera().proj); + WriteAttrTable(elem, static_cast(camera), + &def->Camera().spec, kCameraAttrs, kCameraAttrsN); // camera intrinsics if specified if (camera->sensor_size[0] > 0 && camera->sensor_size[1] > 0) { @@ -636,28 +553,86 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, WriteAttrTxt(elem, "class", light->classname); } WriteAttrTxt(elem, "target", light->get_targetbody()); - WriteAttr(elem, "pos", 3, light->pos); - WriteAttr(elem, "dir", 3, light->dir); } // defaults and regular - WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius); - WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); - WriteAttr(elem, "range", 1, &light->range, &def->Light().range); + WriteAttrTable(elem, static_cast(light), &def->Light().spec, + kLightAttrs, kLightAttrsN); WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); WriteAttrTxt(elem, "texture", light->get_texture()); - WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); - WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); - WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation); - WriteAttr(elem, "cutoff", 1, &light->cutoff, &def->Light().cutoff); - WriteAttr(elem, "exponent", 1, &light->exponent, &def->Light().exponent); - WriteAttr(elem, "ambient", 3, light->ambient, def->Light().ambient); - WriteAttr(elem, "diffuse", 3, light->diffuse, def->Light().diffuse); - WriteAttr(elem, "specular", 3, light->specular, def->Light().specular); - WriteAttrKey(elem, "mode", camlight_map, camlight_sz, light->mode, def->Light().mode); } // write pair +// write the mechanical attributes of an element, driven by the same +// generated rows the reader uses; see the declaration for the contract +template +void mjXWriter::WriteAttrTable(XMLElement* elem, const T* obj, const T* def, + const mjXAttr* rows, int nrow) { + const char* live = reinterpret_cast(obj); + const char* dflt = reinterpret_cast(def); + for (int i = 0; i < nrow; i++) { + const mjXAttr& row = rows[i]; + if (row.handwrite || (writingdefaults && row.nodefault)) { + continue; + } + const char* base = live + row.offset; + // a null default object means the element has no defaults to compare + // against: every defined value is written + const char* dbase = dflt ? dflt + row.offset : nullptr; + const int dkey = dflt ? 0 : -12345; // WriteAttrKey's write-always default + switch (row.kind) { + case mjXAttr::kInt: + WriteAttr(elem, row.attr, row.len, (const int*)base, (const int*)dbase, + /*trim=*/!row.exact); + break; + case mjXAttr::kDouble: + WriteAttr(elem, row.attr, row.len, (const double*)base, + (const double*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kNum: + WriteAttr(elem, row.attr, row.len, (const mjtNum*)base, + (const mjtNum*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kFloat: + WriteAttr(elem, row.attr, row.len, (const float*)base, + (const float*)dbase, /*trim=*/!row.exact); + break; + case mjXAttr::kEnum: + WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const int*)base, + dbase ? *(const int*)dbase : dkey); + break; + case mjXAttr::kEnumByte: + WriteAttrKey(elem, row.attr, row.map, row.mapsz, *(const mjtByte*)base, + dbase ? *(const mjtByte*)dbase : dkey); + break; + case mjXAttr::kBool: + WriteAttrKey(elem, row.attr, bool_map, 2, *(const mjtByte*)base, + dbase ? *(const mjtByte*)dbase : dkey); + break; + case mjXAttr::kFlags: + if (!dbase || *(const int*)base != *(const int*)dbase) { + int value = *(const int*)base; + std::vector data; + for (int j = 0; j < row.mapsz; j++) { + if (value & row.map[j].value) { + data.push_back(row.map[j].value); + } + } + if (!data.empty()) { + WriteAttrKeys(elem, row.attr, row.map, row.mapsz, data.data(), + data.size(), 0); + } + } + break; + default: + // names, strings, files and custom-read attributes: OneX() remnant + break; + } + } +} + + + void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) { // regular if (!writingdefaults) { @@ -670,15 +645,8 @@ void mjXWriter::OnePair(XMLElement* elem, const mjCPair* pair, mjCDef* def) { // defaults and regular WriteAttrTxt(elem, "name", pair->name); - WriteAttrInt(elem, "condim", pair->condim, def->Pair().spec.condim); - WriteAttr(elem, "margin", 1, &pair->margin, &def->Pair().spec.margin); - WriteAttr(elem, "gap", 1, &pair->gap, &def->Pair().spec.gap); - WriteAttr(elem, "adhesion", 1, &pair->adhesion, &def->Pair().spec.adhesion); - WriteAttr(elem, "solref", mjNREF, pair->solref, def->Pair().spec.solref, true); - WriteAttr(elem, "solreffriction", mjNREF, pair->solreffriction, def->Pair().spec.solreffriction, - true); - WriteAttr(elem, "solimp", mjNIMP, pair->solimp, def->Pair().spec.solimp, true); - WriteAttr(elem, "friction", 5, pair->friction, def->Pair().spec.friction); // all 5 values + WriteAttrTable(elem, static_cast(pair), &def->Pair().spec, + kPairAttrs, kPairAttrsN); } @@ -745,9 +713,9 @@ void mjXWriter::OneEquality(XMLElement* elem, const mjCEquality* equality, mjCDe } // defaults and regular - WriteAttrKey(elem, "active", bool_map, 2, equality->active, def->Equality().active); - WriteAttr(elem, "solref", mjNREF, equality->solref, def->Equality().solref, true); - WriteAttr(elem, "solimp", mjNIMP, equality->solimp, def->Equality().solimp, true); + WriteAttrTable(elem, static_cast(equality), + &def->Equality().spec, kEqualityBaseAttrs, + kEqualityBaseAttrsN); } @@ -764,46 +732,23 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def } } - // defaults and regular - WriteAttrInt(elem, "group", tendon->group, def->Tendon().group); - WriteAttr(elem, "solreflimit", mjNREF, tendon->solref_limit, def->Tendon().solref_limit, true); - WriteAttr(elem, "solimplimit", mjNIMP, tendon->solimp_limit, def->Tendon().solimp_limit, true); - WriteAttr(elem, "solreffriction", mjNREF, tendon->solref_friction, def->Tendon().solref_friction, - true); - WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction, - true); - 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); - { - int nstiff = 1+mjNPOLY; - while (nstiff > 1 && tendon->stiffness[nstiff-1] == 0 - && def->Tendon().stiffness[nstiff-1] == 0) nstiff--; - WriteAttr(elem, "stiffness", nstiff, tendon->stiffness, def->Tendon().stiffness); + // defaults and regular; the fixed rows are the spatial rows without the + // appearance attributes, which is exactly the tag difference + if (fixed) { + WriteAttrTable(elem, static_cast(tendon), + &def->Tendon().spec, kFixedAttrs, kFixedAttrsN); + } else { + WriteAttrTable(elem, static_cast(tendon), + &def->Tendon().spec, kSpatialAttrs, kSpatialAttrsN); } - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && tendon->damping[ndamp-1] == 0 - && def->Tendon().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, tendon->damping, def->Tendon().damping); - } - WriteAttr(elem, "armature", 1, &tendon->armature, &def->Tendon().armature); - WriteAttr(elem, "frictionloss", 1, &tendon->frictionloss, &def->Tendon().frictionloss); if (tendon->springlength[0] != tendon->springlength[1] || def->Tendon().springlength[0] != def->Tendon().springlength[1]) { WriteAttr(elem, "springlength", 2, tendon->springlength, def->Tendon().springlength); } else { WriteAttr(elem, "springlength", 1, tendon->springlength, def->Tendon().springlength); } - // spatial only - if (!fixed) { - if (tendon->get_material() != def->Tendon().get_material()) { - WriteAttrTxt(elem, "material", tendon->get_material()); - } - WriteAttr(elem, "width", 1, &tendon->width, &def->Tendon().width); - WriteAttr(elem, "rgba", 4, tendon->rgba, def->Tendon().rgba); + if (!fixed && tendon->get_material() != def->Tendon().get_material()) { + WriteAttrTxt(elem, "material", tendon->get_material()); } // userdata @@ -859,28 +804,9 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe } // defaults and regular - WriteAttrInt(elem, "group", actuator->group, def->Actuator().group); - 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", FalseTrueAuto_map, 3, actuator->ctrllimited, def->Actuator().ctrllimited); - WriteAttr(elem, "ctrlrange", 2, actuator->ctrlrange, def->Actuator().ctrlrange); - WriteAttrKey(elem, "forcelimited", FalseTrueAuto_map, 3, actuator->forcelimited, def->Actuator().forcelimited); - WriteAttr(elem, "forcerange", 2, actuator->forcerange, def->Actuator().forcerange); - 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); - { - int ndamp = 1+mjNPOLY; - while (ndamp > 1 && actuator->damping[ndamp-1] == 0 - && def->Actuator().damping[ndamp-1] == 0) ndamp--; - WriteAttr(elem, "damping", ndamp, actuator->damping, def->Actuator().damping); - } - WriteAttr(elem, "armature", 1, &actuator->armature, &def->Actuator().armature); + WriteAttrTable(elem, static_cast(actuator), + &def->Actuator().spec, kGeneralAttrs, kGeneralAttrsN); WriteAttr(elem, "cranklength", 1, &actuator->cranklength, &def->Actuator().cranklength); - WriteAttrKey(elem, "actearly", bool_map, 2, actuator->actearly, - def->Actuator().actearly); // special handling of actdim which has default value of -1 if (writingdefaults) { WriteAttrInt(elem, "actdim", actuator->actdim, def->Actuator().actdim); @@ -888,8 +814,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe int default_actdim = (actuator->dyntype != mjDYN_NONE && actuator->dyntype != mjDYN_DCMOTOR); WriteAttrInt(elem, "actdim", actuator->actdim, default_actdim); } - WriteAttrKey(elem, "dyntype", dyn_map, dyn_sz, actuator->dyntype, def->Actuator().dyntype); - WriteAttr(elem, "dynprm", mjNDYN, actuator->dynprm, def->Actuator().dynprm); // plugins: write config attributes if (actuator->plugin.active) { @@ -911,8 +835,6 @@ void mjXWriter::OneActuator(XMLElement* elem, const mjCActuator* actuator, mjCDe } WriteAttrTxt(elem, "input", tokens); } - WriteAttr(elem, "velrange", 2, actuator->velrange, def->Actuator().velrange); - WriteAttr(elem, "ffrange", 2, actuator->ffrange, def->Actuator().ffrange); WriteAttrKey(elem, "biastype", bias_map, bias_sz, actuator->biastype, def->Actuator().biastype); WriteAttr(elem, "gainprm", mjNGAIN, actuator->gainprm, def->Actuator().gainprm, true); WriteAttr(elem, "biasprm", mjNBIAS, actuator->biasprm, def->Actuator().biasprm, true); @@ -1000,21 +922,21 @@ string mjXWriter::Write(char *error, size_t error_sz) { Compiler(root); Option(root); Size(root); - Visual(root); Statistic(root); + Visual(root); writingdefaults = true; Default(root, model->Default()); writingdefaults = false; Extension(root); - Custom(root); Asset(root); Body(InsertEnd(root, "worldbody"), model->GetWorld(), nullptr); - Contact(root); Deformable(root); - Equality(root); + Contact(root); Tendon(root); + Equality(root); Actuator(root); Sensor(root); + Custom(root); Keyframe(root); return WriteDoc(doc, error, error_sz); @@ -1063,39 +985,8 @@ void mjXWriter::Option(XMLElement* root) { XMLElement* section = InsertEnd(root, "option"); - // option - WriteAttr(section, "timestep", 1, &model->option.timestep, &opt.timestep); - WriteAttr(section, "impratio", 1, &model->option.impratio, &opt.impratio); - WriteAttr(section, "tolerance", 1, &model->option.tolerance, &opt.tolerance); - WriteAttr(section, "ls_tolerance", 1, &model->option.ls_tolerance, &opt.ls_tolerance); - WriteAttr(section, "noslip_tolerance", 1, &model->option.noslip_tolerance, &opt.noslip_tolerance); - WriteAttr(section, "ccd_tolerance", 1, &model->option.ccd_tolerance, &opt.ccd_tolerance); - WriteAttr(section, "sleep_tolerance", 1, &model->option.sleep_tolerance, &opt.sleep_tolerance); - WriteAttr(section, "gravity", 3, model->option.gravity, opt.gravity); - WriteAttr(section, "wind", 3, model->option.wind, opt.wind); - WriteAttr(section, "magnetic", 3, model->option.magnetic, opt.magnetic); - WriteAttr(section, "density", 1, &model->option.density, &opt.density); - WriteAttr(section, "viscosity", 1, &model->option.viscosity, &opt.viscosity); - - WriteAttr(section, "o_margin", 1, &model->option.o_margin, &opt.o_margin); - WriteAttr(section, "o_solref", mjNREF, model->option.o_solref, opt.o_solref); - WriteAttr(section, "o_solimp", mjNIMP, model->option.o_solimp, opt.o_solimp); - WriteAttr(section, "o_friction", 5, model->option.o_friction, opt.o_friction); - - WriteAttrKey(section, "integrator", integrator_map, integrator_sz, - model->option.integrator, opt.integrator); - WriteAttrKey(section, "cone", cone_map, cone_sz, - model->option.cone, opt.cone); - WriteAttrKey(section, "jacobian", jacobian_map, jacobian_sz, - model->option.jacobian, opt.jacobian); - WriteAttrKey(section, "solver", solver_map, solver_sz, - model->option.solver, opt.solver); - WriteAttrInt(section, "iterations", model->option.iterations, opt.iterations); - WriteAttrInt(section, "ls_iterations", model->option.ls_iterations, opt.ls_iterations); - WriteAttrInt(section, "noslip_iterations", model->option.noslip_iterations, opt.noslip_iterations); - WriteAttrInt(section, "ccd_iterations", model->option.ccd_iterations, opt.ccd_iterations); - WriteAttrInt(section, "sdf_iterations", model->option.sdf_iterations, opt.sdf_iterations); - WriteAttrInt(section, "sdf_initpoints", model->option.sdf_initpoints, opt.sdf_initpoints); + // option; the freshly-defaulted struct is the comparison object + WriteAttrTable(section, &model->option, &opt, kOptionAttrs, kOptionAttrsN); // actuator group disable int disabled_groups[31]; @@ -1165,20 +1056,16 @@ void mjXWriter::Size(XMLElement* root) { WriteAttrTxt(section, "memory", mju_writeNumBytes(model->memory)); } - // write sizes + // deprecated sizes, hand-read into locals with range checks WriteAttrInt(section, "njmax", model->njmax, -1); WriteAttrInt(section, "nconmax", model->nconmax, -1); WriteAttrInt(section, "nstack", model->nstack, -1); - WriteAttrInt(section, "nuserdata", model->nuserdata, 0); - WriteAttrInt(section, "nkey", model->nkey, 0); - WriteAttrInt(section, "nuser_body", model->nuser_body, 0); - WriteAttrInt(section, "nuser_jnt", model->nuser_jnt, 0); - WriteAttrInt(section, "nuser_geom", model->nuser_geom, 0); - WriteAttrInt(section, "nuser_site", model->nuser_site, 0); - WriteAttrInt(section, "nuser_cam", model->nuser_cam, 0); - WriteAttrInt(section, "nuser_tendon", model->nuser_tendon, 0); - WriteAttrInt(section, "nuser_actuator", model->nuser_actuator, 0); - WriteAttrInt(section, "nuser_sensor", model->nuser_sensor, 0); + + // write sizes; the spec defaults are -1 (auto), but compilation resolves + // them to the actual counts, so the comparison object is zero-initialized + mjSpec zerospec = {}; + WriteAttrTable(section, static_cast(model), &zerospec, + kSizeAttrs, kSizeAttrsN); // remove entire section if no attributes if (!section->FirstAttribute()) root->DeleteChild(section); @@ -1189,13 +1076,11 @@ void mjXWriter::Size(XMLElement* root) { // statistic section void mjXWriter::Statistic(XMLElement* root) { XMLElement* section = InsertEnd(root, "statistic"); - mjStatistic* s = &model->stat; - if (mjuu_defined(s->meaninertia)) WriteAttr(section, "meaninertia", 1, &s->meaninertia); - if (mjuu_defined(s->meanmass)) WriteAttr(section, "meanmass", 1, &s->meanmass); - if (mjuu_defined(s->meansize)) WriteAttr(section, "meansize", 1, &s->meansize); - if (mjuu_defined(s->extent)) WriteAttr(section, "extent", 1, &s->extent); - if (mjuu_defined(s->center[0])) WriteAttr(section, "center", 3, s->center); + // statistics are unset (mjNAN) rather than defaulted: there is nothing to + // compare against, and WriteAttr skips the undefined values by itself + WriteAttrTable(section, &model->stat, (const mjStatistic*)nullptr, + kStatisticAttrs, kStatisticAttrsN); // remove entire section if no attributes if (!section->FirstAttribute()) root->DeleteChild(section); @@ -1207,123 +1092,25 @@ void mjXWriter::Statistic(XMLElement* root) { void mjXWriter::Visual(XMLElement* root) { mjVisual visdef, *vis = &model->visual; mj_defaultVisual(&visdef); - XMLElement* elem; XMLElement* section = InsertEnd(root, "visual"); - // global - elem = InsertEnd(section, "global"); - WriteAttrInt(elem, "cameraid", vis->global.cameraid, visdef.global.cameraid); - WriteAttrKey(elem, "orthographic", - bool_map, 2, vis->global.orthographic, visdef.global.orthographic); - WriteAttr(elem, "fovy", 1, &vis->global.fovy, &visdef.global.fovy); - WriteAttr(elem, "ipd", 1, &vis->global.ipd, &visdef.global.ipd); - WriteAttr(elem, "azimuth", 1, &vis->global.azimuth, &visdef.global.azimuth); - WriteAttr(elem, "elevation", 1, &vis->global.elevation, &visdef.global.elevation); - WriteAttr(elem, "linewidth", 1, &vis->global.linewidth, &visdef.global.linewidth); - WriteAttr(elem, "glow", 1, &vis->global.glow, &visdef.global.glow); - WriteAttr(elem, "realtime", 1, &vis->global.realtime, &visdef.global.realtime); - WriteAttrInt(elem, "offwidth", vis->global.offwidth, visdef.global.offwidth); - WriteAttrInt(elem, "offheight", vis->global.offheight, visdef.global.offheight); - WriteAttrKey(elem, "ellipsoidinertia", - bool_map, 2, vis->global.ellipsoidinertia, visdef.global.ellipsoidinertia); - WriteAttrKey(elem, "bvactive", bool_map, 2, vis->global.bvactive, visdef.global.bvactive); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // quality - elem = InsertEnd(section, "quality"); - WriteAttrInt(elem, "shadowsize", vis->quality.shadowsize, visdef.quality.shadowsize); - WriteAttrInt(elem, "offsamples", vis->quality.offsamples, visdef.quality.offsamples); - WriteAttrInt(elem, "numslices", vis->quality.numslices, visdef.quality.numslices); - WriteAttrInt(elem, "numstacks", vis->quality.numstacks, visdef.quality.numstacks); - WriteAttrInt(elem, "numquads", vis->quality.numquads, visdef.quality.numquads); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // headlight - elem = InsertEnd(section, "headlight"); - WriteAttr(elem, "ambient", 3, vis->headlight.ambient, visdef.headlight.ambient); - WriteAttr(elem, "diffuse", 3, vis->headlight.diffuse, visdef.headlight.diffuse); - WriteAttr(elem, "specular", 3, vis->headlight.specular, visdef.headlight.specular); - WriteAttrInt(elem, "active", vis->headlight.active, visdef.headlight.active); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // map - elem = InsertEnd(section, "map"); - WriteAttr(elem, "stiffness", 1, &vis->map.stiffness, &visdef.map.stiffness); - WriteAttr(elem, "stiffnessrot", 1, &vis->map.stiffnessrot, &visdef.map.stiffnessrot); - WriteAttr(elem, "force", 1, &vis->map.force, &visdef.map.force); - WriteAttr(elem, "torque", 1, &vis->map.torque, &visdef.map.torque); - WriteAttr(elem, "alpha", 1, &vis->map.alpha, &visdef.map.alpha); - WriteAttr(elem, "fogstart", 1, &vis->map.fogstart, &visdef.map.fogstart); - WriteAttr(elem, "fogend", 1, &vis->map.fogend, &visdef.map.fogend); - WriteAttr(elem, "znear", 1, &vis->map.znear, &visdef.map.znear); - WriteAttr(elem, "zfar", 1, &vis->map.zfar, &visdef.map.zfar); - WriteAttr(elem, "haze", 1, &vis->map.haze, &visdef.map.haze); - WriteAttr(elem, "shadowclip", 1, &vis->map.shadowclip, &visdef.map.shadowclip); - WriteAttr(elem, "shadowscale", 1, &vis->map.shadowscale, &visdef.map.shadowscale); - WriteAttr(elem, "actuatortendon", 1, &vis->map.actuatortendon, &visdef.map.actuatortendon); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // scale - elem = InsertEnd(section, "scale"); - WriteAttr(elem, "forcewidth", 1, &vis->scale.forcewidth, &visdef.scale.forcewidth); - WriteAttr(elem, "contactwidth", 1, &vis->scale.contactwidth, &visdef.scale.contactwidth); - WriteAttr(elem, "contactheight", 1, &vis->scale.contactheight, &visdef.scale.contactheight); - WriteAttr(elem, "connect", 1, &vis->scale.connect, &visdef.scale.connect); - WriteAttr(elem, "com", 1, &vis->scale.com, &visdef.scale.com); - WriteAttr(elem, "camera", 1, &vis->scale.camera, &visdef.scale.camera); - WriteAttr(elem, "light", 1, &vis->scale.light, &visdef.scale.light); - WriteAttr(elem, "selectpoint", 1, &vis->scale.selectpoint, &visdef.scale.selectpoint); - WriteAttr(elem, "jointlength", 1, &vis->scale.jointlength, &visdef.scale.jointlength); - WriteAttr(elem, "jointwidth", 1, &vis->scale.jointwidth, &visdef.scale.jointwidth); - WriteAttr(elem, "actuatorlength", 1, &vis->scale.actuatorlength, &visdef.scale.actuatorlength); - WriteAttr(elem, "actuatorwidth", 1, &vis->scale.actuatorwidth, &visdef.scale.actuatorwidth); - WriteAttr(elem, "framelength", 1, &vis->scale.framelength, &visdef.scale.framelength); - WriteAttr(elem, "framewidth", 1, &vis->scale.framewidth, &visdef.scale.framewidth); - WriteAttr(elem, "constraint", 1, &vis->scale.constraint, &visdef.scale.constraint); - WriteAttr(elem, "slidercrank", 1, &vis->scale.slidercrank, &visdef.scale.slidercrank); - WriteAttr(elem, "frustum", 1, &vis->scale.frustum, &visdef.scale.frustum); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); - } - - // rgba - elem = InsertEnd(section, "rgba"); - WriteAttr(elem, "fog", 4, vis->rgba.fog, visdef.rgba.fog); - WriteAttr(elem, "haze", 4, vis->rgba.haze, visdef.rgba.haze); - WriteAttr(elem, "force", 4, vis->rgba.force, visdef.rgba.force); - WriteAttr(elem, "inertia", 4, vis->rgba.inertia, visdef.rgba.inertia); - WriteAttr(elem, "joint", 4, vis->rgba.joint, visdef.rgba.joint); - WriteAttr(elem, "actuator", 4, vis->rgba.actuator, visdef.rgba.actuator); - WriteAttr(elem, "actuatornegative", 4, vis->rgba.actuatornegative, visdef.rgba.actuatornegative); - WriteAttr(elem, "actuatorpositive", 4, vis->rgba.actuatorpositive, visdef.rgba.actuatorpositive); - WriteAttr(elem, "com", 4, vis->rgba.com, visdef.rgba.com); - WriteAttr(elem, "camera", 4, vis->rgba.camera, visdef.rgba.camera); - WriteAttr(elem, "light", 4, vis->rgba.light, visdef.rgba.light); - WriteAttr(elem, "selectpoint", 4, vis->rgba.selectpoint, visdef.rgba.selectpoint); - WriteAttr(elem, "connect", 4, vis->rgba.connect, visdef.rgba.connect); - WriteAttr(elem, "contactpoint", 4, vis->rgba.contactpoint, visdef.rgba.contactpoint); - WriteAttr(elem, "contactforce", 4, vis->rgba.contactforce, visdef.rgba.contactforce); - WriteAttr(elem, "contactfriction", 4, vis->rgba.contactfriction, visdef.rgba.contactfriction); - WriteAttr(elem, "contacttorque", 4, vis->rgba.contacttorque, visdef.rgba.contacttorque); - WriteAttr(elem, "contactgap", 4, vis->rgba.contactgap, visdef.rgba.contactgap); - WriteAttr(elem, "rangefinder", 4, vis->rgba.rangefinder, visdef.rgba.rangefinder); - WriteAttr(elem, "constraint", 4, vis->rgba.constraint, visdef.rgba.constraint); - WriteAttr(elem, "slidercrank", 4, vis->rgba.slidercrank, visdef.rgba.slidercrank); - WriteAttr(elem, "crankbroken", 4, vis->rgba.crankbroken, visdef.rgba.crankbroken); - WriteAttr(elem, "frustum", 4, vis->rgba.frustum, visdef.rgba.frustum); - WriteAttr(elem, "bv", 4, vis->rgba.bv, visdef.rgba.bv); - WriteAttr(elem, "bvactive", 4, vis->rgba.bvactive, visdef.rgba.bvactive); - if (!elem->FirstAttribute()) { - section->DeleteChild(elem); + // the sub-sections are projections into mjVisual: their rows carry + // member-path offsets, so one struct pair drives them all + struct { const char* tag; const mjXAttr* rows; int n; } subs[] = { + {"global", kGlobalAttrs, kGlobalAttrsN}, + {"quality", kQualityAttrs, kQualityAttrsN}, + {"headlight", kHeadlightAttrs, kHeadlightAttrsN}, + {"map", kMapAttrs, kMapAttrsN}, + {"scale", kScaleAttrs, kScaleAttrsN}, + {"rgba", kRgbaAttrs, kRgbaAttrsN}, + }; + for (const auto& sub : subs) { + XMLElement* elem = InsertEnd(section, sub.tag); + WriteAttrTable(elem, vis, &visdef, sub.rows, sub.n); + if (!elem->FirstAttribute()) { + section->DeleteChild(elem); + } } // remove entire section if no elements diff --git a/src/xml/xml_native_writer.h b/src/xml/xml_native_writer.h index 09962522..7b31fff5 100644 --- a/src/xml/xml_native_writer.h +++ b/src/xml/xml_native_writer.h @@ -62,6 +62,16 @@ class mjXWriter : public mjXBase { // body/world section void Body(tinyxml2::XMLElement* elem, mjCBody* body, mjCFrame* frame, std::string_view childclass = ""); + // table-driven attribute writing: the mechanical attributes of an element, + // driven by the same generated mjXAttr rows the reader uses. obj and def + // are the element's bound spec struct and its class-default counterpart; + // each bound field is compared against the default at the same offset, so + // attributes equal to their default are skipped. Strings, files and + // custom-read attributes remain in the OneX() remnants. + template + void WriteAttrTable(tinyxml2::XMLElement* elem, const T* obj, const T* def, + const struct mjXAttr* rows, int nrow); + // single element writers, used in defaults and main body void OneFlex(tinyxml2::XMLElement* elem, const mjCFlex* pflex); void OneMesh(tinyxml2::XMLElement* elem, const mjCMesh* pmesh, mjCDef* def); diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index aa45b429..64119458 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -1270,6 +1270,10 @@ void mjXUtil::WriteAttrKey(XMLElement* elem, std::string name, // write attribute- space-separated keywords void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map, int mapsz, int* data, int ndata, int def) { + if (ndata <= 0) { + return; + } + // skip default if (ndata == 1 && data[0] == def) { return; diff --git a/test/engine/engine_plugin_test.cc b/test/engine/engine_plugin_test.cc index 4299c39a..853556a7 100644 --- a/test/engine/engine_plugin_test.cc +++ b/test/engine/engine_plugin_test.cc @@ -551,9 +551,11 @@ TEST_F(EnginePluginTest, SaveXml) { int actuator_end = expected_xml.find(actuator_close) + actuator_close.size(); ASSERT_NE(actuator_end, std::string::npos); ASSERT_LE(actuator_start, actuator_end); + // saved attributes follow schema order; dynprm round-trips as written auto expected_actuator_section = absl::StrReplaceAll( expected_xml.substr(actuator_start, actuator_end - actuator_start), - {{"dynprm=\"0.9\"", "dynprm=\"0.9 0 0 0 0 0 0 0 0 0\""}}); + {{"actdim=\"4\" dyntype=\"filter\" dynprm=\"0.9\"", + "dyntype=\"filter\" dynprm=\"0.9\" actdim=\"4\""}}); EXPECT_THAT(saved_xml, HasSubstr(expected_extension_section)); EXPECT_THAT(saved_xml, HasSubstr(expected_sensor_section)); diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index ca305bdc..11fee00f 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -324,7 +324,7 @@ TEST_F(XMLWriterTest, KeepsJointLimitedFalseIfAutoLimits) { MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); - EXPECT_THAT(saved_xml, HasSubstr("limited=\"false\" range=\"-1 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1 1\" limited=\"false\"")); } TEST_F(XMLWriterTest, DoesNotKeepInferredTendonLimited) { @@ -537,7 +537,7 @@ TEST_F(XMLWriterTest, KeepsCtrllimitedFalse) { MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); - EXPECT_THAT(saved_xml, HasSubstr("ctrllimited=\"false\" ctrlrange=\"-1 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("ctrlrange=\"-1 1\" ctrllimited=\"false\"")); } TEST_F(XMLWriterTest, DoesNotKeepInferredForcelimited) { @@ -702,8 +702,8 @@ TEST_F(XMLWriterTest, WritesActuatorDefaults) { ASSERT_THAT(model.get(), NotNull()); std::string saved_xml = SaveAndReadXml(model.get()); EXPECT_THAT(saved_xml, Not(HasSubstr("mass"))); - EXPECT_THAT(saved_xml, HasSubstr( - " - + @@ -930,8 +930,8 @@ TEST_F(XMLWriterTest, WritesSkin) { EXPECT_THAT(model->nskin, 1); char error[1024]; - MjModelPtr mtemp = LoadModelFromString(SaveAndReadXml(model.get()), - error, sizeof(error)); + MjModelPtr mtemp = + LoadModelFromString(SaveAndReadXml(model.get()), error, sizeof(error)); ASSERT_THAT(mtemp.get(), NotNull()) << error; EXPECT_THAT(mtemp->nskin, 1); } @@ -950,7 +950,7 @@ TEST_F(XMLWriterTest, WritesHfield) { // load model MjModelPtr model = LoadModelFromString(xml); ASSERT_THAT(model.get(), NotNull()); - int size = model->hfield_nrow[0]*model->hfield_ncol[0]; + int size = model->hfield_nrow[0] * model->hfield_ncol[0]; EXPECT_EQ(size, 6); // check that the data is normalized and in row-major, bottom-to-top order @@ -1256,9 +1256,7 @@ class XMLWriterLocaleTest : public MujocoTest { } } - void TearDown() override { - std::setlocale(LC_ALL, old_locale_.c_str()); - } + void TearDown() override { std::setlocale(LC_ALL, old_locale_.c_str()); } private: std::string old_locale_; @@ -1304,7 +1302,6 @@ TEST_F(XMLWriterTest, NonRGBTextures) { mj_deleteModel(model); } - // ---------------- test CopyBack functionality (decompiler) ------------------ using DecompilerTest = MujocoTest; TEST_F(DecompilerTest, SavesStatistics) { @@ -1469,7 +1466,8 @@ TEST_F(XMLWriterTest, ExpandAttach) { mj_addBufferVFS(vfs.get(), "b.xml", xml_child, sizeof(xml_child)); std::array er; - MjModelPtr m = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get()); + MjModelPtr m = + LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get()); ASSERT_THAT(m.get(), NotNull()) << er.data(); std::string saved_xml = SaveAndReadXml(m.get()); @@ -1626,5 +1624,19 @@ TEST_F(XMLWriterTest, BodySimpleRoundtrip) { EXPECT_GT(model->nC, model_auto->nC); } +TEST_F(XMLWriterTest, EmptyFlagsAttribute) { + static constexpr char xml[] = R"( + + + + + + )"; + MjModelPtr model = LoadModelFromString(xml); + ASSERT_THAT(model.get(), NotNull()); + std::string saved_xml = SaveAndReadXml(model.get()); + EXPECT_THAT(saved_xml, Not(HasSubstr("output="))); +} + } // namespace } // namespace mujoco