Add special case support for MJB and TXT files to mj_encode.

PiperOrigin-RevId: 939875974
Change-Id: I57fc753871b348a7f6342aca0a892e74acb74173
This commit is contained in:
Sam Haves
2026-06-29 09:31:49 -07:00
committed by Copybara-Service
parent d83ef0b6b9
commit 4e1795b9a4
5 changed files with 182 additions and 11 deletions
+12 -3
View File
@@ -66,11 +66,20 @@ Parse spec from a file.
.. mujoco-include:: mj_encode
Encode spec/model to a file using a registered encoder.
Encode :ref:`mjSpec` or :ref:`mjModel` to a file. The output format is determined by the file extension (case insensitive) or
``content_type``. Returns the number of bytes written on success, -1 on failure.
Returns the number of bytes written on success, -1 on failure.
The following formats are supported natively, without a registered encoder:
*Nullable:* ``m``, ``vfs``, ``error``
- **MJCF XML** — extension: ``.xml``, content_type: ``text/xml``. If an :ref:`mjSpec` is provided, saves via
:ref:`mj_saveXML`. Otherwise falls back to :ref:`mj_saveLastXML`, which requires a compiled :ref:`mjModel`.
- **MJB** — extension: ``.mjb``. MuJoCo binary format. Requires a compiled :ref:`mjModel`.
- **TXT** — extension: ``.txt``, content_type: ``text/plain``. Human-readable text dump via :ref:`mj_printModel`.
Requires a compiled :ref:`mjModel`.
For all other formats, a registered encoder is looked up via :ref:`mjp_findEncoder`.
*Nullable:* ``s``, ``m``, ``vfs``, ``error``
.. _mj_compile:
+17
View File
@@ -72,6 +72,23 @@ the required size. XML saving automatically compiles the spec before saving.
Save spec to XML file, return 0 on success, -1 otherwise. XML saving requires that the spec first be compiled.
.. _mj_encode:
Encode :ref:`mjSpec` or :ref:`mjModel` to a file. The output format is determined by the file extension (case insensitive) or
``content_type``. Returns the number of bytes written on success, -1 on failure.
The following formats are supported natively, without a registered encoder:
- **MJCF XML** — extension: ``.xml``, content_type: ``text/xml``. If an :ref:`mjSpec` is provided, saves via
:ref:`mj_saveXML`. Otherwise falls back to :ref:`mj_saveLastXML`, which requires a compiled :ref:`mjModel`.
- **MJB** — extension: ``.mjb``. MuJoCo binary format. Requires a compiled :ref:`mjModel`.
- **TXT** — extension: ``.txt``, content_type: ``text/plain``. Human-readable text dump via :ref:`mj_printModel`.
Requires a compiled :ref:`mjModel`.
For all other formats, a registered encoder is looked up via :ref:`mjp_findEncoder`.
*Nullable:* ``s``, ``m``, ``vfs``, ``error``
.. _Mainsimulation:
These are the main entry points to the simulator. Most users will only need to call :ref:`mj_step`, which computes
+2
View File
@@ -10,6 +10,8 @@ General
- Added Nesterov momentum extrapolation with adaptive gradient restart (O'Donoghue-Candès) to the PGS solver,
significantly improving convergence. Overall PGS now requires ~2x fewer iterations.
* :ref:`mj_encode` now supports encoding of MJB and TXT files.
.. admonition:: Breaking API changes
:class: attention
+86 -8
View File
@@ -15,6 +15,7 @@
#include "user/user_api.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdio>
@@ -145,20 +146,97 @@ mjSpec* mj_parse(const char* filename, const char* content_type,
return spec;
}
// Encode spec or model as MJCF XML.
//
// If a spec is provided, it is saved directly to XML. This preserves the original
// structure and any user modifications in the spec.
// If spec is null, the model must be provided, and it is saved using mj_saveLastXML.
// mj_saveLastXML reconstructs the XML from the compiled model state, which may
// differ from the original XML (e.g., losing comments, reordering elements) but
// reflects the actual compiled model.
//
// Returns file size in bytes on success, -1 on failure.
static mjtSize encode_xml(const mjSpec* s, const mjModel* m, const char* filename,
char* error, int error_sz) {
if (s) {
// Save directly from the spec
if (mj_saveXML(s, filename, error, error_sz) < 0) {
return -1;
}
} else {
if (!m) {
if (error) {
strncpy(error, "model is required for XML encoding when spec is null",
error_sz);
error[error_sz - 1] = '\0';
}
return -1;
}
// Reconstruct XML from the compiled model, this will copy values back
// from the mjModel into the last compiled spec and write that out
// to disk. If there was no last compiled spec, such as when loading from
// MJB, this will return fail and we return -1.
if (!mj_saveLastXML(filename, m, error, error_sz)) {
return -1;
}
}
return static_cast<mjtSize>(std::filesystem::file_size(filename));
}
// Encode model as MJB (MuJoCo binary format).
// Requires a compiled model; spec-only encoding is not supported.
// Returns file size in bytes on success, -1 on failure.
static mjtSize encode_mjb(const mjModel* m, const char* filename, char* error, int error_sz) {
if (!m) {
if (error) {
strncpy(error, "model is required for MJB encoding", error_sz);
error[error_sz - 1] = '\0';
}
return -1;
}
mj_saveModel(m, filename, nullptr, 0);
return static_cast<mjtSize>(std::filesystem::file_size(filename));
}
// Encode model as human-readable TXT (via mj_printModel).
// Requires a compiled model; spec-only encoding is not supported.
// Returns file size in bytes on success, -1 on failure.
static mjtSize encode_txt(const mjModel* m, const char* filename, char* error,
int error_sz) {
if (!m) {
if (error) {
strncpy(error, "model is required for TXT encoding", error_sz);
error[error_sz - 1] = '\0';
}
return -1;
}
mj_printModel(m, filename);
return static_cast<mjtSize>(std::filesystem::file_size(filename));
}
// encode spec/model to file
mjtSize mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
const char* content_type, const mjVFS* vfs, char* error,
int error_sz) {
// TODO(shaves) Move MJCF and URDF to encoders/decoders.
// special case handling
// TODO(shaves) write encoder/decoder paths for MJCF, TXT, MJB
auto filepath = mujoco::user::FilePath(filename);
if (filepath.Ext() == ".xml" ||
(content_type && std::strcmp(content_type, "text/xml") == 0)) {
int result = mj_saveXML(s, filename, error, error_sz);
if (result < 0) {
return -1;
}
std::string ext = filepath.Ext();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return static_cast<mjtSize>(std::filesystem::file_size(filename));
if (ext == ".xml" ||
(content_type && std::strcmp(content_type, "text/xml") == 0)) {
return encode_xml(s, m, filename, error, error_sz);
}
if (ext == ".mjb") {
return encode_mjb(m, filename, error, error_sz);
}
if (ext == ".txt" ||
(content_type && std::strcmp(content_type, "text/plain") == 0)) {
return encode_txt(m, filename, error, error_sz);
}
const mjpEncoder* encoder = mjp_findEncoder(filename, content_type);
+65
View File
@@ -18,6 +18,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
@@ -3537,5 +3538,69 @@ TEST_F(MujocoTest, CompilationWarningsClearedOnRecompile) {
mj_deleteSpec(spec);
}
TEST_F(MujocoTest, MjEncodeNativeFormats) {
// simple test spec
mjSpec* spec = mj_makeSpec();
mjsBody* world = mjs_findBody(spec, "world");
mjsBody* body = mjs_addBody(world, nullptr);
mjsGeom* geom = mjs_addGeom(body, nullptr);
geom->size[0] = 1.0;
geom->size[1] = 1.0;
geom->size[2] = 1.0;
mjModel* model = mj_compile(spec, nullptr);
ASSERT_THAT(model, NotNull());
std::filesystem::path tmp_dir = std::filesystem::temp_directory_path();
std::string xml_path = (tmp_dir / "test_encode.xml").string();
std::string mjb_path = (tmp_dir / "test_encode.mjb").string();
std::string txt_path = (tmp_dir / "test_encode.txt").string();
char error[1000];
// XML Encoding with spec
EXPECT_GT(mj_encode(spec, model, xml_path.c_str(), nullptr, nullptr, error,
sizeof(error)),
0);
EXPECT_TRUE(std::filesystem::exists(xml_path));
EXPECT_GT(std::filesystem::file_size(xml_path), 0);
// XML Encoding without spec (should fail because no XML loaded in global
// spec)
std::filesystem::remove(xml_path);
// free global spec
mj_freeLastXML();
EXPECT_EQ(mj_encode(nullptr, model, xml_path.c_str(), nullptr, nullptr, error,
sizeof(error)),
-1);
EXPECT_THAT(error, HasSubstr("No XML model loaded"));
EXPECT_TRUE(std::filesystem::exists(xml_path));
EXPECT_EQ(std::filesystem::file_size(xml_path), 0);
std::filesystem::remove(xml_path);
// MJB Encoding (spec can be null)
EXPECT_GT(mj_encode(nullptr, model, mjb_path.c_str(), nullptr, nullptr, error,
sizeof(error)),
0);
EXPECT_TRUE(std::filesystem::exists(mjb_path));
EXPECT_GT(std::filesystem::file_size(mjb_path), 0);
mjModel* model2 = mj_loadModel(mjb_path.c_str(), nullptr);
EXPECT_THAT(model2, NotNull());
mj_deleteModel(model2);
// TXT Encoding (spec can be null)
EXPECT_GT(mj_encode(nullptr, model, txt_path.c_str(), nullptr, nullptr, error,
sizeof(error)),
0);
EXPECT_TRUE(std::filesystem::exists(txt_path));
EXPECT_GT(std::filesystem::file_size(txt_path), 0);
// Clean up
std::filesystem::remove(mjb_path);
std::filesystem::remove(txt_path);
mj_deleteModel(model);
mj_deleteSpec(spec);
}
} // namespace
} // namespace mujoco