diff --git a/CMake/FindNodeJS.cmake b/CMake/FindNodeJS.cmake new file mode 100644 index 0000000000000000000000000000000000000000..7b36ca67b9ca85c622db70088b0a5e93694cc1e9 --- /dev/null +++ b/CMake/FindNodeJS.cmake @@ -0,0 +1,31 @@ +#[==[ +Provides the following variables: + + * `NodeJS_FOUND`: Whether NodeJS was found or not. + * `NodeJS_INTERPRETER`: Path to the `node` interpreter. +#]==] +find_program (NodeJS_INTERPRETER + NAMES node nodejs + HINTS + "$ENV{NODE_DIR}/bin" + DOC + "Node.js interpreter") + +include (FindPackageHandleStandardArgs) + +if (NodeJS_INTERPRETER) + execute_process(COMMAND "${NodeJS_INTERPRETER}" --version + OUTPUT_VARIABLE _nodejs_version + RESULT_VARIABLE _nodejs_version_result) + if (NOT _nodejs_version_result) + string(REGEX MATCH "v([0-9]+)\\.([0-9]+)\\.([0-9]+)" _nodejs_version_match "${_nodejs_version}") + set(_nodejs_version_major ${CMAKE_MATCH_1}) + set(_nodejs_version_minor ${CMAKE_MATCH_2}) + set(_nodejs_version_patch ${CMAKE_MATCH_3}) + set(_nodejs_version_string "${_nodejs_version_major}.${_nodejs_version_minor}.${_nodejs_version_patch}") + endif () +endif () + +find_package_handle_standard_args (NodeJS + REQUIRED_VARS NodeJS_INTERPRETER + VERSION_VAR _nodejs_version_string) diff --git a/CMake/vtkModuleTesting.cmake b/CMake/vtkModuleTesting.cmake index fb44139872a01324bbd7cd2e6ffb912ff2ccbb36..644c0f0c5f1382243f1c9701e5057de237fd014f 100644 --- a/CMake/vtkModuleTesting.cmake +++ b/CMake/vtkModuleTesting.cmake @@ -117,6 +117,18 @@ function (vtk_module_test_executable name) PRIVATE ${optional_depends_flags}) + # (vtk/vtk#19097) Although vtk_add_test_cxx skips C++ tests for wasm, + # some modules bypass `vtk_add_test_cxx` by directly invoking `ExternalData_add_test` + # for special tests, ex: ImagingCore module does it. + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # allows the test executable to access host file system for test data. + # permit memory growth so that test doesn't run out of memory. + target_link_options("${name}" + PRIVATE + "SHELL:-s ALLOW_MEMORY_GROWTH=1" + "SHELL:-s MAXIMUM_MEMORY=4GB" + "SHELL:-s NODERAWFS=1") + endif () vtk_module_autoinit( TARGETS "${name}" MODULES "${_vtk_build_test}" @@ -394,14 +406,24 @@ function (vtk_add_test_cxx exename _tests) ${MPIEXEC_PREFLAGS}) endif() - ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}" - NAME "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}" - COMMAND "${_vtk_test_cxx_pre_args}" "$<TARGET_FILE:${exename}>" - "${test_arg}" - "${args}" - ${${_vtk_build_test}_ARGS} - ${${test_name}_ARGS} - ${_D} ${_T} ${_V}) + # (vtk/vtk#19097) Disable C++ testing for wasm architecture until + # https://gitlab.kitware.com/vtk/vtk/-/issues/19097 is resolved. + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}" + NAME "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}" + COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} + "--eval" + "process.exit(125);") # all tests are skipped. + else () + ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}" + NAME "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}" + COMMAND "${_vtk_test_cxx_pre_args}" "$<TARGET_FILE:${exename}>" + "${test_arg}" + "${args}" + ${${_vtk_build_test}_ARGS} + ${${test_name}_ARGS} + ${_D} ${_T} ${_V}) + endif () set_tests_properties("${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}" PROPERTIES LABELS "${_vtk_build_test_labels}" @@ -418,6 +440,11 @@ function (vtk_add_test_cxx exename _tests) ENVIRONMENT "LD_PRELOAD=${_vtk_testing_ld_preload}") endif () + # (vtk/vtk#19097) Disable C++ testing for wasm architecture until + # https://gitlab.kitware.com/vtk/vtk/-/issues/19097 is resolved. + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + continue () + endif () list(APPEND ${_tests} "${test_file}") endforeach () @@ -816,6 +843,92 @@ function (vtk_add_test_python) endforeach () endfunction () +#[==[.rst: +JavaScript tests +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. cmake:command:: vtk_add_test_module_javascript_node + + This function declares JavaScript tests run with NodeJS. + Test files are required to use the `mjs` extension. + Additional arguments to `node` can be passed via `_vtk_node_args` variable. + + .. code-block:: cmake + + vtk_add_test_module_javascript_node(<VARNAME> <ARG>...) +#]==] + +#[==[.rst: +The ``_vtk_testing_nodejs_exe`` variable must point to the path of a `node` interpreter. +#]==] + +#[==[.rst +Options: + +- ``NO_DATA`` +- ``NO_OUTPUT`` + +Each argument should be either an option, a test specification, or it is passed +as flags to all tests declared in the group. The list of tests is set in the +``<VARNAME>`` variable in the calling scope. + +Options: + +- ``NO_DATA``: The test does not need to know the test input data directory. If + it does, it is passed on the command line via the ``-D`` flag. +- ``NO_OUTPUT``: The test does not need to write out any data to the + filesystem. If it does, a directory which may be written to is passed via + the ``-T`` flag. + +Additional flags may be passed to tests using the ``${_vtk_build_test}_ARGS`` +variable or the ``<NAME>_ARGS`` variable. +#]==] +function (vtk_add_test_module_javascript_node) + if (NOT _vtk_testing_nodejs_exe) + message(FATAL_ERROR "The \"_vtk_testing_nodejs_exe\" variable must point to a nodejs executable!") + endif () + set(mjs_options + NO_DATA + NO_OUTPUT) + _vtk_test_parse_args("${mjs_options}" "mjs" ${ARGN}) + _vtk_test_set_options("${mjs_options}" "" ${options}) + + set(_vtk_fail_regex + # vtkLogger + "(\n|^)ERROR: " + "ERR\\|" + # vtkDebugLeaks + "instance(s)? still around") + + foreach (name IN LISTS names) + _vtk_test_set_options("${mjs_options}" "local_" ${_${name}_options}) + _vtk_test_parse_name("${name}" "mjs") + set(_D "") + if (NOT local_NO_DATA) + set(_D -D "${_vtk_build_TEST_OUTPUT_DATA_DIRECTORY}") + endif () + + set(_T "") + if (NOT local_NO_OUTPUT) + set(_T -T "${_vtk_build_TEST_OUTPUT_DIRECTORY}") + endif () + ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}" + NAME "${_vtk_build_test}JavaScript-${test_name}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMAND ${_vtk_testing_nodejs_exe} + "${_vtk_node_args}" + "${test_file}" + ${${_vtk_build_test}_ARGS} + ${${test_name}_ARGS} + ${_D} ${_T}) + set_tests_properties("${_vtk_build_test}JavaScript-${test_name}" + PROPERTIES + LABELS "${_vtk_build_test_labels}" + FAIL_REGULAR_EXPRESSION "${_vtk_fail_regex}" + SKIP_RETURN_CODE 125) + endforeach () +endfunction () + #[==[.rst: MPI tests """"""""" diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d9e21b5702ec7fb0d6823160dfb6235aef0d4cd..a1860a9be21399bde50979e8da2f4be180df9d57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,6 +275,15 @@ set("_vtk_module_reason_VTK::Java" if (VTK_WRAP_SERIALIZATION) list(APPEND vtk_requested_modules VTK::SerializationManager) + if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + list(APPEND vtk_requested_modules + VTK::WebAssembly) + else () + list(APPEND vtk_rejected_modules + VTK::WebAssembly) + endif () + set("_vtk_module_reason_VTK::WebAssembly" + "via `VTK_WRAP_SERIALIZATION` AND `CMAKE_SYSTEM_NAME` STREQUAL Emscripten") else () list(APPEND vtk_rejected_modules VTK::SerializationManager) diff --git a/Filters/CellGrid/Testing/Cxx/CMakeLists.txt b/Filters/CellGrid/Testing/Cxx/CMakeLists.txt index affd849747f2ed51f41fb7a1942f223b042c215b..1b0ec9c715e85cd088c26573326a685402deb46a 100644 --- a/Filters/CellGrid/Testing/Cxx/CMakeLists.txt +++ b/Filters/CellGrid/Testing/Cxx/CMakeLists.txt @@ -4,4 +4,3 @@ vtk_add_test_cxx(vtkFiltersCellGridCxxTests tests TestDGCells.cxx,NO_VALID ) vtk_test_cxx_executable(vtkFiltersCellGridCxxTests tests) -set_target_properties(vtkFiltersCellGridCxxTests PROPERTIES LINKER_LANGUAGE CXX) diff --git a/IO/Import/Testing/Cxx/CMakeLists.txt b/IO/Import/Testing/Cxx/CMakeLists.txt index acac5170a0ce6d6d6111ed1ab0884f25fd9102ae..17f53d2916b76a6584b4118fd4792160a72b33a3 100644 --- a/IO/Import/Testing/Cxx/CMakeLists.txt +++ b/IO/Import/Testing/Cxx/CMakeLists.txt @@ -68,8 +68,11 @@ vtk_add_test_cxx(vtkIOImportCxxTests tests TestOBJImporter-Malformed,TestOBJImporter.cxx,NO_VALID DATA{../Data/Input/malformed.obj} ) -set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES PASS_REGULAR_EXPRESSION "Unexpected point indice value") -set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES FAIL_REGULAR_EXPRESSION "") +# (vtk/vtk#19097) This test is skipped on wasm architecture. +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES PASS_REGULAR_EXPRESSION "Unexpected point indice value") + set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES FAIL_REGULAR_EXPRESSION "") +endif () vtk_add_test_cxx(vtkIOImportCxxTests tests TestVRMLImporter.cxx,NO_VALID diff --git a/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512 b/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512 new file mode 100644 index 0000000000000000000000000000000000000000..12a0fef282c167d19d62be3846c4e75368302d7c --- /dev/null +++ b/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512 @@ -0,0 +1 @@ +87ac7e8caa53b1d049be54d5eb4496be6e45ab5af428af53d8844ead7a49edaa5b0926897c0290a3a0014583add415adf12211bd473f1d72d1feeeacd08abf84 diff --git a/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512 b/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512 new file mode 100644 index 0000000000000000000000000000000000000000..a5c40fefdd3b8db9226297c217e40f3ed50ebbd6 --- /dev/null +++ b/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512 @@ -0,0 +1 @@ +adfab1e7584c77e82cdf91a5b18a65e087414634976f45a673988eb0a71f44674cbafbe4d86192093d66ae3e8f2aa742cf1a8e0b159d6bbd89ec0b5933ef8470 diff --git a/Testing/Data/WasmSceneManager/simple.blobs.json.sha512 b/Testing/Data/WasmSceneManager/simple.blobs.json.sha512 new file mode 100644 index 0000000000000000000000000000000000000000..c291f03d6ae1971cd1a4a598f624104956863048 --- /dev/null +++ b/Testing/Data/WasmSceneManager/simple.blobs.json.sha512 @@ -0,0 +1 @@ +be824ebdcd14cf7a4d89673000988ec04e1cce448aba7a3ab33712aae3fd685e537a52ea724f75c6518c5291d143bc5c79e86cdf93b627d6a80b7cea4c9e2917 diff --git a/Web/WebAssembly/CMakeLists.txt b/Web/WebAssembly/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..7f33f84bf3a32c35c95d11c36302f41cf1a46fc4 --- /dev/null +++ b/Web/WebAssembly/CMakeLists.txt @@ -0,0 +1,116 @@ +if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message(FATAL_ERROR + "The VTK::WebAssembly module requires Emscripten compiler.") +endif () + +set(classes + vtkWasmSceneManager) + +vtk_module_add_module(VTK::WebAssembly + CLASSES ${classes}) + +vtk_add_test_mangling(VTK::WebAssembly) + +configure_file("Packaging/Wheel/__init__.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/Wheel/vtk_wasm/__init__.py") +configure_file("Packaging/Wheel/MANIFEST.in.in" + "${CMAKE_CURRENT_BINARY_DIR}/Wheel/MANIFEST.in") +configure_file("Packaging/Wheel/README.md.in" + "${CMAKE_CURRENT_BINARY_DIR}/Wheel/README.md") +configure_file("Packaging/Wheel/setup.cfg.in" + "${CMAKE_CURRENT_BINARY_DIR}/Wheel/setup.cfg") +configure_file("Packaging/Wheel/setup.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/Wheel/setup.py") + +# ----------------------------------------------------------------------------- +# Emscripten compile+link options +# ----------------------------------------------------------------------------- +set(emscripten_link_options) +list(APPEND emscripten_link_options + "-lembind" + "--extern-post-js=${CMAKE_CURRENT_SOURCE_DIR}/post.js" + # "--embind-emit-tsd=vtkWasmSceneManager.ts" + #"--memoryprofiler" + #"--cpuprofiler" + "-sALLOW_MEMORY_GROWTH=1" + "-sALLOW_TABLE_GROWTH=1" + "-sEXPORT_NAME=vtkWasmSceneManager" + "-sENVIRONMENT=node,web" + "-sEXPORTED_RUNTIME_METHODS=['addFunction','UTF8ToString']" + # "-sEXCEPTION_DEBUG=1" # prints stack trace for uncaught C++ exceptions from VTK (very rare, but PITA to figure out) + # "-sGL_DEBUG=1" + # "-sGL_ASSERTIONS=1" + # "-sTRACE_WEBGL_CALLS=1" + "-sMAXIMUM_MEMORY=4GB") +if (CMAKE_SIZEOF_VOID_P EQUAL "8") + list(APPEND emscripten_link_options + "-sMAXIMUM_MEMORY=16GB" + "-sWASM_BIGINT=1") +endif () +# ----------------------------------------------------------------------------- +# Optimizations +# ----------------------------------------------------------------------------- +set(emscripten_optimizations) +set(emscripten_debug_options) +set(vtk_scene_manager_wasm_optimize "BEST") +set(vtk_scene_manager_wasm_optimize_NO_OPTIMIZATION "-O0") +set(vtk_scene_manager_wasm_optimize_LITTLE "-O1") +set(vtk_scene_manager_wasm_optimize_MORE "-O2") +set(vtk_scene_manager_wasm_optimize_BEST "-O3") +set(vtk_scene_manager_wasm_optimize_SMALLEST "-Os") +set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE "-Oz") +set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE_link "--closure=1") + +if (DEFINED "vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}") + list(APPEND emscripten_optimizations + ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}}) + list(APPEND emscripten_link_options + ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}_link}) +else () + message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_optimize=${vtk_scene_manager_wasm_optimize}") +endif () + +set(vtk_scene_manager_wasm_debuginfo "NONE") +set(vtk_scene_manager_wasm_debuginfo_NONE "-g0") +set(vtk_scene_manager_wasm_debuginfo_READABLE_JS "-g1") +set(vtk_scene_manager_wasm_debuginfo_PROFILE "-g2") +set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE "-g3") +set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE_link "-sASSERTIONS=1") +if (DEFINED "vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}") + list(APPEND emscripten_debug_options + ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}}) + list(APPEND emscripten_link_options + ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}_link}) +else () + message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_debuginfo=${vtk_scene_manager_wasm_debuginfo}") +endif () + +vtk_module_add_executable(WasmSceneManager + BASENAME vtkWasmSceneManager + vtkWasmSceneManagerEmBinding.cxx) +target_link_libraries(WasmSceneManager + PRIVATE + VTK::WebAssembly + VTK::RenderingOpenGL2 + VTK::RenderingUI) +add_executable("VTK::WasmSceneManager" ALIAS + WasmSceneManager) +target_compile_options(WasmSceneManager + PRIVATE + ${emscripten_compile_options} + ${emscripten_optimizations} + ${emscripten_debug_options}) +target_link_options(WasmSceneManager + PRIVATE + ${emscripten_link_options} + ${emscripten_optimizations} + ${emscripten_debug_options}) +set_target_properties(WasmSceneManager + PROPERTIES + SUFFIX ".mjs") +# [cmake/cmake#20745](https://gitlab.kitware.com/cmake/cmake/-/issues/20745) +# CMake doesn't install multiple files associated with an executable target. +get_target_property(_vtk_scene_manager_version_suffix WebAssembly VERSION) +install(FILES + "$<TARGET_FILE_DIR:WasmSceneManager>/vtkWasmSceneManager-${_vtk_scene_manager_version_suffix}.wasm" + DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in b/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in new file mode 100644 index 0000000000000000000000000000000000000000..7ad8585a49c324dfefc81a2666783b7d7d75ef36 --- /dev/null +++ b/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in @@ -0,0 +1 @@ +graft vtk_wasm diff --git a/Web/WebAssembly/Packaging/Wheel/README.md.in b/Web/WebAssembly/Packaging/Wheel/README.md.in new file mode 100644 index 0000000000000000000000000000000000000000..0f177166f7afadb686094f2db55038a39692b9a0 --- /dev/null +++ b/Web/WebAssembly/Packaging/Wheel/README.md.in @@ -0,0 +1,3 @@ +# vtk-wasm + +VTK WebAssembly binaries with glue javacript libraries. diff --git a/Web/WebAssembly/Packaging/Wheel/__init__.py.in b/Web/WebAssembly/Packaging/Wheel/__init__.py.in new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/Web/WebAssembly/Packaging/Wheel/setup.cfg.in b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in new file mode 100644 index 0000000000000000000000000000000000000000..33744c4d6b7d2671c5778df021e363e7ce576654 --- /dev/null +++ b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in @@ -0,0 +1,30 @@ +[metadata] +name = vtk-wasm +version = @VTK_VERSION@ +description = VTK WebAssembly binaries with glue javacript libraries +long_description = file: README.md +long_description_content_type = text/x-md +author = Kitware Inc. +license = BSD License +classifiers = + Environment :: Web Environment + License :: OSI Approved :: BSD License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: JavaScript + Programming Language :: C++ + Topic :: Software Development :: Libraries :: Application Frameworks + Topic :: Software Development :: Libraries :: Python Modules +keywords = + WebAssembly + Web + Visualization + +[options] +packages = find: +include_package_data = True +install_requires = + vtk==@VTK_VERSION@ + +[semantic_release] +version_pattern = setup.cfg:version = (\d+\.\d+\.\d+) diff --git a/Web/WebAssembly/Packaging/Wheel/setup.py.in b/Web/WebAssembly/Packaging/Wheel/setup.py.in new file mode 100644 index 0000000000000000000000000000000000000000..606849326a4002007fd42060b51e69a19c18675c --- /dev/null +++ b/Web/WebAssembly/Packaging/Wheel/setup.py.in @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/Web/WebAssembly/Testing/CMakeLists.txt b/Web/WebAssembly/Testing/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..a82329dba97b9e1271a0441a5cf0e4614c49a824 --- /dev/null +++ b/Web/WebAssembly/Testing/CMakeLists.txt @@ -0,0 +1,8 @@ +vtk_module_test_data( + Data/WasmSceneManager/scalar-bar-widget.blobs.json + Data/WasmSceneManager/scalar-bar-widget.states.json + Data/WasmSceneManager/simple.blobs.json) + +if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + add_subdirectory(JavaScript) +endif () diff --git a/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..9e45a96cd06c06b9e79404a709870a535f57843d --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt @@ -0,0 +1,10 @@ +set(vtk_nodejs_min_version "18.8.0") +find_package(NodeJS "${vtk_nodejs_min_version}" REQUIRED) +set(_vtk_testing_nodejs_exe "${NodeJS_INTERPRETER}") + +set(_vtk_node_args + --import "${PROJECT_BINARY_DIR}/bin/vtkWasmSceneManager-${VTK_MAJOR_VERSION}.${VTK_MINOR_VERSION}.mjs") +vtk_add_test_module_javascript_node( + testInitialize.mjs,NO_DATA + testBlobs.mjs, + testStates.mjs) diff --git a/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs new file mode 100644 index 0000000000000000000000000000000000000000..a1ddaf42829bbe606bea4d5eecc42223750feb67 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs @@ -0,0 +1,52 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +async function testBlobs() { + const dataDirectoryIndex = process.argv.indexOf("-D") + 1; + if (dataDirectoryIndex <= 0) { + throw new Error("Please provide path to a blobs file using -D"); + } + const dataDirectory = process.argv[dataDirectoryIndex]; + const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "simple.blobs.json"))); + const manager = await globalThis.createVTKWasmSceneManager({}) + if (!manager.initialize()) { + throw new Error("Failed to initialize scene manager"); + } + + for (let hash in blobs) { + if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) { + throw new Error(`Failed to register blob with hash=${hash}`); + } + } + for (let hash in blobs) { + const blob = manager.getBlob(hash); + if (!(blob instanceof Uint8Array)) { + throw new Error(`getBlob did not return a Uint8Array for hash=${hash}`); + } + if (blob.toString() !== blobs[hash].bytes.toString()) { + throw new Error(`blob for hash=${hash} does not match registered blob.`); + } + } +} + +const tests = [ + { + description: "Register blobs with hashes", + test: testBlobs, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs new file mode 100644 index 0000000000000000000000000000000000000000..acdae02800277c7c5dc445214149b34638c2a478 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs @@ -0,0 +1,27 @@ +async function testInitialize() { + const manager = await globalThis.createVTKWasmSceneManager({}); + if (!manager.initialize()) { + throw new Error(); + } +} +const tests = [ + { + description: "Initialize VTK scene manager", + test: testInitialize, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/Testing/JavaScript/testStates.mjs b/Web/WebAssembly/Testing/JavaScript/testStates.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b9219d88408bf29ec71165dd20b83bf090a960d6 --- /dev/null +++ b/Web/WebAssembly/Testing/JavaScript/testStates.mjs @@ -0,0 +1,60 @@ +import { readFile } from "fs/promises"; +import path from "path"; + +const object_ids = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29, 31, 32] +const exepected_dependencies = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29] + +async function testStates() { + const dataDirectoryIndex = process.argv.indexOf("-D") + 1; + if (dataDirectoryIndex <= 0) { + throw new Error("Please provide path to a blobs file using -D"); + } + const dataDirectory = process.argv[dataDirectoryIndex]; + const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.blobs.json"))); + const states = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.states.json"))); + const manager = await globalThis.createVTKWasmSceneManager({}); + if (!manager.initialize()) { + throw new Error("Failed to initialize scene manager"); + } + for (let i = 0; i < object_ids.length; ++i) { + const object_id = object_ids[i]; + if (!manager.registerState(JSON.stringify(states[object_id]))) { + throw new Error(`Failed to register state at object_id=${object_id}`); + } + } + for (let hash in blobs) { + if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) { + throw new Error(`Failed to register blob with hash=${hash}`); + } + } + manager.updateObjectsFromStates(); + const activeIds = manager.getAllDependencies(0); + if (!(activeIds instanceof Uint32Array)) { + throw new Error("getAllDependencies did not return a Uint32Array"); + } + if (activeIds.toString() != exepected_dependencies.toString()) { + throw new Error(`${activeIds} != ${exepected_dependencies}`); + } +} + +const tests = [ + { + description: "Register states", + test: testStates, + }, +]; + +let exitCode = 0; +for (let test of tests) { + try { + await test.test(); + console.log("✓", test.description); + exitCode |= 0; + } + catch (error) { + console.log("x", test.description); + console.log(error); + exitCode |= 1; + } +} +process.exit(exitCode); diff --git a/Web/WebAssembly/post.js b/Web/WebAssembly/post.js new file mode 100644 index 0000000000000000000000000000000000000000..c13443372cb05e1d4c482fde45968ba43346a852 --- /dev/null +++ b/Web/WebAssembly/post.js @@ -0,0 +1 @@ +globalThis.createVTKWasmSceneManager = vtkWasmSceneManager; diff --git a/Web/WebAssembly/vtk.module b/Web/WebAssembly/vtk.module new file mode 100644 index 0000000000000000000000000000000000000000..ff7a6e6dfcf9700bba7f0b7e06f6586014c7b229 --- /dev/null +++ b/Web/WebAssembly/vtk.module @@ -0,0 +1,12 @@ +NAME + VTK::WebAssembly +LIBRARY_NAME + vtkWebAssembly +SPDX_LICENSE_IDENTIFIER + BSD-3-Clause +SPDX_COPYRIGHT_TEXT + Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +DEPENDS + VTK::SerializationManager +PRIVATE_DEPENDS + VTK::RenderingCore diff --git a/Web/WebAssembly/vtkWasmSceneManager.cxx b/Web/WebAssembly/vtkWasmSceneManager.cxx new file mode 100644 index 0000000000000000000000000000000000000000..b7d0c5ccc416cb90698c4b282b841c64bc8da884 --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManager.cxx @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include "vtkWasmSceneManager.h" + +#include "vtkCallbackCommand.h" +#include "vtkCommand.h" +#include "vtkObjectFactory.h" + +#include "vtkRenderWindow.h" +#include "vtkRenderWindowInteractor.h" + +VTK_ABI_NAMESPACE_BEGIN +//------------------------------------------------------------------------------- +vtkStandardNewMacro(vtkWasmSceneManager); + +//------------------------------------------------------------------------------- +vtkWasmSceneManager::vtkWasmSceneManager() = default; + +//------------------------------------------------------------------------------- +vtkWasmSceneManager::~vtkWasmSceneManager() = default; + +//------------------------------------------------------------------------------- +void vtkWasmSceneManager::PrintSelf(ostream& os, vtkIndent indent) +{ + this->vtkObjectManager::PrintSelf(os, indent); +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::SetSize(vtkTypeUInt32 identifier, int width, int height) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + if (auto iren = renderWindow->GetInteractor()) + { + iren->UpdateSize(width, height); + return true; + } + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::Render(vtkTypeUInt32 identifier) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + renderWindow->Render(); + return true; + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::StartEventLoop(vtkTypeUInt32 identifier) +{ + vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false; + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + auto interactor = renderWindow->GetInteractor(); + std::cout << "Started event loop id=" << identifier + << ", interactor=" << interactor->GetObjectDescription() << '\n'; + interactor->Start(); + return true; + } + return false; +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::StopEventLoop(vtkTypeUInt32 identifier) +{ + auto object = this->GetObjectAtId(identifier); + if (auto renderWindow = vtkRenderWindow::SafeDownCast(object)) + { + auto interactor = renderWindow->GetInteractor(); + std::cout << "Stopping event loop id=" << identifier + << ", interactor=" << interactor->GetObjectDescription() << '\n'; + interactor->TerminateApp(); + return true; + } + return false; +} + +namespace +{ +struct CallbackBridge +{ + vtkWasmSceneManager::ObserverCallbackF f; + vtkTypeUInt32 SenderId; +}; +} + +//------------------------------------------------------------------------------- +unsigned long vtkWasmSceneManager::AddObserver( + vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback) +{ + auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier)); + if (object == nullptr) + { + return 0; + } + vtkNew<vtkCallbackCommand> callbackCmd; + callbackCmd->SetClientData(new CallbackBridge{ callback, identifier }); + callbackCmd->SetClientDataDeleteCallback([](void* clientData) { + auto* bridge = reinterpret_cast<CallbackBridge*>(clientData); + delete bridge; + }); + callbackCmd->SetCallback([](vtkObject*, unsigned long eid, void* clientData, void*) { + auto* bridge = reinterpret_cast<CallbackBridge*>(clientData); + bridge->f(bridge->SenderId, vtkCommand::GetStringFromEventId(eid)); + }); + return object->AddObserver(eventName.c_str(), callbackCmd); +} + +//------------------------------------------------------------------------------- +bool vtkWasmSceneManager::RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag) +{ + + auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier)); + if (object == nullptr) + { + return false; + } + object->RemoveObserver(tag); + return true; +} + +VTK_ABI_NAMESPACE_END diff --git a/Web/WebAssembly/vtkWasmSceneManager.h b/Web/WebAssembly/vtkWasmSceneManager.h new file mode 100644 index 0000000000000000000000000000000000000000..493fa82a25b4f3922cb0f7810fd468bf59e7f076 --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManager.h @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +/** + * @class vtkWasmSceneManager + * @brief vtkWasmSceneManager provides additional functionality that relates to a vtkRenderWindow + * and user interaction. + * + * `vtkWasmSceneManager` is a javascript wrapper of `vtkSceneManager` for managing VTK + * objects, specifically designed for webassembly (wasm). It extends + * functionality of `vtkObjectManager` for managing objects such as `vtkRenderWindow`, + * `vtkRenderWindowInteractor` and enables event-observers in webassembly + * visualization applications. + * + * @sa vtkObjectManager + */ +#ifndef vtkWasmSceneManager_h +#define vtkWasmSceneManager_h + +#include "vtkObjectManager.h" + +#include "vtkSerializationManagerModule.h" // for export macro + +VTK_ABI_NAMESPACE_BEGIN + +class VTKSERIALIZATIONMANAGER_EXPORT vtkWasmSceneManager : public vtkObjectManager +{ +public: + static vtkWasmSceneManager* New(); + vtkTypeMacro(vtkWasmSceneManager, vtkObjectManager); + void PrintSelf(ostream& os, vtkIndent indent) override; + + /** + * Set the size of the `vtkRenderWindow` object at `identifier` to + * the supplied dimesions. + * + * Returns `true` if the obejct at `identifier` is a `vtkRenderWindow` + * with a `vtkRenderWindowInteractor` attached to it, + * `false` otherwise. + */ + bool SetSize(vtkTypeUInt32 identifier, int width, int height); + + /** + * Render the `vtkRenderWindow` object at `identifier`. + * + * Returns `true` if the obejct at `identifier` is a `vtkRenderWindow` + * `false` otherwise. + */ + bool Render(vtkTypeUInt32 identifier); + + /** + * Start event loop of the `vtkRenderWindowInteractor` object at `identifier`. + * + * Returns `true` if the obejct at `identifier` is a `vtkRenderWindowInteractor` + * `false` otherwise. + */ + bool StartEventLoop(vtkTypeUInt32 identifier); + + /** + * Stop event loop of the `vtkRenderWindowInteractor` object at `identifier`. + * + * Returns `true` if the obejct at `identifier` is a `vtkRenderWindowInteractor` + * `false` otherwise. + */ + bool StopEventLoop(vtkTypeUInt32 identifier); + + typedef void (*ObserverCallbackF)(vtkTypeUInt32, const char*); + + /** + * Observes `eventName` event emitted by an object registered at `identifier` + * and invokes `callback` with the `identifier` and `eventName` for every such emission. + * + * Returns the tag of an observer for `eventName`. You can use the tag in `RemoveObserver` + * to stop observing `eventName` event from the object at `identifier` + */ + unsigned long AddObserver( + vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback); + + /** + * Stop observing the object at `identifier`. + * Returns `true` if an object exists at `identifier`, + * `false` otherwise. + */ + bool RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag); + +protected: + vtkWasmSceneManager(); + ~vtkWasmSceneManager() override; + +private: + vtkWasmSceneManager(const vtkWasmSceneManager&) = delete; + void operator=(const vtkWasmSceneManager&) = delete; +}; +VTK_ABI_NAMESPACE_END +#endif diff --git a/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx new file mode 100644 index 0000000000000000000000000000000000000000..5c19ef16237a866aa7ec01a892b0f79fe15819a0 --- /dev/null +++ b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen +// SPDX-License-Identifier: BSD-3-Clause +#include <emscripten.h> +#include <emscripten/bind.h> + +#include "vtkDataArrayRange.h" +#include "vtkOpenGLPolyDataMapper.h" +#include "vtkTypeUInt8Array.h" +#include "vtkWasmSceneManager.h" + +namespace +{ + +#define CHECK_INIT \ + do \ + { \ + if (Manager == nullptr) \ + { \ + std::cerr << "Manager is null. Did you call forget to call initialize()?\n"; \ + } \ + } while (0) + +vtkWasmSceneManager* Manager = nullptr; + +using namespace emscripten; + +thread_local const val Uint8Array = val::global("Uint8Array"); +thread_local const val Uint32Array = val::global("Uint32Array"); +thread_local const val JSON = val::global("JSON"); + +//------------------------------------------------------------------------------- +bool initialize() +{ + Manager = vtkWasmSceneManager::New(); + bool result = Manager->Initialize(); + // Remove the default vtkOpenGLPolyDataMapper as it is not used with wasm build. + Manager->GetSerializer()->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper)); + Manager->GetDeserializer()->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper)); + Manager->GetDeserializer()->UnRegisterConstructor("vtkOpenGLPolyDataMapper"); + return result; +} + +//------------------------------------------------------------------------------- +void finalize() +{ + CHECK_INIT; + Manager->UnRegister(nullptr); +} + +//------------------------------------------------------------------------------- +bool registerState(const std::string& state) +{ + CHECK_INIT; + return Manager->RegisterState(state); +} + +//------------------------------------------------------------------------------- +bool unRegisterState(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->UnRegisterState(identifier); +} + +//------------------------------------------------------------------------------- +val getState(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return JSON.call<val>("parse", Manager->GetState(identifier)); +} + +//------------------------------------------------------------------------------- +bool unRegisterObject(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->UnRegisterObject(identifier); +} + +//------------------------------------------------------------------------------- +bool registerBlob(const std::string& hash, val jsArray) +{ + CHECK_INIT; + if (jsArray.instanceof (val::global("Uint8Array"))) + { + const vtkIdType l = jsArray["length"].as<vtkIdType>(); + auto blob = vtk::TakeSmartPointer(vtkTypeUInt8Array::New()); + blob->SetNumberOfValues(l); + auto blobRange = vtk::DataArrayValueRange(blob); + val memoryView{ typed_memory_view(static_cast<std::size_t>(l), blobRange.data()) }; + memoryView.call<void>("set", jsArray); + return Manager->RegisterBlob(hash, blob); + } + else + { + std::cerr << "Invalid type! Expects instanceof blob == Uint8Array" << std::endl; + return false; + } +} + +//------------------------------------------------------------------------------- +bool unRegisterBlob(const std::string& hash) +{ + CHECK_INIT; + return Manager->UnRegisterBlob(hash); +} + +//------------------------------------------------------------------------------- +val getBlob(const std::string& hash) +{ + CHECK_INIT; + const auto blob = Manager->GetBlob(hash); + val jsBlob = Uint8Array.new_(typed_memory_view(blob->GetNumberOfValues(), blob->GetPointer(0))); + return jsBlob; +} + +//------------------------------------------------------------------------------- +void pruneUnusedBlobs() +{ + CHECK_INIT; + Manager->PruneUnusedBlobs(); +} + +//------------------------------------------------------------------------------- +void clear() +{ + CHECK_INIT; + Manager->Clear(); +} + +//------------------------------------------------------------------------------- +val getAllDependencies(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + const auto ids = Manager->GetAllDependencies(identifier); + val jsIds = Uint32Array.new_(typed_memory_view(ids.size(), ids.data())); + return jsIds; +} +//------------------------------------------------------------------------------- +std::size_t getTotalBlobMemoryUsage() +{ + CHECK_INIT; + return ::Manager->GetTotalBlobMemoryUsage(); +} + +//------------------------------------------------------------------------------- +std::size_t getTotalVTKDataObjectMemoryUsage() +{ + CHECK_INIT; + return ::Manager->GetTotalVTKDataObjectMemoryUsage(); +} + +//------------------------------------------------------------------------------- +void updateObjectsFromStates() +{ + CHECK_INIT; + Manager->UpdateObjectsFromStates(); +} + +//------------------------------------------------------------------------------- +void updateStatesFromObjects() +{ + CHECK_INIT; + Manager->UpdateStatesFromObjects(); +} + +//------------------------------------------------------------------------------- +bool setSize(vtkTypeUInt32 identifier, int width, int height) +{ + CHECK_INIT; + return Manager->SetSize(identifier, width, height); +} + +//------------------------------------------------------------------------------- +bool render(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->Render(identifier); +} + +//------------------------------------------------------------------------------- +bool startEventLoop(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->StartEventLoop(identifier); +} + +//------------------------------------------------------------------------------- +bool stopEventLoop(vtkTypeUInt32 identifier) +{ + CHECK_INIT; + return Manager->StopEventLoop(identifier); +} + +//------------------------------------------------------------------------------- +unsigned long addObserver(vtkTypeUInt32 identifier, std::string eventName, val jsFunc) +{ + CHECK_INIT; + int fp = val::module_property("addFunction")(jsFunc, std::string("vii")).as<int>(); + auto callback = reinterpret_cast<vtkWasmSceneManager::ObserverCallbackF>(fp); + return Manager->AddObserver(identifier, eventName, callback); +} + +//------------------------------------------------------------------------------- +bool removeObserver(vtkTypeUInt32 identifier, unsigned long tag) +{ + CHECK_INIT; + return Manager->RemoveObserver(identifier, tag); +} + +} // namespace + +EMSCRIPTEN_BINDINGS(vtkWasmSceneManager) +{ + function("initialize", ::initialize); + function("finalize", ::finalize); + + function("registerState", ::registerState); + function("unRegisterState", ::unRegisterState); + function("getState", ::getState); + + function("unRegisterObject", ::unRegisterObject); + + function("registerBlob", ::registerBlob); + function("unRegisterBlob", ::unRegisterBlob); + function("getBlob", ::getBlob); + function("pruneUnusedBlobs", ::pruneUnusedBlobs); + + function("clear", ::clear); + + function("getAllDependencies", ::getAllDependencies); + + function("getTotalBlobMemoryUsage", ::getTotalBlobMemoryUsage); + function("getTotalVTKDataObjectMemoryUsage", ::getTotalVTKDataObjectMemoryUsage); + + function("updateObjectsFromStates", ::updateObjectsFromStates); + function("updateStatesFromObjects", ::updateStatesFromObjects); + + function("setSize", ::setSize); + function("render", ::render); + + function("startEventLoop", ::startEventLoop); + function("stopEventLoop", ::stopEventLoop); + + function("addObserver", ::addObserver); + function("removeObserver", ::removeObserver); +} + +int main() +{ + return 0; +}