Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multithreaded version of Jolt to the package #134

Open
jrouwe opened this issue Mar 27, 2024 · 6 comments
Open

Add multithreaded version of Jolt to the package #134

jrouwe opened this issue Mar 27, 2024 · 6 comments

Comments

@jrouwe
Copy link
Owner

jrouwe commented Mar 27, 2024

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.

@PhoenixIllusion
Copy link
Contributor

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:

  1. Required - A way a worker thread can run a configurable script import PostJoltInit, PreThreadStart
  2. 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`

@PhoenixIllusion
Copy link
Contributor

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;

@jrouwe
Copy link
Owner Author

jrouwe commented Jul 18, 2024

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.

@PhoenixIllusion
Copy link
Contributor

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.

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: PhoenixIllusion@ce18a68

@jrouwe
Copy link
Owner Author

jrouwe commented Aug 3, 2024

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.

@jrouwe
Copy link
Owner Author

jrouwe commented Aug 6, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants