Add multithreaded version of Jolt to the package
See #110
We can build Jolt in multithreaded mode now, we should create an example (a plain one and one implementing callbacks) and make the build available in the package.
Regarding an example with callbacks, that is now very challenging again. The latest Emscripten now no longer puts code in the .worker.js file, so all multi-threading code is self-contained in the NPM compiled JS module and out-of-the-box we are without a 'hook' into the threads again.
This is back in the situation where multi-threading is essentially non-functional for all but trivial cases.
The minimal requirements I can think of before it is viable for anything using a JS callback:
- Required - A way a worker thread can run a configurable script import PostJoltInit, PreThreadStart
- Ideally - a way for the main thread to pass data to all workers PostJoltInit, PreThreadStart
It may be possible to hijack the PThread singleton in the module and use the --post-js feature to swizzle/grab the thread-spawning and thread-initializing routines and add one extra step that can support a promise or script imports.
An alternative would be subclassing the ThreadPool and somehow accessing the thread-workers and giving them EM_ASM hooks to call a JS world 'on-thread-create' callback, which then calls 'thread.start' after it has setup, but that is even more non-trivial.
new NPM package jolt-physics/multi-threaded
I tried to approach this as either building them "All At Once (4 to 5x variants)", or build it in two phases. This may be due to my limited knowledge of best CMake practices and techniques, but one challenge of trying to build 'all at once' is that both the core Jolt C++ static library and Glue code will need to be compiled without shared memory flags for [ wasm, wasm-opt, JS ] and compiled with flags for [ multi-threaded ].
The easiest setup I can think of is make a NPM-packaging SH build-script that compiles just the multi-threading/shared-mem/atomics versions, reconfigure CMake, then build the default [wasm, wasm-opt, JS] variants.
set -e # putting this at the very top, this avoids the need for so many on-fail, exit 1 checks
cmake -B Build/$BUILD_TYPE -DENABLE_MULTI_THREADING=ON -DCMAKE_BUILD_TYPE=$BUILD_TYPE "${@}"
cmake --build Build/$BUILD_TYPE -j`nproc` --target jolt-wasm-compat jolt-wasm
for file in ./dist/jolt-physics.*; do
mv "${file}" "${file/physics/physics.multithread}"
done
cmake -B Build/$BUILD_TYPE -DENABLE_MULTI_THREADING=OFF -DCMAKE_BUILD_TYPE=$BUILD_TYPE "${@}"
cmake --build Build/$BUILD_TYPE -j`nproc`
I really wish there was an official method to do this, since anything unofficial is risky and could break at some point in the future, which is one of my main concerns about multithreading in an official package.
With current emscripten, this is an example --post-js swizzle to pass a custom JS module URL and extra args supporting shared arrays buffers.
It tacks on a script URL and extra params if available during worker-thread spawning, and on individual workers it strips them off on and conditionally executes a passed in JS module.
// START: MULTI THREADED
if(ENVIRONMENT_IS_PTHREAD) {
const _invokeEntryPoint = invokeEntryPoint;
invokeEntryPoint = (ptr, arg) => {
if(arg.script) {
import(arg.script).then(module => {
module.default(Module, arg.params).then(() => _invokeEntryPoint(ptr, arg.value));
})
} else {
_invokeEntryPoint(ptr, arg.value);
}
}
} else {
const _spawnThread = spawnThread;
spawnThread = (threadParams) => {
threadParams.arg = { value: threadParams.arg, script: Module['workerScript'], params: Module['workerScriptParams']};
_spawnThread(threadParams);
}
}
// END: MULTI THREADED
usage:
const contactListener = new Jolt.ContactListenerJS();
Jolt['workerScript'] = './on-worker-conveyor-belt.js';
const workerParams = Jolt['workerScriptParams'] = {
contactListenerPtr: Jolt.getPointer(contactListener),
angularBelt: new Uint32Array(new SharedArrayBuffer(4)),
linearBelts: new Uint32Array(new SharedArrayBuffer(20))
}
// workerParams are sent to all workers at this point, and then never again until Thread restarted
// so no more modifying the workerParam entries outside of internal SharedArrayBuffer values
initExample(Jolt, null);
// conveyor demo
workerParams.angularBelt[0] = angularBelt.GetID().GetIndexAndSequenceNumber();
workerParams.linearBelts.set(linearBelts.map(belt => belt.GetID().GetIndexAndSequenceNumber()));
// ensure all SharedArrayBuffer are set prior to the next execution of a PhysicsEngine Step
Sample module:
const onWorker = async (Jolt, args) => {
// values passed during JoltInterface initialization, when the ThreadPool is started
const { contactListenerPtr, linearBelts, angularBelt } = args;
// linearBelts and angularBelt are ui32 arrays of a fixed size, but who's values may be changed on any thread
const contactListener = Jolt.wrapPointer(contactListenerPtr, Jolt.ContactListenerJS);
contactListener.OnContactAdded = (body1, body2, manifold, settings) => { ... }
}
export default onWorker;
Hello, I'm currently on holiday so can't try anything out until I'm back.
Wrt cmake: you usually build different settings to different locations so something like:
cmake -B Build/$BUILD_TYPE$MT
Where $MT indicates if it a multi threading build or not.
Wrt cmake: you usually build different settings to different locations so something like:
cmake -B Build/$BUILD_TYPE$MTWhere $MT indicates if it a multi threading build or not.
This does trigger two downloads since it is making two build folders, and I don't think by default CMake will support caching the git repo anywhere, but there is probably not much overhead in cloning down the original library twice.
This is a PoC of the above workerScript + workerParam idea: https://github.com/PhoenixIllusion/JoltPhysics.js/commit/ce18a68b020f6a1ce5634aea24ca399c014f341b
This does trigger two downloads since it is making two build folders, and I don't think by default CMake will support caching the git repo anywhere, but there is probably not much overhead in cloning down the original library twice.
Yes, that should not be a problem, if you switch between debug/release using build.sh it also downloads the repo 2x.
I've finally looked at the change, to me the solution looks quite reasonable. The main issue is that you're building 2x to Build/$BUILD_TYPE and cmake afaik doesn't pick up changes to -DENABLE_MULTI_THREADING= in that case. So we'd need to build to 2 different folders (this also helps incremental builds). If you're also happy with this solution then send me a PR and I'll make that fix and merge it.