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:
Matias Manevi
2025-10-30 10:37:06 -07:00
committed by Copybara-Service
parent 10f4b36fb7
commit 76e64d96bb
56 changed files with 24362 additions and 22 deletions
+60
View File
@@ -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})
+54
View File
@@ -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
+113
View File
@@ -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
+274
View File
@@ -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();
});
});
+10
View File
@@ -0,0 +1,10 @@
{
"extension": {
"karma": {
"client": {
"pingTimeout": 300000
},
"browserDisconnectTimeout": 300000
}
}
}
+40
View File
@@ -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);
}
+26
View File
@@ -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>
+64
View File
@@ -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()
+27
View File
@@ -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,
},
})