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

emscripten tbb stuff #653

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/manifold.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-22.04
if: github.event.pull_request.draft == false
strategy:
matrix:
parallel_backend: [NONE, TBB]
steps:
- name: Install dependencies
run: |
Expand All @@ -121,11 +124,16 @@ jobs:
source ./emsdk/emsdk_env.sh
mkdir build
cd build
emcmake cmake -DCMAKE_BUILD_TYPE=Release .. && emmake make
# this system processor thing is to avoid TBB from emitting
# architecture-specific compiler flags
emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel \
-DMANIFOLD_PAR=${{matrix.parallel_backend}}\
-DEMSCRIPTEN_SYSTEM_PROCESSOR=web ..
emmake make
- name: Test WASM
run: |
cd build/test
node ./manifold_test.js
node --experimental-wasm-threads ./test_manifold.js
- name: Test examples
run: |
cd bindings/wasm/examples
Expand All @@ -134,6 +142,7 @@ jobs:
npm test
cp ../manifold.* ./dist/
- name: Upload WASM files
if: matrix.parallel_backend == 'NONE'
uses: actions/upload-artifact@v3
with:
name: wasm
Expand Down
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ endif()
if(EMSCRIPTEN)
message("Building for Emscripten")
set(MANIFOLD_FLAGS -fexceptions -D_LIBCUDACXX_HAS_THREAD_API_EXTERNAL -D_LIBCUDACXX_HAS_THREAD_API_CUDA)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1 -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
# ALLOW_MEMORY_GROWTH is slow, so we just allow 4gb of memory usage
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sINITIAL_MEMORY=4gb -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
set(MANIFOLD_PYBIND OFF)
if(MANIFOLD_PAR STREQUAL "TBB")
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -pthread)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -sPTHREAD_POOL_SIZE=\"(typeof window == 'undefined')?4:(()=>navigator.hardwareConcurrency)()\"")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't set it ourselves, PTHREAD_POOL_SIZE defaults to zero and I think it is a link-time constant, so we must set it to some sensible value here...

endif()
set(BUILD_SHARED_LIBS OFF)
endif()

Expand Down
9 changes: 8 additions & 1 deletion bindings/wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/examples/built)
# ensure that interface files are copied over when modified
add_custom_target(js_deps ALL
DEPENDS manifoldjs
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts)
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
${CMAKE_SOURCE_DIR}/patch-emscripten.sh)

if(MANIFOLD_PAR STREQUAL "TBB")
add_custom_command(
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_SOURCE_DIR}/patch-emscripten.sh ${CMAKE_CURRENT_BINARY_DIR})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the patch applied both here and in the manifold.yml file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The patch applied in manifold.yml is for patching the test/manifold_test.js, while the patch here is to patch the binding files. I can remove the package.json so running test/manifold_test.js does not require any patch. I added that mainly for testing and forgot to remove it.

endif()

# copy WASM build back here for publishing to npm
add_custom_command(
Expand Down
3 changes: 2 additions & 1 deletion bindings/wasm/examples/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,8 @@ async function run() {
clearConsole();
console.log('Running...');
const output = await tsWorker.getEmitOutput(editor.getModel().uri.toString());
manifoldWorker.postMessage(output.outputFiles[0].text);
manifoldWorker.postMessage(
{needProps: false, script: output.outputFiles[0].text});
t0 = performance.now();
}

Expand Down
12 changes: 12 additions & 0 deletions bindings/wasm/examples/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ export default defineConfig({
worker: {
format: 'es',
},
plugins: [
{
name: 'configure-response-headers',
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the cross-origin policy required for SharedArrayBuffer to work, there is no way around this. Hope that github pages supports this.

res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
},
},
],
build: {
target: 'esnext',
rollupOptions: {
Expand Down
29 changes: 4 additions & 25 deletions bindings/wasm/examples/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,11 @@

import '@vitest/web-worker';

import {WebIO} from '@gltf-transform/core';
import {expect, suite, test} from 'vitest';

import Module from './built/manifold.js';
import {readMesh, setupIO} from './gltf-io';
import {examples} from './public/examples.js';
import ManifoldWorker from './worker?worker';

const io = setupIO(new WebIO());

const wasm = await Module();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the weird part. For some reason if we load manifold.js multiple times, the subsequent loads will not complete.

Perhaps this is caused by pthread worker loader implementation. No idea about this, will try to come up with a minimal example and submit the issue to emscripten.

I currently workaround this issue by having only a single manifold worker, and move the get properties and genus calls to worker.ts, but this is not ideal.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you ever submit this to emscripten?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to try to reproduce this, will try later.

wasm.setup();

function initialized(worker) {
return new Promise((resolve) => {
worker.onmessage = function(e) {
Expand Down Expand Up @@ -58,29 +50,16 @@ async function runExample(name) {
URL.revokeObjectURL(glbURL);
glbURL = e.data.glbURL;
if (glbURL == null) {
reject('no glbURL)');
}
const docIn = await io.read(glbURL);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const docMesh = node.getMesh();
if (!docMesh) {
continue;
}
const {mesh} = readMesh(docMesh);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the one problem I see with this refactor is we're no longer testing readMesh, and we're not really testing writeMesh either since the test isn't validating its output at all. These functions aren't actually dependent on Manifold, so perhaps we can slot them back in?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we do test writeMesh, in the worker.ts code I am reading the output of writeMesh for the property.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see it now - okay, that's perfect. Thanks!

const manifold = wasm.Manifold(mesh);
const prop = manifold.getProperties();
const genus = manifold.genus();
manifold.delete();
// Return properties of first mesh encountered.
resolve({...prop, genus});
reject('no glbURL');
}
resolve({...e.data.prop, genus: e.data.genus});
} catch (e) {
reject(e);
}
};

worker.postMessage(examples.functionBodies.get(name));
worker.postMessage(
{needProps: true, script: examples.functionBodies.get(name)});
});
}

Expand Down
40 changes: 32 additions & 8 deletions bindings/wasm/examples/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {strToU8, Zippable, zipSync} from 'fflate'
import * as glMatrix from 'gl-matrix';

import Module from './built/manifold';
import {Properties, setupIO, writeMesh} from './gltf-io';
import {Properties, readMesh, setupIO, writeMesh} from './gltf-io';
import {GLTFMaterial, Quat} from './public/editor';
import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold';

Expand Down Expand Up @@ -326,13 +326,15 @@ function log(...args: any[]) {
}

self.onmessage = async (e) => {
const content = 'const globalDefaults = {};\n' + e.data +
'\nreturn exportModels(globalDefaults, typeof result === "undefined" ? undefined : result);\n';
const needProps = e.data.needProps ?? false;
const content = 'const globalDefaults = {};\n' + e.data.script +
'\nreturn exportModels(needProps, globalDefaults, typeof result === "undefined" ? undefined : result);\n';
try {
const f = new Function(
'exportModels', 'glMatrix', 'module', ...exposedFunctions, content);
'needProps', 'exportModels', 'glMatrix', 'module', ...exposedFunctions,
content);
await f(
exportModels, glMatrix, module, //@ts-ignore
needProps, exportModels, glMatrix, module, //@ts-ignore
...exposedFunctions.map(name => module[name]));
} catch (error: any) {
console.log(error.toString());
Expand Down Expand Up @@ -659,7 +661,8 @@ function createNodeFromCache(
return node;
}

async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
async function exportModels(
needProps: boolean, defaults: GlobalDefaults, manifold?: Manifold) {
Object.assign(globalDefaults, GLOBAL_DEFAULTS);
Object.assign(globalDefaults, defaults);

Expand Down Expand Up @@ -777,8 +780,29 @@ async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
[zipFile],
{type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'});

self.postMessage({
const result: any = {
glbURL: URL.createObjectURL(blobGLB),
threeMFURL: URL.createObjectURL(blob3MF)
});
};

if (needProps) {
const docIn = await io.read(result.glbURL);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const docMesh = node.getMesh();
if (!docMesh) {
continue;
}
const {mesh} = readMesh(docMesh) ?? {mesh: undefined};
if (mesh != undefined) {
const manifold = new module.Manifold(mesh as Mesh);
result['prop'] = manifold.getProperties();
result['genus'] = manifold.genus();
manifold.delete();
}
break;
}
}

self.postMessage(result);
}
2 changes: 1 addition & 1 deletion bindings/wasm/helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Manifold LevelSet(uintptr_t funcPtr, Box bounds, float edgeLength,
float level) {
float (*f)(const glm::vec3&) =
reinterpret_cast<float (*)(const glm::vec3&)>(funcPtr);
Mesh m = LevelSet(f, bounds, edgeLength, level);
Mesh m = LevelSet(f, bounds, edgeLength, level, false);
return Manifold(m);
}

Expand Down
5 changes: 3 additions & 2 deletions bindings/wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"manifold.wasm",
"manifold.d.ts",
"manifold-encapsulated-types.d.ts",
"manifold-global-types.d.ts"
"manifold-global-types.d.ts",
"manifold.worker.js"
],
"typings": "manifold.d.ts",
"types": "manifold.d.ts",
Expand Down Expand Up @@ -36,4 +37,4 @@
"url": "https://github.com/elalish/manifold/issues"
},
"homepage": "https://github.com/elalish/manifold#readme"
}
}
4 changes: 1 addition & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@
emmake make -j''${NIX_BUILD_CORES}
'';
checkPhase = ''
cd test
node manifold_test.js
cd ../
node --experimental-wasm-threads ./test/manifold_test.js
'';
installPhase = ''
mkdir -p $out
Expand Down
7 changes: 7 additions & 0 deletions patch-emscripten.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#/usr/bin/env bash
if test -f $1/manifold.js; then
# I still have no idea why this is needed...
sed -i 's/new Worker/new (ENVIRONMENT_IS_NODE ? global.Worker : Worker)/g' $1/manifold.js
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea why global.Worker != Worker, considering there are no let Worker or var Worker anywhere...

Also, Worker is something like this:

class extends EventTarget {
    constructor(url, options2) {
      super();
      this._vw_workerTarget = new EventTarget();
      this._vw_insideListeners = /* @__PURE__ */ new Map();
      this._vw_outsideListeners = /* @__PURE__ */ new Map();
      this._vw_messageQueue = [];
      this.onmessage = null;
      this.onmessageerror = null;
      this.onerror = null;
      const context = {
        onmessage: null,
        name: options2 == null ? void 0 : options2.name,
        close: () => this.terminate(),
        dispatchEvent: (event) => {
          return this._vw_workerTarget.dispatchEvent(event);
        },

while global.Worker is this:

class Worker extends EventEmitter {
  constructor(filename, options = kEmptyObject) {
    super();
    debug(`[${threadId}] create new worker`, filename, options);
    if (options.execArgv)
      validateArray(options.execArgv, 'options.execArgv');

    let argv;
    if (options.argv) {
      validateArray(options.argv, 'options.argv');
      argv = ArrayPrototypeMap(options.argv, String);
    }

    let url, doEval;
    if (options.eval) {
      if (typeof filename !== 'string') {
        throw new ERR_INVALID_ARG_VALUE(
          'options.eval',
          options.eval,
          'must be false when \'filename\' is not a string',
        );
      }
      url = null;
      doEval = 'classic';

global.Worker is the correct target in this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that nodejs dynamic import will define Worker for the module being imported, but I am still not sure why the Worker defined here is the wrong one.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From your other comment it sounds like you think this might be a vitest issue? Have we opened anything on their forums?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is related to vitest, but I cannot make a minimal example yet and don't quite understand why it causes an issue, so I have not yet open anything there.

fi
sed -i 's/var nodeWorkerThreads=require("worker_threads");/const{createRequire:createRequire}=await import("module");var require=createRequire(import.meta.url);var nodeWorkerThreads=require("worker_threads");/g' $1/*.worker.js
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some ES6 compatibility issue. This is something upstream should fix, and pretty easy to fix.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this one that already got fixed upstream?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I think they have not yet released a new version.

sed -i 's/__filename/import.meta.url/g' $1/*.worker.js
3 changes: 2 additions & 1 deletion src/manifold/src/face_op.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
halfedge_.cbegin() + faceEdge[face + 1], projection);
return TriangulateIdx(polys, precision_);
};
#if MANIFOLD_PAR == 'T' && __has_include(<tbb/tbb.h>)
// emscripten does not like fine parallel tasks...
#if MANIFOLD_PAR == 'T' && __has_include(<tbb/tbb.h>) && !defined(__EMSCRIPTEN__)
tbb::task_group group;
// map from face to triangle
tbb::concurrent_unordered_map<int, std::vector<glm::ivec3>> results;
Expand Down
5 changes: 3 additions & 2 deletions src/utilities/include/par.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ inline constexpr ExecutionPolicy autoPolicy(int size) {
return thrust::NAME(thrust::cpp::par, args...); \
}

#if MANIFOLD_PAR != 'T' || \
(TBB_INTERFACE_VERSION >= 10000 && __has_include(<pstl/glue_execution_defs.h>))
#if MANIFOLD_PAR != 'T' || \
(!defined(__EMSCRIPTEN__) && TBB_INTERFACE_VERSION >= 10000 && \
__has_include(<pstl/glue_execution_defs.h>))
#if MANIFOLD_PAR == 'T'
#define STL_DYNAMIC_BACKEND(NAME, RET) \
template <typename Ret = RET, typename... Args> \
Expand Down
Loading