Add JavaScript bindings and WASM support
Co-authored-by: Matija Kecman <matijak@google.com> Co-authored-by: Sebastian Noreña Rendón <sebas.norena@creativa77.com.ar> Co-authored-by: Kyle Bayes <kylebayes@google.com> PiperOrigin-RevId: 826094938 Change-Id: Id2658a187bc078056f4410c71202696753319e64
This commit is contained in:
committed by
Copybara-Service
parent
10f4b36fb7
commit
76e64d96bb
@@ -136,6 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare Linux
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: >
|
||||
@@ -148,12 +149,15 @@ jobs:
|
||||
libxrandr-dev
|
||||
libxi-dev
|
||||
ninja-build
|
||||
|
||||
- name: Prepare macOS
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: brew install ninja
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Prepare Python
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -169,6 +173,26 @@ jobs:
|
||||
source venv/bin/activate
|
||||
python -m pip install --upgrade --require-hashes -r "${repo}/python/build_requirements.txt"
|
||||
python -m pip install --upgrade --require-hashes -r "${repo}/python/build_requirements_usd.txt"
|
||||
|
||||
- name: Setup Node.js for WASM
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install NPM Dependencies for WASM
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
working-directory: wasm
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Setup Emscripten
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
./emsdk/emsdk install 4.0.10
|
||||
./emsdk/emsdk activate 4.0.10
|
||||
|
||||
- name: Configure MuJoCo
|
||||
run: >
|
||||
mkdir build &&
|
||||
@@ -179,9 +203,11 @@ jobs:
|
||||
-DCMAKE_INSTALL_PREFIX:STRING=${{ matrix.tmpdir }}/mujoco_install
|
||||
-DMUJOCO_BUILD_EXAMPLES:BOOL=OFF
|
||||
${{ matrix.cmake_args }}
|
||||
|
||||
- name: Build MuJoCo
|
||||
working-directory: build
|
||||
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
|
||||
|
||||
- name: Copy in the correct VC runtime DLLs (workaround for actions/runner-images#10004)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
working-directory: build
|
||||
@@ -192,12 +218,15 @@ jobs:
|
||||
-Path "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC\14.*" |
|
||||
Sort -Descending | Select-Object -First 1).FullName
|
||||
) 'x64\Microsoft.VC143.CRT\*.dll') "bin\Release"
|
||||
|
||||
- name: Test MuJoCo
|
||||
working-directory: build
|
||||
run: ctest -C Release --output-on-failure .
|
||||
|
||||
- name: Install MuJoCo
|
||||
working-directory: build
|
||||
run: cmake --install .
|
||||
|
||||
- name: Copy plugins (POSIX)
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
working-directory: build
|
||||
@@ -206,6 +235,7 @@ jobs:
|
||||
cp lib/libelasticity.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
|
||||
cp lib/libsensor.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
|
||||
cp lib/libsdf_plugin.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin
|
||||
|
||||
- name: Copy plugins (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
working-directory: build
|
||||
@@ -213,6 +243,7 @@ jobs:
|
||||
cp bin/Release/actuator.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
|
||||
cp bin/Release/elasticity.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin &&
|
||||
cp bin/Release/sensor.dll ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin
|
||||
|
||||
- name: Configure samples
|
||||
working-directory: sample
|
||||
run: >
|
||||
@@ -223,9 +254,11 @@ jobs:
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
|
||||
-Dmujoco_ROOT:STRING=${{ matrix.tmpdir }}/mujoco_install
|
||||
${{ matrix.cmake_args }}
|
||||
|
||||
- name: Build samples
|
||||
working-directory: sample/build
|
||||
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
|
||||
|
||||
- name: Configure simulate
|
||||
working-directory: simulate
|
||||
run: >
|
||||
@@ -236,15 +269,18 @@ jobs:
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
|
||||
-Dmujoco_ROOT:STRING=${{ matrix.tmpdir }}/mujoco_install
|
||||
${{ matrix.cmake_args }}
|
||||
|
||||
- name: Build simulate
|
||||
working-directory: simulate/build
|
||||
run: cmake --build . --config=Release ${{ matrix.cmake_build_args }}
|
||||
|
||||
- name: Make Python sdist
|
||||
shell: bash
|
||||
working-directory: python
|
||||
run: >
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
./make_sdist.sh
|
||||
|
||||
- name: Build Python bindings
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -255,6 +291,7 @@ jobs:
|
||||
MUJOCO_PLUGIN_PATH="${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin"
|
||||
MUJOCO_CMAKE_ARGS="-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF ${{ matrix.cmake_args }}"
|
||||
pip wheel -v --no-deps mujoco-*.tar.gz
|
||||
|
||||
- name: Install Python bindings
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -262,6 +299,7 @@ jobs:
|
||||
run: >
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
pip install --no-index mujoco-*.whl
|
||||
|
||||
- name: Test Python bindings
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -270,6 +308,19 @@ jobs:
|
||||
run: >
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
pytest -v --pyargs mujoco
|
||||
|
||||
- name: Build and Test WASM bindings
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
source emsdk/emsdk_env.sh
|
||||
export PATH="$(pwd)/node_modules/.bin:$PATH"
|
||||
|
||||
emcmake cmake -B build_wasm -DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF
|
||||
cmake --build build_wasm
|
||||
|
||||
npm run test --prefix ./wasm
|
||||
|
||||
- name: Package MJX
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -277,6 +328,7 @@ jobs:
|
||||
run:
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
python -m build .
|
||||
|
||||
- name: Install MJX
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -285,6 +337,7 @@ jobs:
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
pip install --require-hashes -r requirements.txt &&
|
||||
pip install --no-index dist/mujoco_mjx-*.whl
|
||||
|
||||
- name: Test MJX
|
||||
if: ${{ runner.os != 'Windows' }}
|
||||
shell: bash
|
||||
@@ -292,6 +345,7 @@ jobs:
|
||||
run:
|
||||
source ${{ matrix.tmpdir }}/venv/bin/activate &&
|
||||
pytest -n auto -v -k 'not IntegrationTest' --pyargs mujoco.mjx
|
||||
|
||||
- name: Notify team chat
|
||||
shell: bash
|
||||
env:
|
||||
@@ -324,3 +378,4 @@ jobs:
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "${CHATMSG}"
|
||||
|
||||
|
||||
@@ -37,3 +37,8 @@ MUJOCO_LOG.TXT
|
||||
|
||||
# Clang cache
|
||||
.cache/
|
||||
|
||||
# JavaScript bindings build
|
||||
wasm/**/dist/
|
||||
**/node_modules/
|
||||
|
||||
|
||||
+42
-16
@@ -39,14 +39,20 @@ enable_language(CXX)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
|
||||
|
||||
option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON)
|
||||
option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON)
|
||||
option(MUJOCO_BUILD_STUDIO "Build studio library for MuJoCo" OFF)
|
||||
option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON)
|
||||
option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON)
|
||||
option(MUJOCO_WITH_USD "Build with OpenUSD" OFF)
|
||||
option(MUJOCO_USE_FILAMENT "Use filament rendering" OFF)
|
||||
option(MUJOCO_USE_FILAMENT_VULKAN "Use vulkan backend for filament rendering" OFF)
|
||||
if(NOT EMSCRIPTEN)
|
||||
option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON)
|
||||
option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON)
|
||||
option(MUJOCO_BUILD_STUDIO "Build studio library for MuJoCo" OFF)
|
||||
option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON)
|
||||
option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON)
|
||||
option(MUJOCO_WITH_USD "Build with OpenUSD" OFF)
|
||||
option(MUJOCO_USE_FILAMENT "Use filament rendering" OFF)
|
||||
option(MUJOCO_USE_FILAMENT_VULKAN "Use vulkan backend for filament rendering" OFF)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
option(MUJOCO_BUILD_TESTS_WASM "Build tests for WASM bindings" ON)
|
||||
endif()
|
||||
|
||||
# Option to provide a path to an existing USD build directory or to Houdini HFS directory.
|
||||
set(USD_DIR "" CACHE PATH "Path to an existing USD build directory.")
|
||||
@@ -100,7 +106,13 @@ else()
|
||||
set(MUJOCO_RESOURCE_FILES "")
|
||||
endif()
|
||||
|
||||
add_library(mujoco SHARED ${MUJOCO_RESOURCE_FILES})
|
||||
# Emscripten does not support SHARED libs
|
||||
if(NOT EMSCRIPTEN)
|
||||
add_library(mujoco SHARED ${MUJOCO_RESOURCE_FILES})
|
||||
else()
|
||||
add_library(mujoco STATIC ${MUJOCO_RESOURCE_FILES})
|
||||
endif()
|
||||
|
||||
target_include_directories(
|
||||
mujoco
|
||||
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
@@ -108,27 +120,36 @@ target_include_directories(
|
||||
PRIVATE src
|
||||
)
|
||||
|
||||
add_subdirectory(plugin/elasticity)
|
||||
add_subdirectory(plugin/actuator)
|
||||
add_subdirectory(plugin/sensor)
|
||||
add_subdirectory(plugin/sdf)
|
||||
if(NOT EMSCRIPTEN)
|
||||
add_subdirectory(plugin/elasticity)
|
||||
add_subdirectory(plugin/actuator)
|
||||
add_subdirectory(plugin/sensor)
|
||||
add_subdirectory(plugin/sdf)
|
||||
endif()
|
||||
add_subdirectory(src/engine)
|
||||
add_subdirectory(src/user)
|
||||
add_subdirectory(src/xml)
|
||||
add_subdirectory(src/thread)
|
||||
|
||||
if(MUJOCO_USE_FILAMENT)
|
||||
if(MUJOCO_USE_FILAMENT AND NOT EMSCRIPTEN)
|
||||
# Note that, by default, the "src/render" and "src/ui" code is added directly
|
||||
# into the "mujoco" target. However, "mujoco::filament" is a separate,
|
||||
# explicit target. Therefore, if you want to use MuJoCo with Filament, you
|
||||
# will need to explicitly add the "mujoco::filament" target as a link
|
||||
# dependency in your project.
|
||||
add_subdirectory(src/experimental/filament)
|
||||
else()
|
||||
elseif(NOT EMSCRIPTEN)
|
||||
add_subdirectory(src/render)
|
||||
add_subdirectory(src/ui)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
add_subdirectory(wasm)
|
||||
if(MUJOCO_BUILD_TESTS_WASM)
|
||||
add_subdirectory(wasm/tests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
target_compile_definitions(mujoco PRIVATE _GNU_SOURCE CCD_STATIC_DEFINE MUJOCO_DLL_EXPORTS -DMC_IMPLEM_ENABLE)
|
||||
if(MUJOCO_ENABLE_AVX_INTRINSICS)
|
||||
@@ -235,9 +256,14 @@ if(BUILD_TESTING AND MUJOCO_BUILD_TESTS)
|
||||
endif()
|
||||
|
||||
if(NOT (APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS))
|
||||
set(MUJOCO_TARGETS mujoco)
|
||||
if (EMSCRIPTEN)
|
||||
list(APPEND MUJOCO_TARGETS lodepng)
|
||||
endif()
|
||||
|
||||
# Install the libraries.
|
||||
install(
|
||||
TARGETS mujoco
|
||||
TARGETS ${MUJOCO_TARGETS}
|
||||
EXPORT ${PROJECT_NAME}
|
||||
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime
|
||||
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT runtime
|
||||
|
||||
@@ -131,15 +131,11 @@ These packages give users of various languages access to MuJoCo functionality:
|
||||
DeepMind's related environment stack, includes
|
||||
[PyMJCF](https://github.com/google-deepmind/dm_control/blob/main/dm_control/mjcf/README.md),
|
||||
a module for procedural manipulation of MuJoCo models.
|
||||
- [JavaScript bindings and WebAssembly support](/wasm/README.md) (inspired [stillonearth](https://github.com/stillonearth) and [zalo](https://github.com/zalo)'s community projects).
|
||||
- [C# bindings and Unity plug-in](https://mujoco.readthedocs.io/en/stable/unity.html)
|
||||
|
||||
#### Third-party bindings:
|
||||
|
||||
- **WebAssembly**: [mujoco_wasm](https://github.com/zalo/mujoco_wasm) by [@zalo](https://github.com/zalo) with contributions by
|
||||
[@kevinzakka](https://github.com/kevinzakka), based on the [emscripten build](https://github.com/stillonearth/MuJoCo-WASM) by
|
||||
[@stillonearth](https://github.com/stillonearth).
|
||||
|
||||
:arrow_right: [Click here](https://zalo.github.io/mujoco_wasm/) for a live demo of MuJoCo running in your browser.
|
||||
- **MATLAB Simulink**: [Simulink Blockset for MuJoCo Simulator](https://github.com/mathworks-robotics/mujoco-simulink-blockset)
|
||||
by [Manoj Velmurugan](https://github.com/vmanoj1996).
|
||||
- **Swift**: [swift-mujoco](https://github.com/liuliu/swift-mujoco)
|
||||
|
||||
@@ -109,7 +109,11 @@ if(NOT TARGET lodepng)
|
||||
add_library(lodepng STATIC ${LODEPNG_HEADERS} ${LODEPNG_SRCS})
|
||||
target_compile_options(lodepng PRIVATE ${MUJOCO_MACOS_COMPILE_OPTIONS})
|
||||
target_link_options(lodepng PRIVATE ${MUJOCO_MACOS_LINK_OPTIONS})
|
||||
target_include_directories(lodepng PUBLIC ${lodepng_SOURCE_DIR})
|
||||
if(NOT EMSCRIPTEN)
|
||||
target_include_directories(lodepng PUBLIC ${lodepng_SOURCE_DIR})
|
||||
else()
|
||||
target_include_directories(lodepng PUBLIC $<BUILD_INTERFACE:${lodepng_SOURCE_DIR}> $<INSTALL_INTERFACE:include>)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -128,6 +132,10 @@ if(NOT TARGET marchingcubecpp)
|
||||
endif()
|
||||
|
||||
set(QHULL_ENABLE_TESTING OFF)
|
||||
# Patch changes in https://github.com/qhull/qhull/pull/173.patch
|
||||
set(QHULL_PATCH_COMMAND
|
||||
git apply --reject --whitespace=fix ${mujoco_SOURCE_DIR}/cmake/qhull-support-emscripten.patch
|
||||
)
|
||||
|
||||
findorfetch(
|
||||
USE_SYSTEM_PACKAGE
|
||||
@@ -143,6 +151,7 @@ findorfetch(
|
||||
TARGETS
|
||||
qhull
|
||||
EXCLUDE_FROM_ALL
|
||||
PATCH_COMMAND ${QHULL_PATCH_COMMAND}
|
||||
)
|
||||
# MuJoCo includes a file from libqhull_r which is not exported by the qhull include directories.
|
||||
# Add it to the target.
|
||||
@@ -222,6 +231,12 @@ endif()
|
||||
|
||||
set(ENABLE_DOUBLE_PRECISION ON)
|
||||
set(CCD_HIDE_ALL_SYMBOLS ON)
|
||||
|
||||
# Patch changes in https://github.com/danfis/libccd/pull/83.patch
|
||||
set(CCD_PATCH_COMMAND
|
||||
git apply --reject --whitespace=fix ${mujoco_SOURCE_DIR}/cmake/ccd-support-emscripten.patch
|
||||
)
|
||||
|
||||
# update cmake_minimum_required version for compatibility with newer version of cmake
|
||||
if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM ${MUJOCO_CMAKE_MIN_REQ})
|
||||
@@ -241,6 +256,7 @@ findorfetch(
|
||||
TARGETS
|
||||
ccd
|
||||
EXCLUDE_FROM_ALL
|
||||
PATCH_COMMAND ${CCD_PATCH_COMMAND}
|
||||
)
|
||||
if(CMAKE_POLICY_VERSION_MINIMUM_LOCALLY_DEFINED)
|
||||
unset(CMAKE_POLICY_VERSION_MINIMUM)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3 -fexceptions")
|
||||
|
||||
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}/wasm)
|
||||
|
||||
link_directories(${CMAKE_BINARY_DIR}/lib)
|
||||
|
||||
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()
|
||||
|
||||
add_compile_options(-pthread)
|
||||
add_compile_options(-fexceptions)
|
||||
|
||||
# Set Emscripten linker flags
|
||||
set(EMCC_LINKER_FLAGS
|
||||
"--bind"
|
||||
"-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"
|
||||
"-s DISABLE_EXCEPTION_CATCHING=0"
|
||||
"-gsource-map"
|
||||
"-g"
|
||||
"--emit-tsd mujoco_wasm.d.ts"
|
||||
)
|
||||
string (REPLACE ";" " " EMCC_LINKER_FLAGS_STR "${EMCC_LINKER_FLAGS}")
|
||||
|
||||
add_executable(mujoco_wasm ${MUJOCO_WASM_FILES})
|
||||
|
||||
set_target_properties(mujoco_wasm PROPERTIES LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}")
|
||||
|
||||
target_link_libraries(mujoco_wasm ccd lodepng mujoco tinyxml2 qhullstatic_r)
|
||||
|
||||
install(TARGETS mujoco_wasm DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
# MuJoCo JavaScript Bindings
|
||||
|
||||
> [!CAUTION]
|
||||
> **These bindings are not yet ready for general use. They have been added
|
||||
> without announcement while we develop the CI we need to accept pull
|
||||
> requests.**
|
||||
|
||||
These are the canonical JavaScript and TypeScript bindings for the MuJoCo
|
||||
physics engine.
|
||||
|
||||
This package provides a high-level API that allows you to interact with the core
|
||||
MuJoCo engine compiled into a high-performance WebAssembly (WASM) module. These
|
||||
bindings are developed and maintained by Google DeepMind and are always up to
|
||||
date with the latest developments in MuJoCo. For brevity, the documentation
|
||||
below will often refer to “JavaScript” but the concepts apply equally to
|
||||
TypeScript.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> _These bindings are still a WIP. For details, see the [Future Work](#future-work)
|
||||
> section. Also note that development has primarily taken place on Linux using
|
||||
> Google Chrome. If you're working on a different OS or browser, you may
|
||||
> encounter some rough edges._
|
||||
|
||||
## Prerequisites
|
||||
|
||||
> [!NOTE]
|
||||
> Run all the commands in this README from the top-level directory.
|
||||
|
||||
- To compile the [`bindings.cc`](codegen/generated/bindings.cc) file, which
|
||||
generates the `.wasm` WebAssembly file, `.js` JavaScript import, and `.d.ts`
|
||||
TypeScript declaration file, you will need Emscripten SDK version `4.0.10`.
|
||||
Later versions may work but are untested. To set up the SDK, do the
|
||||
following, you can run this anywhere but the rest of the commands in this
|
||||
README only work in the shell where you source the `emsdk_env.sh` script.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
./emsdk/emsdk install 4.0.10
|
||||
./emsdk/emsdk activate 4.0.10
|
||||
source ./emsdk/emsdk_env.sh
|
||||
```
|
||||
|
||||
- To easily run the JavaScript tests and the demo application, `node` and `npm`
|
||||
are required. We recommend managing these using
|
||||
[nvm](https://github.com/nvm-sh/nvm). There are also various JavaScript
|
||||
dependencies needed for the tests, demo, and bindings build process. These
|
||||
dependencies are expected to be located in the `wasm` folder. To install
|
||||
them and ensure they can be found by later commands, run the following:
|
||||
|
||||
```sh
|
||||
npm install --prefix ./wasm
|
||||
export PATH="$(pwd)/wasm/node_modules/.bin:$PATH"
|
||||
```
|
||||
|
||||
- To modify the bindings `python3` is required because the [`bindings.cc`](codegen/generated/bindings.cc)
|
||||
file is generated by a Python script. To run the bindings generator tests,
|
||||
`absl` is required and `pytest` will be helpful. Set up a Python environment
|
||||
with these dependencies as follows:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r python/build_requirements.txt
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> _Emscripten is well-documented. We recommend reading the sections covering the
|
||||
> [Emscripten Compiler Settings](https://emscripten.org/docs/tools_reference/settings_reference.html),
|
||||
> the [Emscripten SDK](https://emscripten.org/docs/tools_reference/emsdk.html),
|
||||
> and the [Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html)
|
||||
> library. To understand the limitations and caveats related to using the
|
||||
> browser as a platform, see the
|
||||
> [Porting](https://emscripten.org/docs/porting/index.html#porting) section._
|
||||
|
||||
## User Guide
|
||||
|
||||
### Bindings Generation
|
||||
|
||||
The [`bindings.cc`](codegen/generated/bindings.cc) file is compiled to generate
|
||||
to `.wasm` WebAssembly file, `.js` JavaScript import, and `.d.ts` TypeScript
|
||||
declaration file. These are the files you'll use to call MuJoCo from JavaScript.
|
||||
To generate them ensure the npm and Emscripten SDK prerequisites are set up and
|
||||
then run the following:
|
||||
|
||||
```sh
|
||||
emcmake cmake -B build && cmake --build build
|
||||
```
|
||||
|
||||
This command will generate the following folders under the project root:
|
||||
|
||||
- `build`: contains MuJoCo compiled using Emscripten.
|
||||
- `wasm/dist`: contains the WebAssembly module, `.js` and `.d.ts` files.
|
||||
|
||||
### Example Application
|
||||
|
||||
After generating the bindings you will be ready to write web applications using
|
||||
MuJoCo. We have provided a basic web application that uses Three.js to render a
|
||||
simple simulation, to try it run this command:
|
||||
|
||||
```sh
|
||||
npm run dev:demo --prefix ./wasm
|
||||
```
|
||||
|
||||
You may prefer to write your entire app in C++ and compile it using Emscripten.
|
||||
If you do this, you won’t need to use these bindings, since you’ll be writing
|
||||
minimal JavaScript, and the granularity of these bindings may be inappropriate
|
||||
(e.g., you might want to call multiple MuJoCo functions in the C++ callback
|
||||
invoked by `requestAnimationFrame`).
|
||||
|
||||
We have also found that a hybrid approach can be helpful, as it is often more
|
||||
convenient to work with browser APIs directly in JavaScript. If you choose to
|
||||
write your application in C++ and compile it using Emscripten, you may want to
|
||||
copy a subset of the `EMSCRIPTEN_BINDINGS` from `bindings.cc` into your
|
||||
application’s source file.
|
||||
|
||||
## Development
|
||||
|
||||
In order to change the bindings you will need to change the [`bindings.cc`](codegen/generated/bindings.cc)
|
||||
file but this should not be done manually. The file is generated using the
|
||||
Python scripts and template files in the [`codegen`](codegen) folder, to edit
|
||||
the bindings you will need to change those files and re-generate [`bindings.cc`](codegen/generated/bindings.cc)
|
||||
using this command:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=python/mujoco python3 -m wasm.codegen.update
|
||||
```
|
||||
|
||||
The codegen scripts use MuJoCo’s Python introspect library to generate the
|
||||
Embind `EMSCRIPTEN_BINDINGS` block that binds C++ functions and classes to
|
||||
JavaScript. The functions and classes that are bound are wrappers around
|
||||
MuJoCo's C API. These wrappers provide a convenient place to add features like
|
||||
bounds checking and nice error reporting.
|
||||
|
||||
### Testing
|
||||
|
||||
1. **JavaScript API tests.**
|
||||
These verify that a wide variety of MuJoCo functions and classes work
|
||||
correctly when called from JavaScript. There are also preliminary benchmarks
|
||||
for JavaScript/C++ shared memory buffers. The enums test is special because
|
||||
it is generated by a Python script. Run the tests as follows:
|
||||
|
||||
```sh
|
||||
npm run test --prefix ./wasm
|
||||
```
|
||||
|
||||
2. **Bindings generator tests.**
|
||||
These are relevant when developing or extending the bindings. The following
|
||||
command finds and runs all `test_*.py` or `*_test.py` files in the `wasm`
|
||||
folder:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=python/mujoco python3 -m pytest ./wasm
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you add/edit an enum in MuJoCo you will need to run the following command
|
||||
> to re-creates the [`enums_tests.ts`](tests/enums_tests.ts) file which checks
|
||||
> that all the enums in the API are bound:
|
||||
>
|
||||
> ```sh
|
||||
> PYTHONPATH=python/mujoco python3 -m wasm.codegen.enums_test_generator
|
||||
> ```
|
||||
|
||||
### Debugging
|
||||
|
||||
We provide a “sandbox” app where you can quickly write code to run in your
|
||||
browser. Write your code in the [`main.ts`](tests/sandbox/main.ts) file and use
|
||||
the following command to execute it in your browser:
|
||||
|
||||
```sh
|
||||
npm run dev:sandbox --prefix ./wasm
|
||||
```
|
||||
|
||||
The page will be blank since the script only logs to the console output. You
|
||||
can add your code at the indicated placeholder and use Chrome DevTools for
|
||||
debugging. It is possible to set up a debug workflow where stack traces and
|
||||
stepping through code across language boundaries work correctly. Our current
|
||||
method to do this only works internally at Google, but it should be possible to
|
||||
replicate the experience with open-source tooling — community suggestions are
|
||||
welcome!
|
||||
|
||||
## Future Work
|
||||
|
||||
1. **Bind all useful APIs.**
|
||||
These bindings are not yet complete. While the main MuJoCo APIs (`mj_step`,
|
||||
`mj_loadXML`, etc.) are well tested, other APIs (e.g., functions from
|
||||
`mjspec.h`) remain untested in real web applications (though test code for
|
||||
the `mjspec` bindings does exist). One notable feature not yet supported in
|
||||
the WASM bindings, which has proved very useful in the Python bindings, is
|
||||
named access methods — where data distributed across multiple arrays in C can
|
||||
be conveniently accessed by name, e.g., `model.geom('mygeom')` or
|
||||
`data.joint('myjoint')`. Currently, this data must be accessed via the
|
||||
`mj_name2id` function. Adding support for these features is a high priority,
|
||||
as it affects user code written in JavaScript.
|
||||
|
||||
2. **Improve the developer experience.**
|
||||
There is still work to be done to improve the developer experience when
|
||||
developing the WASM bindings. The most obvious issue is that bindings
|
||||
generation is not yet fully automated. As a result, it is currently less
|
||||
convenient than we'd like to identify and apply the changes needed to update
|
||||
the bindings. The goal is to eventually automate all binding code generation
|
||||
and clearly communicate what changes are required in the WASM bindings as a
|
||||
result of C++ updates. This problem should only affect developers working on
|
||||
the MuJoCo engine in C++, not end users writing JavaScript.
|
||||
|
||||
3. **Improve the documentation.**
|
||||
The documentation in this README will eventually be merged into the main
|
||||
MuJoCo documentation once the bindings are complete and named access is
|
||||
implemented. We also intend to review the bindings APIs and make adjustments
|
||||
to minimize differences with the Python bindings (while respecting language
|
||||
idioms) to reduce the amount of additional documentation required.
|
||||
|
||||
4. **Improve the [example](#example-application).**
|
||||
We aim to provide an example application that can be easily modified and
|
||||
embedded into a paper project page (see [this example](https://kzakka.com/robopianist/)).
|
||||
This could be achieved by extending the Three.js example or by compiling the
|
||||
MuJoCo toolbox C++ code using the Emscripten toolchain. Community suggestions
|
||||
and contributions are welcome!
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
|
||||
"""Builds WASM bindings for MuJoCo."""
|
||||
|
||||
from introspect import enums as introspect_enums
|
||||
from introspect import functions as introspect_functions
|
||||
|
||||
from wasm.codegen.generators import enums
|
||||
from wasm.codegen.generators import functions
|
||||
from wasm.codegen.generators import structs
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants as _constants
|
||||
from wasm.codegen.helpers import function_utils
|
||||
|
||||
|
||||
class BindingBuilder:
|
||||
"""Builds WASM bindings for MuJoCo."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
template_path_h: str,
|
||||
template_path_cc: str,
|
||||
generated_path_h: str,
|
||||
generated_path_cc: str,
|
||||
):
|
||||
self.generated_path_h = generated_path_h
|
||||
self.generated_path_cc = generated_path_cc
|
||||
with open(template_path_h, "r") as f:
|
||||
self.content_h = f.readlines()
|
||||
with open(template_path_cc, "r") as f:
|
||||
self.content_cc = f.readlines()
|
||||
|
||||
filtered_functions = {
|
||||
name: func
|
||||
for name, func in introspect_functions.FUNCTIONS.items()
|
||||
if not function_utils.is_excluded_function_name(name)
|
||||
and name not in _constants.BOUNDCHECK_FUNCS
|
||||
}
|
||||
self.enums_generator = enums.Generator(introspect_enums.ENUMS)
|
||||
self.functions_generator = functions.Generator(filtered_functions)
|
||||
self.structs_generator = structs.Generator()
|
||||
|
||||
def set_enums(self):
|
||||
"""Generates and sets the enum bindings."""
|
||||
enum_bindings = self.enums_generator.generate()
|
||||
self.content_cc = common.replace_lines_containing_marker(
|
||||
self.content_cc,
|
||||
"// {{ ENUM_BINDINGS }}",
|
||||
enum_bindings,
|
||||
)
|
||||
return self
|
||||
|
||||
def set_headers(self):
|
||||
"""Generates and sets the struct definitions."""
|
||||
struct_hdr_markers_and_content = self.structs_generator.generate_header()
|
||||
|
||||
for marker, content in struct_hdr_markers_and_content:
|
||||
self.content_h = common.replace_lines_containing_marker(
|
||||
self.content_h, marker, content
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def set_structs(self):
|
||||
"""Generates and sets the struct bindings."""
|
||||
|
||||
struct_src_markers_and_content = (
|
||||
self.structs_generator.generate_source()
|
||||
)
|
||||
for marker, content in struct_src_markers_and_content:
|
||||
self.content_cc = common.replace_lines_containing_marker(
|
||||
self.content_cc, marker, content
|
||||
)
|
||||
return self
|
||||
|
||||
def set_functions(self):
|
||||
"""Generates and sets the function wrappers and bindings."""
|
||||
wrapper_functions, function_bindings = (
|
||||
self.functions_generator.generate()
|
||||
)
|
||||
self.content_cc = common.replace_lines_containing_marker(
|
||||
self.content_cc,
|
||||
"// {{ WRAPPER_FUNCTIONS }}",
|
||||
wrapper_functions,
|
||||
)
|
||||
self.content_cc = common.replace_lines_containing_marker(
|
||||
self.content_cc,
|
||||
"// {{ FUNCTION_BINDINGS }}",
|
||||
function_bindings,
|
||||
)
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
"""Writes the generated content to the output files."""
|
||||
common.write_to_file(self.generated_path_h, "".join(self.content_h))
|
||||
common.write_to_file(self.generated_path_cc, "".join(self.content_cc))
|
||||
|
||||
def to_string_header(self) -> str:
|
||||
return "".join(self.content_h)
|
||||
|
||||
def to_string_source(self) -> str:
|
||||
return "".join(self.content_cc)
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from absl.testing import absltest
|
||||
|
||||
from wasm.codegen import binding_builder
|
||||
|
||||
ERROR_MESSAGE = """
|
||||
The file '{}' needs to be updated, please run:
|
||||
update.py as described in wasm/README.md""".lstrip()
|
||||
|
||||
|
||||
class BindingsDiffTest(absltest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
with open(SCRIPT_DIR / 'generated/bindings.h', 'r') as f:
|
||||
self.generated_hdr = f.read()
|
||||
with open(SCRIPT_DIR / 'generated/bindings.cc', 'r') as f:
|
||||
self.generated_src = f.read()
|
||||
self.template_path_h = SCRIPT_DIR / 'templates/bindings.h'
|
||||
self.template_path_cc = SCRIPT_DIR / 'templates/bindings.cc'
|
||||
self.generated_path_h = SCRIPT_DIR / 'generated/bindings.h'
|
||||
self.generated_path_cc = SCRIPT_DIR / 'generated/bindings.cc'
|
||||
|
||||
self.builder = binding_builder.BindingBuilder(
|
||||
self.template_path_h,
|
||||
self.template_path_cc,
|
||||
self.generated_path_h,
|
||||
self.generated_path_cc,
|
||||
)
|
||||
|
||||
def test_bindings_source(self):
|
||||
generator_output = (self.builder.set_enums().set_structs().set_functions().
|
||||
to_string_source())
|
||||
self.assertEqual(
|
||||
generator_output,
|
||||
self.generated_src,
|
||||
msg=ERROR_MESSAGE.format('bindings.cc'),
|
||||
)
|
||||
|
||||
def test_bindings_header(self):
|
||||
generator_output = (self.builder.set_headers().to_string_header())
|
||||
self.assertEqual(
|
||||
generator_output,
|
||||
self.generated_hdr,
|
||||
msg=ERROR_MESSAGE.format('bindings.h'),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
|
||||
"""Tests to ensure that all Mujoco functions and structs are correctly handled.
|
||||
|
||||
This file contains tests that verify:
|
||||
- All functions defined in Mujoco's introspect module are either bound in the
|
||||
generated bindings.cc file or explicitly excluded in constants.py.
|
||||
- All structs defined in Mujoco's introspect module are either bound in the
|
||||
generated bindings.cc file or explicitly skipped in SKIPPED_STRUCTS in
|
||||
constants.py.
|
||||
|
||||
These tests help maintain the integrity of the generated WASM bindings by
|
||||
ensuring that no functions or structs are accidentally missed or incorrectly
|
||||
handled during the code generation process.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import functions as introspect_functions
|
||||
from introspect import structs as introspect_structs
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import function_utils
|
||||
|
||||
|
||||
def _get_resource_content(file_path: str) -> str:
|
||||
"""Reads resource file content using resources.GetResource."""
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
print(f'Warning: Resource {file_path} not found.')
|
||||
return ''
|
||||
except IOError as e:
|
||||
print(f'Error reading resource {file_path}: {e}')
|
||||
return ''
|
||||
|
||||
|
||||
def _get_bound_functions_from_cc() -> set[str]:
|
||||
"""Reads bindings.cc and extracts the names of bound functions."""
|
||||
content = _get_resource_content(
|
||||
Path(__file__).parent / 'generated/bindings.cc'
|
||||
)
|
||||
if not content:
|
||||
return set()
|
||||
|
||||
bound_functions = set()
|
||||
# Find all strings within function("...") calls.
|
||||
matches = re.findall(r'function\("([^"]+)"', content)
|
||||
bound_functions.update(matches)
|
||||
return bound_functions
|
||||
|
||||
|
||||
def _get_bound_structs_from_cc() -> set[str]:
|
||||
"""Reads bindings.cc and extracts the names of bound structs."""
|
||||
content = _get_resource_content(
|
||||
Path(__file__).parent / 'generated/bindings.cc'
|
||||
)
|
||||
if not content:
|
||||
return set()
|
||||
|
||||
bound_structs = set()
|
||||
# Find all strings within class_<...>("...") calls.
|
||||
matches = re.findall(r'class_<[^>]+>\("([^"]+)"\)', content)
|
||||
bound_structs.update(matches)
|
||||
return bound_structs
|
||||
|
||||
|
||||
class BindingCoverageTest(absltest.TestCase):
|
||||
|
||||
def test_function_coverage(self):
|
||||
"""Asserts that each function is either excluded or bound."""
|
||||
all_functions = set(introspect_functions.FUNCTIONS.keys())
|
||||
excluded_functions = {
|
||||
name
|
||||
for name in all_functions
|
||||
if function_utils.is_excluded_function_name(name)
|
||||
}
|
||||
bound_functions = _get_bound_functions_from_cc()
|
||||
|
||||
missing_functions = []
|
||||
for func_name in all_functions:
|
||||
if (
|
||||
func_name not in excluded_functions
|
||||
and func_name not in bound_functions
|
||||
):
|
||||
missing_functions.append(func_name)
|
||||
|
||||
if missing_functions:
|
||||
error_message = (
|
||||
f"""The following functions from functions.py are neither excluded in
|
||||
constants.py nor bound in bindings.cc:
|
||||
|
||||
{", ".join(sorted(missing_functions))}
|
||||
|
||||
Please either add them to a exclusion list in
|
||||
constants.py or create a binding in bindings.cc."""
|
||||
)
|
||||
self.fail(error_message)
|
||||
|
||||
def test_struct_coverage(self):
|
||||
"""Asserts that each struct is either not bound or bound in structs.cc."""
|
||||
bound_structs = _get_bound_structs_from_cc()
|
||||
all_structs = {
|
||||
common.uppercase_first_letter(struct_name)
|
||||
for struct_name in introspect_structs.STRUCTS.keys()
|
||||
}
|
||||
skipped_structs = {
|
||||
common.uppercase_first_letter(struct_name)
|
||||
for struct_name in constants.SKIPPED_STRUCTS
|
||||
}
|
||||
missing_structs = []
|
||||
for struct_name in all_structs:
|
||||
if (
|
||||
struct_name not in skipped_structs
|
||||
and struct_name not in bound_structs
|
||||
):
|
||||
missing_structs.append(struct_name)
|
||||
error_messages = []
|
||||
|
||||
if missing_structs:
|
||||
error_messages.append(
|
||||
f"""The following structs are defined in structs.py but are neither
|
||||
bound in bindings.cc nor listed in SKIPPED_STRUCTS:
|
||||
|
||||
{", ".join(sorted(missing_structs))}
|
||||
|
||||
Please either add them to SKIPPED_STRUCTS or create its binding
|
||||
in bindings.cc."""
|
||||
)
|
||||
|
||||
if error_messages:
|
||||
self.fail('\n\n'.join(error_messages))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
|
||||
"""Generates TypeScript code that tests Mujoco enums."""
|
||||
|
||||
import textwrap
|
||||
|
||||
from introspect import enums as introspect_enums
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
|
||||
|
||||
def generate_typescript_enum_tests():
|
||||
"""Generates TypeScript code that tests Mujoco enums."""
|
||||
output = textwrap.dedent("""\
|
||||
import 'jasmine';
|
||||
|
||||
import { MainModule } from "../dist/mujoco_wasm"
|
||||
import loadMujoco from "../dist/mujoco_wasm.js"
|
||||
|
||||
let mujoco: MainModule;
|
||||
|
||||
describe('Enums', () => {
|
||||
beforeAll(async () => {
|
||||
mujoco = await loadMujoco();
|
||||
});
|
||||
""")
|
||||
|
||||
for enum_decl in introspect_enums.ENUMS.values():
|
||||
enum_name = enum_decl.name
|
||||
output += f"""
|
||||
it('{enum_name} should exist', () => {{
|
||||
expect(mujoco.{enum_name}).toBeDefined();
|
||||
}});\n"""
|
||||
|
||||
output += "});\n"
|
||||
return output
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ts_test_code = generate_typescript_enum_tests()
|
||||
output_file = 'wasm/tests/enums_test.ts'
|
||||
common.write_to_file(output_file, ts_test_code)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
# 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.
|
||||
|
||||
"""Generator for the constants."""
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
|
||||
|
||||
# TODO(manevi): Delete this file and use the genrule to handle the file copying
|
||||
class Generator:
|
||||
"""Generator for the constants."""
|
||||
|
||||
def run(self):
|
||||
"""Runs the generator."""
|
||||
template_cc_file, output_cc_file = common.get_file_path(
|
||||
"templates", "generated", "constants.cc"
|
||||
)
|
||||
|
||||
with open(template_cc_file, "r") as f_template:
|
||||
template_content = f_template.read()
|
||||
|
||||
with open(output_cc_file, "w") as f_output:
|
||||
f_output.write(template_content)
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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.
|
||||
|
||||
"""Generates Embind bindings for MuJoCo enums."""
|
||||
|
||||
from typing import Mapping
|
||||
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import code_builder
|
||||
|
||||
|
||||
class Generator:
|
||||
"""Generates Embind code for MuJoCo enums."""
|
||||
|
||||
def __init__(self, enums: Mapping[str, ast_nodes.EnumDecl]):
|
||||
self.enums = enums
|
||||
|
||||
def _generate_enum_binding(self, enum: ast_nodes.EnumDecl) -> str:
|
||||
"""Generates the Embind code for a single enum."""
|
||||
|
||||
code = f'{code_builder.INDENT}enum_<{enum.name}>("{enum.name}")'
|
||||
|
||||
for value_name in enum.values:
|
||||
code += f'\n{2*code_builder.INDENT}.value("{value_name}", {value_name})'
|
||||
|
||||
code += ";"
|
||||
return code
|
||||
|
||||
def generate(self) -> str:
|
||||
"""Generates all Embind code for the provided enums."""
|
||||
|
||||
code = []
|
||||
for enum in self.enums.values():
|
||||
code.append(self._generate_enum_binding(enum))
|
||||
return "\n\n".join(code) + "\n"
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.generators import enums
|
||||
|
||||
|
||||
class EnumsGeneratorTest(absltest.TestCase):
|
||||
|
||||
def test_generate_enum_bindings(self):
|
||||
|
||||
generator = enums.Generator({
|
||||
"TestEnum": ast_nodes.EnumDecl(
|
||||
name="TestEnum",
|
||||
declname="enum TestEnum_",
|
||||
values={"FIRST_VAL": 0, "SECOND_VAL": 1, "THIRD_VAL": 2},
|
||||
),
|
||||
"AnotherEnum": ast_nodes.EnumDecl(
|
||||
name="AnotherEnum",
|
||||
declname="enum AnotherEnum_",
|
||||
values={"ALPHA": 100, "BETA": 200},
|
||||
),
|
||||
"EmptyEnum": ast_nodes.EnumDecl(
|
||||
name="EmptyEnum",
|
||||
declname="enum EmptyEnum_",
|
||||
values={},
|
||||
),
|
||||
})
|
||||
|
||||
expected_code = """ enum_<TestEnum>("TestEnum")
|
||||
.value("FIRST_VAL", FIRST_VAL)
|
||||
.value("SECOND_VAL", SECOND_VAL)
|
||||
.value("THIRD_VAL", THIRD_VAL);
|
||||
|
||||
enum_<AnotherEnum>("AnotherEnum")
|
||||
.value("ALPHA", ALPHA)
|
||||
.value("BETA", BETA);
|
||||
|
||||
enum_<EmptyEnum>("EmptyEnum");
|
||||
"""
|
||||
|
||||
actual_code = generator.generate()
|
||||
|
||||
self.assertEqual(actual_code, expected_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
|
||||
"""Generates Embind bindings for MuJoCo functions."""
|
||||
|
||||
import pathlib
|
||||
from typing import List, Mapping, TypeAlias
|
||||
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import code_builder
|
||||
from wasm.codegen.helpers import function_utils
|
||||
|
||||
FunctionDecl: TypeAlias = ast_nodes.FunctionDecl
|
||||
FunctionParameterDecl: TypeAlias = ast_nodes.FunctionParameterDecl
|
||||
PointerType: TypeAlias = ast_nodes.PointerType
|
||||
ValueType: TypeAlias = ast_nodes.ValueType
|
||||
Path: TypeAlias = pathlib.Path
|
||||
|
||||
|
||||
class Generator:
|
||||
"""Generates Embind bindings for MuJoCo functions."""
|
||||
|
||||
def __init__(self, functions: Mapping[str, FunctionDecl]):
|
||||
self.direct_bind_functions: List[FunctionDecl] = []
|
||||
self.wrapper_bind_functions: List[FunctionDecl] = []
|
||||
|
||||
for func in functions.values():
|
||||
if function_utils.should_be_wrapped(func):
|
||||
self.wrapper_bind_functions.append(func)
|
||||
else:
|
||||
self.direct_bind_functions.append(func)
|
||||
|
||||
def _generate_wrappers(self) -> str:
|
||||
"""Generates Embind bindings for all functions that need wrappers."""
|
||||
|
||||
code = []
|
||||
for func in self.wrapper_bind_functions:
|
||||
wrapper_code = function_utils.generate_function_wrapper(func)
|
||||
code.append(wrapper_code)
|
||||
|
||||
return "\n\n".join(code)
|
||||
|
||||
def _generate_direct_bindable_functions(self) -> str:
|
||||
"""Generates Embind bindings for all directly bindable functions."""
|
||||
|
||||
result = ""
|
||||
for func in self.direct_bind_functions:
|
||||
result += code_builder.INDENT
|
||||
result += self._generate_function_binding(func)
|
||||
|
||||
return result
|
||||
|
||||
def _generate_function_binding(
|
||||
self, func: FunctionDecl, is_wrapper=False
|
||||
) -> str:
|
||||
"""Generates the Embind code for a single function."""
|
||||
|
||||
js_name, cpp_func = func.name, func.name
|
||||
if is_wrapper:
|
||||
cpp_func += "_wrapper"
|
||||
|
||||
return f'function("{js_name}", &{cpp_func});\n'
|
||||
|
||||
def _generate_wrapper_bindable_functions(self) -> str:
|
||||
"""Generates Embind bindings for all functions that need wrappers."""
|
||||
|
||||
result = ""
|
||||
for func in self.wrapper_bind_functions:
|
||||
result += code_builder.INDENT
|
||||
result += self._generate_function_binding(func, True)
|
||||
|
||||
return result
|
||||
|
||||
def generate(self) -> tuple[str, str]:
|
||||
"""Generates the bindings file for all functions."""
|
||||
|
||||
wrapper_functions = self._generate_wrappers()
|
||||
function_bindings = self._generate_direct_bindable_functions()
|
||||
function_bindings += self._generate_wrapper_bindable_functions()
|
||||
|
||||
return wrapper_functions, function_bindings
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.generators import functions
|
||||
|
||||
|
||||
class FunctionsGeneratorTest(absltest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.generator = functions.Generator({})
|
||||
self.int_type = ast_nodes.ValueType(name="int")
|
||||
|
||||
def test_generate_function_binding_simple_case(self):
|
||||
func_simple_void = ast_nodes.FunctionDecl(
|
||||
name="do_nothing",
|
||||
return_type=ast_nodes.ValueType(name="void"),
|
||||
parameters=tuple(),
|
||||
doc="doc",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.generator._generate_function_binding(func_simple_void),
|
||||
'function("do_nothing", &do_nothing);\n',
|
||||
)
|
||||
|
||||
def test_generate_direct_bindable_functions_simple_filter(self):
|
||||
direct_bind = ast_nodes.FunctionDecl(
|
||||
name="direct_bind",
|
||||
return_type=self.int_type,
|
||||
parameters=(
|
||||
ast_nodes.FunctionParameterDecl(name="val", type=self.int_type),
|
||||
),
|
||||
doc="doc",
|
||||
)
|
||||
needs_wrap = ast_nodes.FunctionDecl(
|
||||
name="needs_wrap",
|
||||
return_type=ast_nodes.PointerType(inner_type=self.int_type),
|
||||
parameters=tuple(),
|
||||
doc="doc",
|
||||
)
|
||||
self.generator = functions.Generator({
|
||||
"direct1": direct_bind,
|
||||
"wrapped1": needs_wrap,
|
||||
})
|
||||
|
||||
generated_code = self.generator._generate_direct_bindable_functions()
|
||||
self.assertIn('function("direct_bind", &direct_bind);\n', generated_code)
|
||||
self.assertNotIn('function("needs_wrap", &needs_wrap);\n', generated_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
"""Generates Embind bindings for MuJoCo structs."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import structs_parser
|
||||
from wasm.codegen.helpers import structs_wrappers_data
|
||||
|
||||
|
||||
class Generator:
|
||||
"""Generates C++ code for binding and wrapping MuJoCo structs."""
|
||||
|
||||
def __init__(self):
|
||||
# Set up the correct input dict based on the structs we want to bind
|
||||
# and already have a wrapper manually created in the template/bindings.cc
|
||||
wrapped_structs = structs_wrappers_data.create_wrapped_structs_set_up_data(
|
||||
constants.STRUCTS_TO_BIND
|
||||
)
|
||||
|
||||
# Traverse the introspect dictionary to get the field
|
||||
# wrapper/bindings statements set up for each struct
|
||||
self.structs_to_bind_data = structs_parser.generate_wasm_bindings(
|
||||
wrapped_structs
|
||||
)
|
||||
|
||||
def generate_header(
|
||||
self
|
||||
) -> list[tuple[str, list[Optional[str]]]]:
|
||||
"""Generates C++ header file for binding and wrapping MuJoCo structs."""
|
||||
autogenned_struct_definitions = []
|
||||
markers_and_content = []
|
||||
|
||||
# Sort by struct name by dependency to ensure deterministic output order
|
||||
sorted_struct_names = structs_parser.sort_structs_by_dependency(
|
||||
constants.STRUCTS_TO_BIND
|
||||
)
|
||||
|
||||
for struct_name in sorted_struct_names:
|
||||
struct_data = self.structs_to_bind_data[struct_name]
|
||||
if struct_data.wrapped_header:
|
||||
autogenned_struct_definitions.append(
|
||||
struct_data.wrapped_header + "\n"
|
||||
)
|
||||
else:
|
||||
markers_and_content.append((
|
||||
f"// INSERT-GENERATED-{struct_data.wrap_name}-DEFINITIONS",
|
||||
[
|
||||
l.definition if l.definition else ""
|
||||
for l in struct_data.wrapped_fields
|
||||
],
|
||||
))
|
||||
markers_and_content.append((
|
||||
"// {{ AUTOGENNED_STRUCT_DEFINITIONS }}",
|
||||
autogenned_struct_definitions,
|
||||
))
|
||||
return markers_and_content
|
||||
|
||||
def generate_source(self) -> list[tuple[str, list[str]]]:
|
||||
"""Generates C++ source file for binding and wrapping MuJoCo structs."""
|
||||
constructors = [
|
||||
(
|
||||
f"// INSERT-GENERATED-{struct_data.wrap_name}-CONSTRUCTOR",
|
||||
[struct_data.wrapped_source],
|
||||
)
|
||||
for _, struct_data in self.structs_to_bind_data.items()
|
||||
]
|
||||
properties = [
|
||||
(
|
||||
f"// INSERT-GENERATED-{struct_data.wrap_name}-BINDINGS",
|
||||
[l.binding for l in struct_data.wrapped_fields],
|
||||
)
|
||||
for _, struct_data in self.structs_to_bind_data.items()
|
||||
]
|
||||
return constructors + properties
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
"""Helper class to build code string line by line with indentation."""
|
||||
|
||||
INDENT = " "
|
||||
|
||||
|
||||
class CodeBuilder:
|
||||
"""Helper class to build code string line by line with indentation."""
|
||||
|
||||
def __init__(self, indent_str: str = INDENT):
|
||||
self._lines = []
|
||||
self._indent_level = 0
|
||||
self._indent_str = indent_str
|
||||
|
||||
def line(self, line_content: str) -> None:
|
||||
"""Adds a line with indentation, special-casing "private:" and "public:"."""
|
||||
indent = self._indent_str * self._indent_level
|
||||
content = line_content.strip()
|
||||
if content == "private:" or content == "public:":
|
||||
self._lines.append(indent[:-1] + line_content)
|
||||
elif content:
|
||||
self._lines.append(indent + line_content)
|
||||
else:
|
||||
self._lines.append("")
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""Returns the complete code string."""
|
||||
return "\n".join(self._lines)
|
||||
|
||||
class IndentBlock:
|
||||
"""Helper class to manage indentation within a `with` statement."""
|
||||
|
||||
def __init__(self, builder: "CodeBuilder", header_line=""):
|
||||
self._builder = builder
|
||||
self._header_line = header_line
|
||||
|
||||
def __enter__(self):
|
||||
line = self._header_line
|
||||
line += " {" if line else "{"
|
||||
self._builder.line(line)
|
||||
self._builder._indent_level += 1
|
||||
return self._builder
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._builder._indent_level > 0:
|
||||
self._builder._indent_level -= 1
|
||||
self._builder.line("}")
|
||||
|
||||
def block(self, header_line="") -> IndentBlock:
|
||||
"""Creates a block including braces and an optional header before the opening brace.
|
||||
|
||||
Use via a `with` statement.
|
||||
|
||||
Args:
|
||||
header_line: Optional header line to add before the opening brace.
|
||||
|
||||
Returns:
|
||||
An IndentBlock instance that manages the indentation.
|
||||
"""
|
||||
return self.IndentBlock(self, header_line)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
"""Tests for the code_builder module."""
|
||||
|
||||
from absl.testing import absltest
|
||||
from wasm.codegen.helpers import code_builder
|
||||
|
||||
|
||||
class CodeBuilderTest(absltest.TestCase):
|
||||
|
||||
def test_code_builder_functionality(self):
|
||||
"""Test nested indentation blocks."""
|
||||
builder = code_builder.CodeBuilder(indent_str=" ")
|
||||
builder.line("let a = 1")
|
||||
with builder.block("function myFunc()"):
|
||||
builder.line("let flag = true")
|
||||
with builder.block("while (flag)"):
|
||||
builder.line("a++")
|
||||
builder.line("flag = a < 10")
|
||||
builder.line("return a")
|
||||
builder.line("print('Done')")
|
||||
|
||||
expected_lines = [
|
||||
"let a = 1",
|
||||
"function myFunc() {",
|
||||
" let flag = true",
|
||||
" while (flag) {",
|
||||
" a++",
|
||||
" flag = a < 10",
|
||||
" }",
|
||||
" return a",
|
||||
"}",
|
||||
"print('Done')",
|
||||
]
|
||||
self.assertEqual(builder.to_string(), "\n".join(expected_lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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.
|
||||
|
||||
"""Utility functions for code generation."""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from wasm.codegen.helpers import constants
|
||||
|
||||
Path = pathlib.Path
|
||||
|
||||
|
||||
def get_default_output_dir() -> str:
|
||||
"""Gets the default output directory (sibling of 'generated' folder)."""
|
||||
# Get the directory of the current file (generator/base.py)
|
||||
current_dir = Path(__file__).parent
|
||||
# Go up one level to the project root and then down to 'generated'
|
||||
default_output_dir = str(current_dir.parent / "generated")
|
||||
return default_output_dir
|
||||
|
||||
|
||||
def get_file_path(
|
||||
template_dir: str, output_dir: str, filename: str
|
||||
) -> tuple[str, str]:
|
||||
"""Constructs the template and output file paths.
|
||||
|
||||
Args:
|
||||
template_dir: The directory containing the template files.
|
||||
output_dir: The directory where the generated files will be saved.
|
||||
filename: The name of the file.
|
||||
|
||||
Returns:
|
||||
A tuple containing the template file path and the output file path.
|
||||
"""
|
||||
template_file = f"wasm/codegen/{template_dir}/{filename}"
|
||||
output_file = f"wasm/codegen/{output_dir}/{filename}"
|
||||
return template_file, output_file
|
||||
|
||||
|
||||
def write_to_file(filepath: str, content: str) -> None:
|
||||
"""Writes content to a file."""
|
||||
output_dir = os.path.dirname(filepath)
|
||||
|
||||
try:
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
with open(filepath, "w") as f:
|
||||
chars = f.write(content)
|
||||
print(f"wrote {chars} characters to file '{filepath}'")
|
||||
except IOError as e:
|
||||
print(f"Error writing to output file: {filepath} - {e}")
|
||||
|
||||
|
||||
def uppercase_first_letter(input_string: str) -> str:
|
||||
"""Uppercases the first letter of a string."""
|
||||
if input_string:
|
||||
return input_string[0].upper() + input_string[1:]
|
||||
return input_string
|
||||
|
||||
|
||||
def try_cast_to_scalar_type(value: str) -> int | float | str:
|
||||
"""Tries to cast a string to an integer, then a float, otherwise returns the original string."""
|
||||
for type_ in [int, float]:
|
||||
try:
|
||||
return type_(value)
|
||||
except ValueError:
|
||||
continue
|
||||
return value
|
||||
|
||||
|
||||
def debug_print(msg: str):
|
||||
"""Prints a message to the console if STRUCT_DEBUG_MODE is enabled."""
|
||||
if constants.STRUCT_DEBUG_MODE:
|
||||
print(msg)
|
||||
|
||||
|
||||
def replace_lines_containing_marker(
|
||||
lines: list[str],
|
||||
marker_to_replace: str,
|
||||
replacement_content: str | list[str],
|
||||
) -> list[str]:
|
||||
"""Replaces lines containing a specific marker with new content."""
|
||||
|
||||
new_lines = []
|
||||
replaced = False
|
||||
for line in lines:
|
||||
if not replaced and marker_to_replace in line:
|
||||
indentation = _get_indentation(line)
|
||||
if isinstance(replacement_content, str):
|
||||
new_lines.append(indentation + replacement_content)
|
||||
elif isinstance(replacement_content, list):
|
||||
for content_line in replacement_content:
|
||||
if not content_line.strip():
|
||||
continue
|
||||
indented_line = (
|
||||
indentation
|
||||
+ content_line.replace("\n", "\n" + indentation)
|
||||
+ "\n"
|
||||
)
|
||||
new_lines.append(indented_line)
|
||||
replaced = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
return new_lines
|
||||
|
||||
|
||||
def _get_indentation(line: str) -> str:
|
||||
"""Returns the indentation of the given line as a string of spaces."""
|
||||
|
||||
indentation = ""
|
||||
for char in line:
|
||||
if char == " ":
|
||||
indentation += " "
|
||||
else:
|
||||
break
|
||||
return indentation
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
from wasm.codegen.helpers import common
|
||||
|
||||
|
||||
class CommonUtilsTest(absltest.TestCase):
|
||||
|
||||
def test_uppercase_first_letter(self):
|
||||
self.assertEqual(common.uppercase_first_letter(""), "")
|
||||
self.assertEqual(common.uppercase_first_letter("hello"), "Hello")
|
||||
self.assertEqual(common.uppercase_first_letter("1st place"), "1st place")
|
||||
self.assertEqual(common.uppercase_first_letter("!wow"), "!wow")
|
||||
self.assertEqual(
|
||||
common.uppercase_first_letter(" leading space"), " leading space"
|
||||
)
|
||||
|
||||
def test_try_cast_to_scalar_type(self):
|
||||
self.assertEqual(common.try_cast_to_scalar_type("123"), 123)
|
||||
self.assertEqual(common.try_cast_to_scalar_type("123.456"), 123.456)
|
||||
self.assertEqual(common.try_cast_to_scalar_type("abc"), "abc")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,474 @@
|
||||
# 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.
|
||||
|
||||
"""Constants used in the code generation process."""
|
||||
|
||||
from introspect import structs as introspect_structs
|
||||
|
||||
PRIMITIVE_TYPES = {
|
||||
# go/keep-sorted start
|
||||
"char",
|
||||
"double",
|
||||
"float",
|
||||
"int",
|
||||
"mjtByte",
|
||||
"mjtMeshBuiltin",
|
||||
"mjtNum",
|
||||
"mjtObj", # Adding this to the primitives because it is used as int,
|
||||
"mjtSize",
|
||||
"size_t",
|
||||
"uint64_t",
|
||||
"uintptr_t",
|
||||
"unsigned char",
|
||||
"unsigned int",
|
||||
"void",
|
||||
# go/keep-sorted end
|
||||
}
|
||||
|
||||
_PLUGIN_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mj_getPluginConfig",
|
||||
"mj_loadAllPluginLibraries",
|
||||
"mj_loadPluginLibrary",
|
||||
"mjc_distance",
|
||||
"mjc_getSDF",
|
||||
"mjc_gradient",
|
||||
"mjp_defaultPlugin",
|
||||
"mjp_defaultResourceProvider",
|
||||
"mjp_getPlugin",
|
||||
"mjp_getPluginAtSlot",
|
||||
"mjp_getResourceProvider",
|
||||
"mjp_getResourceProviderAtSlot",
|
||||
"mjp_pluginCount",
|
||||
"mjp_registerPlugin",
|
||||
"mjp_registerResourceProvider",
|
||||
"mjp_resourceProviderCount",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Functions that are bound as class methods
|
||||
_CLASS_METHODS = [
|
||||
# go/keep-sorted start
|
||||
"mj_compile",
|
||||
"mj_copyData",
|
||||
"mj_copyModel",
|
||||
"mj_copySpec",
|
||||
"mj_deleteData",
|
||||
"mj_deleteModel",
|
||||
"mj_deleteSpec",
|
||||
"mj_loadXML",
|
||||
"mj_makeData",
|
||||
"mj_makeSpec",
|
||||
"mj_parseXML", # TODO(manevi): Bind this function.
|
||||
"mj_parseXMLString",
|
||||
"mj_recompile", # TODO(manevi): Bind this function.
|
||||
"mj_saveXML", # TODO(manevi): Bind this function.
|
||||
"mj_saveXMLString", # TODO(manevi): Bind this function.
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Omitted because not very useful
|
||||
_WRITABLE_ERROR = [
|
||||
"mj_printSchema",
|
||||
]
|
||||
|
||||
# Omitted thread management functions
|
||||
_THREAD_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mju_bindThreadPool",
|
||||
"mju_defaultTask",
|
||||
"mju_taskJoin",
|
||||
"mju_threadPoolCreate",
|
||||
"mju_threadPoolDestroy",
|
||||
"mju_threadPoolEnqueue",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Omitted asset cache functions
|
||||
_ASSET_CACHE_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mj_clearCache",
|
||||
"mj_getCache",
|
||||
"mj_getCacheCapacity",
|
||||
"mj_getCacheSize",
|
||||
"mj_setCacheCapacity",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Omitted Virtual Filesystem (VFS) functions
|
||||
_VFS_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mj_addBufferVFS",
|
||||
"mj_addFileVFS",
|
||||
"mj_defaultVFS",
|
||||
"mj_deleteFileVFS",
|
||||
"mj_deleteVFS",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Omitted irrelevant visual functions
|
||||
_VISUAL_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mjv_averageCamera",
|
||||
"mjv_copyData",
|
||||
"mjv_copyModel",
|
||||
"mjv_defaultScene",
|
||||
"mjv_freeScene",
|
||||
"mjv_makeScene",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
_MEMORY_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mj_freeLastXML",
|
||||
"mj_freeStack",
|
||||
"mj_loadModel",
|
||||
"mj_markStack",
|
||||
"mj_saveModel",
|
||||
"mj_stackAllocByte",
|
||||
"mj_stackAllocInt",
|
||||
"mj_stackAllocNum",
|
||||
"mj_warning",
|
||||
"mjs_bodyToFrame",
|
||||
"mju_boxQPmalloc",
|
||||
"mju_clearHandlers",
|
||||
"mju_error",
|
||||
"mju_error_i",
|
||||
"mju_error_s",
|
||||
"mju_free",
|
||||
"mju_malloc",
|
||||
"mju_strncpy",
|
||||
"mju_warning",
|
||||
"mju_warning_i",
|
||||
"mju_warning_s",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
_GETTERS_AND_SETTERS = [
|
||||
# go/keep-sorted start
|
||||
"mjs_appendFloatVec",
|
||||
"mjs_appendIntVec",
|
||||
"mjs_appendString",
|
||||
"mjs_getDouble",
|
||||
"mjs_getPluginAttributes",
|
||||
"mjs_getString",
|
||||
"mjs_getUserValue",
|
||||
"mjs_setBuffer",
|
||||
"mjs_setDouble",
|
||||
"mjs_setFloat",
|
||||
"mjs_setInStringVec",
|
||||
"mjs_setInt",
|
||||
"mjs_setPluginAttributes",
|
||||
"mjs_setString",
|
||||
"mjs_setStringVec",
|
||||
"mjs_setUserValue",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
_UTILITY_FUNCTIONS = [
|
||||
# go/keep-sorted start
|
||||
"mju_getXMLDependencies",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# List of functions that should be skipped during the code generation process.
|
||||
SKIPPED_FUNCTIONS = (
|
||||
_CLASS_METHODS +
|
||||
_THREAD_FUNCTIONS +
|
||||
_MEMORY_FUNCTIONS +
|
||||
_PLUGIN_FUNCTIONS +
|
||||
_GETTERS_AND_SETTERS +
|
||||
_VISUAL_FUNCTIONS +
|
||||
_ASSET_CACHE_FUNCTIONS +
|
||||
_VFS_FUNCTIONS +
|
||||
_WRITABLE_ERROR +
|
||||
_UTILITY_FUNCTIONS
|
||||
)
|
||||
|
||||
# Functions that require special wrappers to infer sizes and make additional
|
||||
# validation checks. These functions are not bound automatically but are
|
||||
# written by hand instead.
|
||||
BOUNDCHECK_FUNCS = [
|
||||
# go/keep-sorted start
|
||||
"mj_addM",
|
||||
"mj_angmomMat",
|
||||
"mj_applyFT",
|
||||
"mj_constraintUpdate",
|
||||
"mj_differentiatePos",
|
||||
"mj_fullM",
|
||||
"mj_geomDistance",
|
||||
"mj_getState",
|
||||
"mj_integratePos",
|
||||
"mj_jac",
|
||||
"mj_jacBody",
|
||||
"mj_jacBodyCom",
|
||||
"mj_jacDot",
|
||||
"mj_jacGeom",
|
||||
"mj_jacPointAxis",
|
||||
"mj_jacSite",
|
||||
"mj_jacSubtreeCom",
|
||||
"mj_mulJacTVec",
|
||||
"mj_mulJacVec",
|
||||
"mj_mulM",
|
||||
"mj_mulM2",
|
||||
"mj_multiRay",
|
||||
"mj_normalizeQuat",
|
||||
"mj_rne",
|
||||
"mj_saveLastXML",
|
||||
"mj_setLengthRange",
|
||||
"mj_setState",
|
||||
"mj_solveM",
|
||||
"mj_solveM2",
|
||||
"mjd_inverseFD",
|
||||
"mjd_subQuat",
|
||||
"mjd_transitionFD",
|
||||
"mju_L1",
|
||||
"mju_add",
|
||||
"mju_addScl",
|
||||
"mju_addTo",
|
||||
"mju_addToScl",
|
||||
"mju_band2Dense",
|
||||
"mju_bandMulMatVec",
|
||||
"mju_boxQP",
|
||||
"mju_cholFactor",
|
||||
"mju_cholFactorBand",
|
||||
"mju_cholSolve",
|
||||
"mju_cholSolveBand",
|
||||
"mju_cholUpdate",
|
||||
"mju_copy",
|
||||
"mju_d2n",
|
||||
"mju_decodePyramid",
|
||||
"mju_dense2Band",
|
||||
"mju_dense2sparse",
|
||||
"mju_dot",
|
||||
"mju_encodePyramid",
|
||||
"mju_eye",
|
||||
"mju_f2n",
|
||||
"mju_fill",
|
||||
"mju_insertionSort",
|
||||
"mju_insertionSortInt",
|
||||
"mju_isZero",
|
||||
"mju_mulMatMat",
|
||||
"mju_mulMatMatT",
|
||||
"mju_mulMatTMat",
|
||||
"mju_mulMatTVec",
|
||||
"mju_mulMatVec",
|
||||
"mju_mulVecMatVec",
|
||||
"mju_n2d",
|
||||
"mju_n2f",
|
||||
"mju_norm",
|
||||
"mju_normalize",
|
||||
"mju_printMatSparse",
|
||||
"mju_scl",
|
||||
"mju_sparse2dense",
|
||||
"mju_sqrMatTD",
|
||||
"mju_sub",
|
||||
"mju_subFrom",
|
||||
"mju_sum",
|
||||
"mju_symmetrize",
|
||||
"mju_transpose",
|
||||
"mju_zero",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# List of structs that should be skipped during the code generation process.
|
||||
SKIPPED_STRUCTS = [
|
||||
# go/keep-sorted start
|
||||
"mjCache",
|
||||
"mjSDF",
|
||||
"mjTask",
|
||||
"mjThreadPool",
|
||||
"mjUI",
|
||||
"mjrContext",
|
||||
"mjrRect",
|
||||
"mjuiDef",
|
||||
"mjuiItem",
|
||||
"mjuiSection",
|
||||
"mjuiState",
|
||||
"mjuiThemeColor",
|
||||
"mjuiThemeSpacing",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Dictionary that maps anonymous structs to their parent struct and field name.
|
||||
# Anonymous structs are not defined as independent structs in the MuJoCo
|
||||
# codebase, but they are part of other structs. This dictionary is used to
|
||||
# handle them as if they were independent structs.
|
||||
ANONYMOUS_STRUCTS = {
|
||||
# go/keep-sorted start
|
||||
"mjVisualGlobal": {"parent": "mjVisual", "field_name": "global"},
|
||||
"mjVisualHeadlight": {"parent": "mjVisual", "field_name": "headlight"},
|
||||
"mjVisualMap": {"parent": "mjVisual", "field_name": "map"},
|
||||
"mjVisualQuality": {"parent": "mjVisual", "field_name": "quality"},
|
||||
"mjVisualRgba": {"parent": "mjVisual", "field_name": "rgba"},
|
||||
"mjVisualScale": {"parent": "mjVisual", "field_name": "scale"},
|
||||
# go/keep-sorted end
|
||||
}
|
||||
|
||||
# This list is created by subtracting the skipped structs from the list of all
|
||||
# structs and adding the anonymous structs.
|
||||
STRUCTS_TO_BIND = list(
|
||||
(set(introspect_structs.STRUCTS.keys()) - set(SKIPPED_STRUCTS)).union(
|
||||
ANONYMOUS_STRUCTS.keys()
|
||||
)
|
||||
)
|
||||
|
||||
# List of structs that do not have a default constructor.
|
||||
NO_DEFAULT_CONSTRUCTORS = [
|
||||
# go/keep-sorted start
|
||||
"mjContact",
|
||||
"mjSolverStat",
|
||||
"mjStatistic",
|
||||
"mjTimerStat",
|
||||
"mjWarningStat",
|
||||
"mjsCompiler",
|
||||
"mjsDefault",
|
||||
"mjsElement",
|
||||
"mjsExclude",
|
||||
"mjsWrap",
|
||||
"mjvGLCamera",
|
||||
"mjvLight",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# List of `mjData` fields where the array size should be obtained from other
|
||||
# `mjData` members, instead of from `mjModel` members. This is typically the
|
||||
# case for fields that are dynamically allocated during the simulation.
|
||||
MJDATA_SIZES = [
|
||||
# go/keep-sorted start
|
||||
"contact",
|
||||
"efc_AR",
|
||||
"efc_AR_colind",
|
||||
"efc_AR_rowadr",
|
||||
"efc_AR_rownnz",
|
||||
"efc_D",
|
||||
"efc_J",
|
||||
"efc_JT",
|
||||
"efc_JT_colind",
|
||||
"efc_J_colind",
|
||||
"efc_J_rowadr",
|
||||
"efc_J_rownnz",
|
||||
"efc_J_rowsuper",
|
||||
"efc_KBIP",
|
||||
"efc_R",
|
||||
"efc_aref",
|
||||
"efc_b",
|
||||
"efc_diagApprox",
|
||||
"efc_force",
|
||||
"efc_frictionloss",
|
||||
"efc_id",
|
||||
"efc_island",
|
||||
"efc_margin",
|
||||
"efc_pos",
|
||||
"efc_state",
|
||||
"efc_type",
|
||||
"efc_vel",
|
||||
"iLDiagInv",
|
||||
"iM_rowadr",
|
||||
"iM_rownnz",
|
||||
"iacc",
|
||||
"iacc_smooth",
|
||||
"iefc_D",
|
||||
"iefc_J",
|
||||
"iefc_JT",
|
||||
"iefc_JT_colind",
|
||||
"iefc_JT_rowadr",
|
||||
"iefc_JT_rownnz",
|
||||
"iefc_JT_rowsuper",
|
||||
"iefc_J_colind",
|
||||
"iefc_J_rowadr",
|
||||
"iefc_J_rownnz",
|
||||
"iefc_J_rowsuper",
|
||||
"iefc_R",
|
||||
"iefc_aref",
|
||||
"iefc_force",
|
||||
"iefc_frictionloss",
|
||||
"iefc_id",
|
||||
"iefc_state",
|
||||
"iefc_type",
|
||||
"ifrc_constraint",
|
||||
"ifrc_smooth",
|
||||
"island_dofadr",
|
||||
"island_dofnum",
|
||||
"island_efcadr",
|
||||
"island_efcind",
|
||||
"island_efcnum",
|
||||
"island_idofadr",
|
||||
"island_iefcadr",
|
||||
"island_ne",
|
||||
"island_nefc",
|
||||
"island_nf",
|
||||
"island_nv",
|
||||
"map_efc2iefc",
|
||||
"map_iefc2efc",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
# Dictionary where keys are the struct names and the values are lists of the
|
||||
# fields that are manually specified in the structs.h template file.
|
||||
MANUALLY_ADDED_FIELDS_FROM_TEMPLATE = {
|
||||
# go/keep-sorted start
|
||||
"MjData": ["solver", "timer", "warning", "contact"],
|
||||
"MjSpec": ["option", "visual", "stat", "element", "compiler"],
|
||||
"MjvScene": [
|
||||
"model",
|
||||
"lights",
|
||||
"camera",
|
||||
"geoms",
|
||||
"geomorder",
|
||||
"flexedgeadr",
|
||||
"flexedgenum",
|
||||
"flexvertadr",
|
||||
"flexvertnum",
|
||||
"flexfaceadr",
|
||||
"flexfacenum",
|
||||
"flexfaceused",
|
||||
"flexedge",
|
||||
"flexvert",
|
||||
"skinfacenum",
|
||||
"skinvertadr",
|
||||
"skinvertnum",
|
||||
"skinvert",
|
||||
"skinnormal",
|
||||
"flexface",
|
||||
"flexnormal",
|
||||
"flextexcoord",
|
||||
# go/keep-sorted end
|
||||
],
|
||||
}
|
||||
|
||||
# Dictionary that maps byte array fields to their corresponding size members.
|
||||
# When generating the code for these fields, a specific cast to `uint8_t*` is
|
||||
# required for embind. This dictionary is used to register those fields and
|
||||
# their sizes.
|
||||
BYTE_FIELDS = {
|
||||
"buffer": {"size": "nbuffer"},
|
||||
"arena": {"size": "narena"},
|
||||
}
|
||||
|
||||
# Boolean flag to enable debug prints during the struct wrapper and binding
|
||||
# generation process. When set to `True`, it will print additional information
|
||||
# about the steps being executed.
|
||||
STRUCT_DEBUG_MODE = False
|
||||
|
||||
# These structs require specific function calls for creation and/or deletion,
|
||||
# or some of their fields need to be handled manually for now;
|
||||
# making their wrapper constructors/destructors non-trivial.
|
||||
HARDCODED_WRAPPER_STRUCTS = [
|
||||
"MjData",
|
||||
"MjModel",
|
||||
"MjvScene",
|
||||
"MjSpec",
|
||||
"MjVisual",
|
||||
]
|
||||
@@ -0,0 +1,350 @@
|
||||
# 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.
|
||||
|
||||
"""Helper functions for processing and generating bindings for MuJoCo functions."""
|
||||
|
||||
from typing import List, Set, Tuple, cast
|
||||
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import code_builder
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants
|
||||
|
||||
|
||||
PRIMITIVE_TYPES = constants.PRIMITIVE_TYPES
|
||||
uppercase_first_letter = common.uppercase_first_letter
|
||||
|
||||
|
||||
def param_is_primitive_value(param: ast_nodes.FunctionParameterDecl) -> bool:
|
||||
"""Checks if param is a primitive value type."""
|
||||
if isinstance(param.type, ast_nodes.ValueType):
|
||||
return param.type.name in PRIMITIVE_TYPES
|
||||
return False
|
||||
|
||||
|
||||
def param_is_pointer_to_primitive_value(
|
||||
param: ast_nodes.FunctionParameterDecl,
|
||||
) -> bool:
|
||||
"""Checks if param is a pointer to a primitive value."""
|
||||
return (
|
||||
isinstance(param.type, ast_nodes.PointerType)
|
||||
or isinstance(param.type, ast_nodes.ArrayType)
|
||||
) and (
|
||||
isinstance(param.type.inner_type, ast_nodes.ValueType)
|
||||
and param.type.inner_type.name in PRIMITIVE_TYPES
|
||||
)
|
||||
|
||||
|
||||
def param_is_pointer_to_struct(param: ast_nodes.FunctionParameterDecl) -> bool:
|
||||
"""Checks if param is a pointer to a struct."""
|
||||
return (
|
||||
isinstance(param.type, ast_nodes.PointerType)
|
||||
or isinstance(param.type, ast_nodes.ArrayType)
|
||||
) and (
|
||||
isinstance(param.type.inner_type, ast_nodes.ValueType)
|
||||
and param.type.inner_type.name not in PRIMITIVE_TYPES
|
||||
)
|
||||
|
||||
|
||||
def return_is_value_of_type(
|
||||
func: ast_nodes.FunctionDecl, allowed_types: Set[str]
|
||||
) -> bool:
|
||||
"""Checks if func returns an allowed value type."""
|
||||
return (
|
||||
isinstance(func.return_type, ast_nodes.ValueType)
|
||||
and func.return_type.name in allowed_types
|
||||
)
|
||||
|
||||
|
||||
def return_is_pointer_to_struct(func: ast_nodes.FunctionDecl) -> bool:
|
||||
"""Checks if func returns a pointer to a struct."""
|
||||
return (
|
||||
isinstance(func.return_type, ast_nodes.PointerType)
|
||||
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
|
||||
and func.return_type.inner_type.name not in PRIMITIVE_TYPES
|
||||
)
|
||||
|
||||
|
||||
def return_is_pointer_to_primitive(func: ast_nodes.FunctionDecl) -> bool:
|
||||
"""Checks if func returns a pointer to a primitive value."""
|
||||
return (
|
||||
isinstance(func.return_type, ast_nodes.PointerType)
|
||||
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
|
||||
and func.return_type.inner_type.name in PRIMITIVE_TYPES
|
||||
)
|
||||
|
||||
|
||||
def get_const_qualifier(func: ast_nodes.FunctionDecl) -> str:
|
||||
"""Returns the const qualifier of func's return type."""
|
||||
if (
|
||||
isinstance(func.return_type, ast_nodes.PointerType)
|
||||
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
|
||||
and func.return_type.inner_type.is_const
|
||||
):
|
||||
return "const "
|
||||
return ""
|
||||
|
||||
|
||||
def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool:
|
||||
"""Checks if a MuJoCo function needs a wrapper function."""
|
||||
return (
|
||||
return_is_pointer_to_primitive(func)
|
||||
or return_is_pointer_to_struct(func)
|
||||
or any(
|
||||
param_is_pointer_to_primitive_value(param)
|
||||
or isinstance(param.type, ast_nodes.ArrayType)
|
||||
or param_is_pointer_to_struct(param)
|
||||
for param in func.parameters
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def generate_function_wrapper(func: ast_nodes.FunctionDecl) -> str:
|
||||
"""Generates C++ code for a wrapper function."""
|
||||
|
||||
builder = code_builder.CodeBuilder()
|
||||
# Build function header
|
||||
params_unpack_statements = get_params_unpack_statements(func.parameters)
|
||||
wrapper_params_list = get_params_string(func.parameters)
|
||||
not_nullable_params = get_params_notnullable(func.parameters)
|
||||
wrapper_params_str = ", ".join(wrapper_params_list)
|
||||
ret_type = get_compatible_return_type(func)
|
||||
builder.line(f"{ret_type} {func.name}_wrapper({wrapper_params_str})")
|
||||
|
||||
# Build function body
|
||||
with builder.block():
|
||||
invoker_params_list = get_params_string_maybe_with_conversion(
|
||||
func.parameters
|
||||
)
|
||||
invoker_params_str = ", ".join(invoker_params_list)
|
||||
invoker_call = f"{func.name}({invoker_params_str})"
|
||||
invoker_statement = get_compatible_return_call(func, invoker_call)
|
||||
for p in not_nullable_params:
|
||||
builder.line(f"CHECK_VAL({p});")
|
||||
for unpack_statement in params_unpack_statements:
|
||||
builder.line(unpack_statement)
|
||||
builder.line(f"{invoker_statement};")
|
||||
return builder.to_string()
|
||||
|
||||
|
||||
def get_params_notnullable(
|
||||
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
|
||||
) -> List[str]:
|
||||
"""Generates list of param names for checking if they aren't null/undefined."""
|
||||
|
||||
not_nullable_params = []
|
||||
for p in ast_params:
|
||||
if (
|
||||
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
# We only check for char because others are checked in the unpacker
|
||||
# and we don't want to check twice.
|
||||
and p.type.inner_type.name == "char"
|
||||
and not p.nullable
|
||||
):
|
||||
not_nullable_params.append(p.name)
|
||||
return not_nullable_params
|
||||
|
||||
|
||||
def get_params_unpack_statements(
|
||||
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
|
||||
) -> List[str]:
|
||||
"""Generates C++ statements to unpack JS values for pointer/array parameters."""
|
||||
|
||||
params_unpack_statements = []
|
||||
for p in ast_params:
|
||||
if (
|
||||
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
and p.type.inner_type.name in PRIMITIVE_TYPES
|
||||
):
|
||||
if p.type.inner_type.name == "char":
|
||||
# param is Javascript string
|
||||
continue
|
||||
|
||||
if p.type.inner_type.is_const:
|
||||
# param is Javascript number[]
|
||||
params_unpack_statements.append(
|
||||
f"UNPACK_ARRAY({p.type.inner_type.name}, {p.name});"
|
||||
)
|
||||
else:
|
||||
# param is TypedArray or a WasmBuffer
|
||||
params_unpack_statements.append(
|
||||
f"UNPACK_VALUE({p.type.inner_type.name}, {p.name});"
|
||||
)
|
||||
return params_unpack_statements
|
||||
|
||||
|
||||
def get_params_string(
|
||||
parameters: Tuple[ast_nodes.FunctionParameterDecl, ...]
|
||||
) -> List[str]:
|
||||
"""Generates a list of C++ parameter declarations as strings."""
|
||||
|
||||
result = []
|
||||
for p in parameters:
|
||||
if (
|
||||
isinstance(p.type, ast_nodes.PointerType)
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
and p.type.inner_type.name not in PRIMITIVE_TYPES
|
||||
):
|
||||
# Pointer to struct parameters
|
||||
const_qualifier = "const " if p.type.inner_type.is_const else ""
|
||||
result.append(
|
||||
f"{const_qualifier}{uppercase_first_letter(p.type.inner_type.name)}&"
|
||||
f" {p.name}"
|
||||
)
|
||||
elif (
|
||||
isinstance(p.type, ast_nodes.ValueType)
|
||||
and p.type.name in PRIMITIVE_TYPES
|
||||
):
|
||||
# Primitive value parameters
|
||||
const_qualifier = "const " if p.type.is_const else ""
|
||||
result.append(f"{const_qualifier}{p.type} {p.name}")
|
||||
elif (
|
||||
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
and p.type.inner_type.name in PRIMITIVE_TYPES
|
||||
):
|
||||
# Pointer to primitive value parameters or arrays
|
||||
if p.type.inner_type.name == "char":
|
||||
if p.nullable:
|
||||
result.append(f"const NullableString& {p.name}")
|
||||
else:
|
||||
result.append(f"const String& {p.name}")
|
||||
elif (
|
||||
p.type.inner_type.name
|
||||
in ["int", "float", "double", "mjtNum", "mjtByte"]
|
||||
and p.type.inner_type.is_const
|
||||
):
|
||||
result.append(f"const NumberArray& {p.name}")
|
||||
else:
|
||||
result.append(f"const val& {p.name}")
|
||||
else:
|
||||
# This case should ideally not be reached if AST is well-formed
|
||||
# and types are categorized by the helper booleans correctly.
|
||||
raise TypeError(
|
||||
"Unable to generate param string. Unhandled parameter type:"
|
||||
f" {p.type} for param '{p.name}'"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_params_string_maybe_with_conversion(
|
||||
ast_params: Tuple[ast_nodes.FunctionParameterDecl, ...],
|
||||
) -> List[str]:
|
||||
"""Generates C++ expressions for passing compatible params from JS to MuJoCo C-API functions."""
|
||||
|
||||
native_params = []
|
||||
for p in ast_params:
|
||||
if param_is_pointer_to_struct(p):
|
||||
native_params.append(f"{p.name}.get()")
|
||||
elif param_is_primitive_value(p):
|
||||
native_params.append(p.name)
|
||||
elif (
|
||||
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
and p.type.inner_type.name in PRIMITIVE_TYPES
|
||||
and p.type.inner_type.name != "char"
|
||||
):
|
||||
native_params.append(f"{p.name}_.data()")
|
||||
elif (
|
||||
isinstance(p.type, (ast_nodes.PointerType, ast_nodes.ArrayType))
|
||||
and isinstance(p.type.inner_type, ast_nodes.ValueType)
|
||||
and p.type.inner_type.name == "char"
|
||||
):
|
||||
const_qualifier = "const " if p.type.inner_type.is_const else ""
|
||||
native_params.append(
|
||||
f"{p.name}.as<{const_qualifier}std::string>().data()"
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Unhandled parameter type for conversion: {p.type} for param"
|
||||
f" '{p.name}'"
|
||||
)
|
||||
return native_params
|
||||
|
||||
|
||||
def get_compatible_return_call(
|
||||
func: ast_nodes.FunctionDecl, invoker: str
|
||||
) -> str:
|
||||
"""Generates embind compatible return value conversion."""
|
||||
|
||||
if return_is_value_of_type(func, {"void"}):
|
||||
return invoker
|
||||
if isinstance(func.return_type, ast_nodes.PointerType) and isinstance(
|
||||
func.return_type.inner_type, ast_nodes.ValueType
|
||||
):
|
||||
if func.return_type.inner_type.name == "char":
|
||||
return f"return std::string({invoker})"
|
||||
elif func.return_type.inner_type.name == "mjString":
|
||||
return f"return *{invoker}"
|
||||
if return_is_pointer_to_struct(func):
|
||||
return get_converted_struct_to_class(func, invoker)
|
||||
if return_is_value_of_type(func, PRIMITIVE_TYPES):
|
||||
return f"return {invoker}"
|
||||
raise RuntimeError(
|
||||
"Failed to calculate return value conversion for function"
|
||||
f" {func.name} that returns '{func.return_type}'"
|
||||
)
|
||||
|
||||
|
||||
def get_compatible_return_type(func: ast_nodes.FunctionDecl) -> str:
|
||||
"""Creates embind compatible return type."""
|
||||
|
||||
if (
|
||||
isinstance(func.return_type, ast_nodes.PointerType)
|
||||
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
|
||||
and func.return_type.inner_type.name in ["char", "mjString"]
|
||||
):
|
||||
return "std::string"
|
||||
if (
|
||||
isinstance(func.return_type, ast_nodes.PointerType)
|
||||
and isinstance(func.return_type.inner_type, ast_nodes.ValueType)
|
||||
and func.return_type.inner_type.name not in PRIMITIVE_TYPES
|
||||
):
|
||||
const_qualifier = get_const_qualifier(func)
|
||||
return f"""{const_qualifier}std::optional<{uppercase_first_letter(func.return_type.inner_type.name)}>"""
|
||||
if (
|
||||
isinstance(func.return_type, ast_nodes.ValueType)
|
||||
and func.return_type.name in PRIMITIVE_TYPES
|
||||
):
|
||||
return f"{func.return_type.name}"
|
||||
return "val"
|
||||
|
||||
|
||||
def get_converted_struct_to_class(
|
||||
func: ast_nodes.FunctionDecl, invoker: str
|
||||
) -> str:
|
||||
"""Generates a C++ function invocation for a struct return-type function."""
|
||||
|
||||
const_qualifier = get_const_qualifier(func)
|
||||
return_type = cast(ast_nodes.PointerType, func.return_type)
|
||||
struct_name = cast(ast_nodes.ValueType, return_type.inner_type).name
|
||||
class_constructor = uppercase_first_letter(struct_name)
|
||||
return_str = f"{class_constructor}(result)"
|
||||
return f"""{const_qualifier}{struct_name}* result = {invoker};
|
||||
if (result == nullptr) {{
|
||||
return std::nullopt;
|
||||
}}
|
||||
return {return_str}"""
|
||||
|
||||
|
||||
def is_excluded_function_name(func_name: str) -> bool:
|
||||
"""Checks if a function name should be excluded from direct binding."""
|
||||
return (
|
||||
func_name.startswith("mjr_")
|
||||
or func_name.startswith("mjui_")
|
||||
or func_name in constants.SKIPPED_FUNCTIONS
|
||||
)
|
||||
@@ -0,0 +1,236 @@
|
||||
# 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.
|
||||
|
||||
from typing import TypeAlias
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import function_utils
|
||||
|
||||
|
||||
PrimitiveTypes: TypeAlias = constants.PRIMITIVE_TYPES
|
||||
ValueType: TypeAlias = ast_nodes.ValueType
|
||||
PointerType: TypeAlias = ast_nodes.PointerType
|
||||
ArrayType: TypeAlias = ast_nodes.ArrayType
|
||||
FunctionParameterDecl: TypeAlias = ast_nodes.FunctionParameterDecl
|
||||
FunctionDecl: TypeAlias = ast_nodes.FunctionDecl
|
||||
|
||||
|
||||
class FunctionUtilsTest(absltest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.struct_type = ValueType("MyStruct")
|
||||
self.ptr_to_int = PointerType(ValueType("int"))
|
||||
self.func_ret_ptr_int = FunctionDecl(
|
||||
"func_pi", PointerType(ValueType("int")), [], "doc"
|
||||
)
|
||||
self.func_ret_ptr_struct = FunctionDecl(
|
||||
"func_ps", PointerType(ValueType("MyStruct")), [], "doc"
|
||||
)
|
||||
|
||||
def test_return_is_value_of_type(self):
|
||||
self.assertTrue(
|
||||
function_utils.return_is_value_of_type(
|
||||
FunctionDecl("func_i", ValueType("int"), [], "doc"), PrimitiveTypes
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.return_is_value_of_type(
|
||||
FunctionDecl("func_s", ValueType("MyStruct"), [], "doc"),
|
||||
PrimitiveTypes,
|
||||
)
|
||||
)
|
||||
|
||||
def test_return_is_pointer_to_struct(self):
|
||||
self.assertTrue(
|
||||
function_utils.return_is_pointer_to_struct(self.func_ret_ptr_struct)
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.return_is_pointer_to_struct(self.func_ret_ptr_int)
|
||||
)
|
||||
|
||||
def test_return_is_pointer_to_primitive(self):
|
||||
self.assertTrue(
|
||||
function_utils.return_is_pointer_to_primitive(self.func_ret_ptr_int)
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.return_is_pointer_to_primitive(self.func_ret_ptr_struct)
|
||||
)
|
||||
|
||||
def test_param_is_primitive_value(self):
|
||||
param_prim_val = FunctionParameterDecl("prim_v", ValueType("int"))
|
||||
param_arr = FunctionParameterDecl(
|
||||
"arr_v", ArrayType(ValueType("int"), extents=(10,))
|
||||
)
|
||||
|
||||
self.assertTrue(function_utils.param_is_primitive_value(param_prim_val))
|
||||
self.assertFalse(function_utils.param_is_primitive_value(param_arr))
|
||||
|
||||
def test_param_is_pointer_to_primitive_value(self):
|
||||
param_ptr_to_prim = FunctionParameterDecl("p_prim", self.ptr_to_int)
|
||||
param_arr_of_prim = FunctionParameterDecl(
|
||||
"a_prim", ArrayType(ValueType("int"), extents=(10,))
|
||||
)
|
||||
param_ptr_to_struct = FunctionParameterDecl(
|
||||
name="p_struct", type=PointerType(inner_type=self.struct_type)
|
||||
)
|
||||
self.assertTrue(
|
||||
function_utils.param_is_pointer_to_primitive_value(param_ptr_to_prim)
|
||||
)
|
||||
self.assertTrue(
|
||||
function_utils.param_is_pointer_to_primitive_value(param_arr_of_prim)
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.param_is_pointer_to_primitive_value(param_ptr_to_struct)
|
||||
)
|
||||
|
||||
def test_param_is_pointer_to_struct(self):
|
||||
param_arr_of_struct = FunctionParameterDecl(
|
||||
"a_struct", ArrayType(self.struct_type, extents=(5,))
|
||||
)
|
||||
param_ptr_to_struct = FunctionParameterDecl(
|
||||
"p_struct", PointerType(self.struct_type)
|
||||
)
|
||||
param_ptr_to_ptr = FunctionParameterDecl(
|
||||
"p_ptr", PointerType(self.ptr_to_int)
|
||||
)
|
||||
self.assertTrue(
|
||||
function_utils.param_is_pointer_to_struct(param_arr_of_struct)
|
||||
)
|
||||
self.assertTrue(
|
||||
function_utils.param_is_pointer_to_struct(param_ptr_to_struct)
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.param_is_pointer_to_struct(param_ptr_to_ptr)
|
||||
)
|
||||
|
||||
def test_should_be_wrapped_with_primitive_ptr_return(self):
|
||||
func = FunctionDecl(
|
||||
name="get_data",
|
||||
return_type=PointerType(ValueType("int")),
|
||||
parameters=tuple(),
|
||||
doc="Returns int pointer",
|
||||
)
|
||||
self.assertTrue(function_utils.should_be_wrapped(func))
|
||||
|
||||
def test_generate_function_wrapper_for_simple_func(self):
|
||||
func = FunctionDecl(
|
||||
name="get_id",
|
||||
return_type=ValueType("int"),
|
||||
parameters=tuple(),
|
||||
doc="Returns an integer ID",
|
||||
)
|
||||
result = function_utils.generate_function_wrapper(func)
|
||||
self.assertEqual(result, """int get_id_wrapper()
|
||||
{
|
||||
return get_id();
|
||||
}""")
|
||||
|
||||
def test_generate_function_wrapper_checking_param(self):
|
||||
parameters = (
|
||||
FunctionParameterDecl(
|
||||
name="mat",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="mjtNum", is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name="nr",
|
||||
type=ValueType(name="int"),
|
||||
),
|
||||
)
|
||||
func = FunctionDecl(
|
||||
name="get_id",
|
||||
return_type=ValueType("int"),
|
||||
parameters=parameters,
|
||||
doc="Returns an integer ID",
|
||||
)
|
||||
result = function_utils.generate_function_wrapper(func)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"""int get_id_wrapper(const NumberArray& mat, int nr)
|
||||
{
|
||||
UNPACK_ARRAY(mjtNum, mat);
|
||||
return get_id(mat_.data(), nr);
|
||||
}""",
|
||||
)
|
||||
|
||||
def test_get_params_string_with_struct_ptr(self):
|
||||
param = FunctionParameterDecl(
|
||||
name="my_struct",
|
||||
type=PointerType(ValueType("mystruct")),
|
||||
)
|
||||
result = function_utils.get_params_string((param,))
|
||||
self.assertEqual(result, ["Mystruct& my_struct"])
|
||||
|
||||
def test_get_params_string_maybe_with_conversion_struct_ptr(self):
|
||||
param = FunctionParameterDecl(
|
||||
name="s",
|
||||
type=PointerType(ValueType("customstruct")),
|
||||
)
|
||||
result = function_utils.get_params_string_maybe_with_conversion((param,))
|
||||
self.assertEqual(result, ["s.get()"])
|
||||
|
||||
def test_get_compatible_return_call(self):
|
||||
func = FunctionDecl(
|
||||
name="noop",
|
||||
return_type=ValueType("void"),
|
||||
parameters=tuple(),
|
||||
doc="does nothing",
|
||||
)
|
||||
result = function_utils.get_compatible_return_call(func, "noop()")
|
||||
self.assertEqual(result, "noop()")
|
||||
|
||||
def test_get_compatible_return_type(self):
|
||||
func = FunctionDecl(
|
||||
name="get_name",
|
||||
return_type=PointerType(ValueType("char")),
|
||||
parameters=tuple(),
|
||||
doc="returns name",
|
||||
)
|
||||
result = function_utils.get_compatible_return_type(func)
|
||||
self.assertEqual(result.strip(), "std::string")
|
||||
|
||||
def test_get_converted_struct_to_class(self):
|
||||
func = FunctionDecl(
|
||||
name="get_struct",
|
||||
return_type=PointerType(ValueType("mystruct")),
|
||||
parameters=tuple(),
|
||||
doc="returns struct",
|
||||
)
|
||||
result = function_utils.get_converted_struct_to_class(func, "get_struct()")
|
||||
self.assertIn("mystruct* result = get_struct();", result)
|
||||
self.assertIn("return Mystruct(result)", result)
|
||||
|
||||
def test_is_excluded_function_name(self):
|
||||
self.assertTrue(function_utils.is_excluded_function_name("mjr_function"))
|
||||
self.assertTrue(function_utils.is_excluded_function_name("mjui_function"))
|
||||
self.assertTrue(function_utils.is_excluded_function_name("mju_malloc"))
|
||||
self.assertTrue(function_utils.is_excluded_function_name("mj_makeData"))
|
||||
self.assertFalse(
|
||||
function_utils.is_excluded_function_name("mjv_updateScene")
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.is_excluded_function_name("mj_normalFunction")
|
||||
)
|
||||
self.assertFalse(
|
||||
function_utils.is_excluded_function_name("mju_someOtherFunction")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,195 @@
|
||||
# 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.
|
||||
|
||||
"""Code builder for struct constructor code."""
|
||||
|
||||
from typing import List, cast
|
||||
|
||||
from introspect import ast_nodes
|
||||
from introspect import structs as introspect_structs
|
||||
|
||||
from wasm.codegen.helpers import code_builder
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import structs_wrappers_data
|
||||
|
||||
|
||||
def _has_nested_wrapper_members(
|
||||
struct_info: ast_nodes.StructDecl
|
||||
) -> bool:
|
||||
"""Checks if the struct contains other wrapped structs as direct members."""
|
||||
for field in struct_info.fields:
|
||||
struct_field = cast(ast_nodes.StructFieldDecl, field)
|
||||
if isinstance(struct_field.type, ast_nodes.ValueType):
|
||||
if struct_field.type.name in constants.STRUCTS_TO_BIND:
|
||||
return True
|
||||
if isinstance(struct_field.type, ast_nodes.ArrayType):
|
||||
if isinstance(struct_field.type.inner_type, ast_nodes.ValueType):
|
||||
if struct_field.type.inner_type.name in constants.STRUCTS_TO_BIND:
|
||||
return True
|
||||
if isinstance(struct_field.type, ast_nodes.PointerType):
|
||||
if isinstance(struct_field.type.inner_type, ast_nodes.ValueType):
|
||||
if struct_field.type.inner_type.name in constants.STRUCTS_TO_BIND:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _build_struct_header_internal(
|
||||
struct_name: str,
|
||||
wrapped_fields: List[structs_wrappers_data.WrappedFieldData],
|
||||
fields_with_init: List[structs_wrappers_data.WrappedFieldData],
|
||||
use_shallow_copy: bool = False,
|
||||
is_mjs: bool = False,
|
||||
):
|
||||
"""Builds the C++ header file code for a struct."""
|
||||
wrapper_name = common.uppercase_first_letter(struct_name)
|
||||
builder = code_builder.CodeBuilder()
|
||||
with builder.block(f"struct {wrapper_name}"):
|
||||
if not is_mjs:
|
||||
builder.line(f"{wrapper_name}();")
|
||||
builder.line(f"{wrapper_name}(const {wrapper_name} &);")
|
||||
builder.line(f"{wrapper_name} &operator=(const {wrapper_name} &);")
|
||||
|
||||
builder.line(f"explicit {wrapper_name}({struct_name} *ptr);")
|
||||
builder.line(f"~{wrapper_name}();")
|
||||
|
||||
if use_shallow_copy:
|
||||
builder.line(f"std::unique_ptr<{wrapper_name}> copy();")
|
||||
|
||||
for field in wrapped_fields:
|
||||
if field.definition and field not in fields_with_init:
|
||||
for line in field.definition.splitlines():
|
||||
builder.line(line)
|
||||
|
||||
builder.line(f"{struct_name}* get() const {{ return ptr_; }}")
|
||||
builder.line(f"void set({struct_name}* ptr) {{ ptr_ = ptr; }}")
|
||||
builder.line("")
|
||||
builder.line("private:")
|
||||
builder.line(f"{struct_name}* ptr_;")
|
||||
builder.line("bool owned_ = false;")
|
||||
|
||||
if is_mjs and fields_with_init:
|
||||
builder.line("")
|
||||
builder.line("public:")
|
||||
for field in fields_with_init:
|
||||
if field.definition:
|
||||
builder.line(f"{field.definition}")
|
||||
return builder.to_string()+";"
|
||||
|
||||
|
||||
def build_struct_header(
|
||||
struct_name: str,
|
||||
use_shallow_copy: bool = False,
|
||||
fields_with_init: List[structs_wrappers_data.WrappedFieldData] = [],
|
||||
wrapped_fields: List[structs_wrappers_data.WrappedFieldData] = [],
|
||||
):
|
||||
"""Builds the C++ header file code for a struct."""
|
||||
struct_info = introspect_structs.STRUCTS.get(struct_name)
|
||||
|
||||
if struct_name.startswith("mjs"):
|
||||
return _build_struct_header_internal(
|
||||
struct_name,
|
||||
wrapped_fields,
|
||||
fields_with_init,
|
||||
use_shallow_copy,
|
||||
is_mjs=True,
|
||||
)
|
||||
|
||||
if (
|
||||
(
|
||||
common.uppercase_first_letter(struct_name)
|
||||
not in constants.HARDCODED_WRAPPER_STRUCTS
|
||||
)
|
||||
and struct_info
|
||||
and not _has_nested_wrapper_members(struct_info)
|
||||
):
|
||||
return _build_struct_header_internal(
|
||||
struct_name, wrapped_fields, [], use_shallow_copy, is_mjs=False
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def build_struct_source(
|
||||
struct_name: str,
|
||||
mj_default_func: str | None = None,
|
||||
fields_with_init: List[structs_wrappers_data.WrappedFieldData] = [],
|
||||
use_shallow_copy: bool = False,
|
||||
):
|
||||
"""Builds the C++ .cc file code for a struct."""
|
||||
wrapper_name = common.uppercase_first_letter(struct_name)
|
||||
is_mjs_struct = "Mjs" in wrapper_name
|
||||
builder = code_builder.CodeBuilder()
|
||||
|
||||
fields_init = ""
|
||||
if fields_with_init:
|
||||
fields_init = "".join(
|
||||
field_with_init.initialization
|
||||
for field_with_init in fields_with_init
|
||||
)
|
||||
# constructor passing native ptr
|
||||
builder.line(
|
||||
f"{wrapper_name}::{wrapper_name}({struct_name} *ptr) :"
|
||||
f" ptr_(ptr){fields_init} {{}}"
|
||||
)
|
||||
# constructor with default values
|
||||
if not is_mjs_struct:
|
||||
with builder.block(
|
||||
f"{wrapper_name}::{wrapper_name}() : ptr_(new"
|
||||
f" {struct_name}){fields_init}"
|
||||
):
|
||||
builder.line("owned_ = true;")
|
||||
if mj_default_func:
|
||||
builder.line(f"{mj_default_func}(ptr_);")
|
||||
# copy constructor
|
||||
if use_shallow_copy and not is_mjs_struct:
|
||||
with builder.block(
|
||||
f"{wrapper_name}::{wrapper_name}(const"
|
||||
f" {wrapper_name} &other)"
|
||||
+ (f" : {wrapper_name}()" if not is_mjs_struct else "")
|
||||
):
|
||||
builder.line("*ptr_ = *other.get();")
|
||||
if fields_with_init:
|
||||
for field_with_init in fields_with_init:
|
||||
if field_with_init.ptr_copy_reset is not None:
|
||||
builder.line(field_with_init.ptr_copy_reset)
|
||||
# assignment operator
|
||||
with builder.block(
|
||||
f"{wrapper_name}&"
|
||||
f" {wrapper_name}::operator=(const"
|
||||
f" {wrapper_name} &other)"
|
||||
):
|
||||
with builder.block("if (this == &other)"):
|
||||
builder.line("return *this;")
|
||||
builder.line("*ptr_ = *other.get();")
|
||||
if fields_with_init:
|
||||
for field_with_init in fields_with_init:
|
||||
if field_with_init.ptr_copy_reset is not None:
|
||||
builder.line(field_with_init.ptr_copy_reset)
|
||||
builder.line("return *this;")
|
||||
# destructor
|
||||
if is_mjs_struct:
|
||||
builder.line(f"{wrapper_name}::~{wrapper_name}() {{}}")
|
||||
else:
|
||||
with builder.block(f"{wrapper_name}::~{wrapper_name}()"):
|
||||
builder.line("if (owned_ && ptr_) delete ptr_;")
|
||||
# copy function
|
||||
if use_shallow_copy:
|
||||
with builder.block(
|
||||
f"std::unique_ptr<{wrapper_name}>"
|
||||
f" {wrapper_name}::copy()"
|
||||
):
|
||||
builder.line(
|
||||
f"return std::make_unique<{wrapper_name}>(*this);"
|
||||
)
|
||||
return builder.to_string()
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import ast_nodes
|
||||
from wasm.codegen.helpers import struct_constructor_code_builder
|
||||
from wasm.codegen.helpers import struct_field_handler
|
||||
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
ValueType = ast_nodes.ValueType
|
||||
PointerType = ast_nodes.PointerType
|
||||
ArrayType = ast_nodes.ArrayType
|
||||
StructDecl = ast_nodes.StructDecl
|
||||
|
||||
|
||||
class StructConstructorCodeBuilderTest(absltest.TestCase):
|
||||
|
||||
def test_constructor_code_with_default_function(self):
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_source(
|
||||
"mjLROpt", "mj_defaultLROpt"
|
||||
),
|
||||
"""
|
||||
MjLROpt::MjLROpt(mjLROpt *ptr) : ptr_(ptr) {}
|
||||
MjLROpt::MjLROpt() : ptr_(new mjLROpt) {
|
||||
owned_ = true;
|
||||
mj_defaultLROpt(ptr_);
|
||||
}
|
||||
MjLROpt::~MjLROpt() {
|
||||
if (owned_ && ptr_) delete ptr_;
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_constructor_code_without_default_function(self):
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_source("mjLROpt"),
|
||||
"""
|
||||
MjLROpt::MjLROpt(mjLROpt *ptr) : ptr_(ptr) {}
|
||||
MjLROpt::MjLROpt() : ptr_(new mjLROpt) {
|
||||
owned_ = true;
|
||||
}
|
||||
MjLROpt::~MjLROpt() {
|
||||
if (owned_ && ptr_) delete ptr_;
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_constructor_code_with_fields_with_init(self):
|
||||
field_with_init = StructFieldDecl(
|
||||
name="element",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="mjsElement"),
|
||||
),
|
||||
doc="",
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field_with_init, "MjsTexture"
|
||||
).generate()
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_source(
|
||||
"mjsTexture",
|
||||
"mjs_defaultTexture",
|
||||
[wrapped_field_data],
|
||||
),
|
||||
"""
|
||||
MjsTexture::MjsTexture(mjsTexture *ptr) : ptr_(ptr), element(ptr_->element) {}
|
||||
MjsTexture::~MjsTexture() {}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_constructor_code_with_shallow_copy(self):
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_source(
|
||||
"mjvLight", use_shallow_copy=True
|
||||
),
|
||||
"""MjvLight::MjvLight(mjvLight *ptr) : ptr_(ptr) {}
|
||||
MjvLight::MjvLight() : ptr_(new mjvLight) {
|
||||
owned_ = true;
|
||||
}
|
||||
MjvLight::MjvLight(const MjvLight &other) : MjvLight() {
|
||||
*ptr_ = *other.get();
|
||||
}
|
||||
MjvLight& MjvLight::operator=(const MjvLight &other) {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
*ptr_ = *other.get();
|
||||
return *this;
|
||||
}
|
||||
MjvLight::~MjvLight() {
|
||||
if (owned_ && ptr_) delete ptr_;
|
||||
}
|
||||
std::unique_ptr<MjvLight> MjvLight::copy() {
|
||||
return std::make_unique<MjvLight>(*this);
|
||||
}""".strip(),
|
||||
)
|
||||
|
||||
|
||||
def test_build_struct_header_with_nested_wrappers(self):
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_header("mjData"),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
def test_build_struct_header_basic_struct(self):
|
||||
self.assertEqual(
|
||||
struct_constructor_code_builder.build_struct_header("mjLROpt"),
|
||||
"""
|
||||
struct MjLROpt {
|
||||
MjLROpt();
|
||||
MjLROpt(const MjLROpt &);
|
||||
MjLROpt &operator=(const MjLROpt &);
|
||||
explicit MjLROpt(mjLROpt *ptr);
|
||||
~MjLROpt();
|
||||
mjLROpt* get() const { return ptr_; }
|
||||
void set(mjLROpt* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjLROpt* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
|
||||
"""Class to build the C++ code for a struct field wrapper."""
|
||||
|
||||
from introspect import ast_nodes
|
||||
from wasm.codegen.helpers import code_builder
|
||||
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
ValueType = ast_nodes.ValueType
|
||||
|
||||
|
||||
def build_primitive_type_definition(field: StructFieldDecl) -> str:
|
||||
"""Builds the C++ code for a primitive type field wrapper."""
|
||||
if not isinstance(field.type, ValueType):
|
||||
raise ValueError(f"{field.type} must be ValueType.")
|
||||
builder = code_builder.CodeBuilder()
|
||||
# build getter for primitive type field
|
||||
with builder.block(f"{field.type.name} {field.name}() const"):
|
||||
builder.line(f"return ptr_->{field.name};")
|
||||
# build setter for primitive type field
|
||||
with builder.block(f"void set_{field.name}({field.type.name} value)"):
|
||||
builder.line(f"ptr_->{field.name} = value;")
|
||||
return builder.to_string()
|
||||
|
||||
|
||||
def build_memory_view_definition(
|
||||
field: StructFieldDecl, array_size_str: str, ptr_expr: str
|
||||
) -> str:
|
||||
"""Builds the C++ code for a pointer type field wrapper."""
|
||||
builder = code_builder.CodeBuilder()
|
||||
with builder.block(f"emscripten::val {field.name}() const"):
|
||||
builder.line(
|
||||
"return"
|
||||
f" emscripten::val(emscripten::typed_memory_view({array_size_str},"
|
||||
f" {ptr_expr}));"
|
||||
)
|
||||
return builder.to_string()
|
||||
|
||||
|
||||
def build_string_field_definition(field: StructFieldDecl) -> str:
|
||||
"""Builds the C++ code for a string type field wrapper."""
|
||||
builder = code_builder.CodeBuilder()
|
||||
with builder.block(f"mjString {field.name}() const"):
|
||||
builder.line(
|
||||
f'return (ptr_ && ptr_->{field.name}) ? *(ptr_->{field.name}) : "";'
|
||||
)
|
||||
with builder.block(f"void set_{field.name}(const mjString& value)"):
|
||||
with builder.block(f"if (ptr_ && ptr_->{field.name})"):
|
||||
builder.line(f"*(ptr_->{field.name}) = value;")
|
||||
return builder.to_string()
|
||||
|
||||
|
||||
def build_mjvec_pointer_definition(
|
||||
field: StructFieldDecl, vector_type: str
|
||||
) -> str:
|
||||
"""Builds the C++ code for a mjVec type field wrapper."""
|
||||
ptr_field_expr = f"*(ptr_->{field.name})"
|
||||
if vector_type == "mjByteVec":
|
||||
vector_type = "std::vector<uint8_t>"
|
||||
ptr_field_expr = (
|
||||
f"*(reinterpret_cast<std::vector<uint8_t>*>(ptr_->{field.name}))"
|
||||
)
|
||||
builder = code_builder.CodeBuilder()
|
||||
with builder.block(f"{vector_type} &{field.name}() const"):
|
||||
builder.line(f"return {ptr_field_expr};")
|
||||
return builder.to_string()
|
||||
|
||||
|
||||
def build_simple_property_binding(
|
||||
field: StructFieldDecl,
|
||||
struct_wrapper_name: str,
|
||||
add_setter: bool = False,
|
||||
add_return_value_policy_as_ref: bool = False,
|
||||
) -> str:
|
||||
"""Builds the C++ code for a simple property binding."""
|
||||
builder = code_builder.CodeBuilder()
|
||||
setter_txt = ""
|
||||
if add_setter:
|
||||
setter_txt = f", &{struct_wrapper_name}::set_{field.name}"
|
||||
if add_return_value_policy_as_ref:
|
||||
as_reference_txt = ", reference()"
|
||||
else:
|
||||
as_reference_txt = ""
|
||||
builder.line(
|
||||
f'.property("{field.name}",'
|
||||
f" &{struct_wrapper_name}::{field.name}{setter_txt}{as_reference_txt})"
|
||||
)
|
||||
return builder.to_string()
|
||||
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import struct_field_code_builder
|
||||
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
ValueType = ast_nodes.ValueType
|
||||
PointerType = ast_nodes.PointerType
|
||||
ArrayType = ast_nodes.ArrayType
|
||||
|
||||
|
||||
class StructFieldCodeBuilderTest(absltest.TestCase):
|
||||
|
||||
def test_primitive_type_definition(self):
|
||||
field = StructFieldDecl(
|
||||
name="ngeom",
|
||||
type=ValueType(name="int"),
|
||||
doc="number of geoms",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_primitive_type_definition(field),
|
||||
"""
|
||||
int ngeom() const {
|
||||
return ptr_->ngeom;
|
||||
}
|
||||
void set_ngeom(int value) {
|
||||
ptr_->ngeom = value;
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_memory_view_definition(self):
|
||||
field = StructFieldDecl(
|
||||
name="geom_rgba",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="float"),
|
||||
),
|
||||
doc="rgba when material is omitted",
|
||||
array_extent=("ngeom", 4),
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_memory_view_definition(
|
||||
field, "ptr_->ngeom * 4", "ptr_->geom_rgba"
|
||||
),
|
||||
"""
|
||||
emscripten::val geom_rgba() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(ptr_->ngeom * 4, ptr_->geom_rgba));
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_string_field_definition(self):
|
||||
field = StructFieldDecl(
|
||||
name="string_field",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="mjString"),
|
||||
),
|
||||
doc="rgba when material is omitted",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_string_field_definition(field),
|
||||
"""
|
||||
mjString string_field() const {
|
||||
return (ptr_ && ptr_->string_field) ? *(ptr_->string_field) : "";
|
||||
}
|
||||
void set_string_field(const mjString& value) {
|
||||
if (ptr_ && ptr_->string_field) {
|
||||
*(ptr_->string_field) = value;
|
||||
}
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
|
||||
def test_mjvec_pointer_definition(self):
|
||||
field = StructFieldDecl(
|
||||
name="vector_field",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="mjDoubleVec"),
|
||||
),
|
||||
doc="",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_mjvec_pointer_definition(
|
||||
field, "mjDoubleVec"
|
||||
),
|
||||
"""
|
||||
mjDoubleVec &vector_field() const {
|
||||
return *(ptr_->vector_field);
|
||||
}""".strip(),
|
||||
)
|
||||
|
||||
def test_mjbyte_vec_pointer_definition(self):
|
||||
field = StructFieldDecl(
|
||||
name="vector_field",
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name="mjByteVec"),
|
||||
),
|
||||
doc="",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_mjvec_pointer_definition(
|
||||
field, "mjByteVec"
|
||||
),
|
||||
"""
|
||||
std::vector<uint8_t> &vector_field() const {
|
||||
return *(reinterpret_cast<std::vector<uint8_t>*>(ptr_->vector_field));
|
||||
}""".strip(),
|
||||
)
|
||||
|
||||
def test_simple_property_binding(self):
|
||||
field = StructFieldDecl(
|
||||
name="ngeom",
|
||||
type=ValueType(name="int"),
|
||||
doc="number of geoms",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_simple_property_binding(
|
||||
field, "MjModel"
|
||||
),
|
||||
'.property("ngeom", &MjModel::ngeom)',
|
||||
)
|
||||
|
||||
def test_simple_property_binding_with_setter(self):
|
||||
field = StructFieldDecl(
|
||||
name="ngeom",
|
||||
type=ValueType(name="int"),
|
||||
doc="",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_simple_property_binding(
|
||||
field, "MjModel", True
|
||||
),
|
||||
'.property("ngeom", &MjModel::ngeom,'
|
||||
" &MjModel::set_ngeom)",
|
||||
)
|
||||
|
||||
def test_simple_property_binding_with_return_value_policy_as_ref(self):
|
||||
field = StructFieldDecl(
|
||||
name="ngeom",
|
||||
type=ValueType(name="int"),
|
||||
doc="",
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_code_builder.build_simple_property_binding(
|
||||
field,
|
||||
"MjModel",
|
||||
add_setter=True,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
'.property("ngeom", &MjModel::ngeom,'
|
||||
" &MjModel::set_ngeom,"
|
||||
" reference())",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,358 @@
|
||||
# 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.
|
||||
|
||||
"""Class to handle the different struct field types, and provide the c++ code for the wrappers and bindings."""
|
||||
|
||||
import math
|
||||
from typing import Tuple, Union, cast
|
||||
from introspect import ast_nodes
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import struct_field_code_builder
|
||||
from wasm.codegen.helpers import structs_wrappers_data
|
||||
|
||||
AnonymousStructDecl = ast_nodes.AnonymousStructDecl
|
||||
ArrayType = ast_nodes.ArrayType
|
||||
PointerType = ast_nodes.PointerType
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
ValueType = ast_nodes.ValueType
|
||||
WrappedFieldData = structs_wrappers_data.WrappedFieldData
|
||||
|
||||
debug_print = common.debug_print
|
||||
|
||||
|
||||
class StructFieldHandler:
|
||||
"""Class to handle the different struct field types, and provide the c++ code for the definitions and bindings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
field: StructFieldDecl,
|
||||
struct_wrapper_name: str,
|
||||
):
|
||||
self.field = field
|
||||
self.struct_wrapper_name = struct_wrapper_name
|
||||
self.simple_property_binding = (
|
||||
struct_field_code_builder.build_simple_property_binding(
|
||||
self.field, self.struct_wrapper_name
|
||||
)
|
||||
)
|
||||
self.manually_added_fields = (
|
||||
constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.get(
|
||||
self.struct_wrapper_name, {}
|
||||
)
|
||||
)
|
||||
|
||||
def generate(self) -> WrappedFieldData:
|
||||
"""Generates the C++ definition and binding code for the struct field."""
|
||||
field_type = self.field.type
|
||||
if isinstance(field_type, ValueType) and (
|
||||
field_type.name in constants.PRIMITIVE_TYPES
|
||||
or field_type.name.startswith("mjt")
|
||||
):
|
||||
return self._handle_primitive()
|
||||
elif isinstance(field_type, PointerType):
|
||||
return self._handle_pointer()
|
||||
elif isinstance(field_type, ArrayType):
|
||||
return self._handle_array()
|
||||
elif isinstance(field_type, ValueType) and field_type.name.startswith("mj"):
|
||||
return self._handle_mj_struct()
|
||||
elif isinstance(field_type, AnonymousStructDecl):
|
||||
return self._handle_anonymous_struct()
|
||||
return self._undefined()
|
||||
|
||||
def _handle_primitive(self) -> WrappedFieldData:
|
||||
"""Handles the generation of C++ definition and binding code for primitive fields."""
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
struct_field_code_builder.build_primitive_type_definition(
|
||||
self.field
|
||||
)
|
||||
),
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=True,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
is_primitive_or_fixed_size=True,
|
||||
)
|
||||
|
||||
def _handle_pointer(self) -> WrappedFieldData:
|
||||
"""Handles the generation of C++ definition and binding code for pointer fields."""
|
||||
if not isinstance(self.field.type, PointerType):
|
||||
raise ValueError(
|
||||
f"Expected PointerType, got {type(self.field.type)} for field"
|
||||
f" {self.field.name}"
|
||||
)
|
||||
field_type: PointerType = self.field.type
|
||||
inner_type_name = (
|
||||
field_type.inner_type.name
|
||||
if isinstance(field_type.inner_type, ValueType)
|
||||
else ""
|
||||
)
|
||||
ptr_field_expr = f"ptr_->{self.field.name}"
|
||||
array_size_str = ""
|
||||
|
||||
if self.field.array_extent:
|
||||
array_size_str = parse_array_extent(
|
||||
self.field.array_extent, self.struct_wrapper_name, self.field.name
|
||||
)
|
||||
elif self.field.name in constants.BYTE_FIELDS.keys():
|
||||
# for byte fields, we need to cast the pointer to uint8_t*
|
||||
# so embind can correctly interpret the memory view
|
||||
ptr_field_expr = (
|
||||
f"static_cast<uint8_t*>({ptr_field_expr})"
|
||||
)
|
||||
# for these byte fields, there is no array_extent, so we add the size of
|
||||
# in the config file based in the documentation
|
||||
extent = (constants.BYTE_FIELDS[self.field.name]["size"],)
|
||||
array_size_str = parse_array_extent(
|
||||
extent, self.struct_wrapper_name, self.field.name
|
||||
)
|
||||
elif inner_type_name == "mjString":
|
||||
return WrappedFieldData(
|
||||
definition=struct_field_code_builder.build_string_field_definition(
|
||||
self.field
|
||||
),
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=True,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
)
|
||||
elif inner_type_name.startswith("mj") and inner_type_name.endswith("Vec"):
|
||||
return WrappedFieldData(
|
||||
definition=struct_field_code_builder.build_mjvec_pointer_definition(
|
||||
self.field, inner_type_name
|
||||
),
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=False,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
)
|
||||
elif inner_type_name in constants.PRIMITIVE_TYPES:
|
||||
return self._get_manual_definition(
|
||||
comment_type="primitive pointer field with complex extents"
|
||||
)
|
||||
|
||||
if (
|
||||
inner_type_name.startswith("mj")
|
||||
and inner_type_name not in constants.PRIMITIVE_TYPES
|
||||
):
|
||||
debug_print(
|
||||
f"\tcomplex pointer type: needs manual wrapper: {self.field.name}"
|
||||
)
|
||||
# it's a pointer to a single struct,
|
||||
# like the `element` field in mjs structs
|
||||
# and the struct is not manually added
|
||||
if (
|
||||
not self.field.array_extent
|
||||
and self.struct_wrapper_name
|
||||
not in constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.keys()
|
||||
):
|
||||
ptr_field = cast(PointerType, self.field.type)
|
||||
wrapper_field_name = common.uppercase_first_letter(
|
||||
cast(ValueType, ptr_field.inner_type).name
|
||||
)
|
||||
return WrappedFieldData(
|
||||
definition=f"{wrapper_field_name} {self.field.name};",
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=False,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
initialization=f", {self.field.name}(ptr_->{self.field.name})",
|
||||
)
|
||||
else:
|
||||
debug_print(
|
||||
"\tcomplex pointer type with array extent: needs manual wrapper:"
|
||||
f" {self.field.name}"
|
||||
)
|
||||
return self._get_manual_definition(comment_type="complex pointer field")
|
||||
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
struct_field_code_builder.build_memory_view_definition(
|
||||
self.field, array_size_str, ptr_field_expr
|
||||
)
|
||||
),
|
||||
binding=self.simple_property_binding,
|
||||
)
|
||||
|
||||
def _handle_array(self) -> WrappedFieldData:
|
||||
"""Handles the generation of C++ definition and binding code for array fields."""
|
||||
field_type = self.field.type
|
||||
if not isinstance(field_type, ArrayType):
|
||||
raise ValueError(
|
||||
f"Expected ArrayType, got {type(field_type)} for field"
|
||||
f" {self.field.name}"
|
||||
)
|
||||
inner_type = field_type.inner_type
|
||||
size = math.prod(field_type.extents)
|
||||
|
||||
if isinstance(inner_type, ValueType):
|
||||
if inner_type.name in constants.PRIMITIVE_TYPES:
|
||||
ptr_expr = f"ptr_->{self.field.name}"
|
||||
if len(field_type.extents) > 1:
|
||||
# for multi-dimensional arrays, we need to cast the field
|
||||
# to a pointer, so embind can correctly interpret the memory
|
||||
# view
|
||||
ptr_expr = f"reinterpret_cast<{inner_type.name}*>({ptr_expr})"
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
struct_field_code_builder.build_memory_view_definition(
|
||||
self.field, str(size), ptr_expr
|
||||
)
|
||||
),
|
||||
binding=self.simple_property_binding,
|
||||
is_primitive_or_fixed_size=True,
|
||||
)
|
||||
elif inner_type.name.startswith("mj") and not inner_type.name.startswith(
|
||||
"mjt"
|
||||
):
|
||||
debug_print(f"\tarray to vector wrapper needed: {self.field.name}")
|
||||
return self._get_manual_definition(comment_type="array field")
|
||||
|
||||
debug_print(f"\tNOT IMPLEMENTED ARRAY field: {self.field.name}")
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
f"// TODO: NOT IMPLEMENTED ARRAY wrapper for {self.field.name}"
|
||||
),
|
||||
binding=f"// TODO: NOT IMPLEMENTED ARRAY binding for {self.field.name}",
|
||||
)
|
||||
|
||||
def _handle_mj_struct(self) -> WrappedFieldData:
|
||||
"""Handles the generation of C++ definition and binding code for mj struct fields."""
|
||||
if (
|
||||
isinstance(self.field.type, ValueType)
|
||||
and self.field.name not in self.manually_added_fields
|
||||
and self.field.type.name in constants.STRUCTS_TO_BIND
|
||||
):
|
||||
# TODO(manevi): Find a better way to do this instead of checking the
|
||||
# struct wrapper name.
|
||||
definition = ""
|
||||
if self.struct_wrapper_name not in constants.HARDCODED_WRAPPER_STRUCTS:
|
||||
wrapper_field_name = common.uppercase_first_letter(self.field.type.name)
|
||||
definition = f"{wrapper_field_name} {self.field.name};"
|
||||
return WrappedFieldData(
|
||||
definition=definition,
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=False,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
initialization=f", {self.field.name}(&ptr_->{self.field.name})",
|
||||
ptr_copy_reset=f"{self.field.name}.set(&ptr_->{self.field.name});",
|
||||
is_primitive_or_fixed_size=True,
|
||||
)
|
||||
return self._get_manual_definition(comment_type="struct field")
|
||||
|
||||
def _handle_anonymous_struct(self) -> WrappedFieldData:
|
||||
"""Handles the generation of C++ definition and binding code for anonymous struct fields."""
|
||||
|
||||
anonymous_struct_name = ""
|
||||
for name, value in constants.ANONYMOUS_STRUCTS.items():
|
||||
if (
|
||||
common.uppercase_first_letter(value["parent"])
|
||||
== self.struct_wrapper_name
|
||||
and value["field_name"] == self.field.name
|
||||
):
|
||||
anonymous_struct_name = name
|
||||
break
|
||||
|
||||
if (
|
||||
isinstance(self.field.type, AnonymousStructDecl)
|
||||
and self.field.name not in self.manually_added_fields
|
||||
and anonymous_struct_name in constants.STRUCTS_TO_BIND
|
||||
):
|
||||
return WrappedFieldData(
|
||||
binding=struct_field_code_builder.build_simple_property_binding(
|
||||
self.field,
|
||||
self.struct_wrapper_name,
|
||||
add_setter=False,
|
||||
add_return_value_policy_as_ref=True,
|
||||
),
|
||||
initialization=f", {self.field.name}(&ptr_->{self.field.name})",
|
||||
ptr_copy_reset=f"{self.field.name}.set(&ptr_->{self.field.name});",
|
||||
is_primitive_or_fixed_size=True,
|
||||
)
|
||||
return self._get_manual_definition(comment_type="anonymous struct field")
|
||||
|
||||
def _undefined(self) -> WrappedFieldData:
|
||||
"""This function adds a TODO comment for fields that are not handled by this class yet."""
|
||||
return WrappedFieldData(
|
||||
definition=f"// TODO: UNDEFINED definition for {self.field.name}",
|
||||
binding=f"// TODO: UNDEFINED binding for {self.field.name}",
|
||||
)
|
||||
|
||||
def _get_manual_definition(self, comment_type: str = "") -> WrappedFieldData:
|
||||
"""Helper method to generate a comment as a definition for manually added fields."""
|
||||
if self.field.name in self.manually_added_fields:
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
f"// {comment_type} is defined manually. {self.field.name}"
|
||||
),
|
||||
binding=self.simple_property_binding,
|
||||
)
|
||||
|
||||
return WrappedFieldData(
|
||||
definition=(
|
||||
f"// TODO: Define {comment_type} manually for {self.field.name}"
|
||||
),
|
||||
binding=f"// TODO: {self.simple_property_binding}",
|
||||
)
|
||||
|
||||
|
||||
def parse_array_extent(
|
||||
extents: Tuple[Union[str, int], ...], wrapper_name: str, field_name: str
|
||||
) -> str:
|
||||
"""Parses the array extent of a field, returning a string representing the resolved extents."""
|
||||
if not extents:
|
||||
return ""
|
||||
return " * ".join(
|
||||
resolve_extent(extent, wrapper_name, field_name) for extent in extents
|
||||
)
|
||||
|
||||
|
||||
def resolve_extent(
|
||||
extent: Union[str, int], wrapper_name: str, field_name: str
|
||||
) -> str:
|
||||
"""Resolves the extent of an array, handling integers and references to other struct fields.
|
||||
|
||||
Args:
|
||||
extent: The extent to resolve, can be an int or a string referencing a
|
||||
field.
|
||||
wrapper_name: The name of the struct wrapper.
|
||||
field_name: The name of the field being processed.
|
||||
|
||||
Returns:
|
||||
A string representing the resolved extent, either as a number or a field
|
||||
reference.
|
||||
"""
|
||||
if isinstance(extent, int):
|
||||
return str(extent)
|
||||
# if starts with mj, it's a mujoco constant,
|
||||
# so we don't need to get a parent struct ptr
|
||||
if extent.startswith("mj"):
|
||||
return str(extent)
|
||||
if wrapper_name == "MjData" and field_name not in constants.MJDATA_SIZES:
|
||||
var_name = "model"
|
||||
else:
|
||||
var_name = "ptr_"
|
||||
return f"{var_name}->{extent}"
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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.
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import ast_nodes
|
||||
|
||||
from wasm.codegen.helpers import struct_field_handler
|
||||
|
||||
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
ValueType = ast_nodes.ValueType
|
||||
PointerType = ast_nodes.PointerType
|
||||
ArrayType = ast_nodes.ArrayType
|
||||
|
||||
|
||||
class StructFieldHandlerTest(absltest.TestCase):
|
||||
|
||||
def test_scalar_field(self):
|
||||
"""Test that a scalar type field is handled correctly."""
|
||||
field_scalar = StructFieldDecl(
|
||||
name='ngeom',
|
||||
type=ValueType(name='int'),
|
||||
doc='number of geoms',
|
||||
)
|
||||
|
||||
field_handler_scalar = struct_field_handler.StructFieldHandler(
|
||||
field_scalar, 'MjModel'
|
||||
)
|
||||
wrapped_field_data = field_handler_scalar.generate()
|
||||
self.assertEqual(
|
||||
wrapped_field_data.definition,
|
||||
"""
|
||||
int ngeom() const {
|
||||
return ptr_->ngeom;
|
||||
}
|
||||
void set_ngeom(int value) {
|
||||
ptr_->ngeom = value;
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
self.assertEqual(
|
||||
wrapped_field_data.binding,
|
||||
'.property("ngeom", &MjModel::ngeom, &MjModel::set_ngeom, reference())',
|
||||
)
|
||||
|
||||
def test_pointer_type_field(self):
|
||||
"""Test that a pointer type field is handled correctly."""
|
||||
field = StructFieldDecl(
|
||||
name='geom_rgba',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='float'),
|
||||
),
|
||||
doc='rgba when material is omitted',
|
||||
array_extent=('ngeom', 4),
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field, 'MjModel'
|
||||
).generate()
|
||||
|
||||
self.assertEqual(
|
||||
wrapped_field_data.definition,
|
||||
("""
|
||||
emscripten::val geom_rgba() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(ptr_->ngeom * 4, ptr_->geom_rgba));
|
||||
}
|
||||
""".strip()),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
wrapped_field_data.binding,
|
||||
'.property("geom_rgba", &MjModel::geom_rgba)',
|
||||
)
|
||||
|
||||
def test_pointer_type_field_for_byte_type(self):
|
||||
"""Test that a pointer type field for a byte type is handled correctly."""
|
||||
field = StructFieldDecl(
|
||||
name='buffer',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='void'),
|
||||
),
|
||||
doc='main buffer; all pointers point in it (nbuffer bytes)',
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field, 'MjData'
|
||||
).generate()
|
||||
|
||||
self.assertEqual(
|
||||
wrapped_field_data.definition,
|
||||
("""
|
||||
emscripten::val buffer() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(model->nbuffer, static_cast<uint8_t*>(ptr_->buffer)));
|
||||
}
|
||||
""".strip()),
|
||||
)
|
||||
assert (
|
||||
wrapped_field_data.binding
|
||||
== '.property("buffer", &MjData::buffer)'
|
||||
)
|
||||
|
||||
def test_pointer_type_field_for_mj_struct(self):
|
||||
"""Test that a pointer type field for a mj struct is handled correctly."""
|
||||
field = StructFieldDecl(
|
||||
name='element',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjsElement'),
|
||||
),
|
||||
doc='',
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field, 'MjsTexture'
|
||||
).generate()
|
||||
self.assertEqual(wrapped_field_data.definition, "MjsElement element;")
|
||||
|
||||
self.assertEqual(
|
||||
wrapped_field_data.binding,
|
||||
'.property("element", &MjsTexture::element, reference())',
|
||||
)
|
||||
self.assertEqual(
|
||||
wrapped_field_data.initialization,
|
||||
', element(ptr_->element)',
|
||||
)
|
||||
|
||||
def test_array_type_field(self):
|
||||
"""Test that an array type field is handled correctly."""
|
||||
field = StructFieldDecl(
|
||||
name='gravity',
|
||||
type=ArrayType(
|
||||
inner_type=ValueType(name='mjtNum'),
|
||||
extents=(3,),
|
||||
),
|
||||
doc='gravitational acceleration',
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field, 'MjOption'
|
||||
).generate()
|
||||
|
||||
self.assertEqual(
|
||||
wrapped_field_data.definition,
|
||||
("""
|
||||
emscripten::val gravity() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(3, ptr_->gravity));
|
||||
}
|
||||
""".strip()),
|
||||
)
|
||||
self.assertEqual(
|
||||
wrapped_field_data.binding,
|
||||
'.property("gravity", &MjOption::gravity)',
|
||||
)
|
||||
|
||||
def test_array_field_with_multi_dimensional_array(self):
|
||||
"""Test that multi-dimensional arrays are handled correctly."""
|
||||
field = StructFieldDecl(
|
||||
name='multi_dim_array',
|
||||
type=ArrayType(
|
||||
inner_type=ValueType(name='float'),
|
||||
extents=(3, 4),
|
||||
),
|
||||
doc='description',
|
||||
)
|
||||
wrapped_field_data = struct_field_handler.StructFieldHandler(
|
||||
field, 'MjModel'
|
||||
).generate()
|
||||
self.assertEqual(
|
||||
wrapped_field_data.definition,
|
||||
"""
|
||||
emscripten::val multi_dim_array() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(12, reinterpret_cast<float*>(ptr_->multi_dim_array)));
|
||||
}
|
||||
""".strip(),
|
||||
)
|
||||
self.assertEqual(
|
||||
wrapped_field_data.binding,
|
||||
'.property("multi_dim_array", &MjModel::multi_dim_array)',
|
||||
)
|
||||
|
||||
def test_parse_array_extent(self):
|
||||
"""Test that parse_array_extent handles various cases correctly."""
|
||||
self.assertEqual(
|
||||
struct_field_handler.parse_array_extent((1, 2), 'MjModel', 'geom_rgba'),
|
||||
'1 * 2',
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_handler.parse_array_extent(
|
||||
(1, 'ngeom'), 'MjModel', 'geom_rgba'
|
||||
),
|
||||
'1 * ptr_->ngeom',
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_handler.parse_array_extent(
|
||||
(1, 'mjConstant'), 'MjModel', 'geom_rgba'
|
||||
),
|
||||
'1 * mjConstant',
|
||||
)
|
||||
|
||||
def test_resolve_extent(self):
|
||||
"""Test that resolve_extent handles various cases correctly."""
|
||||
# for integer just return the number
|
||||
self.assertEqual(
|
||||
struct_field_handler.resolve_extent(1, 'MjModel', 'geom_rgba'), '1'
|
||||
)
|
||||
|
||||
# for string that does not start with mj, it's a member of the struct
|
||||
self.assertEqual(
|
||||
struct_field_handler.resolve_extent('ngeom', 'MjModel', 'geom_rgba'),
|
||||
'ptr_->ngeom',
|
||||
)
|
||||
|
||||
# when it's MjData and the field is in MJDATA_SIZES, it should use ptr_->
|
||||
self.assertEqual(
|
||||
struct_field_handler.resolve_extent('size_value', 'MjData', 'efc_AR'),
|
||||
'ptr_->size_value',
|
||||
)
|
||||
# when it's MjData and the field is not in MJDATA_SIZES,
|
||||
# it should use model->
|
||||
self.assertEqual(
|
||||
struct_field_handler.resolve_extent(
|
||||
'size_value', 'MjData', 'data_field'
|
||||
),
|
||||
'model->size_value',
|
||||
)
|
||||
self.assertEqual(
|
||||
struct_field_handler.resolve_extent(
|
||||
'mjConstant', 'MjModel', 'model_field'
|
||||
),
|
||||
'mjConstant',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
absltest.main()
|
||||
@@ -0,0 +1,201 @@
|
||||
# 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.
|
||||
|
||||
"""Parser for MuJoCo structs."""
|
||||
|
||||
import collections
|
||||
from typing import Dict, List
|
||||
|
||||
from introspect import ast_nodes
|
||||
from introspect import structs
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
from wasm.codegen.helpers import constants
|
||||
from wasm.codegen.helpers import struct_constructor_code_builder
|
||||
from wasm.codegen.helpers import struct_field_handler
|
||||
from wasm.codegen.helpers import structs_wrappers_data
|
||||
|
||||
|
||||
WrappedFieldData = structs_wrappers_data.WrappedFieldData
|
||||
WrappedStructData = structs_wrappers_data.WrappedStructData
|
||||
StructFieldHandler = struct_field_handler.StructFieldHandler
|
||||
|
||||
AnonymousStructDecl = ast_nodes.AnonymousStructDecl
|
||||
StructFieldDecl = ast_nodes.StructFieldDecl
|
||||
debug_print = common.debug_print
|
||||
|
||||
introspect_structs = structs.STRUCTS
|
||||
|
||||
|
||||
def generate_wasm_bindings(
|
||||
wrapped_structs: Dict[str, WrappedStructData],
|
||||
) -> Dict[str, WrappedStructData]:
|
||||
"""Generates WASM bindings for MuJoCo structs."""
|
||||
|
||||
for struct_name, wrap_data in wrapped_structs.items():
|
||||
if struct_name in introspect_structs:
|
||||
struct_fields = introspect_structs[struct_name].fields
|
||||
elif struct_name in constants.ANONYMOUS_STRUCTS:
|
||||
anonymous_struct = _get_anonymous_struct_field(struct_name)
|
||||
if not anonymous_struct or not isinstance(
|
||||
anonymous_struct.type, AnonymousStructDecl
|
||||
):
|
||||
raise RuntimeError(f"Anonymous struct not found: {struct_name}")
|
||||
struct_fields = anonymous_struct.type.fields
|
||||
else:
|
||||
raise RuntimeError(f"Struct not found: {struct_name}")
|
||||
|
||||
debug_print(f"Wrapping struct: {struct_name}")
|
||||
|
||||
fields_with_init: List[WrappedFieldData] = []
|
||||
for field in struct_fields:
|
||||
field_gen = StructFieldHandler(field, wrap_data.wrap_name).generate()
|
||||
# If the struct has at least one non-primitive or fixed size field
|
||||
# we avoid shallow copy to avoid uninitialized memory.
|
||||
if not field_gen.is_primitive_or_fixed_size:
|
||||
wrap_data.use_shallow_copy = False
|
||||
if field_gen.initialization:
|
||||
fields_with_init.append(field_gen)
|
||||
wrap_data.wrapped_fields.append(field_gen)
|
||||
|
||||
wrap_data.wrapped_header = (
|
||||
struct_constructor_code_builder.build_struct_header(
|
||||
struct_name,
|
||||
wrap_data.use_shallow_copy,
|
||||
fields_with_init,
|
||||
wrap_data.wrapped_fields,
|
||||
)
|
||||
)
|
||||
wrap_data.wrapped_source = (
|
||||
struct_constructor_code_builder.build_struct_source(
|
||||
struct_name,
|
||||
get_default_func_name(struct_name),
|
||||
fields_with_init,
|
||||
wrap_data.use_shallow_copy,
|
||||
)
|
||||
)
|
||||
return wrapped_structs
|
||||
|
||||
|
||||
def _get_anonymous_struct_field(
|
||||
anonymous_structs_key: str,
|
||||
) -> StructFieldDecl | None:
|
||||
"""Looks up the given key in the anonymous_structs dict and generates bindings for its fields."""
|
||||
info = constants.ANONYMOUS_STRUCTS[anonymous_structs_key]
|
||||
parent_decl = introspect_structs[info["parent"]]
|
||||
target_field = next(
|
||||
(
|
||||
f
|
||||
for f in parent_decl.fields
|
||||
if hasattr(f, "name")
|
||||
and f.name == info["field_name"]
|
||||
and hasattr(f, "type")
|
||||
and isinstance(f.type, AnonymousStructDecl)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return target_field
|
||||
|
||||
|
||||
def get_default_func_name(struct_name: str) -> str:
|
||||
"""Returns the default function name for the given struct."""
|
||||
if (
|
||||
struct_name in constants.ANONYMOUS_STRUCTS.keys()
|
||||
or struct_name in constants.NO_DEFAULT_CONSTRUCTORS
|
||||
or (
|
||||
common.uppercase_first_letter(struct_name)
|
||||
in constants.MANUALLY_ADDED_FIELDS_FROM_TEMPLATE.keys()
|
||||
)
|
||||
):
|
||||
return ""
|
||||
elif struct_name.startswith("mjs"):
|
||||
return f"mjs_default{struct_name.removeprefix('mjs')}"
|
||||
elif struct_name.startswith("mjv"):
|
||||
return f"mjv_default{struct_name.removeprefix('mjv')}"
|
||||
else:
|
||||
return f"mj_default{struct_name.removeprefix('mj')}"
|
||||
|
||||
|
||||
def _get_field_struct_type(field_type):
|
||||
"""Extracts the base struct name if the field type is a struct or pointer to a struct."""
|
||||
if isinstance(field_type, ast_nodes.ValueType):
|
||||
return field_type.name
|
||||
if isinstance(field_type, ast_nodes.PointerType):
|
||||
if isinstance(field_type.inner_type, ast_nodes.ValueType):
|
||||
return field_type.inner_type.name
|
||||
return None
|
||||
|
||||
|
||||
def sort_structs_by_dependency(struct_names: List[str]) -> List[str]:
|
||||
"""Sorts structs based on their field dependencies using topological sort.
|
||||
|
||||
Structs with no dependencies on other structs in the list come first.
|
||||
If struct A has a field of type struct B, B must come before A in the
|
||||
sorted list.
|
||||
|
||||
Args:
|
||||
struct_names: A list of struct names to sort.
|
||||
|
||||
Returns:
|
||||
A new list of struct names sorted by dependency.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If a cyclic dependency is detected.
|
||||
"""
|
||||
adj = collections.defaultdict(list)
|
||||
in_degree = collections.defaultdict(int)
|
||||
struct_set = set(struct_names)
|
||||
sorted_struct_names = sorted(struct_names)
|
||||
|
||||
for struct_name in sorted_struct_names:
|
||||
if struct_name not in introspect_structs:
|
||||
# Skip anonymous or other structs not in the main introspect map
|
||||
continue
|
||||
|
||||
struct_decl = introspect_structs[struct_name]
|
||||
for field in struct_decl.fields:
|
||||
if isinstance(field, ast_nodes.AnonymousStructDecl):
|
||||
continue
|
||||
|
||||
field_type_name = _get_field_struct_type(field.type)
|
||||
if (
|
||||
field_type_name
|
||||
and field_type_name != struct_name
|
||||
and field_type_name in struct_set
|
||||
):
|
||||
if struct_name not in adj[field_type_name]:
|
||||
adj[field_type_name].append(struct_name)
|
||||
in_degree[struct_name] += 1
|
||||
|
||||
queue = collections.deque(
|
||||
[name for name in sorted_struct_names if in_degree[name] == 0]
|
||||
)
|
||||
sorted_list = []
|
||||
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
sorted_list.append(u)
|
||||
for v in adj[u]:
|
||||
in_degree[v] -= 1
|
||||
if in_degree[v] == 0:
|
||||
queue.append(v)
|
||||
|
||||
if len(sorted_list) == len(struct_names):
|
||||
return sorted_list
|
||||
else:
|
||||
remaining = set(struct_names) - set(sorted_list)
|
||||
raise RuntimeError(
|
||||
"Cycle detected in struct dependencies, involving: "
|
||||
f"{', '.join(sorted(list(remaining)))}"
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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.
|
||||
|
||||
"""Tests for structs_parser."""
|
||||
|
||||
from absl.testing import absltest
|
||||
from introspect import ast_nodes
|
||||
from introspect import structs as introspect_structs
|
||||
from wasm.codegen.helpers import structs_parser
|
||||
from wasm.codegen.helpers import structs_wrappers_data
|
||||
|
||||
|
||||
class StructsParserTest(absltest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.wrapped_structs = structs_parser.generate_wasm_bindings(
|
||||
structs_wrappers_data.create_wrapped_structs_set_up_data([
|
||||
"mjModel",
|
||||
"mjData",
|
||||
"mjVisualGlobal",
|
||||
"mjVisualQuality",
|
||||
"mjVisual",
|
||||
])
|
||||
)
|
||||
|
||||
def test_sort_structs_by_dependency(self):
|
||||
mock_introspect_structs = {
|
||||
"mjA": ast_nodes.StructDecl(
|
||||
name="mjA",
|
||||
declname="mjA",
|
||||
fields=[
|
||||
ast_nodes.StructFieldDecl(
|
||||
doc="", name="b_field", type=ast_nodes.ValueType(name="mjB")
|
||||
)
|
||||
],
|
||||
),
|
||||
"mjB": ast_nodes.StructDecl(
|
||||
name="mjB",
|
||||
declname="mjB",
|
||||
fields=[
|
||||
ast_nodes.StructFieldDecl(
|
||||
doc="", name="c_field", type=ast_nodes.ValueType(name="mjC")
|
||||
)
|
||||
],
|
||||
),
|
||||
"mjC": ast_nodes.StructDecl(name="mjC", declname="mjC", fields=[]),
|
||||
"mjD": ast_nodes.StructDecl(name="mjD", declname="mjD", fields=[]),
|
||||
}
|
||||
with absltest.mock.patch.dict(
|
||||
introspect_structs.STRUCTS, mock_introspect_structs
|
||||
):
|
||||
struct_names = ["mjA", "mjB", "mjC", "mjD"]
|
||||
sorted_names = structs_parser.sort_structs_by_dependency(struct_names)
|
||||
self.assertEqual(sorted_names, ["mjC", "mjD", "mjB", "mjA"])
|
||||
|
||||
def test_generate_wasm_bindings(self):
|
||||
self.assertEqual(self.wrapped_structs["mjModel"].wrap_name, "MjModel")
|
||||
self.assertEqual(self.wrapped_structs["mjData"].wrap_name, "MjData")
|
||||
self.assertEqual(
|
||||
self.wrapped_structs["mjVisualGlobal"].wrap_name, "MjVisualGlobal"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.wrapped_structs["mjVisualQuality"].wrap_name, "MjVisualQuality"
|
||||
)
|
||||
self.assertEqual(self.wrapped_structs["mjVisual"].wrap_name, "MjVisual")
|
||||
self.assertNotEmpty(self.wrapped_structs["mjModel"].wrapped_fields)
|
||||
self.assertNotEmpty(self.wrapped_structs["mjData"].wrapped_fields)
|
||||
self.assertNotEmpty(self.wrapped_structs["mjVisualGlobal"].wrapped_fields)
|
||||
self.assertNotEmpty(self.wrapped_structs["mjVisualQuality"].wrapped_fields)
|
||||
self.assertNotEmpty(self.wrapped_structs["mjVisual"].wrapped_fields)
|
||||
|
||||
def test_generate_wasm_bindings_with_error(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
structs_parser.generate_wasm_bindings(
|
||||
structs_wrappers_data.create_wrapped_structs_set_up_data(
|
||||
["mjFakeStruct"]
|
||||
)
|
||||
)
|
||||
with self.assertRaises(RuntimeError):
|
||||
structs_parser.generate_wasm_bindings(
|
||||
structs_wrappers_data.create_wrapped_structs_set_up_data(
|
||||
["mjFakeAnonymousStruct"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_get_default_func_name_mjv(self):
|
||||
self.assertEqual(
|
||||
structs_parser.get_default_func_name("mjvPerturb"), "mjv_defaultPerturb"
|
||||
)
|
||||
|
||||
def test_get_default_func_name(self):
|
||||
self.assertEqual(
|
||||
structs_parser.get_default_func_name("mjOption"), "mj_defaultOption"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
|
||||
"""Classes used by the parser to generate the structs wrappers and bindings statements."""
|
||||
|
||||
import dataclasses
|
||||
from typing import Dict, List
|
||||
|
||||
from wasm.codegen.helpers import common
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class WrappedFieldData:
|
||||
"""Data class for struct field definition and binding."""
|
||||
|
||||
# Line for struct field binding
|
||||
binding: str
|
||||
|
||||
# Line for struct field definition
|
||||
definition: str | None = None
|
||||
|
||||
# Initialization code for fields that require it
|
||||
initialization: str | None = None
|
||||
|
||||
# Statement to reset the inner pointer when copying the field
|
||||
ptr_copy_reset: str | None = None
|
||||
|
||||
# Whether the field is a primitive or fixed size
|
||||
is_primitive_or_fixed_size: bool = False
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class WrappedStructData:
|
||||
"""Data class for struct wrapper definition and binding."""
|
||||
|
||||
# Name of wrapper struct
|
||||
wrap_name: str
|
||||
|
||||
# List of WrappedFieldData for this struct
|
||||
wrapped_fields: List[WrappedFieldData]
|
||||
|
||||
# Struct header code
|
||||
wrapped_header: str
|
||||
|
||||
# Struct source code
|
||||
wrapped_source: str
|
||||
|
||||
# Whether to use shallow copy for this struct
|
||||
use_shallow_copy: bool = True
|
||||
|
||||
|
||||
def create_wrapped_structs_set_up_data(
|
||||
struct_names: List[str],
|
||||
) -> Dict[str, WrappedStructData]:
|
||||
"""Creates a dictionary of WrappedStructData for the given struct names."""
|
||||
return {
|
||||
name: WrappedStructData(
|
||||
wrap_name=common.uppercase_first_letter(name),
|
||||
wrapped_fields=[],
|
||||
wrapped_header="",
|
||||
wrapped_source="",
|
||||
)
|
||||
for name in struct_names
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,348 @@
|
||||
// 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.
|
||||
|
||||
// NOLINTBEGIN(whitespace/line_length)
|
||||
#ifndef MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
|
||||
#define MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::wasm {
|
||||
|
||||
// Create the types for anonymous structs
|
||||
using mjVisualGlobal = decltype(::mjVisual::global);
|
||||
using mjVisualQuality = decltype(::mjVisual::quality);
|
||||
using mjVisualHeadlight = decltype(::mjVisual::headlight);
|
||||
using mjVisualMap = decltype(::mjVisual::map);
|
||||
using mjVisualScale = decltype(::mjVisual::scale);
|
||||
using mjVisualRgba = decltype(::mjVisual::rgba);
|
||||
|
||||
// {{ AUTOGENNED_STRUCT_DEFINITIONS }}
|
||||
struct MjVisualGlobal {
|
||||
MjVisualGlobal();
|
||||
explicit MjVisualGlobal(mjVisualGlobal *ptr);
|
||||
MjVisualGlobal(const MjVisualGlobal &);
|
||||
MjVisualGlobal &operator=(const MjVisualGlobal &);
|
||||
~MjVisualGlobal();
|
||||
std::unique_ptr<MjVisualGlobal> copy();
|
||||
// INSERT-GENERATED-MjVisualGlobal-DEFINITIONS
|
||||
mjVisualGlobal* get() const { return ptr_; }
|
||||
void set(mjVisualGlobal* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualGlobal* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisualQuality {
|
||||
MjVisualQuality();
|
||||
explicit MjVisualQuality(mjVisualQuality *ptr);
|
||||
MjVisualQuality(const MjVisualQuality &);
|
||||
MjVisualQuality &operator=(const MjVisualQuality &);
|
||||
~MjVisualQuality();
|
||||
std::unique_ptr<MjVisualQuality> copy();
|
||||
// INSERT-GENERATED-MjVisualQuality-DEFINITIONS
|
||||
mjVisualQuality* get() const { return ptr_; }
|
||||
void set(mjVisualQuality* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualQuality* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisualHeadlight {
|
||||
MjVisualHeadlight();
|
||||
explicit MjVisualHeadlight(mjVisualHeadlight *ptr);
|
||||
MjVisualHeadlight(const MjVisualHeadlight &);
|
||||
MjVisualHeadlight &operator=(const MjVisualHeadlight &);
|
||||
~MjVisualHeadlight();
|
||||
std::unique_ptr<MjVisualHeadlight> copy();
|
||||
// INSERT-GENERATED-MjVisualHeadlight-DEFINITIONS
|
||||
mjVisualHeadlight* get() const { return ptr_; }
|
||||
void set(mjVisualHeadlight* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualHeadlight* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisualMap {
|
||||
MjVisualMap();
|
||||
explicit MjVisualMap(mjVisualMap *ptr);
|
||||
MjVisualMap(const MjVisualMap &);
|
||||
MjVisualMap &operator=(const MjVisualMap &);
|
||||
~MjVisualMap();
|
||||
std::unique_ptr<MjVisualMap> copy();
|
||||
// INSERT-GENERATED-MjVisualMap-DEFINITIONS
|
||||
mjVisualMap* get() const { return ptr_; }
|
||||
void set(mjVisualMap* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualMap* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisualScale {
|
||||
MjVisualScale();
|
||||
explicit MjVisualScale(mjVisualScale *ptr);
|
||||
MjVisualScale(const MjVisualScale &);
|
||||
MjVisualScale &operator=(const MjVisualScale &);
|
||||
~MjVisualScale();
|
||||
std::unique_ptr<MjVisualScale> copy();
|
||||
// INSERT-GENERATED-MjVisualScale-DEFINITIONS
|
||||
mjVisualScale* get() const { return ptr_; }
|
||||
void set(mjVisualScale* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualScale* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisualRgba {
|
||||
MjVisualRgba();
|
||||
explicit MjVisualRgba(mjVisualRgba *ptr);
|
||||
MjVisualRgba(const MjVisualRgba &);
|
||||
MjVisualRgba &operator=(const MjVisualRgba &);
|
||||
~MjVisualRgba();
|
||||
std::unique_ptr<MjVisualRgba> copy();
|
||||
// INSERT-GENERATED-MjVisualRgba-DEFINITIONS
|
||||
mjVisualRgba* get() const { return ptr_; }
|
||||
void set(mjVisualRgba* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisualRgba* ptr_;
|
||||
bool owned_ = false;
|
||||
};
|
||||
|
||||
struct MjVisual {
|
||||
MjVisual();
|
||||
explicit MjVisual(mjVisual *ptr_);
|
||||
MjVisual(const MjVisual &);
|
||||
MjVisual &operator=(const MjVisual &);
|
||||
~MjVisual();
|
||||
std::unique_ptr<MjVisual> copy();
|
||||
// INSERT-GENERATED-MjVisual-DEFINITIONS
|
||||
mjVisual* get() const { return ptr_; }
|
||||
void set(mjVisual* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjVisual* ptr_;
|
||||
bool owned_ = false;
|
||||
|
||||
public:
|
||||
MjVisualGlobal global;
|
||||
MjVisualQuality quality;
|
||||
MjVisualHeadlight headlight;
|
||||
MjVisualMap map;
|
||||
MjVisualScale scale;
|
||||
MjVisualRgba rgba;
|
||||
};
|
||||
|
||||
struct MjModel {
|
||||
explicit MjModel(mjModel *m);
|
||||
explicit MjModel(const MjModel &other);
|
||||
~MjModel();
|
||||
std::unique_ptr<MjModel> copy();
|
||||
// INSERT-GENERATED-MjModel-DEFINITIONS
|
||||
mjModel* get() const { return ptr_; }
|
||||
void set(mjModel* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjModel* ptr_;
|
||||
|
||||
public:
|
||||
MjOption opt;
|
||||
MjStatistic stat;
|
||||
MjVisual vis;
|
||||
};
|
||||
|
||||
struct MjData {
|
||||
MjData(MjModel *m);
|
||||
explicit MjData(const MjModel &, const MjData &);
|
||||
~MjData();
|
||||
std::vector<MjSolverStat> InitSolverArray();
|
||||
std::vector<MjTimerStat> InitTimerArray();
|
||||
std::vector<MjWarningStat> InitWarningArray();
|
||||
std::vector<MjContact> contact() const;
|
||||
std::unique_ptr<MjData> copy();
|
||||
// INSERT-GENERATED-MjData-DEFINITIONS
|
||||
mjData* get() const { return ptr_; }
|
||||
void set(mjData* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjData* ptr_;
|
||||
|
||||
public:
|
||||
mjModel *model;
|
||||
std::vector<MjSolverStat> solver;
|
||||
std::vector<MjTimerStat> timer;
|
||||
std::vector<MjWarningStat> warning;
|
||||
};
|
||||
|
||||
struct MjvScene {
|
||||
MjvScene();
|
||||
MjvScene(MjModel *m, int maxgeom);
|
||||
// MjvScene(const MjvScene &);
|
||||
~MjvScene();
|
||||
std::unique_ptr<MjvScene> copy();
|
||||
int GetSumFlexFaces() const;
|
||||
std::vector<MjvLight> InitLightsArray();
|
||||
std::vector<MjvGLCamera> InitCameraArray();
|
||||
|
||||
std::vector<MjvGeom> geoms() const;
|
||||
|
||||
emscripten::val geomorder() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->ngeom, ptr_->geomorder));
|
||||
}
|
||||
emscripten::val flexedgeadr() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgeadr));
|
||||
}
|
||||
emscripten::val flexedgenum() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexedgenum));
|
||||
}
|
||||
emscripten::val flexvertadr() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertadr));
|
||||
}
|
||||
emscripten::val flexvertnum() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexvertnum));
|
||||
}
|
||||
emscripten::val flexfaceadr() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceadr));
|
||||
}
|
||||
emscripten::val flexfacenum() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfacenum));
|
||||
}
|
||||
emscripten::val flexfaceused() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nflex, ptr_->flexfaceused));
|
||||
}
|
||||
emscripten::val flexedge() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(2 * model->nflexedge, ptr_->flexedge));
|
||||
}
|
||||
emscripten::val flexvert() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(3 * model->nflexvert, ptr_->flexvert));
|
||||
}
|
||||
emscripten::val skinfacenum() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinfacenum));
|
||||
}
|
||||
emscripten::val skinvertadr() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertadr));
|
||||
}
|
||||
emscripten::val skinvertnum() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(ptr_->nskin, ptr_->skinvertnum));
|
||||
}
|
||||
emscripten::val skinvert() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinvert));
|
||||
}
|
||||
emscripten::val skinnormal() const {
|
||||
return emscripten::val(
|
||||
emscripten::typed_memory_view(3 * model->nskinvert, ptr_->skinnormal));
|
||||
}
|
||||
emscripten::val flexface() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(
|
||||
9 * MjvScene::GetSumFlexFaces(), ptr_->flexface));
|
||||
}
|
||||
emscripten::val flexnormal() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(
|
||||
9 * MjvScene::GetSumFlexFaces(), ptr_->flexnormal));
|
||||
}
|
||||
emscripten::val flextexcoord() const {
|
||||
return emscripten::val(emscripten::typed_memory_view(
|
||||
6 * MjvScene::GetSumFlexFaces(), ptr_->flextexcoord));
|
||||
}
|
||||
// INSERT-GENERATED-MjvScene-DEFINITIONS
|
||||
mjvScene* get() const { return ptr_; }
|
||||
void set(mjvScene* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjvScene* ptr_;
|
||||
bool owned_ = false;
|
||||
|
||||
public:
|
||||
mjModel *model;
|
||||
std::vector<MjvLight> lights;
|
||||
std::vector<MjvGLCamera> camera;
|
||||
};
|
||||
|
||||
struct MjSpec {
|
||||
MjSpec();
|
||||
explicit MjSpec(mjSpec *ptr);
|
||||
MjSpec(const MjSpec &);
|
||||
MjSpec &operator=(const MjSpec &);
|
||||
~MjSpec();
|
||||
std::unique_ptr<MjSpec> copy();
|
||||
// INSERT-GENERATED-MjSpec-DEFINITIONS
|
||||
mjSpec* get() const { return ptr_; }
|
||||
void set(mjSpec* ptr) { ptr_ = ptr; }
|
||||
|
||||
private:
|
||||
mjSpec* ptr_;
|
||||
bool owned_ = false;
|
||||
|
||||
public:
|
||||
MjOption option;
|
||||
MjVisual visual;
|
||||
MjStatistic stat;
|
||||
MjsCompiler compiler;
|
||||
MjsElement element;
|
||||
};
|
||||
|
||||
// TODO: Refactor, Structs Manually added so functions.cc compile -- //
|
||||
struct MjpResourceProvider {
|
||||
MjpResourceProvider(mjpResourceProvider *ptr_) { ptr = ptr_; };
|
||||
~MjpResourceProvider() {}
|
||||
mjpResourceProvider *get() const { return ptr; }
|
||||
mjpResourceProvider *ptr;
|
||||
};
|
||||
|
||||
struct MjpPlugin {
|
||||
MjpPlugin(mjpPlugin *ptr_) { ptr = ptr_; };
|
||||
~MjpPlugin() {}
|
||||
mjpPlugin *get() const { return ptr; }
|
||||
mjpPlugin *ptr;
|
||||
};
|
||||
|
||||
// TODO: Factory and debug helper functions, some should be removed when
|
||||
// functions are generated -- //
|
||||
std::unique_ptr<MjModel> loadFromXML(std::string filename);
|
||||
void step(MjModel *model, MjData *data);
|
||||
void error(const std::string &msg);
|
||||
void kinematics(MjModel *m, MjData *d);
|
||||
std::unique_ptr<MjSpec> parseXMLString(const std::string &xml);
|
||||
std::unique_ptr<MjsBody> findBody(MjSpec *spec, const std::string &name);
|
||||
std::unique_ptr<MjsGeom> findGeom(MjSpec *spec, const std::string &name);
|
||||
|
||||
} // namespace mujoco::wasm
|
||||
|
||||
#endif // MUJOCO_WASM_CODEGEN_GENERATED_BINDINGS_H_
|
||||
// NOLINTEND(whitespace/line_length)
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
|
||||
"""Generates WASM bindings for MuJoCo's API.
|
||||
|
||||
This script leverages MuJoCo's introspect dicts to gather information
|
||||
about its internal structures and then uses a code generation framework to
|
||||
produce corresponding WASM bindings.
|
||||
"""
|
||||
|
||||
from wasm.codegen import binding_builder
|
||||
from wasm.codegen.helpers import common
|
||||
|
||||
|
||||
def generate_all_bindings():
|
||||
"""Generates WASM bindings for MuJoCo."""
|
||||
template_path_h, generated_path_h = common.get_file_path(
|
||||
"templates", "generated", "bindings.h"
|
||||
)
|
||||
template_path_cc, generated_path_cc = common.get_file_path(
|
||||
"templates", "generated", "bindings.cc"
|
||||
)
|
||||
builder = binding_builder.BindingBuilder(
|
||||
template_path_h, template_path_cc, generated_path_h, generated_path_cc
|
||||
)
|
||||
builder.set_enums().set_headers().set_structs().set_functions().build()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_all_bindings()
|
||||
@@ -0,0 +1,498 @@
|
||||
// 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.
|
||||
|
||||
import * as THREE from "three"
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
|
||||
import loadMujoco from "../dist/mujoco_wasm.js"
|
||||
|
||||
declare function loadMujoco(): Promise<MainModule>;
|
||||
|
||||
let mujoco: any;
|
||||
|
||||
const modelXml = `
|
||||
<mujoco model="Particle">
|
||||
<statistic extent="1.5" meansize=".05"/>
|
||||
|
||||
<option timestep="0.005" jacobian="sparse"/>
|
||||
|
||||
<visual>
|
||||
<rgba haze="0.15 0.25 0.35 1"/>
|
||||
<quality shadowsize="4096"/>
|
||||
<map stiffness="700" shadowscale="0.5" fogstart="10" fogend="15" zfar="40" haze="0.3"/>
|
||||
</visual>
|
||||
|
||||
<worldbody>
|
||||
<light directional="true" diffuse=".4 .4 .4" specular="0.1 0.1 0.1" pos="0 0 5.0" dir="0 0 -1" castshadow="false"/>
|
||||
<light directional="true" diffuse=".6 .6 .6" specular="0.2 0.2 0.2" pos="0 0 4" dir="0 0 -1"/>
|
||||
|
||||
<geom name="ground" type="plane" size="0 0 1" pos="0 0 0" quat="1 0 0 0" condim="1"/>
|
||||
|
||||
<body mocap="true" pos="-.1 .05 0" zaxis=".5 0 1">
|
||||
<geom type="capsule" size=".1 .1" group="1" condim="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<option solver="CG" tolerance="1e-6" timestep=".01"/>
|
||||
|
||||
<size memory="1G"/>
|
||||
|
||||
<visual>
|
||||
<map stiffness="100"/>
|
||||
</visual>
|
||||
|
||||
<default>
|
||||
<default class="wall">
|
||||
<geom type="plane" size=".5 .5 .05"/>
|
||||
</default>
|
||||
</default>
|
||||
|
||||
<worldbody>
|
||||
<geom name="+x" class="wall" zaxis="1 0 0" pos="-.5 0 -.25"/>
|
||||
<geom name="-x" class="wall" zaxis="-1 0 0" pos=".5 0 -.25"/>
|
||||
<geom name="+y" class="wall" zaxis="0 1 0" pos="0 -.5 -.25"/>
|
||||
<geom name="-y" class="wall" zaxis="0 -1 0" pos="0 .5 -.25"/>
|
||||
<replicate count="10" offset=".07 0 0">
|
||||
<replicate count="10" offset="0 .07 0">
|
||||
<replicate count="10" offset="0 0 .07">
|
||||
<body pos="-.315 -.315 1">
|
||||
<joint type="slide" axis="1 0 0"/>
|
||||
<joint type="slide" axis="0 1 0"/>
|
||||
<joint type="slide" axis="0 0 1"/>
|
||||
<geom size=".025" rgba=".8 .2 .1 1" condim="1"/>
|
||||
</body>
|
||||
</replicate>
|
||||
</replicate>
|
||||
</replicate>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
`;
|
||||
|
||||
// Backport of CapsuleGeometry class introduced in THREE.js r139
|
||||
class CapsuleGeometry extends THREE.BufferGeometry {
|
||||
readonly parameters: {
|
||||
readonly radius: number,
|
||||
readonly length: number,
|
||||
readonly capSegments: number,
|
||||
readonly radialSegments: number
|
||||
};
|
||||
|
||||
constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
|
||||
const path = new THREE.Path();
|
||||
path.absarc(0, -length / 2, radius, Math.PI * 1.5, 0, false);
|
||||
path.absarc(0, length / 2, radius, 0, Math.PI * 0.5, false);
|
||||
const latheGeometry =
|
||||
new THREE.LatheGeometry(path.getPoints(capSegments), radialSegments);
|
||||
|
||||
super();
|
||||
this.setIndex(latheGeometry.getIndex());
|
||||
this.setAttribute('position', latheGeometry.getAttribute('position'));
|
||||
this.setAttribute('normal', latheGeometry.getAttribute('normal'));
|
||||
this.setAttribute('uv', latheGeometry.getAttribute('uv'));
|
||||
|
||||
this.type = 'CapsuleGeometry';
|
||||
|
||||
this.parameters = {
|
||||
radius,
|
||||
length,
|
||||
capSegments,
|
||||
radialSegments,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class App {
|
||||
// TODO(matijak): We can use better types here by doing the following:
|
||||
// https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#typescript-definitions
|
||||
mjModel: any;
|
||||
mjData: any;
|
||||
mjvOption: any;
|
||||
mjvPerturb: any;
|
||||
mjvCamera: any;
|
||||
mjvScene: any;
|
||||
|
||||
paused = false;
|
||||
frameId: number|null = null;
|
||||
maxGeoms: number = 2 ** 15;
|
||||
|
||||
scene: THREE.Scene;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
controls: OrbitControls;
|
||||
meshes: THREE.Mesh[] = [];
|
||||
bufferGeometryCache = new Map<string, THREE.BufferGeometry>();
|
||||
|
||||
constructor() {
|
||||
this.mjvPerturb = new mujoco.MjvPerturb();
|
||||
this.mjvOption = new mujoco.MjvOption();
|
||||
this.mjvCamera = new mujoco.MjvCamera();
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
document.body.appendChild(this.renderer.domElement);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
45, window.innerWidth / window.innerHeight, .1, 1000);
|
||||
this.camera.up.set(0, 0, 1); // Mujoco uses z-up
|
||||
this.camera.position.set(-2, 0, 2);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Release all C++ objects
|
||||
if (this.mjvScene) {
|
||||
this.mjvScene.delete();
|
||||
}
|
||||
if (this.mjvCamera) {
|
||||
this.mjvCamera.delete();
|
||||
}
|
||||
if (this.mjvPerturb) {
|
||||
this.mjvPerturb.delete();
|
||||
}
|
||||
if (this.mjvOption) {
|
||||
this.mjvOption.delete();
|
||||
}
|
||||
if (this.mjData) {
|
||||
this.mjData.delete();
|
||||
}
|
||||
if (this.mjModel) {
|
||||
this.mjModel.delete();
|
||||
}
|
||||
|
||||
// Release all the THREE.js objects
|
||||
this.meshes.forEach((mesh) => {
|
||||
if (mesh.material) {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach(material => material.dispose());
|
||||
} else {
|
||||
mesh.material.dispose();
|
||||
}
|
||||
}
|
||||
if (mesh.geometry) {
|
||||
mesh.geometry.dispose();
|
||||
}
|
||||
});
|
||||
this.bufferGeometryCache.clear();
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.dispose();
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose();
|
||||
}
|
||||
|
||||
// Stop the animation loop since we've disposed of all the data
|
||||
if (this.frameId) {
|
||||
cancelAnimationFrame(this.frameId);
|
||||
this.frameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
loadModel(xmlContent: string) {
|
||||
// Write xml as a file so that mujoco can find it
|
||||
(mujoco as any).FS.writeFile('/working/model.xml', xmlContent);
|
||||
|
||||
this.mjModel = mujoco.MjModel.loadFromXML('/working/model.xml');
|
||||
if (!app.mjModel) {
|
||||
throw new Error('Failed to load model');
|
||||
}
|
||||
this.mjData = new mujoco.MjData(this.mjModel);
|
||||
if (!this.mjData) {
|
||||
throw new Error('Failed to load data');
|
||||
}
|
||||
|
||||
this.initScene();
|
||||
}
|
||||
|
||||
pauseButton() {
|
||||
this.paused = !this.paused;
|
||||
const button = document.getElementById('pause-button');
|
||||
if (button) {
|
||||
button.textContent = this.paused ? 'Resume' : 'Pause';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(matijak): Fix the bug where contact cylinders are wrong if the
|
||||
// simulation is reset while they are being visualized
|
||||
resetButton() {
|
||||
if (this.mjModel && this.mjData) {
|
||||
console.log('Resetting model and data');
|
||||
|
||||
mujoco.mj_resetData(this.mjModel, this.mjData);
|
||||
mujoco.mj_forward(this.mjModel, this.mjData);
|
||||
|
||||
this.clearScene();
|
||||
this.initScene();
|
||||
}
|
||||
}
|
||||
|
||||
contactButton() {
|
||||
const index = mujoco.mjtVisFlag.mjVIS_CONTACTPOINT.value;
|
||||
const value = this.mjvOption.flags[index];
|
||||
this.mjvOption.flags[index] = !value;
|
||||
|
||||
const button = document.getElementById('contact-button');
|
||||
if (button) {
|
||||
button.textContent =
|
||||
this.mjvOption.flags[index] ? 'Hide Contacts' : 'Show Contacts';
|
||||
}
|
||||
|
||||
this.clearScene();
|
||||
this.initScene();
|
||||
}
|
||||
|
||||
initScene() {
|
||||
this.mjvScene = new mujoco.MjvScene(this.mjModel, this.maxGeoms);
|
||||
|
||||
const pointLight = new THREE.PointLight(0xffffff, .4);
|
||||
pointLight.position.set(0, 0, 2);
|
||||
pointLight.castShadow = true;
|
||||
pointLight.shadow.mapSize.set(2048, 2048);
|
||||
this.scene.add(pointLight);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, .2);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const spotLight = new THREE.SpotLight(0xffffff, .2);
|
||||
spotLight.position.set(0, 0, 2);
|
||||
spotLight.target.position.set(0, 0, 0);
|
||||
spotLight.castShadow = true;
|
||||
spotLight.shadow.mapSize.set(2048, 2048);
|
||||
this.scene.add(spotLight);
|
||||
this.scene.add(spotLight.target);
|
||||
}
|
||||
|
||||
clearScene() {
|
||||
// clear cached meshes
|
||||
this.meshes.forEach((mesh) => {
|
||||
if (mesh.material) {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach(material => material.dispose());
|
||||
} else {
|
||||
mesh.material.dispose();
|
||||
}
|
||||
}
|
||||
if (mesh.geometry) {
|
||||
mesh.geometry.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
this.bufferGeometryCache.clear();
|
||||
this.meshes.length = 0;
|
||||
while (this.scene.children.length > 0) {
|
||||
this.scene.remove(this.scene.children[0]);
|
||||
}
|
||||
this.mjvScene.delete();
|
||||
}
|
||||
|
||||
getBufferGeometry(mjvGeom: any): [boolean, THREE.BufferGeometry] {
|
||||
if (!(mjvGeom instanceof mujoco.MjvGeom)) {
|
||||
throw new Error('mjvGeom is not an instance of mujoco.MjvGeom');
|
||||
}
|
||||
|
||||
// Lookup the geometry and return it if found
|
||||
const key = JSON.stringify([mjvGeom.type, mjvGeom.size, mjvGeom.dataid]);
|
||||
const found = this.bufferGeometryCache.get(key);
|
||||
if (found) {
|
||||
return [false, found];
|
||||
}
|
||||
|
||||
// Create geometry
|
||||
let geom: THREE.BufferGeometry;
|
||||
if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_PLANE.value) {
|
||||
geom = new THREE.PlaneGeometry(
|
||||
2 * (mjvGeom.size[0] ? mjvGeom.size[0] : 10000),
|
||||
2 * (mjvGeom.size[1] ? mjvGeom.size[1] : 10000));
|
||||
const uv = geom.getAttribute('uv');
|
||||
for (let i = 0; i < uv.count; ++i) {
|
||||
uv.setY(i, 1 - uv.getY(i));
|
||||
}
|
||||
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_SPHERE.value) {
|
||||
geom = new THREE.SphereGeometry(mjvGeom.size[0]);
|
||||
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CAPSULE.value) {
|
||||
geom = new CapsuleGeometry(mjvGeom.size[0], 2 * mjvGeom.size[2], 32, 16);
|
||||
geom.rotateX(0.5 * Math.PI);
|
||||
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_BOX.value) {
|
||||
geom = new THREE.BoxGeometry(
|
||||
2 * mjvGeom.size[0], 2 * mjvGeom.size[1], 2 * mjvGeom.size[2]);
|
||||
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_CYLINDER.value) {
|
||||
geom = new THREE.CylinderGeometry(
|
||||
mjvGeom.size[0], mjvGeom.size[1], 2 * mjvGeom.size[2], 32);
|
||||
geom.rotateX(0.5 * Math.PI);
|
||||
} else if (mjvGeom.type === mujoco.mjtGeom.mjGEOM_ELLIPSOID.value) {
|
||||
geom = new THREE.SphereGeometry(1);
|
||||
geom.scale(mjvGeom.size[0], mjvGeom.size[1], mjvGeom.size[2]);
|
||||
} else {
|
||||
console.log('Unsupported geom type: ', mjvGeom.type);
|
||||
geom = new THREE.BufferGeometry();
|
||||
}
|
||||
|
||||
this.bufferGeometryCache.set(key, geom);
|
||||
return [true, geom];
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.mjModel || !this.mjData) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.controls.update();
|
||||
|
||||
// Simulate physics for 1/60 sec
|
||||
if (!app.paused) {
|
||||
let sim_start = app.mjData.time;
|
||||
while (app.mjData.time - sim_start < 1. / 60.) {
|
||||
mujoco.mj_step(app.mjModel, app.mjData);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the mujoco scene
|
||||
mujoco.mjv_updateScene(
|
||||
this.mjModel, this.mjData, this.mjvOption, this.mjvPerturb,
|
||||
this.mjvCamera, mujoco.mjtCatBit.mjCAT_ALL.value, this.mjvScene);
|
||||
|
||||
const geoms = this.mjvScene.geoms;
|
||||
for (let i = 0; i < geoms.size(); i++) {
|
||||
const mjvGeom = geoms.get(i);
|
||||
|
||||
let mesh: THREE.Mesh;
|
||||
if (i < this.meshes.length) {
|
||||
mesh = this.meshes[i];
|
||||
} else {
|
||||
const mjvGeom = geoms.get(i);
|
||||
const [added, geom] = this.getBufferGeometry(mjvGeom);
|
||||
|
||||
// Create material
|
||||
let material = new THREE.MeshPhongMaterial();
|
||||
material.color.setRGB(
|
||||
mjvGeom.rgba[0], mjvGeom.rgba[1], mjvGeom.rgba[2]);
|
||||
material.opacity = mjvGeom.rgba[3];
|
||||
material.transparent = mjvGeom.rgba[3] !== 0;
|
||||
|
||||
// Create mesh
|
||||
mesh = new THREE.Mesh(geom, material);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
this.meshes.push(mesh);
|
||||
this.scene.add(mesh);
|
||||
}
|
||||
|
||||
mesh.matrixAutoUpdate = false;
|
||||
const sz = 1;
|
||||
mesh.matrix.set(
|
||||
mjvGeom.mat[0], mjvGeom.mat[1], mjvGeom.mat[2] * sz, mjvGeom.pos[0],
|
||||
mjvGeom.mat[3], mjvGeom.mat[4], mjvGeom.mat[5] * sz, mjvGeom.pos[1],
|
||||
mjvGeom.mat[6], mjvGeom.mat[7], mjvGeom.mat[8] * sz, mjvGeom.pos[2],
|
||||
0, 0, 0, 1);
|
||||
mesh.matrixWorldNeedsUpdate = true;
|
||||
|
||||
mjvGeom.delete();
|
||||
}
|
||||
|
||||
geoms.delete();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
run() {
|
||||
const animate = () => {
|
||||
try {
|
||||
this.update();
|
||||
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Simulation error:', error);
|
||||
}
|
||||
|
||||
// Request next frame
|
||||
this.frameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
// Request first frame
|
||||
this.frameId = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
function setupWindowEvents() {
|
||||
// Add an event listener to clean up when the page is unloaded
|
||||
// Tip: put "window.dispatchEvent(new Event('unload'))" in the console to test
|
||||
window.addEventListener('unload', () => {
|
||||
app.dispose();
|
||||
|
||||
(mujoco as any).FS.unmount('/working');
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.code === 'Backspace') {
|
||||
app.resetButton();
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.code === 'Space') {
|
||||
app.pauseButton();
|
||||
}
|
||||
});
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'c') {
|
||||
app.contactButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let app: App;
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
mujoco = await loadMujoco();
|
||||
|
||||
// Set up emscripten virtual file system
|
||||
(mujoco as any).FS.mkdir('/working');
|
||||
(mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working');
|
||||
|
||||
app = new App();
|
||||
|
||||
setupWindowEvents();
|
||||
|
||||
// Note: all elements will be destroyed with the page
|
||||
const pauseButtonElement = document.getElementById('pause-button');
|
||||
if (pauseButtonElement) {
|
||||
pauseButtonElement.onclick = () => app.pauseButton();
|
||||
}
|
||||
const resetButtonElement = document.getElementById('reset-button');
|
||||
if (resetButtonElement) {
|
||||
resetButtonElement.onclick = () => app.resetButton();
|
||||
}
|
||||
const contactButtonElement = document.getElementById('contact-button');
|
||||
if (contactButtonElement) {
|
||||
contactButtonElement.onclick = () => app.contactButton();
|
||||
}
|
||||
|
||||
app.loadModel(modelXml);
|
||||
|
||||
app.run();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Initialization error: ', error);
|
||||
app.dispose();
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MuJoCo WebAssembly</title>
|
||||
<style>
|
||||
#button-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./app.ts"></script>
|
||||
<div id="button-container">
|
||||
<button id="pause-button">Pause</button>
|
||||
<button id="reset-button">Reset</button>
|
||||
<button id="contact-button">Show Contacts</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
root: "demo_app",
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: "../demo-dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
})
|
||||
Generated
+1707
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "mujoco_wasm",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"description": "MuJoCo JavaScript Bindings",
|
||||
"directories": {
|
||||
"example": "examples",
|
||||
"lib": "lib",
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --loader ts-node/esm --trace-warnings tests/run-tests.mjs",
|
||||
"dev:sandbox": "vite --config tests/sandbox/vite.sandbox.config.ts",
|
||||
"build:sandbox": "vite build --config tests/sandbox/vite.sandbox.config.ts",
|
||||
"dev:demo": "vite --config demo_app/vite.demo.config.ts",
|
||||
"build:demo": "vite build --config demo_app/vite.demo.config.ts"
|
||||
},
|
||||
"author": "Google DeepMind",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "^5.1.8",
|
||||
"@types/node": "^24.1.0",
|
||||
"jasmine": "^5.9.0",
|
||||
"three": "^0.178.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "5.8.2",
|
||||
"vite": "^7.0.6"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3")
|
||||
|
||||
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}/wasm)
|
||||
|
||||
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()
|
||||
|
||||
add_compile_options(-pthread)
|
||||
|
||||
# Set Emscripten linker flags
|
||||
set(EMCC_LINKER_FLAGS
|
||||
"--bind"
|
||||
"-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}")
|
||||
|
||||
target_link_libraries(mujoco_wasm_benchmark ccd lodepng mujoco tinyxml2 qhullstatic_r)
|
||||
|
||||
install(TARGETS mujoco_wasm_benchmark DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.
|
||||
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "unpack.h"
|
||||
|
||||
namespace mujoco::wasm {
|
||||
using emscripten::val;
|
||||
|
||||
EMSCRIPTEN_DECLARE_VAL_TYPE(NumberArray);
|
||||
|
||||
// TODO(matijak): Add a benchmark where the buffer shared with JS and C++ is
|
||||
// created in C++ rather than JS.
|
||||
|
||||
val BenchmarkSortNumberArray(const NumberArray& vec) {
|
||||
UNPACK_ARRAY(mjtNum, vec);
|
||||
std::sort(vec_.data(), vec_.data() + vec_.size());
|
||||
return val::array(vec_.data(), vec_.data() + vec_.size());
|
||||
}
|
||||
|
||||
val BenchmarkSortDoubleBuffer(const val& vec) {
|
||||
UNPACK_VALUE(mjtNum, vec);
|
||||
std::sort(vec_.data(), vec_.data() + vec_.size());
|
||||
return vec;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(mujoco_benchmark_functions) {
|
||||
emscripten::class_<WasmBuffer<mjtNum>>("DoubleBuffer")
|
||||
.constructor<int>()
|
||||
.class_function("FromArray", &WasmBuffer<mjtNum>::FromArray)
|
||||
.function("GetPointer", &WasmBuffer<mjtNum>::GetPointer)
|
||||
.function("GetElementCount", &WasmBuffer<mjtNum>::GetElementCount)
|
||||
.function("GetView", &WasmBuffer<mjtNum>::GetView);
|
||||
emscripten::function("SortNumberArray", &BenchmarkSortNumberArray);
|
||||
emscripten::function("SortDoubleBuffer", &BenchmarkSortDoubleBuffer);
|
||||
emscripten::register_type<NumberArray>("number[]");
|
||||
}
|
||||
|
||||
} // namespace mujoco::wasm
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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.
|
||||
|
||||
import 'jasmine';
|
||||
|
||||
import { MainModule, DoubleBuffer } from "../dist/mujoco_wasm_benchmark"
|
||||
import loadMujoco from "../dist/mujoco_wasm_benchmark.js"
|
||||
|
||||
describe('MuJoCo WASM Benchmark Tests', () => {
|
||||
let mujoco: MainModule;
|
||||
|
||||
function isNumberArraySorted(arr: number[]): boolean {
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
if (arr[i] > arr[i + 1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function verifySorted(state: any, kind: string): void {
|
||||
let array: number[] = [];
|
||||
if (kind === 'NumberArray') {
|
||||
array = state as number[];
|
||||
} else {
|
||||
const bufferView = (state as DoubleBuffer).GetView();
|
||||
array = Array.from(bufferView) as number[];
|
||||
}
|
||||
|
||||
const sorted = isNumberArraySorted(array);
|
||||
if (!sorted) {
|
||||
console.error(` Verification failed for ${kind}. Array was ${array}`);
|
||||
}
|
||||
}
|
||||
|
||||
function runBenchmark(iterations: number, items: number, kind: string) {
|
||||
let state: any;
|
||||
let func: (state: any) => any;
|
||||
if (kind === 'NumberArray') {
|
||||
state = new Array(items).map(() => Math.random())
|
||||
func = (state) => mujoco.SortNumberArray(state);
|
||||
} else if (kind === 'DoubleBuffer') {
|
||||
state = mujoco.DoubleBuffer.FromArray(
|
||||
new Array(items).map(() => Math.random()));
|
||||
func = (state) => mujoco.SortDoubleBuffer(state);
|
||||
} else {
|
||||
throw new Error(`Unsupported benchmark type: ${kind}`);
|
||||
}
|
||||
|
||||
// Warmup JIT compiler to get stable results
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const sortedState = func(state);
|
||||
verifySorted(sortedState, kind);
|
||||
}
|
||||
|
||||
// Measurement
|
||||
const totalStartTime = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
func(state);
|
||||
}
|
||||
const totalEndTime = performance.now();
|
||||
|
||||
if (kind === 'DoubleBuffer') {
|
||||
state.delete();
|
||||
}
|
||||
|
||||
// Report results
|
||||
const totalTime = totalEndTime - totalStartTime;
|
||||
const avgTimeMilliseconds = totalTime / iterations;
|
||||
console.log(`Benchmark: "Sort ${kind} ${iterations} iterations with ${
|
||||
items} items" - AVG time per call: ${
|
||||
avgTimeMilliseconds.toFixed(2)} ms`);
|
||||
return {
|
||||
totalTime, avgTimeMilliseconds,
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
mujoco = await loadMujoco();
|
||||
});
|
||||
|
||||
it('should benchmark NumberArray and DoubleBuffer and compare results',
|
||||
() => {
|
||||
const na1 = runBenchmark(100, 1_000, 'NumberArray');
|
||||
const na2 = runBenchmark(100, 400_000, 'NumberArray');
|
||||
|
||||
const db1 = runBenchmark(100, 1_000, 'DoubleBuffer');
|
||||
const db2 = runBenchmark(100, 400_000, 'DoubleBuffer');
|
||||
|
||||
// The actual time should be much faster than that but the intention of
|
||||
// the check is to catch huge regressions without a flakey test
|
||||
const _100ms = 100;
|
||||
|
||||
expect(db2.avgTimeMilliseconds).toBeLessThan(_100ms);
|
||||
|
||||
expect(db1.totalTime).toBeLessThan(na1.totalTime);
|
||||
expect(db2.totalTime).toBeLessThan(na2.totalTime);
|
||||
|
||||
expect(db1.avgTimeMilliseconds).toBeLessThan(na1.avgTimeMilliseconds);
|
||||
expect(db2.avgTimeMilliseconds).toBeLessThan(na2.avgTimeMilliseconds);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
// 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.
|
||||
|
||||
import 'jasmine';
|
||||
|
||||
import { MainModule } from "../dist/mujoco_wasm"
|
||||
import loadMujoco from "../dist/mujoco_wasm.js"
|
||||
|
||||
let mujoco: MainModule;
|
||||
|
||||
describe('Enums', () => {
|
||||
beforeAll(async () => {
|
||||
mujoco = await loadMujoco();
|
||||
});
|
||||
|
||||
it('mjtDisableBit should exist', () => {
|
||||
expect(mujoco.mjtDisableBit).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtEnableBit should exist', () => {
|
||||
expect(mujoco.mjtEnableBit).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtJoint should exist', () => {
|
||||
expect(mujoco.mjtJoint).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtGeom should exist', () => {
|
||||
expect(mujoco.mjtGeom).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtCamLight should exist', () => {
|
||||
expect(mujoco.mjtCamLight).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtLightType should exist', () => {
|
||||
expect(mujoco.mjtLightType).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtTexture should exist', () => {
|
||||
expect(mujoco.mjtTexture).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtTextureRole should exist', () => {
|
||||
expect(mujoco.mjtTextureRole).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtColorSpace should exist', () => {
|
||||
expect(mujoco.mjtColorSpace).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtIntegrator should exist', () => {
|
||||
expect(mujoco.mjtIntegrator).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtCone should exist', () => {
|
||||
expect(mujoco.mjtCone).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtJacobian should exist', () => {
|
||||
expect(mujoco.mjtJacobian).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtSolver should exist', () => {
|
||||
expect(mujoco.mjtSolver).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtEq should exist', () => {
|
||||
expect(mujoco.mjtEq).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtWrap should exist', () => {
|
||||
expect(mujoco.mjtWrap).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtTrn should exist', () => {
|
||||
expect(mujoco.mjtTrn).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtDyn should exist', () => {
|
||||
expect(mujoco.mjtDyn).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtGain should exist', () => {
|
||||
expect(mujoco.mjtGain).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtBias should exist', () => {
|
||||
expect(mujoco.mjtBias).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtObj should exist', () => {
|
||||
expect(mujoco.mjtObj).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtSensor should exist', () => {
|
||||
expect(mujoco.mjtSensor).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtStage should exist', () => {
|
||||
expect(mujoco.mjtStage).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtDataType should exist', () => {
|
||||
expect(mujoco.mjtDataType).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtConDataField should exist', () => {
|
||||
expect(mujoco.mjtConDataField).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtSameFrame should exist', () => {
|
||||
expect(mujoco.mjtSameFrame).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtLRMode should exist', () => {
|
||||
expect(mujoco.mjtLRMode).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtFlexSelf should exist', () => {
|
||||
expect(mujoco.mjtFlexSelf).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtSDFType should exist', () => {
|
||||
expect(mujoco.mjtSDFType).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtTaskStatus should exist', () => {
|
||||
expect(mujoco.mjtTaskStatus).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtState should exist', () => {
|
||||
expect(mujoco.mjtState).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtConstraint should exist', () => {
|
||||
expect(mujoco.mjtConstraint).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtConstraintState should exist', () => {
|
||||
expect(mujoco.mjtConstraintState).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtWarning should exist', () => {
|
||||
expect(mujoco.mjtWarning).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtTimer should exist', () => {
|
||||
expect(mujoco.mjtTimer).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtCatBit should exist', () => {
|
||||
expect(mujoco.mjtCatBit).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtMouse should exist', () => {
|
||||
expect(mujoco.mjtMouse).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtPertBit should exist', () => {
|
||||
expect(mujoco.mjtPertBit).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtCamera should exist', () => {
|
||||
expect(mujoco.mjtCamera).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtLabel should exist', () => {
|
||||
expect(mujoco.mjtLabel).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtFrame should exist', () => {
|
||||
expect(mujoco.mjtFrame).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtVisFlag should exist', () => {
|
||||
expect(mujoco.mjtVisFlag).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtRndFlag should exist', () => {
|
||||
expect(mujoco.mjtRndFlag).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtStereo should exist', () => {
|
||||
expect(mujoco.mjtStereo).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtPluginCapabilityBit should exist', () => {
|
||||
expect(mujoco.mjtPluginCapabilityBit).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtGridPos should exist', () => {
|
||||
expect(mujoco.mjtGridPos).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtFramebuffer should exist', () => {
|
||||
expect(mujoco.mjtFramebuffer).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtDepthMap should exist', () => {
|
||||
expect(mujoco.mjtDepthMap).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtFontScale should exist', () => {
|
||||
expect(mujoco.mjtFontScale).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtFont should exist', () => {
|
||||
expect(mujoco.mjtFont).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtGeomInertia should exist', () => {
|
||||
expect(mujoco.mjtGeomInertia).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtMeshInertia should exist', () => {
|
||||
expect(mujoco.mjtMeshInertia).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtMeshBuiltin should exist', () => {
|
||||
expect(mujoco.mjtMeshBuiltin).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtBuiltin should exist', () => {
|
||||
expect(mujoco.mjtBuiltin).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtMark should exist', () => {
|
||||
expect(mujoco.mjtMark).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtLimited should exist', () => {
|
||||
expect(mujoco.mjtLimited).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtAlignFree should exist', () => {
|
||||
expect(mujoco.mjtAlignFree).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtInertiaFromGeom should exist', () => {
|
||||
expect(mujoco.mjtInertiaFromGeom).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtOrientation should exist', () => {
|
||||
expect(mujoco.mjtOrientation).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtButton should exist', () => {
|
||||
expect(mujoco.mjtButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtEvent should exist', () => {
|
||||
expect(mujoco.mjtEvent).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtItem should exist', () => {
|
||||
expect(mujoco.mjtItem).toBeDefined();
|
||||
});
|
||||
|
||||
it('mjtSection should exist', () => {
|
||||
expect(mujoco.mjtSection).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extension": {
|
||||
"karma": {
|
||||
"client": {
|
||||
"pingTimeout": 300000
|
||||
},
|
||||
"browserDisconnectTimeout": 300000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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.
|
||||
|
||||
import Jasmine from 'jasmine';
|
||||
|
||||
const jasmine = new Jasmine();
|
||||
|
||||
jasmine.loadConfig({
|
||||
spec_dir: 'tests',
|
||||
spec_files: ['**/*_test.ts'],
|
||||
jsLoader: 'import',
|
||||
random: false,
|
||||
stopSpecOnExpectationFailure: false,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Starting Jasmine test run...');
|
||||
const result = await jasmine.execute();
|
||||
console.log(`Jasmine test run finished. Status: ${result.overallStatus}`);
|
||||
|
||||
if (result.overallStatus === 'failed') {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test runner script failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MuJoCo WebAssembly</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.
|
||||
|
||||
import { MainModule, MjData, MjModel } from "../../dist/mujoco_wasm"
|
||||
import loadMujoco from "../../dist/mujoco_wasm.js"
|
||||
|
||||
declare function loadMujoco(): Promise<MainModule>;
|
||||
|
||||
async function main() {
|
||||
const mujoco: MainModule = await loadMujoco();
|
||||
|
||||
(mujoco as any).FS.mkdir('/working');
|
||||
(mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working');
|
||||
|
||||
const xmlContent = `
|
||||
<mujoco model="Box falling">
|
||||
<option viscosity="1"/>
|
||||
<worldbody>
|
||||
<light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
|
||||
<geom name="MyFloor" type="plane" size="1 1 0.1" rgba=".9 0 0 1" user="5 4 3 2 1"/>
|
||||
<geom name="MyWall" type="plane" size="0.1 1 0.1" rgba="0 1 0 1" user="5 4 3"/>
|
||||
<body pos="0 0 1" name="MyBox">
|
||||
<joint type="free"/>
|
||||
<geom name="MyBoxGeom" type="box" size=".1 .2 .3" rgba="0 .9 0 1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
</mujoco>`;
|
||||
|
||||
(mujoco as any).FS.writeFile('/working/hello.xml', xmlContent);
|
||||
let model: MjModel|undefined;
|
||||
let data: MjData|undefined;
|
||||
|
||||
try {
|
||||
console.log('Hello world!: Loading model');
|
||||
model = mujoco.MjModel.loadFromXML('/working/hello.xml');
|
||||
if (!model) {
|
||||
throw new Error('Failed to load model');
|
||||
}
|
||||
data = new mujoco.MjData(model);
|
||||
if (!data) {
|
||||
throw new Error('Failed to load data');
|
||||
}
|
||||
|
||||
// Add your test code here...
|
||||
|
||||
} finally {
|
||||
model?.delete();
|
||||
data?.delete();
|
||||
(mujoco as any).FS.unmount('/working');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
root: 'tests/sandbox',
|
||||
base: './',
|
||||
build: {
|
||||
outDir: '../../sandbox-dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"include": ["tests/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.
|
||||
|
||||
#include "unpack.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace mujoco::wasm {
|
||||
|
||||
std::string StripWrapperSuffix(const char* func) {
|
||||
const char* suffix = "_wrapper";
|
||||
size_t name_len = strlen(func);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
|
||||
if (name_len >= suffix_len &&
|
||||
strcmp(func + name_len - suffix_len, suffix) == 0) {
|
||||
return std::string(func, name_len - suffix_len);
|
||||
}
|
||||
return std::string(func);
|
||||
}
|
||||
|
||||
} // namespace mujoco::wasm
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
// 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.
|
||||
|
||||
#ifndef MUJOCO_WASM_UNPACK_H_
|
||||
#define MUJOCO_WASM_UNPACK_H_
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
#include <emscripten/val.h>
|
||||
|
||||
#include <cinttypes> // NOLINT required for PRId64
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "engine/engine_util_errmem.h"
|
||||
|
||||
namespace mujoco::wasm {
|
||||
|
||||
// Helper to strip "_wrapper" from function names.
|
||||
std::string StripWrapperSuffix(const char* func);
|
||||
|
||||
// Utility class to write/read from the Heap shared by C++ and Javascript
|
||||
template <typename T>
|
||||
class WasmBuffer {
|
||||
private:
|
||||
// Note: Embind does not support binding more than one constructor with the
|
||||
// same argument count so we bind the factory function fromArray instead
|
||||
explicit WasmBuffer(const emscripten::val& array) {
|
||||
std::vector<T> cpp_array = convertJSArrayToNumberVector<T>(array);
|
||||
bytes_.resize(cpp_array.size() * sizeof(T));
|
||||
if (cpp_array.size() > 0) {
|
||||
memcpy(bytes_.data(), cpp_array.data(), bytes_.size());
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// Creates a buffer with the given element count
|
||||
explicit WasmBuffer(int element_count = 0) {
|
||||
bytes_.resize(element_count * sizeof(T));
|
||||
}
|
||||
|
||||
// Creates a buffer by copying data from a (typed) array
|
||||
static WasmBuffer<T> FromArray(const emscripten::val& array) {
|
||||
return WasmBuffer<T>(array);
|
||||
}
|
||||
|
||||
// Returns the pointer to the data in the buffer
|
||||
uintptr_t GetPointer() { return reinterpret_cast<uintptr_t>(bytes_.data()); }
|
||||
|
||||
// Returns the number of elements in the buffer
|
||||
int GetElementCount() { return bytes_.size() / sizeof(T); }
|
||||
|
||||
// Returns a TypedArray view of the buffer
|
||||
emscripten::val GetView() {
|
||||
return emscripten::val(emscripten::typed_memory_view(
|
||||
bytes_.size() / sizeof(T), reinterpret_cast<const T*>(bytes_.data())));
|
||||
}
|
||||
|
||||
void Zero() {
|
||||
if (!bytes_.empty()) {
|
||||
memset(bytes_.data(), 0, bytes_.size());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::byte> bytes_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class UnpackedParam {
|
||||
// The C++ representation of the parameter data
|
||||
std::variant<std::monostate, std::vector<T>, std::span<T>> data_;
|
||||
|
||||
// Printable representations of the param and function name used for errors
|
||||
const char* repr_;
|
||||
const char* func_;
|
||||
|
||||
explicit UnpackedParam(const char* repr, const char* func)
|
||||
: data_(std::monostate{}), repr_(repr), func_(func) {}
|
||||
|
||||
UnpackedParam(std::vector<T>&& array, const char* repr, const char* func)
|
||||
: data_(std::move(array)), repr_(repr), func_(func) {}
|
||||
|
||||
UnpackedParam(T* data, std::size_t count, const char* repr, const char* func)
|
||||
: data_(std::span<T>(data, count)), repr_(repr), func_(func) {}
|
||||
|
||||
// Returns true and raises an error if the val is null or undefined.
|
||||
static bool ErrorOnNullOrUndefined(const emscripten::val& p,
|
||||
const char* func,
|
||||
const char* expected_type) {
|
||||
if (p.isUndefined()) {
|
||||
mju_error("[%s] Invalid argument. Expected a %s, got undefined.",
|
||||
StripWrapperSuffix(func).c_str(), expected_type);
|
||||
return true;
|
||||
} else if (p.isNull()) {
|
||||
mju_error("[%s] Invalid argument. Expected a %s, got null.",
|
||||
StripWrapperSuffix(func).c_str(), expected_type);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the val is null or undefined. Use when these are expected.
|
||||
static bool IsNullOrUndefined(const emscripten::val& p) {
|
||||
return p.isUndefined() || p.isNull();
|
||||
}
|
||||
|
||||
public:
|
||||
// Create from a nullable Javascript val. Call via UNPACK_NULLABLE_VALUE.
|
||||
static UnpackedParam<T> FromNullableValue(const emscripten::val& p,
|
||||
const char* repr,
|
||||
const char* func) {
|
||||
if (IsNullOrUndefined(p)) {
|
||||
return UnpackedParam<T>(repr, func);
|
||||
}
|
||||
return FromValue(p, repr, func);
|
||||
}
|
||||
|
||||
// Create from a nullable Javascript number[]. Call via UNPACK_NULLABLE_ARRAY.
|
||||
static UnpackedParam<T> FromNullableArray(const emscripten::val& p,
|
||||
const char* repr,
|
||||
const char* func) {
|
||||
if (IsNullOrUndefined(p)) {
|
||||
return UnpackedParam<T>(repr, func);
|
||||
}
|
||||
return UnpackedParam<T>(convertJSArrayToNumberVector<T>(p), repr, func);
|
||||
}
|
||||
|
||||
// Create from a Javascript number[]. Call via UNPACK_ARRAY.
|
||||
static UnpackedParam<T> FromArray(const emscripten::val& p, const char* repr,
|
||||
const char* func) {
|
||||
ErrorOnNullOrUndefined(p, func, "number[]");
|
||||
return UnpackedParam<T>(convertJSArrayToNumberVector<T>(p), repr, func);
|
||||
}
|
||||
|
||||
// Creates an UnpackedParam from a Javascript a TypedArray or a WasmBuffer.
|
||||
// Call via UNPACK_VALUE.
|
||||
static UnpackedParam<T> FromValue(const emscripten::val& p, const char* repr,
|
||||
const char* func) {
|
||||
ErrorOnNullOrUndefined(p, func, "TypedArray or WasmBuffer");
|
||||
|
||||
if (!p["byteOffset"].isUndefined()) { // Javascript TypedArray
|
||||
T* data = reinterpret_cast<T*>(p["byteOffset"].as<uintptr_t>());
|
||||
std::size_t count = p["length"].as<std::size_t>();
|
||||
return UnpackedParam<T>(data, count, repr, func);
|
||||
} else if (!p["GetPointer"].isUndefined()) { // C++ WasmBuffer
|
||||
WasmBuffer<T>& buffer = p.as<WasmBuffer<T>&>();
|
||||
T* data = reinterpret_cast<T*>(buffer.GetPointer());
|
||||
std::size_t count = buffer.GetElementCount();
|
||||
return UnpackedParam<T>(data, count, repr, func);
|
||||
}
|
||||
|
||||
// TODO(manevi): This error message is not 100% accurate, WasmBuffer class
|
||||
// isn't surfaced to JS developers
|
||||
auto param = UnpackedParam<T>(repr, func);
|
||||
mju_error(
|
||||
"[%s] Invalid argument. Expected TypedArray or WasmBuffer, got "
|
||||
"unknown type for %s.",
|
||||
param.func().c_str(), param.repr());
|
||||
return param;
|
||||
}
|
||||
|
||||
// Returns true if the parameter is not null. Used in if conditions.
|
||||
explicit operator bool() const {
|
||||
return !std::holds_alternative<std::monostate>(data_);
|
||||
}
|
||||
|
||||
// Returns the printable representation of the parameter for use in error
|
||||
// messages.
|
||||
const char* repr() const { return repr_; }
|
||||
|
||||
// Returns the name of the function the parameter is used in.
|
||||
std::string func() const { return StripWrapperSuffix(func_); }
|
||||
|
||||
// Returns the size of the parameter. Returns 0 if the parameter is null.
|
||||
std::size_t size() const {
|
||||
if (std::holds_alternative<std::vector<T>>(data_)) {
|
||||
return std::get<std::vector<T>>(data_).size();
|
||||
} else if (std::holds_alternative<std::span<T>>(data_)) {
|
||||
return std::get<std::span<T>>(data_).size();
|
||||
}
|
||||
mju_error("[%s] [%s] UnpackedParam is null", func().c_str(), repr());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns a pointer to the data of the parameter. Returns nullptr if the
|
||||
// parameter is null.
|
||||
const T* data() const {
|
||||
if (std::holds_alternative<std::vector<T>>(data_)) {
|
||||
return std::get<std::vector<T>>(data_).data();
|
||||
} else if (std::holds_alternative<std::span<T>>(data_)) {
|
||||
return std::get<std::span<T>>(data_).data();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns a non-const pointer to the data of the parameter. Returns nullptr
|
||||
// if the parameter is null.
|
||||
T* data() {
|
||||
if (std::holds_alternative<std::vector<T>>(data_)) {
|
||||
return std::get<std::vector<T>>(data_).data();
|
||||
} else if (std::holds_alternative<std::span<T>>(data_)) {
|
||||
return const_cast<T*>(std::get<std::span<T>>(data_).data());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(matijak): When the bindings are fully auto-generated we could replace
|
||||
// these macros with a function calls something like this:
|
||||
//
|
||||
// template <typename T, typename U>
|
||||
// UnpackedParam<T> Unpack(U&& u, const char* u_name,
|
||||
// const std::source_location location = std::source_location::current()) {
|
||||
// return UnpackedParam<T>::FromValue(std::forward<U>(u), u_name,
|
||||
// location.file_name(), location.line(), location.function_name());
|
||||
// }
|
||||
|
||||
#define UNPACK_VALUE(T, p) \
|
||||
UnpackedParam<T> p##_ = UnpackedParam<T>::FromValue(p, #p, __func__)
|
||||
|
||||
#define UNPACK_ARRAY(T, p) \
|
||||
UnpackedParam<T> p##_ = UnpackedParam<T>::FromArray(p, #p, __func__)
|
||||
|
||||
#define UNPACK_NULLABLE_VALUE(T, p) \
|
||||
UnpackedParam<T> p##_ = UnpackedParam<T>::FromNullableValue(p, #p, __func__)
|
||||
|
||||
#define UNPACK_NULLABLE_ARRAY(T, p) \
|
||||
UnpackedParam<T> p##_ = UnpackedParam<T>::FromNullableArray(p, #p, __func__)
|
||||
|
||||
// Raises an error if x##_.size() is not equal to expr.
|
||||
// Assumes UnpackedParam x##_ is defined.
|
||||
#define CHECK_SIZE(x, expr) \
|
||||
if (x##_) { \
|
||||
if (static_cast<int64_t>(x##_.size()) != static_cast<int64_t>(expr)) { \
|
||||
mju_error("[%s] %s must have size %" PRId64 ", got %" PRId64, \
|
||||
x##_.func().c_str(), x##_.repr(), static_cast<int64_t>(expr), \
|
||||
static_cast<int64_t>(x##_.size())); \
|
||||
} \
|
||||
}
|
||||
|
||||
// Raises an error if x##_.size() is not equal to y##_.size().
|
||||
// Assumes UnpackedParams x##_ and y##_ are defined.
|
||||
#define CHECK_SIZES(x, y) \
|
||||
if (x##_ && y##_) { \
|
||||
if (static_cast<int64_t>(x##_.size()) != \
|
||||
static_cast<int64_t>(y##_.size())) { \
|
||||
mju_error("[%s] %s and %s must have equal size, got %" PRId64 \
|
||||
" and %" PRId64, \
|
||||
x##_.func().c_str(), x##_.repr(), y##_.repr(), \
|
||||
static_cast<int64_t>(x##_.size()), \
|
||||
static_cast<int64_t>(y##_.size())); \
|
||||
} \
|
||||
}
|
||||
|
||||
// Raises an error if x##_.size() is not a perfect square.
|
||||
// Assumes UnpackedParam x##_ is defined. Defines x##_sqrt as an int.
|
||||
#define CHECK_PERFECT_SQUARE(x) \
|
||||
const int x##_sqrt = static_cast<int>(round(sqrt(x##_.size()))); \
|
||||
if (x##_sqrt * x##_sqrt != x##_.size()) { \
|
||||
mjERROR("[%s] %s must be a perfect square, got %" PRId64, \
|
||||
x##_.func().c_str(), x##_.repr(), \
|
||||
static_cast<int64_t>(x##_.size())); \
|
||||
}
|
||||
|
||||
// Raises an error if x##_.size() is not divisible by divisor.
|
||||
// Assumes UnpackedParam x##_ is defined. Defines x##_div as an std::div_t.
|
||||
#define CHECK_DIVISIBLE(x, divisor) \
|
||||
const std::div_t x##_div = \
|
||||
std::div(static_cast<int>(x##_.size()), static_cast<int>(divisor)); \
|
||||
if (x##_div.rem != 0) { \
|
||||
mju_error("[%s] %s must be divisible by %d, got quot=%d rem=%d", \
|
||||
x##_.func().c_str(), x##_.repr(), static_cast<int>(divisor), \
|
||||
x##_div.quot, x##_div.rem); \
|
||||
}
|
||||
|
||||
} // namespace mujoco::wasm
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // MUJOCO_WASM_UNPACK_H_
|
||||
Reference in New Issue
Block a user