From 477a14dcb28a29851a283ebd4c5d4979a5e81292 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Mon, 15 Dec 2025 11:56:41 -0800 Subject: [PATCH] Add WASM binding for `mj_compile`. It does not support model assets management via VFS yet. PiperOrigin-RevId: 844864882 Change-Id: I422d7896ed024fc6f5c3fa923a58cdc9059f3bee --- wasm/codegen/generated/bindings.cc | 11 +++++++++++ wasm/codegen/generators/constants.py | 2 +- wasm/codegen/templates/bindings.cc | 10 ++++++++++ wasm/tests/bindings_test.ts | 24 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index a0763b9e..27ce3adb 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -33,6 +33,7 @@ #include #include #include +#include #include "engine/engine_util_errmem.h" #include "wasm/unpack.h" @@ -7823,6 +7824,15 @@ std::unique_ptr parseXMLString_wrapper(const std::string &xml) { return std::unique_ptr(new MjSpec(ptr)); } +std::unique_ptr mj_compile_wrapper(const MjSpec& spec) { + mjSpec* spec_ptr = spec.get(); + mjModel* model = mj_compile(spec_ptr, nullptr); + if (!model || mjs_isWarning(spec_ptr)) { + mju_error("%s", mjs_getError(spec_ptr)); + } + return std::unique_ptr(new MjModel(model)); +} + void error_wrapper(const String& msg) { mju_error("%s\n", msg.as().data()); } int mj_saveLastXML_wrapper(const String& filename, const MjModel& m) { @@ -12308,6 +12318,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { function("mj_comPos", &mj_comPos_wrapper); function("mj_comVel", &mj_comVel_wrapper); function("mj_compareFwdInv", &mj_compareFwdInv_wrapper); + function("mj_compile", &mj_compile_wrapper); function("mj_constraintUpdate", &mj_constraintUpdate_wrapper); function("mj_contactForce", &mj_contactForce_wrapper); function("mj_copyBack", &mj_copyBack_wrapper); diff --git a/wasm/codegen/generators/constants.py b/wasm/codegen/generators/constants.py index 1cb0ef23..8bd3d71f 100644 --- a/wasm/codegen/generators/constants.py +++ b/wasm/codegen/generators/constants.py @@ -64,7 +64,6 @@ _SKIPPED_PLUGIN_FUNCTIONS: tuple[str, ...] = ( # Functions that are bound as class methods _SKIPPED_CLASS_METHODS: tuple[str, ...] = ( # go/keep-sorted start - "mj_compile", "mj_copyData", "mj_copyModel", "mj_copySpec", @@ -192,6 +191,7 @@ _SKIPPED_UTILITY_FUNCTIONS: tuple[str, ...] = ( # These functions are not bound automatically but are written by hand instead. MANUAL_WRAPPER_FUNCTIONS: tuple[str, ...] = ( # go/keep-sorted start + "mj_compile", "mj_saveLastXML", "mj_setLengthRange", "mju_error", diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index e74ab501..dfae0925 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -33,6 +33,7 @@ #include #include #include +#include #include "engine/engine_util_errmem.h" #include "wasm/unpack.h" @@ -406,6 +407,15 @@ std::unique_ptr parseXMLString_wrapper(const std::string &xml) { return std::unique_ptr(new MjSpec(ptr)); } +std::unique_ptr mj_compile_wrapper(const MjSpec& spec) { + mjSpec* spec_ptr = spec.get(); + mjModel* model = mj_compile(spec_ptr, nullptr); + if (!model || mjs_isWarning(spec_ptr)) { + mju_error("%s", mjs_getError(spec_ptr)); + } + return std::unique_ptr(new MjModel(model)); +} + void error_wrapper(const String& msg) { mju_error("%s\n", msg.as().data()); } int mj_saveLastXML_wrapper(const String& filename, const MjModel& m) { diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index cc14f9dd..56614d8b 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -1802,4 +1802,28 @@ describe('MuJoCo WASM Bindings', () => { unlinkXMLFile(tempXmlFilename); } }); + + it('should compile a spec from XML string', () => { + let spec = null; + let model = null; + try { + spec = mujoco.parseXMLString(TEST_XML); + expect(spec).not.toBeNull(); + + model = mujoco.mj_compile(spec); + expect(model).not.toBeNull(); + expect(model.nq).toBeGreaterThan(0); + + const jointId = + mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT.value, 'myhinge'); + expect(jointId).toBeGreaterThanOrEqual(0); + } finally { + if (spec) { + spec.delete(); + } + if (model) { + model.delete(); + } + } + }); });