From c23b5e8420f68d875a2fb0cc1391319dd92bd186 Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Fri, 17 Apr 2026 01:28:40 -0700 Subject: [PATCH] Fix WASM build issues, improve test coverage and improve cmake files * Ensure compiler.usethread=0 is set before compilation in single-threaded WASM builds * Explicitly enable exceptions for the single-threaded WASM module * Test the single-threaded module in addition to the multi-threaded module in internal and external CI * Consolidate wasm/tests/CMakeLists.txt into wasm/CMakeLists.txt, fixing the single-threaded benchmark test build which was missing shared linker flags PiperOrigin-RevId: 901149728 Change-Id: If4946e29e5116610782d218906cf3f5940c1cdd0 --- .github/workflows/build_steps.sh | 9 ++-- CMakeLists.txt | 3 -- src/user/user_model.cc | 14 ++++++ wasm/CMakeLists.txt | 76 ++++++++++++++++++++++++-------- wasm/tests/CMakeLists.txt | 63 -------------------------- 5 files changed, 78 insertions(+), 87 deletions(-) delete mode 100644 wasm/tests/CMakeLists.txt diff --git a/.github/workflows/build_steps.sh b/.github/workflows/build_steps.sh index 4c55e5d4..c173333f 100755 --- a/.github/workflows/build_steps.sh +++ b/.github/workflows/build_steps.sh @@ -212,25 +212,28 @@ build_test_wasm() { echo "Building and testing WASM bindings..." source emsdk/emsdk_env.sh export PATH="$(pwd)/node_modules/.bin:$PATH" - - echo "Building Multi-Threaded version..." + echo "Build MuJoCo with Emscripten (Multi-Threaded)..." emcmake cmake -B build_wasm_mt \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF \ -DMUJOCO_WASM_THREADS=ON \ $WASM_CMAKE_ARGS cmake --build build_wasm_mt --parallel $(nproc) + echo "Run bindings tests for Multi-Threaded version..." + npm run test --prefix ./wasm + echo "Moving Multi-Thread version under mt subfolder..." mkdir -p wasm/dist/mt mv wasm/dist/mujoco.* wasm/dist/mt/ - echo "Building Single-Threaded version..." + echo "Build MuJoCo with Emscripten (Single-Threaded)..." emcmake cmake -B build_wasm_st \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF \ -DMUJOCO_WASM_THREADS=OFF \ $WASM_CMAKE_ARGS cmake --build build_wasm_st --parallel $(nproc) + echo "Run bindings tests for Single-Threaded version..." npm run test --prefix ./wasm } diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c349167..36a64004 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,9 +132,6 @@ endif() if(EMSCRIPTEN) add_subdirectory(wasm) - if(MUJOCO_BUILD_TESTS_WASM) - add_subdirectory(wasm/tests) - endif() endif() diff --git a/src/user/user_model.cc b/src/user/user_model.cc index f3206d13..64048736 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -4967,6 +4967,20 @@ void mjCModel::ResolveKeyframes(const mjModel* m) { } void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { +#if defined(__EMSCRIPTEN__) && !defined(MUJOCO_WASM_THREADS) + // The MuJoCo compiler defaults to usethread=1, which causes it to try to + // create pthreads for compilation. In the single-threaded WASM build, this + // crashes because there is no threading support, so we disable threading on + // the internal compiler struct (not the spec) to avoid permanently mutating + // the spec (which would cause usethread="false" to appear in a saved XML). + struct ScopedDisableThreading { + mjtByte& ref; + mjtByte saved; + explicit ScopedDisableThreading(mjtByte& r) : ref(r), saved(r) { ref = 0; } + ~ScopedDisableThreading() { ref = saved; } + } disable_usethread(compiler.usethread); +#endif + // check if nan test works double test = mjNAN; if (mjuu_defined(test)) { diff --git a/wasm/CMakeLists.txt b/wasm/CMakeLists.txt index 80ed4feb..158e6b11 100644 --- a/wasm/CMakeLists.txt +++ b/wasm/CMakeLists.txt @@ -22,16 +22,14 @@ include_directories(${PROJECT_SOURCE_DIR}) link_directories(${CMAKE_BINARY_DIR}/lib) -file(GLOB MUJOCO_WASM_FILES - "codegen/generated/*.cc" - "unpack.cc" -) +# Set Emscripten compile flags. +# -fexceptions is required for val::throw_() (used by ThrowMujocoErrorToJS) to +# actually throw a JS exception. Without it, Emscripten compiles throw as a +# no-op. In the MT build, -pthread implicitly enables exception support, but +# the ST build needs it set explicitly here. +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions") -if(NOT MUJOCO_WASM_FILES) - message(FATAL_ERROR "No source files found in codegen/generated/") -endif() - -# Set Emscripten linker flags +# Set Emscripten linker flags shared by all WASM targets. set(EMCC_LINKER_FLAGS "--bind" "-s ASSERTIONS=1" @@ -42,9 +40,9 @@ set(EMCC_LINKER_FLAGS "-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','MEMFS']" "-s EXPORT_NAME=loadMujoco" "-s DISABLE_EXCEPTION_CATCHING=0" + "-fexceptions" "-gsource-map" "-g" - "--emit-tsd mujoco.d.ts" ) if(MUJOCO_WASM_THREADS) list(APPEND EMCC_LINKER_FLAGS @@ -53,7 +51,28 @@ if(MUJOCO_WASM_THREADS) ) add_definitions(-DMUJOCO_WASM_THREADS) endif() -string (REPLACE ";" " " EMCC_LINKER_FLAGS_STR "${EMCC_LINKER_FLAGS}") + +# Common link libraries for all WASM targets. The mujoco library is linked as a +# whole archive to avoid losing plugin registration such as obj_decoder and +# stl_decoder. +set(MUJOCO_WASM_LINK_LIBRARIES + -Wl,--whole-archive mujoco -Wl,--no-whole-archive + ccd lodepng tinyxml2 qhullstatic_r +) + +# --- Main WASM bindings target --- + +file(GLOB MUJOCO_WASM_FILES + "codegen/generated/*.cc" + "unpack.cc" +) + +if(NOT MUJOCO_WASM_FILES) + message(FATAL_ERROR "No source files found in codegen/generated/") +endif() + +set(MUJOCO_WASM_LINKER_FLAGS ${EMCC_LINKER_FLAGS} "--emit-tsd mujoco.d.ts") +string(REPLACE ";" " " MUJOCO_WASM_LINKER_FLAGS_STR "${MUJOCO_WASM_LINKER_FLAGS}") add_executable(mujoco_wasm ${MUJOCO_WASM_FILES}) @@ -61,15 +80,36 @@ add_executable(mujoco_wasm ${MUJOCO_WASM_FILES}) # `mujoco` library target, but emit artifacts named `mujoco.*` by setting the # output name. Also apply the emscripten linker flags to the wasm target. set_target_properties(mujoco_wasm PROPERTIES - LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}" + LINK_FLAGS "${MUJOCO_WASM_LINKER_FLAGS_STR}" OUTPUT_NAME "mujoco" ) -# Link the mujoco library as a whole archive to avoid losing plugin -# registration such as obj_decoder and stl_decoder. -target_link_libraries(mujoco_wasm PRIVATE - -Wl,--whole-archive mujoco -Wl,--no-whole-archive - ccd lodepng tinyxml2 qhullstatic_r -) +target_link_libraries(mujoco_wasm PRIVATE ${MUJOCO_WASM_LINK_LIBRARIES}) install(TARGETS mujoco_wasm DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR}) + +# --- Benchmark target --- + +if(MUJOCO_BUILD_TESTS_WASM) + file(GLOB MUJOCO_WASM_BENCHMARK_FILES + "tests/benchmark_test.cc" + "unpack.cc" + ) + + if(NOT MUJOCO_WASM_BENCHMARK_FILES) + message(FATAL_ERROR "No benchmark source files found") + endif() + + set(BENCHMARK_LINKER_FLAGS ${EMCC_LINKER_FLAGS} "--emit-tsd mujoco_wasm_benchmark.d.ts") + string(REPLACE ";" " " BENCHMARK_LINKER_FLAGS_STR "${BENCHMARK_LINKER_FLAGS}") + + add_executable(mujoco_wasm_benchmark ${MUJOCO_WASM_BENCHMARK_FILES}) + + set_target_properties(mujoco_wasm_benchmark PROPERTIES + LINK_FLAGS "${BENCHMARK_LINKER_FLAGS_STR}" + ) + + target_link_libraries(mujoco_wasm_benchmark PRIVATE ${MUJOCO_WASM_LINK_LIBRARIES}) + + install(TARGETS mujoco_wasm_benchmark DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR}) +endif() diff --git a/wasm/tests/CMakeLists.txt b/wasm/tests/CMakeLists.txt deleted file mode 100644 index f6d74289..00000000 --- a/wasm/tests/CMakeLists.txt +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2025 DeepMind Technologies Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/wasm/dist") - -set(CMAKE_INSTALL_PREFIX ${PROJECT_SOURCE_DIR}/wasm) - -include_directories(${PROJECT_SOURCE_DIR}/include) -include_directories(${PROJECT_SOURCE_DIR}/src) -include_directories(${PROJECT_SOURCE_DIR}) - -link_directories(${CMAKE_BINARY_DIR}/lib) - -file(GLOB MUJOCO_WASM_FILES - "benchmark_test.cc" - "../unpack.cc" -) - -if(NOT MUJOCO_WASM_FILES) - message(FATAL_ERROR "No source files found") -endif() - -# Set Emscripten linker flags -set(EMCC_LINKER_FLAGS - "--bind" - "-pthread" - "-s PTHREAD_POOL_SIZE=navigator.hardwareConcurrency" - "-s ASSERTIONS=1" - "-s ALLOW_MEMORY_GROWTH=1" - "-s EXPORT_ES6=1" - "-s MODULARIZE=1" - "-s FORCE_FILESYSTEM=1" - "-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','MEMFS']" - "-s EXPORT_NAME=loadMujoco" - "-gsource-map" - "-g" - "--emit-tsd mujoco_wasm_benchmark.d.ts" -) -string (REPLACE ";" " " EMCC_LINKER_FLAGS_STR "${EMCC_LINKER_FLAGS}") - -add_executable(mujoco_wasm_benchmark ${MUJOCO_WASM_FILES}) - -set_target_properties(mujoco_wasm_benchmark PROPERTIES LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}") - -# Link the mujoco library as a whole archive to avoid losing plugin -# registration such as obj_decoder and stl_decoder. -target_link_libraries(mujoco_wasm_benchmark PRIVATE - -Wl,--whole-archive mujoco -Wl,--no-whole-archive - ccd lodepng tinyxml2 qhullstatic_r -) - -install(TARGETS mujoco_wasm_benchmark DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})