From a2c0206d0780c407050683be78161180fe113f8d Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:23:22 +0200 Subject: [PATCH 01/11] NativeDawn --- .github/workflows/build-android.yml | 22 + .github/workflows/build-ios.yml | 30 + .github/workflows/build-linux.yml | 33 +- .github/workflows/build-macos.yml | 24 +- .github/workflows/build-win32.yml | 25 +- .github/workflows/ci.yml | 37 + .../Android/BabylonNative/build.gradle | 12 + Apps/Playground/CMakeLists.txt | 79 +- Apps/Playground/Scripts/dawn_bootstrap.js | 359 +++ Apps/Playground/Win32/App.cpp | 444 ++- CMakeLists.txt | 70 + Plugins/CMakeLists.txt | 4 + Plugins/NativeDawn/CMakeLists.txt | 25 + .../Include/Babylon/Plugins/NativeDawn.h | 28 + Plugins/NativeDawn/Source/ImageDecode.cpp | 114 + Plugins/NativeDawn/Source/NativeDawn.cpp | 2722 +++++++++++++++++ 16 files changed, 4003 insertions(+), 25 deletions(-) create mode 100644 Apps/Playground/Scripts/dawn_bootstrap.js create mode 100644 Plugins/NativeDawn/CMakeLists.txt create mode 100644 Plugins/NativeDawn/Include/Babylon/Plugins/NativeDawn.h create mode 100644 Plugins/NativeDawn/Source/ImageDecode.cpp create mode 100644 Plugins/NativeDawn/Source/NativeDawn.cpp diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 1a04d5ee3..a2e0ba2ad 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -9,6 +9,10 @@ on: js-engine: required: true type: string + nativedawn: + required: false + type: boolean + default: false env: NDK_VERSION: '28.2.13676358' @@ -40,6 +44,7 @@ jobs: fi - name: Build Playground ${{ inputs.js-engine }} + if: ${{ !inputs.nativedawn }} working-directory: Apps/Playground/Android run: | chmod +x gradlew @@ -48,3 +53,20 @@ jobs: -PARM64Only \ -PNDK_VERSION=${{ env.NDK_VERSION }} \ -PSANITIZERS=OFF + + # NativeDawn (WebGPU/Dawn, Vulkan backend) build: enabling the plugin + # force-disables NativeEngine. The Android app native lib has no Dawn code + # path, so build only the NativeDawn plugin target (via the -PNativeDawn + # gradle property, which also restricts the CMake `targets`) to validate it + # compiles/links; the full APK is not assembled. + - name: Build NativeDawn ${{ inputs.js-engine }} + if: ${{ inputs.nativedawn }} + working-directory: Apps/Playground/Android + run: | + chmod +x gradlew + ./gradlew :BabylonNative:externalNativeBuildRelease \ + -PJSEngine=${{ inputs.js-engine }} \ + -PARM64Only \ + -PNDK_VERSION=${{ env.NDK_VERSION }} \ + -PSANITIZERS=OFF \ + -PNativeDawn=ON diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 49d22f804..0b0a31397 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -14,6 +14,10 @@ on: required: false type: string default: macos-latest + nativedawn: + required: false + type: boolean + default: false # Absorb transient npm registry / CDN flakes during `npm install` in Apps/CMakeLists.txt. # npm's default fetch-retries is 2; bump to 5 (npm's own exponential backoff handles spacing). @@ -32,6 +36,7 @@ jobs: run: sudo xcode-select --switch /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer - name: Generate iOS solution + if: ${{ !inputs.nativedawn }} run: | cmake -G Xcode -B build/iOS \ -D IOS=ON \ @@ -40,6 +45,7 @@ jobs: -D CMAKE_IOS_INSTALL_COMBINED=NO - name: Build Playground iOS + if: ${{ !inputs.nativedawn }} run: | xcodebuild \ -project build/iOS/BabylonNative.xcodeproj \ @@ -47,3 +53,27 @@ jobs: -sdk iphoneos \ -configuration Release \ CODE_SIGNING_ALLOWED=NO + + # NativeDawn (WebGPU/Dawn, Metal backend) build: enabling the plugin + # force-disables NativeEngine. The iOS Playground host has no Dawn code + # path, so build the NativeDawn plugin target (plugin + Dawn) to validate it + # compiles/links. + - name: Generate iOS solution (NativeDawn) + if: ${{ inputs.nativedawn }} + run: | + cmake -G Xcode -B build/iOS \ + -D IOS=ON \ + -D DEPLOYMENT_TARGET=${{ inputs.deployment-target }} \ + -D BABYLON_DEBUG_TRACE=ON \ + -D CMAKE_IOS_INSTALL_COMBINED=NO \ + -D BABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON + + - name: Build NativeDawn iOS + if: ${{ inputs.nativedawn }} + run: | + xcodebuild \ + -project build/iOS/BabylonNative.xcodeproj \ + -scheme NativeDawn \ + -sdk iphoneos \ + -configuration Release \ + CODE_SIGNING_ALLOWED=NO diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index aa60abf74..367c0fed4 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -16,6 +16,10 @@ on: required: false type: boolean default: false + nativedawn: + required: false + type: boolean + default: false env: UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:symbolize=1 @@ -40,7 +44,12 @@ jobs: sudo apt-get update sudo apt-get install -y libjavascriptcoregtk-4.1-dev libgl1-mesa-dev libcurl4-openssl-dev libwayland-dev clang ninja-build + - name: Install Vulkan packages (NativeDawn) + if: ${{ inputs.nativedawn }} + run: sudo apt-get install -y libvulkan-dev libx11-xcb-dev + - name: Build X11 + if: ${{ !inputs.nativedawn }} run: | cmake -G Ninja -B build/Linux \ -D JAVASCRIPTCORE_LIBRARY=/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so \ @@ -53,30 +62,50 @@ jobs: -D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} . ninja -C build/Linux + # NativeDawn (WebGPU/Dawn, Vulkan backend) build: enabling the plugin + # force-disables NativeEngine. The Linux Playground host has no Dawn code + # path, so build the NativeDawn plugin target (plugin + Dawn) to validate it + # compiles/links; the bgfx validation/unit tests don't apply. + - name: Build NativeDawn + if: ${{ inputs.nativedawn }} + run: | + cmake -G Ninja -B build/Linux \ + -D JAVASCRIPTCORE_LIBRARY=/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so \ + -D NAPI_JAVASCRIPT_ENGINE=${{ inputs.js-engine }} \ + -D CMAKE_BUILD_TYPE=RelWithDebInfo \ + -D BX_CONFIG_DEBUG=ON \ + -D OpenGL_GL_PREFERENCE=GLVND \ + -D BABYLON_DEBUG_TRACE=ON \ + -D BABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON . + ninja -C build/Linux NativeDawn + - name: Enable Core Dump + if: ${{ !inputs.nativedawn }} run: sudo sysctl -w kernel.core_pattern=core.%p - name: Validation Tests + if: ${{ !inputs.nativedawn }} run: | cd build/Linux/Apps/Playground ulimit -c unlimited xvfb-run ./Playground app:///Scripts/validation_native.js - name: Unit Tests + if: ${{ !inputs.nativedawn }} run: | cd build/Linux/Apps/UnitTests ulimit -c unlimited xvfb-run ./UnitTests - name: Module Load Test - if: ${{ !inputs.enable-sanitizers }} + if: ${{ !inputs.enable-sanitizers && !inputs.nativedawn }} run: | cd build/Linux/Apps/ModuleLoadTest ulimit -c unlimited xvfb-run ./ModuleLoadTest - name: Upload Rendered Pictures - if: always() + if: ${{ always() && !inputs.nativedawn }} uses: actions/upload-artifact@v6 with: name: ${{ inputs.cc }}-${{ inputs.js-engine }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-rendered-pictures diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 8437034c5..078877e6c 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -23,6 +23,10 @@ on: required: false type: string default: '' + nativedawn: + required: false + type: boolean + default: false env: UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:symbolize=1 @@ -43,6 +47,7 @@ jobs: run: sudo xcode-select --switch /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer - name: Generate macOS solution + if: ${{ !inputs.nativedawn }} run: | cmake -G "${{ inputs.generator }}" -B build/macOS \ ${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \ @@ -51,25 +56,42 @@ jobs: -D BABYLON_NATIVE_TESTS_USE_NOOP_METAL_DEVICE=ON - name: Build Playground macOS + if: ${{ !inputs.nativedawn }} run: cmake --build build/macOS --target Playground --config RelWithDebInfo - name: Build UnitTests macOS + if: ${{ !inputs.nativedawn }} run: cmake --build build/macOS --target UnitTests --config RelWithDebInfo - name: Build ModuleLoadTest macOS + if: ${{ !inputs.nativedawn }} run: cmake --build build/macOS --target ModuleLoadTest --config RelWithDebInfo + # NativeDawn (WebGPU/Dawn, Metal backend) build: enabling the plugin + # force-disables NativeEngine. The macOS Playground host has no Dawn code + # path, so build the NativeDawn plugin target (plugin + Dawn) to validate it + # compiles/links; the bgfx unit tests don't apply. + - name: Build NativeDawn macOS + if: ${{ inputs.nativedawn }} + run: | + cmake -G "${{ inputs.generator }}" -B build/macOS \ + -D BABYLON_DEBUG_TRACE=ON \ + -D BABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON + cmake --build build/macOS --target NativeDawn --config RelWithDebInfo + - name: Enable Core Dump + if: ${{ !inputs.nativedawn }} run: sudo sysctl -w kern.corefile=%N.core.%P - name: Run UnitTests macOS + if: ${{ !inputs.nativedawn }} run: | cd build/macOS/Apps/UnitTests/RelWithDebInfo ulimit -c unlimited ./UnitTests - name: Run ModuleLoadTest macOS - if: ${{ !inputs.enable-sanitizers }} + if: ${{ !inputs.enable-sanitizers && !inputs.nativedawn }} run: | cd build/macOS/Apps/ModuleLoadTest/RelWithDebInfo ulimit -c unlimited diff --git a/.github/workflows/build-win32.yml b/.github/workflows/build-win32.yml index b73bd85bb..e35284fad 100644 --- a/.github/workflows/build-win32.yml +++ b/.github/workflows/build-win32.yml @@ -18,6 +18,10 @@ on: required: false type: boolean default: false + nativedawn: + required: false + type: boolean + default: false # Absorb transient npm registry / CDN flakes during `npm install` in Apps/CMakeLists.txt. # npm's default fetch-retries is 2; bump to 5 (npm's own exponential backoff handles spacing). @@ -55,6 +59,16 @@ jobs: echo "js_define=$JS_DEFINE" >> "$GITHUB_OUTPUT" echo "solution_name=Win32_${{ inputs.platform }}${SUFFIX}" >> "$GITHUB_OUTPUT" echo "sanitizer_flag=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }}" >> "$GITHUB_OUTPUT" + # NativeDawn (WebGPU/Dawn) build: enable the plugin (which force-disables + # NativeEngine) and build only the Playground target, since the + # bgfx-based validation/unit/module tests don't apply to this backend. + if [ "${{ inputs.nativedawn }}" = "true" ]; then + echo "dawn_flag=-D BABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON" >> "$GITHUB_OUTPUT" + echo "build_target=--target Playground" >> "$GITHUB_OUTPUT" + else + echo "dawn_flag=" >> "$GITHUB_OUTPUT" + echo "build_target=" >> "$GITHUB_OUTPUT" + fi - name: Generate solution shell: cmd @@ -63,6 +77,7 @@ jobs: -B build/${{ steps.vars.outputs.solution_name }} ^ -A ${{ inputs.platform }} ^ ${{ steps.vars.outputs.js_define }} ^ + ${{ steps.vars.outputs.dawn_flag }} ^ -D BX_CONFIG_DEBUG=ON ^ -D GRAPHICS_API=${{ inputs.graphics-api }} ^ -D BGFX_CONFIG_MAX_FRAME_BUFFERS=256 ^ @@ -72,7 +87,7 @@ jobs: - name: Build shell: cmd run: | - cmake --build build/${{ steps.vars.outputs.solution_name }} --config RelWithDebInfo -- /m + cmake --build build/${{ steps.vars.outputs.solution_name }} --config RelWithDebInfo ${{ steps.vars.outputs.build_target }} -- /m - name: Enable Crash Dumps shell: cmd @@ -104,14 +119,14 @@ jobs: ) - name: Validation Tests - if: ${{ inputs.graphics-api != 'D3D12' }} + if: ${{ inputs.graphics-api != 'D3D12' && !inputs.nativedawn }} shell: cmd run: | cd build\${{ steps.vars.outputs.solution_name }}\Apps\Playground\RelWithDebInfo Playground app:///Scripts/validation_native.js - name: Upload Rendered Pictures - if: ${{ inputs.graphics-api != 'D3D12' && !cancelled() }} + if: ${{ inputs.graphics-api != 'D3D12' && !inputs.nativedawn && !cancelled() }} uses: actions/upload-artifact@v6 with: name: ${{ steps.vars.outputs.solution_name }}-${{ inputs.graphics-api }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-rendered-pictures @@ -134,14 +149,14 @@ jobs: Copy-Item -Path "build\${{ steps.vars.outputs.solution_name }}\Apps\Playground\RelWithDebInfo\Playground.*" -Destination "$env:RUNNER_TEMP\Dumps\" -ErrorAction SilentlyContinue - name: Unit Tests - if: ${{ inputs.graphics-api != 'D3D12' }} + if: ${{ inputs.graphics-api != 'D3D12' && !inputs.nativedawn }} shell: cmd run: | cd build\${{ steps.vars.outputs.solution_name }}\Apps\UnitTests\RelWithDebInfo UnitTests - name: Module Load Test - if: ${{ !inputs.enable-sanitizers }} + if: ${{ !inputs.enable-sanitizers && !inputs.nativedawn }} shell: cmd run: | cd build\${{ steps.vars.outputs.solution_name }}\Apps\ModuleLoadTest\RelWithDebInfo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e11ed8fd5..0bd11e523 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,3 +198,40 @@ jobs: Win32_Installation: uses: ./.github/workflows/test-install-win32.yml + + # ── NativeDawn (WebGPU / Dawn backend, replaces NativeEngine) ── + # These build with BABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON, which force-disables + # NativeEngine. Dawn is fetched (git) only for these jobs. Win32 builds the + # full WebGPU Playground; the other platforms build the NativeDawn plugin + # target (plugin + Dawn) since only the Win32 host has a Dawn code path. + Win32_x64_NativeDawn: + uses: ./.github/workflows/build-win32.yml + with: + platform: x64 + nativedawn: true + + MacOS_NativeDawn: + uses: ./.github/workflows/build-macos.yml + with: + nativedawn: true + + iOS_NativeDawn: + uses: ./.github/workflows/build-ios.yml + with: + deployment-target: '18.0' + nativedawn: true + + Ubuntu_NativeDawn: + uses: ./.github/workflows/build-linux.yml + with: + cc: clang + cxx: clang++ + js-engine: JavaScriptCore + nativedawn: true + + Android_NativeDawn: + uses: ./.github/workflows/build-android.yml + with: + runs-on: ubuntu-latest + js-engine: V8 + nativedawn: true diff --git a/Apps/Playground/Android/BabylonNative/build.gradle b/Apps/Playground/Android/BabylonNative/build.gradle index 3f075645d..092663a5a 100644 --- a/Apps/Playground/Android/BabylonNative/build.gradle +++ b/Apps/Playground/Android/BabylonNative/build.gradle @@ -37,6 +37,15 @@ if (project.hasProperty("importHostCompilers")) { cmakeArguments.add("-DIMPORT_HOST_COMPILERS=${project.property('importHostCompilers')}") } +// NativeDawn (WebGPU/Dawn, Vulkan backend) build: enabling the plugin +// force-disables NativeEngine. The Android app native lib has no Dawn code +// path, so restrict the CMake build to the NativeDawn plugin target (see the +// `targets` call below) to validate it compiles/links. +def nativeDawn = project.hasProperty("NativeDawn") && project.property("NativeDawn") == "ON" +if (nativeDawn) { + cmakeArguments.add("-DBABYLON_NATIVE_PLUGIN_NATIVEDAWN=ON") +} + configurations { natives } android { @@ -56,6 +65,9 @@ android { cmake { abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64" arguments(*cmakeArguments) + if (nativeDawn) { + targets "NativeDawn" + } cppFlags += ["-Wno-deprecated-literal-operator"] } } diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index e24dd7be0..f4d144477 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -17,6 +17,7 @@ set(SCRIPTS "Scripts/experience.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" + "Scripts/dawn_bootstrap.js" "Scripts/config.json") set(SOURCES @@ -27,6 +28,16 @@ set(SOURCES "Shared/PlaygroundScripts.cpp" "Shared/PlaygroundScripts.h") +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN AND WIN32 AND NOT WINDOWS_STORE) + # PlaygroundScripts.cpp is Embedding-specific (LoadBootstrapScripts takes an + # Embedding::Runtime&). The NativeDawn host in Win32/App.cpp bypasses + # Embedding and loads the bootstrap scripts itself, so drop this source to + # avoid pulling the Embedding headers into the Dawn build. + list(REMOVE_ITEM SOURCES + "Shared/PlaygroundScripts.cpp" + "Shared/PlaygroundScripts.h") +endif() + if(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) if(IOS) @@ -144,26 +155,66 @@ endif() target_include_directories(Playground PRIVATE ".") -# The Embedding layer links and initializes the full canonical set of -# polyfills/plugins (Blob, Canvas, Console, File, GraphicsDevice, the -# Native* plugins, ScriptLoader, ShaderCache, the polyfills, etc.) and -# forwards them transitively, so the Playground only needs to list the -# libraries it consumes directly: -# - Embedding : the Runtime + View API the host is built on. -# - bx : used by Shared/Diagnostics.cpp. -# - TestUtils : used by Win32/X11 --test mode. -target_link_libraries(Playground - PRIVATE Embedding - PRIVATE bx - PRIVATE TestUtils - ${ADDITIONAL_LIBRARIES} - ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN AND WIN32 AND NOT WINDOWS_STORE) + # NativeDawn (WebGPU-via-Dawn) path. The Embedding layer's View always + # constructs the bgfx Graphics device, which is exactly what this backend + # replaces, so the Dawn host in Win32/App.cpp bypasses Embedding and drives + # a direct AppRuntime + ScriptLoader + NativeDawn pipeline instead. Link the + # polyfills/plugins that host consumes directly (Embedding used to forward + # them transitively). Only the Win32 host has a Dawn code path; other + # platforms keep the Embedding path even when the NativeDawn plugin is built. + target_compile_definitions(Playground PRIVATE BABYLON_NATIVE_PLUGIN_NATIVEDAWN=1) + target_link_libraries(Playground + PRIVATE AppRuntime + PRIVATE ScriptLoader + PRIVATE Console + PRIVATE Window + PRIVATE Performance + PRIVATE Scheduling + PRIVATE XMLHttpRequest + PRIVATE Fetch + PRIVATE Blob + PRIVATE File + PRIVATE TextDecoder + PRIVATE TextEncoder + PRIVATE AbortController + PRIVATE URL + PRIVATE NativeDawn + PRIVATE napi + PRIVATE bx + ${ADDITIONAL_LIBRARIES} + ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) +else() + # The Embedding layer links and initializes the full canonical set of + # polyfills/plugins (Blob, Canvas, Console, File, GraphicsDevice, the + # Native* plugins, ScriptLoader, ShaderCache, the polyfills, etc.) and + # forwards them transitively, so the Playground only needs to list the + # libraries it consumes directly: + # - Embedding : the Runtime + View API the host is built on. + # - bx : used by Shared/Diagnostics.cpp. + # - TestUtils : used by Win32/X11 --test mode. + target_link_libraries(Playground + PRIVATE Embedding + PRIVATE bx + PRIVATE TestUtils + ${ADDITIONAL_LIBRARIES} + ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) +endif() # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 # If we can set minimum required to 3.26+, then we can use the `copy -t` syntax instead. add_custom_command(TARGET Playground POST_BUILD COMMAND ${CMAKE_COMMAND} -E $>,copy,true> $ $ COMMAND_EXPAND_LISTS) +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN AND WIN32 AND EXISTS "$ENV{SystemRoot}/System32/d3dcompiler_47.dll") + # Dawn's D3D12 backend dynamically loads d3dcompiler_47.dll (FXC). The + # Windows SDK ships a stub placeholder on PATH, so copy the real System32 DLL + # beside the exe to guarantee RequestDevice succeeds. + add_custom_command(TARGET Playground POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$ENV{SystemRoot}/System32/d3dcompiler_47.dll" "$" + COMMENT "Copying d3dcompiler_47.dll for Dawn") +endif() + if(ANGLE_LIBEGL) add_custom_command(TARGET Playground POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ANGLE_LIBEGL}" "$" diff --git a/Apps/Playground/Scripts/dawn_bootstrap.js b/Apps/Playground/Scripts/dawn_bootstrap.js new file mode 100644 index 000000000..cb264d74d --- /dev/null +++ b/Apps/Playground/Scripts/dawn_bootstrap.js @@ -0,0 +1,359 @@ +// dawn_bootstrap.js — makes the default Playground scene scripts (experience.js +// and friends, written against BABYLON.NativeEngine) run on the Dawn/WebGPU +// backend provided by the NativeDawn plugin. +// +// Loaded by Win32/App.cpp (NativeDawn build) after babylon.max.js + addons. +// The native side installs navigator.gpu, _nativeDawnGetContext/Present/ +// SurfaceSize, _nativeDawnDecodeImage, _nativeDawnReadFileBytes, and the +// standard polyfills. This script: +// * installs the no-DOM canvas / image shims WebGPUEngine needs, +// * drives requestAnimationFrame from the native per-frame globalThis.frame(), +// * pre-creates and initializes a WebGPUEngine, then aliases +// BABYLON.NativeEngine so the scene scripts' synchronous +// `new BABYLON.NativeEngine()` returns the ready WebGPU engine, +// * reads the scene script (default experience.js) natively and evaluates it +// once the engine is ready. + +(function () { + "use strict"; + + function log() { + console.log("[dawn-bootstrap] " + Array.prototype.join.call(arguments, " ")); + } + + // ---- shared DOM event registry ------------------------------------------ + // Babylon's WebDeviceInputSystem attaches its pointer/keyboard listeners to + // whichever element engine.getInputElement() returns and (for some events) to + // window/document. To route native Win32 input regardless of which target and + // event-name prefix ("pointer" vs "mouse") Babylon picks, canvas, document and + // window all funnel their addEventListener calls into this single registry, + // and __dawnInput dispatches to it. + var g_evtListeners = {}; + function addShared(name, cb) { + if (typeof cb !== "function") { return; } + (g_evtListeners[name] = g_evtListeners[name] || []).push(cb); + } + function removeShared(name, cb) { + var a = g_evtListeners[name]; + if (!a) { return; } + var i = a.indexOf(cb); + if (i !== -1) { a.splice(i, 1); } + } + function dispatchShared(evt) { + var a = g_evtListeners[evt.type]; + if (!a) { return; } + var copy = a.slice(); + for (var i = 0; i < copy.length; ++i) { + try { copy[i].call(evt.currentTarget || null, evt); } + catch (e) { console.error("[dawn-bootstrap] listener error: " + (e && e.stack ? e.stack : e)); } + } + } + + // ---- no-DOM canvas / DOM shims ------------------------------------------ + function makeCanvas(width, height) { + var canvas = { + width: width, + height: height, + clientWidth: width, + clientHeight: height, + style: {}, + getContext: function (type) { + if (type === "webgpu") { + return _nativeDawnGetContext(); + } + return null; + }, + getBoundingClientRect: function () { + return { x: 0, y: 0, left: 0, top: 0, right: this.width, bottom: this.height, width: this.width, height: this.height }; + }, + setAttribute: function () {}, + removeAttribute: function () {}, + addEventListener: function (name, cb) { addShared(name, cb); }, + removeEventListener: function (name, cb) { removeShared(name, cb); }, + dispatchEvent: function (evt) { dispatchShared(evt); return true; }, + // Pointer-capture no-ops (Babylon's input system may call these). + setPointerCapture: function () {}, + releasePointerCapture: function () {}, + hasPointerCapture: function () { return false; }, + focus: function () {}, + getRootNode: function () { return this; }, + }; + return canvas; + } + + if (typeof globalThis.document === "undefined") { + globalThis.document = { + createElement: function (tag) { + if (tag === "canvas") { return makeCanvas(1280, 720); } + if (tag === "img") { return new globalThis.Image(); } + return { style: {}, setAttribute: function () {}, appendChild: function () {} }; + }, + addEventListener: function (name, cb) { addShared(name, cb); }, + removeEventListener: function (name, cb) { removeShared(name, cb); }, + getElementById: function () { return null; }, + body: { appendChild: function () {}, style: {} }, + }; + } + globalThis.addEventListener = function (name, cb) { addShared(name, cb); }; + globalThis.removeEventListener = function (name, cb) { removeShared(name, cb); }; + + if (typeof globalThis.location === "undefined") { + globalThis.location = { + href: "file:///", origin: "file://", protocol: "file:", host: "", hostname: "", + pathname: "/", search: "", hash: "", + }; + } + + // ---- image decoding via the native bimg decoder -------------------------- + (function installImageShim() { + var blobRegistry = {}; + var blobSeq = 0; + if (typeof URL !== "undefined") { + if (typeof URL.createObjectURL !== "function") { + URL.createObjectURL = function (blob) { + var id = "blob:nativedawn/" + (++blobSeq); + blobRegistry[id] = blob; + return id; + }; + } + if (typeof URL.revokeObjectURL !== "function") { + URL.revokeObjectURL = function (url) { delete blobRegistry[url]; }; + } + } + function toArrayBuffer(src) { + if (src instanceof ArrayBuffer) return Promise.resolve(src); + if (ArrayBuffer.isView(src)) return Promise.resolve(src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)); + if (src && typeof src.arrayBuffer === "function") return src.arrayBuffer(); + if (typeof src === "string") { + var blob = blobRegistry[src]; + if (blob) return toArrayBuffer(blob); + return fetch(src).then(function (r) { return r.arrayBuffer(); }); + } + return Promise.reject(new Error("toArrayBuffer: unsupported source")); + } + if (typeof globalThis.createImageBitmap === "undefined") { + globalThis.createImageBitmap = function (src) { + return Promise.resolve().then(function () { + if (src && src.__pixels) return src; + return toArrayBuffer(src).then(function (ab) { + if (!ab || ab.byteLength === 0) { + return { width: 1, height: 1, __pixels: new ArrayBuffer(4), close: function () {} }; + } + var bmp = _nativeDawnDecodeImage(ab); + bmp.close = function () {}; + return bmp; + }); + }); + }; + } + function ImageShim() { + var img = { + width: 0, height: 0, naturalWidth: 0, naturalHeight: 0, + __pixels: null, onload: null, onerror: null, crossOrigin: null, + complete: false, _src: "", + decode: function () { return Promise.resolve(); }, + addEventListener: function (name, cb) { if (name === "load") img.onload = cb; else if (name === "error") img.onerror = cb; }, + removeEventListener: function () {}, + }; + Object.defineProperty(img, "src", { + get: function () { return img._src; }, + set: function (v) { + img._src = v; + toArrayBuffer(v).then(function (ab) { + var d = _nativeDawnDecodeImage(ab); + img.width = img.naturalWidth = d.width; + img.height = img.naturalHeight = d.height; + img.__pixels = d.__pixels; + img.complete = true; + if (typeof img.onload === "function") img.onload({ target: img }); + }).catch(function (e) { + if (typeof img.onerror === "function") img.onerror(e); + }); + }, + }); + return img; + } + if (typeof globalThis.Image === "undefined") { globalThis.Image = ImageShim; } + if (typeof globalThis.HTMLImageElement === "undefined") { globalThis.HTMLImageElement = ImageShim; } + })(); + + // ---- requestAnimationFrame pump ------------------------------------------ + // WebGPUEngine.runRenderLoop schedules its frame via requestAnimationFrame. + // The native App.cpp frame loop calls globalThis.frame() once per iteration; + // we flush queued rAF callbacks (which run the engine's begin/render/end, + // submitting GPU work), then present. + var rafQueue = []; + globalThis.requestAnimationFrame = function (cb) { rafQueue.push(cb); return rafQueue.length; }; + globalThis.cancelAnimationFrame = function () {}; + globalThis.frame = function () { + var q = rafQueue; + rafQueue = []; + for (var i = 0; i < q.length; ++i) { + try { q[i](performance.now()); } + catch (e) { console.error("[dawn-bootstrap] rAF error: " + (e && e.stack ? e.stack : e)); } + } + if (typeof _nativeDawnPresent === "function") { _nativeDawnPresent(); } + }; + + // The Window polyfill provides `window` (addEventListener no-op, atob, ...) + // but not the timer / animation methods and its addEventListener drops + // handlers. The scene scripts call window.setTimeout / window.requestAnimation + // Frame, and Babylon's input system attaches some listeners to window; forward + // timers to the globals (setTimeout via the Scheduling polyfill) and route + // window/global event registration into the shared registry. + if (globalThis.window) { + var w = globalThis.window; + try { w.setTimeout = globalThis.setTimeout; } catch (e) {} + try { w.clearTimeout = globalThis.clearTimeout; } catch (e) {} + try { w.setInterval = globalThis.setInterval; } catch (e) {} + try { w.clearInterval = globalThis.clearInterval; } catch (e) {} + try { w.requestAnimationFrame = globalThis.requestAnimationFrame; } catch (e) {} + try { w.cancelAnimationFrame = globalThis.cancelAnimationFrame; } catch (e) {} + try { w.addEventListener = function (name, cb) { addShared(name, cb); }; } catch (e) {} + try { w.removeEventListener = function (name, cb) { removeShared(name, cb); }; } catch (e) {} + try { w.dispatchEvent = function (evt) { dispatchShared(evt); return true; }; } catch (e) {} + } + + // Advertise pointer/mouse/wheel event constructors. Babylon's + // Scene.simulatePointer* build events via `new PointerEvent(type, init)`, and + // its input system uses `window.PointerEvent` for feature detection. Provide + // real constructors that copy the init dictionary and expose the event API. + function makeEventClass() { + return function (type, init) { + init = init || {}; + this.type = type; + for (var k in init) { if (Object.prototype.hasOwnProperty.call(init, k)) { this[k] = init[k]; } } + if (typeof this.preventDefault !== "function") { this.preventDefault = function () {}; } + if (typeof this.stopPropagation !== "function") { this.stopPropagation = function () {}; } + if (typeof this.stopImmediatePropagation !== "function") { this.stopImmediatePropagation = function () {}; } + }; + } + globalThis.PointerEvent = makeEventClass(); + globalThis.MouseEvent = globalThis.MouseEvent || makeEventClass(); + globalThis.WheelEvent = globalThis.WheelEvent || makeEventClass(); + if (typeof globalThis.Event === "undefined") { globalThis.Event = makeEventClass(); } + if (globalThis.window) { + try { globalThis.window.PointerEvent = globalThis.PointerEvent; } catch (e) {} + try { globalThis.window.MouseEvent = globalThis.MouseEvent; } catch (e) {} + try { globalThis.window.WheelEvent = globalThis.WheelEvent; } catch (e) {} + } + + // ---- engine + scene load ------------------------------------------------- + var surf = (typeof _nativeDawnSurfaceSize === "function") ? _nativeDawnSurfaceSize() : { width: 1280, height: 720 }; + var canvas = makeCanvas(surf.width, surf.height); + globalThis.__dawnCanvas = canvas; + + log("creating WebGPUEngine " + surf.width + "x" + surf.height); + var engine = new BABYLON.WebGPUEngine(canvas, { + antialias: false, stencil: true, premultipliedAlpha: false, enableAllFeatures: false, + }); + engine.enableOfflineSupport = false; + engine.disableManifestCheck = true; + + // ---- native input + resize bridge --------------------------------------- + // Win32/App.cpp drains the message-thread pointer/resize queue each frame + // (on the JS thread) and calls these globals. Pointer input is injected + // through Scene.simulatePointer* which drives scene.onPointerObservable (and + // hence the camera controls) directly, independent of the DOM/device-input + // system selection (which doesn't attach in this no-NativeInput WebGPU host). + var pointerIdSeq = 1; + var lastX = 0; + var lastY = 0; + var PET = (typeof BABYLON !== "undefined" && BABYLON.PointerEventTypes) || {}; + function buildPointerEventInit(name, x, y, button, buttons, deltaY, movementX, movementY) { + return { + type: name, + clientX: x, clientY: y, + offsetX: x, offsetY: y, + x: x, y: y, pageX: x, pageY: y, screenX: x, screenY: y, + movementX: movementX, movementY: movementY, + pointerId: pointerIdSeq, + pointerType: "mouse", + button: button, + buttons: buttons, + deltaX: 0, deltaY: deltaY, deltaZ: 0, deltaMode: 0, + detail: 0, + ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, + preventDefault: function () {}, stopPropagation: function () {}, + stopImmediatePropagation: function () {}, + }; + } + globalThis.__dawnInput = function (type, x, y, button, buttons, deltaY) { + var sc = globalThis.__dawnEngine && globalThis.__dawnEngine.scenes && globalThis.__dawnEngine.scenes[0]; + if (!sc) { return; } + var movementX = x - lastX; + var movementY = y - lastY; + lastX = x; + lastY = y; + if (type === "pointerdown") { pointerIdSeq++; } + try { + var pick = new BABYLON.PickingInfo(); + if (type === "pointerdown") { + sc.simulatePointerDown(pick, buildPointerEventInit("pointerdown", x, y, button, buttons, deltaY, movementX, movementY)); + } else if (type === "pointermove") { + sc.simulatePointerMove(pick, buildPointerEventInit("pointermove", x, y, button, buttons, deltaY, movementX, movementY)); + } else if (type === "pointerup") { + sc.simulatePointerUp(pick, buildPointerEventInit("pointerup", x, y, button, buttons, deltaY, movementX, movementY)); + } else if (type === "wheel" && sc.onPointerObservable && PET.POINTERWHEEL !== undefined) { + var wevt = buildPointerEventInit("wheel", x, y, button, buttons, deltaY, movementX, movementY); + var pi = new BABYLON.PointerInfo(PET.POINTERWHEEL, wevt, pick); + sc.onPointerObservable.notifyObservers(pi, PET.POINTERWHEEL); + } + } catch (e) { + console.error("[dawn-bootstrap] input error: " + (e && e.stack ? e.stack : e)); + } + }; + + globalThis.__dawnResize = function (w, h) { + canvas.width = w; + canvas.height = h; + canvas.clientWidth = w; + canvas.clientHeight = h; + if (globalThis.__dawnEngine) { + try { globalThis.__dawnEngine.setSize(w, h, true); } + catch (e) { console.error("[dawn-bootstrap] setSize error: " + (e && e.stack ? e.stack : e)); } + } + // Some Babylon code listens for the window "resize" event to re-read size. + if (globalThis.window && typeof globalThis.window.dispatchEvent === "function") { + try { globalThis.window.dispatchEvent({ type: "resize" }); } catch (e) {} + } + }; + + function loadSceneSource() { + var path = globalThis.__sceneFsPath; + if (!path || typeof _nativeDawnReadFileBytes !== "function") { + return null; + } + var bytes = _nativeDawnReadFileBytes(path); + if (!bytes) { + return null; + } + return new TextDecoder("utf-8").decode(new Uint8Array(bytes)); + } + + engine.initAsync().then(function () { + globalThis.__dawnEngine = engine; + // The scene scripts construct their engine with `new BABYLON.NativeEngine()`; + // return the ready WebGPU engine instead so they run unmodified on WebGPU. + BABYLON.NativeEngine = function () { return globalThis.__dawnEngine; }; + log("WebGPUEngine ready (" + engine.getCaps().maxTextureSize + " max tex); running scene"); + var code = loadSceneSource(); + if (code === null) { + console.error("[dawn-bootstrap] could not read scene source at " + globalThis.__sceneFsPath); + return; + } + (0, eval)(code + "\n//# sourceURL=" + (globalThis.__sceneName || "scene.js") + "\n"); + + // Ensure each scene's InputManager is attached so simulatePointer* input + // (from __dawnInput) is processed. The scene scripts attach camera + // controls but don't necessarily call scene.attachControl(). + setTimeout(function () { + (engine.scenes || []).forEach(function (sc) { + try { if (sc.attachControl) { sc.attachControl(); } } + catch (e) { console.error("[dawn-bootstrap] attachControl error: " + e); } + }); + }, 0); + }).catch(function (e) { + console.error("[dawn-bootstrap] init/scene load failed: " + (e && e.stack ? e.stack : e)); + }); +})(); diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 1a3640d27..523b61943 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,8 +1,444 @@ // App.cpp : Defines the entry point for the application. // -// Built on Babylon::Embedding: the cross-platform Runtime + View API -// handles plugin/polyfill setup, GPU device construction, frame rendering, -// and input forwarding. +// Two mutually-exclusive build configurations, selected by the CMake option +// BABYLON_NATIVE_PLUGIN_NATIVEDAWN: +// +// * Default (bgfx / NativeEngine): built on Babylon::Embedding — the +// cross-platform Runtime + View API handles plugin/polyfill setup, GPU +// device construction, frame rendering, and input forwarding. +// +// * NativeDawn (WebGPU via Dawn): the Embedding View always constructs the +// bgfx Graphics device, which this backend replaces, so the Dawn host +// below bypasses Embedding and drives a direct AppRuntime + ScriptLoader + +// NativeDawn pipeline. It installs navigator.gpu, loads the same Babylon +// bootstrap scripts, then runs the default scene (experience.js) on +// WebGPU via dawn_bootstrap.js. + +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#pragma comment(lib, "Shlwapi.lib") +#pragma comment(lib, "Shell32.lib") + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + std::optional g_runtime; + std::optional g_loader; + std::atomic g_frameInFlight{false}; + HWND g_hwnd{}; + + // Win32 input + resize are produced on the host (message) thread and consumed + // on the JS thread once per frame. A small mutex-protected queue marshals + // pointer events; resize is coalesced to the latest pending size. + struct PointerEvent + { + std::string type; // "pointerdown" | "pointermove" | "pointerup" | "wheel" + int x = 0; + int y = 0; + int button = -1; // DOM button: 0 left, 1 middle, 2 right, -1 none + int buttons = 0; // DOM buttons bitmask: 1 left, 2 right, 4 middle + double deltaY = 0.0; + }; + std::mutex g_inputMutex; + std::vector g_pointerEvents; + bool g_resizePending = false; + uint32_t g_pendingWidth = 0; + uint32_t g_pendingHeight = 0; + + int DomButtonsFromWParam(WPARAM wParam) + { + int buttons = 0; + if (wParam & MK_LBUTTON) buttons |= 1; + if (wParam & MK_RBUTTON) buttons |= 2; + if (wParam & MK_MBUTTON) buttons |= 4; + return buttons; + } + + void PushPointerEvent(const char* type, int x, int y, int button, int buttons, double deltaY) + { + std::lock_guard lock{g_inputMutex}; + g_pointerEvents.push_back(PointerEvent{type, x, y, button, buttons, deltaY}); + } + + std::filesystem::path ExeDir() + { + wchar_t buf[MAX_PATH]{}; + ::GetModuleFileNameW(nullptr, buf, MAX_PATH); + return std::filesystem::path(buf).parent_path(); + } + + std::string FileUrl(const std::filesystem::path& path) + { + char url[2048]; + DWORD length = ARRAYSIZE(url); + if (FAILED(::UrlCreateFromPathA( + reinterpret_cast(path.u8string().c_str()), url, &length, 0))) + { + return {}; + } + return std::string(url, length); + } + + // JSON/JS-escape a Windows filesystem path (backslashes) so it can be + // embedded in a double-quoted JS string literal. + std::string JsEscapePath(const std::filesystem::path& path) + { + std::string s = path.string(); + std::string out; + for (char c : s) + { + if (c == '\\' || c == '"') out += '\\'; + out += c; + } + return out; + } + + LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) + { + switch (message) + { + case WM_DESTROY: + ::PostQuitMessage(0); + return 0; + + case WM_SIZE: + { + if (wParam != SIZE_MINIMIZED) + { + std::lock_guard lock{g_inputMutex}; + g_pendingWidth = static_cast(LOWORD(lParam)); + g_pendingHeight = static_cast(HIWORD(lParam)); + g_resizePending = true; + } + return 0; + } + + case WM_MOUSEMOVE: + PushPointerEvent("pointermove", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + -1, DomButtonsFromWParam(wParam), 0.0); + return 0; + + case WM_LBUTTONDOWN: + ::SetCapture(hWnd); + PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 0, DomButtonsFromWParam(wParam), 0.0); + return 0; + case WM_LBUTTONUP: + ::ReleaseCapture(); + PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 0, DomButtonsFromWParam(wParam), 0.0); + return 0; + + case WM_RBUTTONDOWN: + ::SetCapture(hWnd); + PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 2, DomButtonsFromWParam(wParam), 0.0); + return 0; + case WM_RBUTTONUP: + ::ReleaseCapture(); + PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 2, DomButtonsFromWParam(wParam), 0.0); + return 0; + + case WM_MBUTTONDOWN: + ::SetCapture(hWnd); + PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 1, DomButtonsFromWParam(wParam), 0.0); + return 0; + case WM_MBUTTONUP: + ::ReleaseCapture(); + PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), + 1, DomButtonsFromWParam(wParam), 0.0); + return 0; + + case WM_MOUSEWHEEL: + { + // Wheel coords are in screen space; convert to client space. + POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + ::ScreenToClient(hWnd, &pt); + const int delta = GET_WHEEL_DELTA_WPARAM(wParam); + // DOM wheel deltaY is positive when scrolling down (away from user), + // opposite sign to Win32's WHEEL_DELTA. + PushPointerEvent("wheel", pt.x, pt.y, -1, 0, -static_cast(delta)); + return 0; + } + + default: + return ::DefWindowProcW(hWnd, message, wParam, lParam); + } + } +} + +int APIENTRY wWinMain(_In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE, + _In_ LPWSTR, + _In_ int nCmdShow) +{ + ::SetConsoleOutputCP(CP_UTF8); + std::setvbuf(stdout, nullptr, _IONBF, 0); + std::setvbuf(stderr, nullptr, _IONBF, 0); + + Diagnostics::Initialize(); + std::fprintf(stderr, "[Playground] starting (NativeDawn / WebGPU, no bgfx)\n"); + + // Window. + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = hInstance; + wc.hCursor = ::LoadCursor(nullptr, IDC_ARROW); + wc.lpszClassName = L"PlaygroundDawnWindow"; + ::RegisterClassExW(&wc); + + const uint32_t width = 1280; + const uint32_t height = 720; + g_hwnd = ::CreateWindowW(wc.lpszClassName, L"BabylonNative Playground (NativeDawn / WebGPU)", + WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, + static_cast(width), static_cast(height), + nullptr, nullptr, hInstance, nullptr); + if (!g_hwnd) + { + std::fprintf(stderr, "[Playground] CreateWindow failed\n"); + return 1; + } + ::ShowWindow(g_hwnd, nCmdShow); + ::UpdateWindow(g_hwnd); + + RECT rc{}; + ::GetClientRect(g_hwnd, &rc); + const uint32_t clientW = static_cast(rc.right - rc.left); + const uint32_t clientH = static_cast(rc.bottom - rc.top); + + // Runtime (V8). Uncaught JS exceptions go to the diagnostics banner. + Babylon::AppRuntime::Options runtimeOptions{}; + runtimeOptions.UnhandledExceptionHandler = [](const Napi::Error& error) { + const std::string message = Napi::GetErrorString(error); + Diagnostics::DumpFailure("UNCAUGHT JS ERROR", nullptr, 0, 0, "%s", message.c_str()); + Diagnostics::SetExitCode(1); + Diagnostics::PrintFinishLine(); + std::quick_exit(1); + }; + g_runtime.emplace(std::move(runtimeOptions)); + g_loader.emplace(*g_runtime); + + // Install polyfills + NativeDawn on the JS thread (the Dawn device is + // created here, on the JS thread, so all later Dawn calls stay on it). + void* hwnd = g_hwnd; + g_loader->Dispatch([hwnd, clientW, clientH](Napi::Env env) { + // The JS thread needs a properly winrt-initialized apartment for + // UrlLib's XMLHttpRequest backend (Windows.Foundation.Uri / HTTP); + // the bare CoInitializeEx done by AppRuntime is not sufficient. + try + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + } + catch (const winrt::hresult_error&) + { + } + + Babylon::Polyfills::Console::Initialize(env, + [](const char* message, Babylon::Polyfills::Console::LogLevel level) { + std::FILE* out = level == Babylon::Polyfills::Console::LogLevel::Error ? stderr : stdout; + std::fprintf(out, "%s", message ? message : ""); + }); + // Pre-define devicePixelRatio so the Window polyfill's Graphics(bgfx)- + // backed accessor isn't the only source (it throws without a device). + env.Global().Set("devicePixelRatio", Napi::Number::New(env, 1)); + Babylon::Polyfills::Window::Initialize(env); + Babylon::Polyfills::Scheduling::Initialize(env); + Babylon::Polyfills::Performance::Initialize(env); + Babylon::Polyfills::TextDecoder::Initialize(env); + Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::Blob::Initialize(env); + Babylon::Polyfills::File::Initialize(env); + Babylon::Polyfills::URL::Initialize(env); + Babylon::Polyfills::AbortController::Initialize(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); + Babylon::Polyfills::Fetch::Initialize(env); + Babylon::Plugins::NativeDawn::Initialize(env, hwnd, clientW, clientH); + }); + + // Resolve the scene script: default experience.js, or the first command-line + // argument (a filesystem path) if provided. + const std::filesystem::path scripts = ExeDir() / "Scripts"; + std::filesystem::path scenePath = scripts / "experience.js"; + { + int argc = 0; + LPWSTR* argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv != nullptr) + { + if (argc > 1) + { + std::filesystem::path arg(argv[1]); + scenePath = arg.is_relative() ? (scripts / arg.filename()) : arg; + } + ::LocalFree(argv); + } + } + + // Load the Babylon bootstrap scripts (same set as the Embedding Playground), + // skipping any that aren't staged. recast.js is intentionally omitted (asm.js + // incompatible with v8jsi). + const char* bootstrap[] = { + "ammo.js", + "babylon.max.js", + "babylonjs.addons.js", + "babylonjs.loaders.js", + "babylonjs.materials.js", + "babylon.gui.js", + "meshwriter.min.js", + "babylonjs.serializers.js", + }; + for (const char* name : bootstrap) + { + const std::filesystem::path p = scripts / name; + if (std::filesystem::exists(p)) + { + g_loader->LoadScript(FileUrl(p)); + } + } + + // Expose roots + the scene path (read natively by dawn_bootstrap.js so it can + // eval the scene once the WebGPU engine is ready). + { + std::string scriptsRoot = FileUrl(scripts); + if (!scriptsRoot.empty() && scriptsRoot.back() != '/') scriptsRoot += '/'; + std::string js = "globalThis.__scriptsRoot = \"" + scriptsRoot + "\";\n" + "globalThis.__sceneFsPath = \"" + JsEscapePath(scenePath) + "\";\n" + "globalThis.__sceneName = \"" + scenePath.filename().string() + "\";\n"; + g_loader->Eval(js, "Playground-dawn-roots.js"); + } + + // dawn_bootstrap.js pre-inits WebGPUEngine, aliases BABYLON.NativeEngine, and + // evaluates the scene script once ready. + g_loader->LoadScript(FileUrl(scripts / "dawn_bootstrap.js")); + + // Main loop: pump Win32 messages and dispatch one JS frame at a time + // (throttled so the JS queue doesn't flood). The frame callback runs on the + // JS thread, calls the JS frame() (which flushes requestAnimationFrame + // callbacks and presents) and ticks Dawn. + MSG msg{}; + while (msg.message != WM_QUIT) + { + if (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + continue; + } + + if (!g_frameInFlight.exchange(true)) + { + g_loader->Dispatch([](Napi::Env env) { + // Apply a coalesced resize (from WM_SIZE) before rendering so the + // Dawn swapchain and the engine's back buffer match this frame. + bool resized = false; + uint32_t newW = 0; + uint32_t newH = 0; + std::vector events; + { + std::lock_guard lock{g_inputMutex}; + if (g_resizePending) + { + g_resizePending = false; + resized = true; + newW = g_pendingWidth; + newH = g_pendingHeight; + } + events.swap(g_pointerEvents); + } + + if (resized) + { + Babylon::Plugins::NativeDawn::ResizeSurface(newW, newH); + Napi::Value resizeFn = env.Global().Get("__dawnResize"); + if (resizeFn.IsFunction()) + { + resizeFn.As().Call({ + Napi::Number::New(env, newW), + Napi::Number::New(env, newH)}); + } + } + + if (!events.empty()) + { + Napi::Value inputFn = env.Global().Get("__dawnInput"); + if (inputFn.IsFunction()) + { + Napi::Function fn = inputFn.As(); + for (const PointerEvent& e : events) + { + fn.Call({ + Napi::String::New(env, e.type), + Napi::Number::New(env, e.x), + Napi::Number::New(env, e.y), + Napi::Number::New(env, e.button), + Napi::Number::New(env, e.buttons), + Napi::Number::New(env, e.deltaY)}); + } + } + } + + Napi::Value frame = env.Global().Get("frame"); + if (frame.IsFunction()) + { + frame.As().Call({}); + } + Babylon::Plugins::NativeDawn::Tick(env); + g_frameInFlight.store(false); + }); + } + else + { + ::Sleep(1); + } + } + + g_loader.reset(); + g_runtime.reset(); + Diagnostics::SetExitCode(static_cast(msg.wParam)); + Diagnostics::PrintFinishLine(); + return static_cast(msg.wParam); +} + +#else // BABYLON_NATIVE_PLUGIN_NATIVEDAWN #include "App.h" @@ -558,3 +994,5 @@ INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) } return (INT_PTR)FALSE; } + +#endif // BABYLON_NATIVE_PLUGIN_NATIVEDAWN diff --git a/CMakeLists.txt b/CMakeLists.txt index d37aac35e..2dc8cdc7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,14 @@ FetchContent_Declare(CMakeExtensions GIT_REPOSITORY https://github.com/BabylonJS/CMakeExtensions.git GIT_TAG 631780e42886e5f12bfd1a5568c7395f1d657f43 EXCLUDE_FROM_ALL) +# Dawn (WebGPU) — only populated when BABYLON_NATIVE_PLUGIN_NATIVEDAWN is ON +# (see the guarded FetchContent_MakeAvailable below). Declaring it here is free; +# no clone happens unless the plugin is enabled. +FetchContent_Declare(dawn + GIT_REPOSITORY https://github.com/google/dawn.git + GIT_TAG a44d7a3d78f23c680491c0fc04f53a1df62e02ff + GIT_SHALLOW TRUE + EXCLUDE_FROM_ALL) FetchContent_Declare(glslang GIT_REPOSITORY https://github.com/BabylonJS/glslang.git GIT_TAG 284e4301e5a6b44b279635276588db7cdd942624 @@ -117,6 +125,7 @@ option(BABYLON_NATIVE_CHECK_THREAD_AFFINITY "Checks thread safety in the graphic option(BABYLON_NATIVE_PLUGIN_EXTERNALTEXTURE "Include Babylon Native Plugin ExternalTexture." ON) option(BABYLON_NATIVE_PLUGIN_NATIVECAMERA "Include Babylon Native Plugin NativeCamera." ON) option(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE "Include Babylon Native Plugin NativeCapture." ON) +option(BABYLON_NATIVE_PLUGIN_NATIVEDAWN "Include experimental Babylon Native Plugin NativeDawn (WebGPU via Dawn)." OFF) option(BABYLON_NATIVE_PLUGIN_NATIVEENCODING "Include Babylon Native Plugin NativeEncoding." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE "Include Babylon Native Plugin NativeEngine." ON) option(BABYLON_NATIVE_PLUGIN_NATIVEENGINE_WEBP "Enable WebP image parsing in NativeEngine (via bimg)." ON) @@ -143,6 +152,17 @@ option(BABYLON_NATIVE_POLYFILL_URL "Include Babylon Native Polyfill URL." ON) option(BABYLON_NATIVE_POLYFILL_WEBSOCKET "Include Babylon Native Polyfill WebSocket." ON) option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) +# NativeDawn (experimental WebGPU-via-Dawn backend) and NativeEngine (bgfx +# backend) are mutually exclusive: they are two implementations of the same +# rendering role. Enabling NativeDawn forces NativeEngine off so the Playground +# (and Embedding layer) install exactly one graphics backend / _native engine. +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) + if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) + message(STATUS "BABYLON_NATIVE_PLUGIN_NATIVEDAWN is ON: forcing BABYLON_NATIVE_PLUGIN_NATIVEENGINE OFF (mutually exclusive backends).") + endif() + set(BABYLON_NATIVE_PLUGIN_NATIVEENGINE OFF CACHE BOOL "Include Babylon Native Plugin NativeEngine." FORCE) +endif() + # Embedding option(BABYLON_NATIVE_EMBEDDING "Build the cross-platform Babylon::Embedding facade (Runtime + View)." ON) option(BABYLON_NATIVE_EMBEDDING_ANDROID "Build the Android JNI interop layer for Babylon::Embedding." OFF) @@ -304,6 +324,56 @@ if(BABYLON_DEBUG_TRACE) endif() add_subdirectory(Dependencies) + +# ---- Experimental NativeDawn plugin: pull in Dawn (WebGPU) ---- +# Dawn is a large dependency, so it is only fetched + built when NativeDawn is +# enabled. It is declared (git) alongside the other dependencies above but only +# populated here, inside this guard, so builds without +# BABYLON_NATIVE_PLUGIN_NATIVEDAWN never clone Dawn. +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) + set(DAWN_FETCH_DEPENDENCIES ON CACHE BOOL "" FORCE) + set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_CMD_TOOLS OFF CACHE BOOL "" FORCE) + + # Build exactly one GPU backend per platform, with the matching Tint shader + # writer. The WGSL reader (input) is always required; the SPIR-V/GLSL readers, + # the WGSL writer and the IR-binary/GLSL-validator are never needed here. + set(TINT_BUILD_SPV_READER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_WGSL_READER ON CACHE BOOL "" FORCE) + set(TINT_BUILD_WGSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_GLSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_GLSL_VALIDATOR OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_IR_BINARY OFF CACHE BOOL "" FORCE) + + # Start with every GPU backend + shader writer off; enable one set per platform. + set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_MSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_SPV_WRITER OFF CACHE BOOL "" FORCE) + + if(WIN32) + # Windows: Direct3D 12 (HLSL). + set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) + set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE) + elseif(APPLE) + # macOS / iOS: Metal (MSL). + set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE) + set(TINT_BUILD_MSL_WRITER ON CACHE BOOL "" FORCE) + else() + # Android / Linux: Vulkan (SPIR-V). + set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE) + set(TINT_BUILD_SPV_WRITER ON CACHE BOOL "" FORCE) + endif() + + FetchContent_MakeAvailable_With_Message(dawn) +endif() + add_subdirectory(Core) add_subdirectory(Plugins) add_subdirectory(Polyfills) diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index 38b7aaa18..8782895e9 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -10,6 +10,10 @@ if(BABYLON_NATIVE_PLUGIN_NATIVECAPTURE) add_subdirectory(NativeCapture) endif() +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) + add_subdirectory(NativeDawn) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEENCODING) add_subdirectory(NativeEncoding) endif() diff --git a/Plugins/NativeDawn/CMakeLists.txt b/Plugins/NativeDawn/CMakeLists.txt new file mode 100644 index 000000000..c8a60c331 --- /dev/null +++ b/Plugins/NativeDawn/CMakeLists.txt @@ -0,0 +1,25 @@ +set(SOURCES + "Include/Babylon/Plugins/NativeDawn.h" + "Source/ImageDecode.cpp" + "Source/NativeDawn.cpp") + +add_library(NativeDawn ${SOURCES}) + +target_include_directories(NativeDawn + PUBLIC "Include") + +target_link_libraries(NativeDawn + PUBLIC napi + PRIVATE JsRuntime + PRIVATE webgpu_dawn + PRIVATE bx + PRIVATE bimg + PRIVATE bimg_decode) + +if(WIN32) + target_compile_definitions(NativeDawn + PRIVATE NOMINMAX) +endif() + +set_property(TARGET NativeDawn PROPERTY FOLDER Plugins) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Plugins/NativeDawn/Include/Babylon/Plugins/NativeDawn.h b/Plugins/NativeDawn/Include/Babylon/Plugins/NativeDawn.h new file mode 100644 index 000000000..adb627d29 --- /dev/null +++ b/Plugins/NativeDawn/Include/Babylon/Plugins/NativeDawn.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +namespace Babylon::Plugins::NativeDawn +{ + // Initializes the NativeDawn plugin: creates a Dawn (WebGPU) instance, + // adapter, device and a surface bound to the given native window, then + // installs `navigator.gpu` and the WebGPU global objects into `env`. + // + // This is the clean-room replacement for the bgfx NativeEngine path: all + // rendering goes through Dawn, no bgfx. Win32 only for now (`window` is an + // HWND). + void Initialize(Napi::Env env, void* window, uint32_t width, uint32_t height); + + // Reconfigures the Dawn surface (and cached drawing-buffer size) to the given + // dimensions. Must be called on the JS thread (where the Dawn device lives), + // e.g. from the app's per-frame dispatch when the native window has resized. + // Does not touch the native window itself. + void ResizeSurface(uint32_t width, uint32_t height); + + // Drives one host-side frame: ticks the Dawn device (callbacks) and, if the + // JS side has rendered into the current surface texture, presents it. Call + // once per native frame from the app loop. + void Tick(Napi::Env env); +} diff --git a/Plugins/NativeDawn/Source/ImageDecode.cpp b/Plugins/NativeDawn/Source/ImageDecode.cpp new file mode 100644 index 000000000..a9f8af73a --- /dev/null +++ b/Plugins/NativeDawn/Source/ImageDecode.cpp @@ -0,0 +1,114 @@ +// Native image decoder for NativeDawn, backed by bimg (bgfx's image library). +// Decodes PNG/JPEG/etc. encoded bytes to tightly packed 32bpp RGBA so glTF +// embedded textures work in this headless (no-DOM) environment. bimg is the +// same decoder the bgfx-based NativeEngine uses, so behaviour matches the rest +// of Babylon Native (grayscale unpacking, etc.). + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Babylon::Plugins::NativeDawn +{ + namespace + { + bx::AllocatorI& Allocator() + { + static bx::DefaultAllocator s_allocator; + return s_allocator; + } + + // Grayscale textures come back from bimg as R8/RG8. Expand them so RGB is + // the grayscale value and A is either opaque (R8) or the second channel + // (RG8), matching NativeEngine's behaviour. + void UnpackGrayscale(const bimg::ImageContainer& image, std::vector& out) + { + const uint32_t pixels = image.m_width * image.m_height; + const bool hasAlpha = image.m_format == bimg::TextureFormat::RG8; + const uint32_t srcBpp = hasAlpha ? 2u : 1u; + const auto* src = static_cast(image.m_data); + out.resize(static_cast(pixels) * 4u); + for (uint32_t i = 0; i < pixels; ++i) + { + const uint8_t gray = src[i * srcBpp]; + uint8_t* dst = out.data() + static_cast(i) * 4u; + dst[0] = dst[1] = dst[2] = gray; + dst[3] = hasAlpha ? src[i * srcBpp + 1u] : 0xFFu; + } + } + } + + // Decodes an encoded image to tightly-packed RGBA8 via bimg. Returns false on + // failure. out is resized to width*height*4 on success. + bool DecodeRGBA(const uint8_t* data, size_t size, std::vector& out, int& width, int& height) + { + out.clear(); + width = 0; + height = 0; + if (data == nullptr || size == 0) + { + return false; + } + + // ErrorIgnore keeps bimg from tripping its internal BX_ERROR_SCOPE assert + // on unrecognized/corrupt input; it returns nullptr instead. + bx::ErrorIgnore err; + bimg::ImageContainer* image = bimg::imageParse( + &Allocator(), data, static_cast(size), bimg::TextureFormat::Count, &err); + if (image == nullptr) + { + std::fprintf(stderr, "[NativeDawn][bimg] imageParse failed\n"); + return false; + } + + bool ok = true; + switch (image->m_format) + { + case bimg::TextureFormat::RGBA8: + { + out.resize(image->m_size); + std::memcpy(out.data(), image->m_data, image->m_size); + break; + } + case bimg::TextureFormat::R8: + case bimg::TextureFormat::RG8: + { + UnpackGrayscale(*image, out); + break; + } + default: + { + // Convert any other decoded format (RGB8, BGRA8, float, ...) to RGBA8. + bimg::ImageContainer* rgba = bimg::imageConvert( + &Allocator(), bimg::TextureFormat::RGBA8, *image, false); + if (rgba == nullptr) + { + std::fprintf(stderr, "[NativeDawn][bimg] imageConvert to RGBA8 failed (format=%d)\n", + static_cast(image->m_format)); + ok = false; + } + else + { + out.resize(rgba->m_size); + std::memcpy(out.data(), rgba->m_data, rgba->m_size); + bimg::imageFree(rgba); + } + break; + } + } + + if (ok) + { + width = static_cast(image->m_width); + height = static_cast(image->m_height); + } + bimg::imageFree(image); + return ok; + } +} diff --git a/Plugins/NativeDawn/Source/NativeDawn.cpp b/Plugins/NativeDawn/Source/NativeDawn.cpp new file mode 100644 index 000000000..bbd33cd65 --- /dev/null +++ b/Plugins/NativeDawn/Source/NativeDawn.cpp @@ -0,0 +1,2722 @@ +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Babylon::Plugins::NativeDawn +{ + // Defined in ImageDecode.cpp (bimg): encoded image bytes -> RGBA8. + bool DecodeRGBA(const uint8_t* data, size_t size, std::vector& out, int& width, int& height); + + namespace + { + // Global Dawn state for the experiment. Single window / single device. + struct State + { + wgpu::Instance instance; + wgpu::Adapter adapter; + wgpu::Device device; + wgpu::Queue queue; + wgpu::Surface surface; + wgpu::TextureFormat surfaceFormat = wgpu::TextureFormat::BGRA8Unorm; + wgpu::Texture currentSurfaceTexture; + void* hwnd = nullptr; + uint32_t width = 0; + uint32_t height = 0; + bool ready = false; + }; + + State g_state; + + void LogDeviceError(const wgpu::Device&, wgpu::ErrorType type, wgpu::StringView message) + { + std::fprintf(stderr, "[NativeDawn] device error (%d): %.*s\n", + static_cast(type), static_cast(message.length), message.data); + } + + bool CreateDeviceAndSurface(void* window, uint32_t width, uint32_t height) + { + g_state.hwnd = window; + g_state.width = width; + g_state.height = height; + + // Instance with synchronous WaitAny support. + static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; + wgpu::InstanceDescriptor instDesc{}; + instDesc.requiredFeatureCount = 1; + instDesc.requiredFeatures = &kTimedWaitAny; + g_state.instance = wgpu::CreateInstance(&instDesc); + if (!g_state.instance) + { + std::fprintf(stderr, "[NativeDawn] CreateInstance failed\n"); + return false; + } + + // Adapter (high performance). + wgpu::RequestAdapterOptions adapterOpts{}; + adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance; + wgpu::Future af = g_state.instance.RequestAdapter(&adapterOpts, wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message, wgpu::Adapter* out) { + if (status == wgpu::RequestAdapterStatus::Success) + { + *out = std::move(adapter); + } + else + { + std::fprintf(stderr, "[NativeDawn] RequestAdapter failed: %.*s\n", + static_cast(message.length), message.data); + } + }, + &g_state.adapter); + g_state.instance.WaitAny(af, UINT64_MAX); + if (!g_state.adapter) + { + return false; + } + + // Device. + wgpu::DeviceDescriptor devDesc{}; + devDesc.SetUncapturedErrorCallback(&LogDeviceError); + wgpu::Future df = g_state.adapter.RequestDevice(&devDesc, wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::RequestDeviceStatus status, wgpu::Device device, wgpu::StringView message, wgpu::Device* out) { + if (status == wgpu::RequestDeviceStatus::Success) + { + *out = std::move(device); + } + else + { + std::fprintf(stderr, "[NativeDawn] RequestDevice failed: %.*s\n", + static_cast(message.length), message.data); + } + }, + &g_state.device); + g_state.instance.WaitAny(df, UINT64_MAX); + if (!g_state.device) + { + return false; + } + g_state.queue = g_state.device.GetQueue(); + + // Surface from the native window (Win32 HWND). +#if defined(_WIN32) + wgpu::SurfaceSourceWindowsHWND chained{}; + chained.hwnd = window; + chained.hinstance = ::GetModuleHandle(nullptr); + wgpu::SurfaceDescriptor surfDesc{}; + surfDesc.nextInChain = &chained; + g_state.surface = g_state.instance.CreateSurface(&surfDesc); +#endif + if (!g_state.surface) + { + std::fprintf(stderr, "[NativeDawn] CreateSurface failed\n"); + return false; + } + + // Pick the preferred format and configure. + wgpu::SurfaceCapabilities caps{}; + g_state.surface.GetCapabilities(g_state.adapter, &caps); + if (caps.formatCount > 0) + { + g_state.surfaceFormat = caps.formats[0]; + } + + wgpu::SurfaceConfiguration cfg{}; + cfg.device = g_state.device; + cfg.format = g_state.surfaceFormat; + cfg.usage = wgpu::TextureUsage::RenderAttachment; + cfg.width = width; + cfg.height = height; + cfg.presentMode = wgpu::PresentMode::Fifo; + g_state.surface.Configure(&cfg); + + g_state.ready = true; + std::fprintf(stderr, "[NativeDawn] Dawn device + surface ready (%ux%u, format=%d)\n", + width, height, static_cast(g_state.surfaceFormat)); + return true; + } + + // Milestone test: clear the surface to a solid color via Dawn, no bgfx. + void ClearToColor(float r, float g, float b) + { + if (!g_state.ready) + { + return; + } + + wgpu::SurfaceTexture st{}; + g_state.surface.GetCurrentTexture(&st); + if (!st.texture) + { + std::fprintf(stderr, "[NativeDawn] GetCurrentTexture: null\n"); + return; + } + + wgpu::TextureView view = st.texture.CreateView(); + + wgpu::RenderPassColorAttachment color{}; + color.view = view; + color.loadOp = wgpu::LoadOp::Clear; + color.storeOp = wgpu::StoreOp::Store; + color.clearValue = {r, g, b, 1.0f}; + + wgpu::RenderPassDescriptor passDesc{}; + passDesc.colorAttachmentCount = 1; + passDesc.colorAttachments = &color; + + wgpu::CommandEncoder encoder = g_state.device.CreateCommandEncoder(); + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); + pass.End(); + wgpu::CommandBuffer commands = encoder.Finish(); + g_state.queue.Submit(1, &commands); + g_state.surface.Present(); + } + } + + // ========================================================================= + // WebGPU (navigator.gpu) bindings implemented over Dawn (wgpu C++). + // + // Each GPU object is a plain Napi::Object carrying a hidden "_h" External + // wrapping the wgpu handle and a "_type" tag string. Methods are attached as + // Napi::Functions; they capture the owning handle by value (all wgpu methods + // are const) and read handles out of arguments via GetH(). Promises + // resolve synchronously since the Dawn adapter/device already exist. + // ========================================================================= + namespace + { + // ---- small JS helpers ------------------------------------------------ + inline bool IsNullish(Napi::Value v) { return v.IsUndefined() || v.IsNull(); } + + std::string SvToStr(const wgpu::StringView& sv) + { + if (sv.data == nullptr) return {}; + if (sv.length == static_cast(-1)) return std::string(sv.data); + return std::string(sv.data, sv.length); + } + + template + void SetMethod(Napi::Object obj, const char* name, Fn&& fn) + { + obj.Set(name, Napi::Function::New(obj.Env(), std::forward(fn), name)); + } + + Napi::Object NewGPUObject(Napi::Env env, const char* type) + { + Napi::Object o = Napi::Object::New(env); + o.Set("_type", Napi::String::New(env, type)); + return o; + } + + template + Napi::External MakeExt(Napi::Env env, const T& h) + { + return Napi::External::New(env, new T(h), [](Napi::Env, T* p) { delete p; }); + } + + template + void SetHandle(Napi::Object o, const T& h) + { + o.Set("_h", MakeExt(o.Env(), h)); + } + + template + T* GetH(Napi::Value v) + { + if (!v.IsObject()) return nullptr; + Napi::Object o = v.As(); + if (!o.Has("_h")) return nullptr; + Napi::Value h = o.Get("_h"); + if (!h.IsExternal()) return nullptr; + return h.As>().Data(); + } + + std::string TypeTag(Napi::Value v) + { + if (!v.IsObject()) return {}; + Napi::Object o = v.As(); + if (!o.Has("_type")) return {}; + Napi::Value t = o.Get("_type"); + return t.IsString() ? t.As().Utf8Value() : std::string{}; + } + + // ---- property readers ------------------------------------------------ + std::string PropStr(Napi::Object o, const char* k) + { + if (!o.Has(k)) return {}; + Napi::Value v = o.Get(k); + if (!v.IsString()) return {}; + return v.As().Utf8Value(); + } + uint32_t PropU32(Napi::Object o, const char* k, uint32_t def) + { + if (!o.Has(k)) return def; + Napi::Value v = o.Get(k); + if (!v.IsNumber()) return def; + return v.As().Uint32Value(); + } + uint64_t PropU64(Napi::Object o, const char* k, uint64_t def) + { + if (!o.Has(k)) return def; + Napi::Value v = o.Get(k); + if (!v.IsNumber()) return def; + return static_cast(v.As().Int64Value()); + } + int32_t PropI32(Napi::Object o, const char* k, int32_t def) + { + if (!o.Has(k)) return def; + Napi::Value v = o.Get(k); + if (!v.IsNumber()) return def; + return v.As().Int32Value(); + } + double PropF64(Napi::Object o, const char* k, double def) + { + if (!o.Has(k)) return def; + Napi::Value v = o.Get(k); + if (!v.IsNumber()) return def; + return v.As().DoubleValue(); + } + bool PropBool(Napi::Object o, const char* k, bool def) + { + if (!o.Has(k)) return def; + Napi::Value v = o.Get(k); + if (!v.IsBoolean()) return def; + return v.As().Value(); + } + bool PropPresent(Napi::Object o, const char* k) + { + return o.Has(k) && !IsNullish(o.Get(k)); + } + + // ---- argument readers ------------------------------------------------ + bool ArgIsUndef(const Napi::CallbackInfo& info, size_t i) + { + return i >= info.Length() || IsNullish(info[i]); + } + uint32_t ArgU32(const Napi::CallbackInfo& info, size_t i, uint32_t def) + { + if (i >= info.Length() || !info[i].IsNumber()) return def; + return info[i].As().Uint32Value(); + } + uint64_t ArgU64(const Napi::CallbackInfo& info, size_t i, uint64_t def) + { + if (i >= info.Length() || !info[i].IsNumber()) return def; + return static_cast(info[i].As().Int64Value()); + } + int32_t ArgI32(const Napi::CallbackInfo& info, size_t i, int32_t def) + { + if (i >= info.Length() || !info[i].IsNumber()) return def; + return info[i].As().Int32Value(); + } + double ArgF64(const Napi::CallbackInfo& info, size_t i, double def) + { + if (i >= info.Length() || !info[i].IsNumber()) return def; + return info[i].As().DoubleValue(); + } + + // ---- byte access for ArrayBuffer / TypedArray / DataView ------------- + struct Bytes { uint8_t* data; size_t size; }; + Bytes GetBytes(Napi::Value v) + { + if (v.IsTypedArray()) + { + Napi::TypedArray ta = v.As(); + uint8_t* base = static_cast(ta.ArrayBuffer().Data()); + return { base + ta.ByteOffset(), ta.ByteLength() }; + } + if (v.IsArrayBuffer()) + { + Napi::ArrayBuffer ab = v.As(); + return { static_cast(ab.Data()), ab.ByteLength() }; + } + if (v.IsDataView()) + { + Napi::DataView dv = v.As(); + uint8_t* base = static_cast(dv.ArrayBuffer().Data()); + return { base + dv.ByteOffset(), dv.ByteLength() }; + } + return { nullptr, 0 }; + } + + // IEEE-754 float32 -> float16 (half). Adequate for the [0,1] image data + // that copyExternalImageToTexture converts into half-float textures. + uint16_t FloatToHalf(float value) + { + uint32_t x; + std::memcpy(&x, &value, sizeof(x)); + const uint32_t sign = (x >> 16) & 0x8000u; + int32_t exponent = static_cast((x >> 23) & 0xFFu) - 127 + 15; + const uint32_t mantissa = x & 0x7FFFFFu; + if (exponent <= 0) + { + return static_cast(sign); + } + if (exponent >= 31) + { + return static_cast(sign | 0x7C00u); + } + return static_cast(sign | (static_cast(exponent) << 10) | (mantissa >> 13)); + } + + // ---- GPUExtent3D / GPUOrigin3D / GPUColor parsing -------------------- + wgpu::Extent3D ParseExtent3D(Napi::Value v) + { + wgpu::Extent3D e{}; + e.width = 1; e.height = 1; e.depthOrArrayLayers = 1; + if (v.IsArray()) + { + Napi::Array a = v.As(); + if (a.Length() > 0 && a.Get(0u).IsNumber()) e.width = a.Get(0u).As().Uint32Value(); + if (a.Length() > 1 && a.Get(1u).IsNumber()) e.height = a.Get(1u).As().Uint32Value(); + if (a.Length() > 2 && a.Get(2u).IsNumber()) e.depthOrArrayLayers = a.Get(2u).As().Uint32Value(); + } + else if (v.IsObject()) + { + Napi::Object o = v.As(); + e.width = PropU32(o, "width", 1); + e.height = PropU32(o, "height", 1); + e.depthOrArrayLayers = PropU32(o, "depthOrArrayLayers", 1); + } + if (e.height == 0) e.height = 1; + if (e.depthOrArrayLayers == 0) e.depthOrArrayLayers = 1; + return e; + } + wgpu::Origin3D ParseOrigin3D(Napi::Value v) + { + wgpu::Origin3D o3{}; + if (v.IsArray()) + { + Napi::Array a = v.As(); + if (a.Length() > 0 && a.Get(0u).IsNumber()) o3.x = a.Get(0u).As().Uint32Value(); + if (a.Length() > 1 && a.Get(1u).IsNumber()) o3.y = a.Get(1u).As().Uint32Value(); + if (a.Length() > 2 && a.Get(2u).IsNumber()) o3.z = a.Get(2u).As().Uint32Value(); + } + else if (v.IsObject()) + { + Napi::Object o = v.As(); + o3.x = PropU32(o, "x", 0); + o3.y = PropU32(o, "y", 0); + o3.z = PropU32(o, "z", 0); + } + return o3; + } + wgpu::Color ParseColor(Napi::Value v) + { + wgpu::Color c{}; + if (v.IsArray()) + { + Napi::Array a = v.As(); + if (a.Length() > 0 && a.Get(0u).IsNumber()) c.r = a.Get(0u).As().DoubleValue(); + if (a.Length() > 1 && a.Get(1u).IsNumber()) c.g = a.Get(1u).As().DoubleValue(); + if (a.Length() > 2 && a.Get(2u).IsNumber()) c.b = a.Get(2u).As().DoubleValue(); + if (a.Length() > 3 && a.Get(3u).IsNumber()) c.a = a.Get(3u).As().DoubleValue(); + } + else if (v.IsObject()) + { + Napi::Object o = v.As(); + c.r = PropF64(o, "r", 0); + c.g = PropF64(o, "g", 0); + c.b = PropF64(o, "b", 0); + c.a = PropF64(o, "a", 0); + } + return c; + } + + // ---- enum string -> wgpu mappings ------------------------------------ + wgpu::TextureFormat textureFormat(const std::string& s) + { + using F = wgpu::TextureFormat; + if (s == "r8unorm") return F::R8Unorm; + if (s == "r8snorm") return F::R8Snorm; + if (s == "r8uint") return F::R8Uint; + if (s == "r8sint") return F::R8Sint; + if (s == "r16uint") return F::R16Uint; + if (s == "r16sint") return F::R16Sint; + if (s == "r16float") return F::R16Float; + if (s == "rg8unorm") return F::RG8Unorm; + if (s == "rg8snorm") return F::RG8Snorm; + if (s == "rg8uint") return F::RG8Uint; + if (s == "rg8sint") return F::RG8Sint; + if (s == "r32uint") return F::R32Uint; + if (s == "r32sint") return F::R32Sint; + if (s == "r32float") return F::R32Float; + if (s == "rg16uint") return F::RG16Uint; + if (s == "rg16sint") return F::RG16Sint; + if (s == "rg16float") return F::RG16Float; + if (s == "rgba8unorm") return F::RGBA8Unorm; + if (s == "rgba8unorm-srgb") return F::RGBA8UnormSrgb; + if (s == "rgba8snorm") return F::RGBA8Snorm; + if (s == "rgba8uint") return F::RGBA8Uint; + if (s == "rgba8sint") return F::RGBA8Sint; + if (s == "bgra8unorm") return F::BGRA8Unorm; + if (s == "bgra8unorm-srgb") return F::BGRA8UnormSrgb; + if (s == "rgb9e5ufloat") return F::RGB9E5Ufloat; + if (s == "rgb10a2uint") return F::RGB10A2Uint; + if (s == "rgb10a2unorm") return F::RGB10A2Unorm; + if (s == "rg11b10ufloat") return F::RG11B10Ufloat; + if (s == "rg32uint") return F::RG32Uint; + if (s == "rg32sint") return F::RG32Sint; + if (s == "rg32float") return F::RG32Float; + if (s == "rgba16uint") return F::RGBA16Uint; + if (s == "rgba16sint") return F::RGBA16Sint; + if (s == "rgba16float") return F::RGBA16Float; + if (s == "rgba32uint") return F::RGBA32Uint; + if (s == "rgba32sint") return F::RGBA32Sint; + if (s == "rgba32float") return F::RGBA32Float; + if (s == "stencil8") return F::Stencil8; + if (s == "depth16unorm") return F::Depth16Unorm; + if (s == "depth24plus") return F::Depth24Plus; + if (s == "depth24plus-stencil8") return F::Depth24PlusStencil8; + if (s == "depth32float") return F::Depth32Float; + if (s == "depth32float-stencil8") return F::Depth32FloatStencil8; + if (s == "bc1-rgba-unorm") return F::BC1RGBAUnorm; + if (s == "bc1-rgba-unorm-srgb") return F::BC1RGBAUnormSrgb; + if (s == "bc2-rgba-unorm") return F::BC2RGBAUnorm; + if (s == "bc2-rgba-unorm-srgb") return F::BC2RGBAUnormSrgb; + if (s == "bc3-rgba-unorm") return F::BC3RGBAUnorm; + if (s == "bc3-rgba-unorm-srgb") return F::BC3RGBAUnormSrgb; + if (s == "bc4-r-unorm") return F::BC4RUnorm; + if (s == "bc4-r-snorm") return F::BC4RSnorm; + if (s == "bc5-rg-unorm") return F::BC5RGUnorm; + if (s == "bc5-rg-snorm") return F::BC5RGSnorm; + if (s == "bc6h-rgb-ufloat") return F::BC6HRGBUfloat; + if (s == "bc6h-rgb-float") return F::BC6HRGBFloat; + if (s == "bc7-rgba-unorm") return F::BC7RGBAUnorm; + if (s == "bc7-rgba-unorm-srgb") return F::BC7RGBAUnormSrgb; + return F::Undefined; + } + const char* textureFormatStr(wgpu::TextureFormat f) + { + using F = wgpu::TextureFormat; + switch (f) + { + case F::R8Unorm: return "r8unorm"; + case F::R8Snorm: return "r8snorm"; + case F::R8Uint: return "r8uint"; + case F::R8Sint: return "r8sint"; + case F::R16Uint: return "r16uint"; + case F::R16Sint: return "r16sint"; + case F::R16Float: return "r16float"; + case F::RG8Unorm: return "rg8unorm"; + case F::RG8Snorm: return "rg8snorm"; + case F::RG8Uint: return "rg8uint"; + case F::RG8Sint: return "rg8sint"; + case F::R32Uint: return "r32uint"; + case F::R32Sint: return "r32sint"; + case F::R32Float: return "r32float"; + case F::RG16Uint: return "rg16uint"; + case F::RG16Sint: return "rg16sint"; + case F::RG16Float: return "rg16float"; + case F::RGBA8Unorm: return "rgba8unorm"; + case F::RGBA8UnormSrgb: return "rgba8unorm-srgb"; + case F::RGBA8Snorm: return "rgba8snorm"; + case F::RGBA8Uint: return "rgba8uint"; + case F::RGBA8Sint: return "rgba8sint"; + case F::BGRA8Unorm: return "bgra8unorm"; + case F::BGRA8UnormSrgb: return "bgra8unorm-srgb"; + case F::RGB9E5Ufloat: return "rgb9e5ufloat"; + case F::RGB10A2Uint: return "rgb10a2uint"; + case F::RGB10A2Unorm: return "rgb10a2unorm"; + case F::RG11B10Ufloat: return "rg11b10ufloat"; + case F::RG32Uint: return "rg32uint"; + case F::RG32Sint: return "rg32sint"; + case F::RG32Float: return "rg32float"; + case F::RGBA16Uint: return "rgba16uint"; + case F::RGBA16Sint: return "rgba16sint"; + case F::RGBA16Float: return "rgba16float"; + case F::RGBA32Uint: return "rgba32uint"; + case F::RGBA32Sint: return "rgba32sint"; + case F::RGBA32Float: return "rgba32float"; + case F::Stencil8: return "stencil8"; + case F::Depth16Unorm: return "depth16unorm"; + case F::Depth24Plus: return "depth24plus"; + case F::Depth24PlusStencil8: return "depth24plus-stencil8"; + case F::Depth32Float: return "depth32float"; + case F::Depth32FloatStencil8: return "depth32float-stencil8"; + default: return "bgra8unorm"; + } + } + wgpu::VertexFormat vertexFormat(const std::string& s) + { + using F = wgpu::VertexFormat; + if (s == "uint8x2") return F::Uint8x2; + if (s == "uint8x4") return F::Uint8x4; + if (s == "sint8x2") return F::Sint8x2; + if (s == "sint8x4") return F::Sint8x4; + if (s == "unorm8x2") return F::Unorm8x2; + if (s == "unorm8x4") return F::Unorm8x4; + if (s == "snorm8x2") return F::Snorm8x2; + if (s == "snorm8x4") return F::Snorm8x4; + if (s == "uint16x2") return F::Uint16x2; + if (s == "uint16x4") return F::Uint16x4; + if (s == "sint16x2") return F::Sint16x2; + if (s == "sint16x4") return F::Sint16x4; + if (s == "unorm16x2") return F::Unorm16x2; + if (s == "unorm16x4") return F::Unorm16x4; + if (s == "snorm16x2") return F::Snorm16x2; + if (s == "snorm16x4") return F::Snorm16x4; + if (s == "float16x2") return F::Float16x2; + if (s == "float16x4") return F::Float16x4; + if (s == "float32") return F::Float32; + if (s == "float32x2") return F::Float32x2; + if (s == "float32x3") return F::Float32x3; + if (s == "float32x4") return F::Float32x4; + if (s == "uint32") return F::Uint32; + if (s == "uint32x2") return F::Uint32x2; + if (s == "uint32x3") return F::Uint32x3; + if (s == "uint32x4") return F::Uint32x4; + if (s == "sint32") return F::Sint32; + if (s == "sint32x2") return F::Sint32x2; + if (s == "sint32x3") return F::Sint32x3; + if (s == "sint32x4") return F::Sint32x4; + return F::Float32; + } + wgpu::IndexFormat indexFormat(const std::string& s) + { + if (s == "uint16") return wgpu::IndexFormat::Uint16; + if (s == "uint32") return wgpu::IndexFormat::Uint32; + return wgpu::IndexFormat::Undefined; + } + wgpu::PrimitiveTopology primitiveTopology(const std::string& s) + { + using T = wgpu::PrimitiveTopology; + if (s == "point-list") return T::PointList; + if (s == "line-list") return T::LineList; + if (s == "line-strip") return T::LineStrip; + if (s == "triangle-list") return T::TriangleList; + if (s == "triangle-strip") return T::TriangleStrip; + return T::TriangleList; + } + wgpu::CullMode cullMode(const std::string& s) + { + if (s == "none") return wgpu::CullMode::None; + if (s == "front") return wgpu::CullMode::Front; + if (s == "back") return wgpu::CullMode::Back; + return wgpu::CullMode::None; + } + wgpu::FrontFace frontFace(const std::string& s) + { + if (s == "cw") return wgpu::FrontFace::CW; + if (s == "ccw") return wgpu::FrontFace::CCW; + return wgpu::FrontFace::CCW; + } + wgpu::CompareFunction compareFunction(const std::string& s) + { + using C = wgpu::CompareFunction; + if (s == "never") return C::Never; + if (s == "less") return C::Less; + if (s == "equal") return C::Equal; + if (s == "less-equal") return C::LessEqual; + if (s == "greater") return C::Greater; + if (s == "not-equal") return C::NotEqual; + if (s == "greater-equal") return C::GreaterEqual; + if (s == "always") return C::Always; + return C::Undefined; + } + wgpu::StencilOperation stencilOperation(const std::string& s) + { + using O = wgpu::StencilOperation; + if (s == "keep") return O::Keep; + if (s == "zero") return O::Zero; + if (s == "replace") return O::Replace; + if (s == "invert") return O::Invert; + if (s == "increment-clamp") return O::IncrementClamp; + if (s == "decrement-clamp") return O::DecrementClamp; + if (s == "increment-wrap") return O::IncrementWrap; + if (s == "decrement-wrap") return O::DecrementWrap; + return O::Keep; + } + wgpu::BlendFactor blendFactor(const std::string& s) + { + using B = wgpu::BlendFactor; + if (s == "zero") return B::Zero; + if (s == "one") return B::One; + if (s == "src") return B::Src; + if (s == "one-minus-src") return B::OneMinusSrc; + if (s == "src-alpha") return B::SrcAlpha; + if (s == "one-minus-src-alpha") return B::OneMinusSrcAlpha; + if (s == "dst") return B::Dst; + if (s == "one-minus-dst") return B::OneMinusDst; + if (s == "dst-alpha") return B::DstAlpha; + if (s == "one-minus-dst-alpha") return B::OneMinusDstAlpha; + if (s == "src-alpha-saturated") return B::SrcAlphaSaturated; + if (s == "constant") return B::Constant; + if (s == "one-minus-constant") return B::OneMinusConstant; + return B::One; + } + wgpu::BlendOperation blendOperation(const std::string& s) + { + using O = wgpu::BlendOperation; + if (s == "add") return O::Add; + if (s == "subtract") return O::Subtract; + if (s == "reverse-subtract") return O::ReverseSubtract; + if (s == "min") return O::Min; + if (s == "max") return O::Max; + return O::Add; + } + wgpu::AddressMode addressMode(const std::string& s) + { + if (s == "clamp-to-edge") return wgpu::AddressMode::ClampToEdge; + if (s == "repeat") return wgpu::AddressMode::Repeat; + if (s == "mirror-repeat") return wgpu::AddressMode::MirrorRepeat; + return wgpu::AddressMode::ClampToEdge; + } + wgpu::FilterMode filterMode(const std::string& s) + { + if (s == "nearest") return wgpu::FilterMode::Nearest; + if (s == "linear") return wgpu::FilterMode::Linear; + return wgpu::FilterMode::Nearest; + } + wgpu::MipmapFilterMode mipmapFilterMode(const std::string& s) + { + if (s == "nearest") return wgpu::MipmapFilterMode::Nearest; + if (s == "linear") return wgpu::MipmapFilterMode::Linear; + return wgpu::MipmapFilterMode::Nearest; + } + wgpu::TextureViewDimension textureViewDimension(const std::string& s) + { + using D = wgpu::TextureViewDimension; + if (s == "1d") return D::e1D; + if (s == "2d") return D::e2D; + if (s == "2d-array") return D::e2DArray; + if (s == "cube") return D::Cube; + if (s == "cube-array") return D::CubeArray; + if (s == "3d") return D::e3D; + return D::Undefined; + } + wgpu::TextureDimension textureDimension(const std::string& s) + { + using D = wgpu::TextureDimension; + if (s == "1d") return D::e1D; + if (s == "2d") return D::e2D; + if (s == "3d") return D::e3D; + return D::e2D; + } + wgpu::TextureSampleType textureSampleType(const std::string& s) + { + using T = wgpu::TextureSampleType; + if (s == "float") return T::Float; + if (s == "unfilterable-float") return T::UnfilterableFloat; + if (s == "depth") return T::Depth; + if (s == "sint") return T::Sint; + if (s == "uint") return T::Uint; + return T::Float; + } + wgpu::StorageTextureAccess storageTextureAccess(const std::string& s) + { + using A = wgpu::StorageTextureAccess; + if (s == "write-only") return A::WriteOnly; + if (s == "read-only") return A::ReadOnly; + if (s == "read-write") return A::ReadWrite; + return A::Undefined; + } + wgpu::SamplerBindingType samplerBindingType(const std::string& s) + { + using S = wgpu::SamplerBindingType; + if (s == "filtering") return S::Filtering; + if (s == "non-filtering") return S::NonFiltering; + if (s == "comparison") return S::Comparison; + return S::Filtering; + } + wgpu::BufferBindingType bufferBindingType(const std::string& s) + { + using B = wgpu::BufferBindingType; + if (s == "uniform") return B::Uniform; + if (s == "storage") return B::Storage; + if (s == "read-only-storage") return B::ReadOnlyStorage; + return B::Uniform; + } + wgpu::LoadOp loadOp(const std::string& s) + { + if (s == "load") return wgpu::LoadOp::Load; + if (s == "clear") return wgpu::LoadOp::Clear; + return wgpu::LoadOp::Load; + } + wgpu::StoreOp storeOp(const std::string& s) + { + if (s == "store") return wgpu::StoreOp::Store; + if (s == "discard") return wgpu::StoreOp::Discard; + return wgpu::StoreOp::Store; + } + wgpu::VertexStepMode vertexStepMode(const std::string& s) + { + if (s == "vertex") return wgpu::VertexStepMode::Vertex; + if (s == "instance") return wgpu::VertexStepMode::Instance; + return wgpu::VertexStepMode::Vertex; + } + wgpu::FeatureName featureName(const std::string& s) + { + using F = wgpu::FeatureName; + if (s == "depth-clip-control") return F::DepthClipControl; + if (s == "depth32float-stencil8") return F::Depth32FloatStencil8; + if (s == "texture-compression-bc") return F::TextureCompressionBC; + if (s == "texture-compression-etc2") return F::TextureCompressionETC2; + if (s == "texture-compression-astc") return F::TextureCompressionASTC; + if (s == "timestamp-query") return F::TimestampQuery; + if (s == "indirect-first-instance") return F::IndirectFirstInstance; + if (s == "shader-f16") return F::ShaderF16; + if (s == "rg11b10ufloat-renderable") return F::RG11B10UfloatRenderable; + if (s == "bgra8unorm-storage") return F::BGRA8UnormStorage; + if (s == "float32-filterable") return F::Float32Filterable; + if (s == "dual-source-blending") return F::DualSourceBlending; + return F(0); + } + wgpu::QueryType queryType(const std::string& s) + { + if (s == "occlusion") return wgpu::QueryType::Occlusion; + if (s == "timestamp") return wgpu::QueryType::Timestamp; + return wgpu::QueryType::Occlusion; + } + wgpu::PowerPreference powerPreference(const std::string& s) + { + if (s == "low-power") return wgpu::PowerPreference::LowPower; + if (s == "high-performance") return wgpu::PowerPreference::HighPerformance; + return wgpu::PowerPreference::Undefined; + } + + // ---- limits ---------------------------------------------------------- + void FillLimits(Napi::Object o, const wgpu::Limits& L) + { + Napi::Env env = o.Env(); + auto N = [&](const char* k, double v) { o.Set(k, Napi::Number::New(env, v)); }; + N("maxTextureDimension1D", L.maxTextureDimension1D); + N("maxTextureDimension2D", L.maxTextureDimension2D); + N("maxTextureDimension3D", L.maxTextureDimension3D); + N("maxTextureArrayLayers", L.maxTextureArrayLayers); + N("maxBindGroups", L.maxBindGroups); + N("maxBindGroupsPlusVertexBuffers", L.maxBindGroupsPlusVertexBuffers); + N("maxBindingsPerBindGroup", L.maxBindingsPerBindGroup); + N("maxDynamicUniformBuffersPerPipelineLayout", L.maxDynamicUniformBuffersPerPipelineLayout); + N("maxDynamicStorageBuffersPerPipelineLayout", L.maxDynamicStorageBuffersPerPipelineLayout); + N("maxSampledTexturesPerShaderStage", L.maxSampledTexturesPerShaderStage); + N("maxSamplersPerShaderStage", L.maxSamplersPerShaderStage); + N("maxStorageBuffersPerShaderStage", L.maxStorageBuffersPerShaderStage); + N("maxStorageTexturesPerShaderStage", L.maxStorageTexturesPerShaderStage); + N("maxUniformBuffersPerShaderStage", L.maxUniformBuffersPerShaderStage); + N("maxUniformBufferBindingSize", static_cast(L.maxUniformBufferBindingSize)); + N("maxStorageBufferBindingSize", static_cast(L.maxStorageBufferBindingSize)); + N("minUniformBufferOffsetAlignment", L.minUniformBufferOffsetAlignment); + N("minStorageBufferOffsetAlignment", L.minStorageBufferOffsetAlignment); + N("maxVertexBuffers", L.maxVertexBuffers); + N("maxBufferSize", static_cast(L.maxBufferSize)); + N("maxVertexAttributes", L.maxVertexAttributes); + N("maxVertexBufferArrayStride", L.maxVertexBufferArrayStride); + N("maxInterStageShaderVariables", L.maxInterStageShaderVariables); + N("maxColorAttachments", L.maxColorAttachments); + N("maxColorAttachmentBytesPerSample", L.maxColorAttachmentBytesPerSample); + N("maxComputeWorkgroupStorageSize", L.maxComputeWorkgroupStorageSize); + N("maxComputeInvocationsPerWorkgroup", L.maxComputeInvocationsPerWorkgroup); + N("maxComputeWorkgroupSizeX", L.maxComputeWorkgroupSizeX); + N("maxComputeWorkgroupSizeY", L.maxComputeWorkgroupSizeY); + N("maxComputeWorkgroupSizeZ", L.maxComputeWorkgroupSizeZ); + N("maxComputeWorkgroupsPerDimension", L.maxComputeWorkgroupsPerDimension); + } + + // ---- surface state --------------------------------------------------- + bool g_surfaceConfigured = false; + bool g_currentTextureAcquired = false; + + // ---- forward declarations of object builders ------------------------- + Napi::Object MakeAdapter(Napi::Env env); + Napi::Object MakeDevice(Napi::Env env); + Napi::Object MakeQueue(Napi::Env env); + Napi::Object MakeBuffer(Napi::Env env, wgpu::Buffer h, uint64_t size, uint32_t usage, bool mapped); + Napi::Object MakeTexture(Napi::Env env, wgpu::Texture h, uint32_t w, uint32_t ht, uint32_t depth, + uint32_t mip, uint32_t sample, const std::string& fmt, uint32_t usage, const std::string& dim); + Napi::Object MakeTextureView(Napi::Env env, wgpu::TextureView h); + Napi::Object MakeSampler(Napi::Env env, wgpu::Sampler h); + Napi::Object MakeBindGroupLayout(Napi::Env env, wgpu::BindGroupLayout h); + Napi::Object MakeBindGroup(Napi::Env env, wgpu::BindGroup h); + Napi::Object MakePipelineLayout(Napi::Env env, wgpu::PipelineLayout h); + Napi::Object MakeShaderModule(Napi::Env env, wgpu::ShaderModule h); + Napi::Object MakeRenderPipeline(Napi::Env env, wgpu::RenderPipeline h); + Napi::Object MakeComputePipeline(Napi::Env env, wgpu::ComputePipeline h); + Napi::Object MakeCommandEncoder(Napi::Env env, wgpu::CommandEncoder h); + Napi::Object MakeRenderPassEncoder(Napi::Env env, wgpu::RenderPassEncoder h); + Napi::Object MakeComputePassEncoder(Napi::Env env, wgpu::ComputePassEncoder h); + Napi::Object MakeRenderBundleEncoder(Napi::Env env, wgpu::RenderBundleEncoder h); + Napi::Object MakeRenderBundle(Napi::Env env, wgpu::RenderBundle h); + Napi::Object MakeCommandBuffer(Napi::Env env, wgpu::CommandBuffer h); + Napi::Object MakeQuerySet(Napi::Env env, wgpu::QuerySet h, uint32_t count); + Napi::Object MakeCanvasContext(Napi::Env env); + Napi::Object DoCreateRenderPipeline(Napi::Env env, Napi::Value descVal); + Napi::Object DoCreateComputePipeline(Napi::Env env, Napi::Value descVal); + + // ---- set-like helper for .features ----------------------------------- // Returns a real JS Set populated with the enabled WebGPU feature name + // strings, so JS gets forEach/has/size/iteration for free (the WebGPU + // spec exposes GPUSupportedFeatures as a setlike). + template + Napi::Object MakeFeatureSet(Napi::Env env, HasFn hasFn) + { + static const char* const kFeatureNames[] = { + "depth-clip-control", + "depth32float-stencil8", + "texture-compression-bc", + "texture-compression-bc-sliced-3d", + "texture-compression-etc2", + "texture-compression-astc", + "texture-compression-astc-sliced-3d", + "timestamp-query", + "indirect-first-instance", + "shader-f16", + "rg11b10ufloat-renderable", + "bgra8unorm-storage", + "float32-filterable", + "float32-blendable", + "clip-distances", + "dual-source-blending", + "subgroups", + }; + + Napi::Function setCtor = env.Global().Get("Set").As(); + Napi::Object set = setCtor.New({}).As(); + Napi::Function add = set.Get("add").As(); + for (const char* name : kFeatureNames) + { + if (hasFn(std::string(name))) + { + add.Call(set, {Napi::String::New(env, name)}); + } + } + return set; + } + + // ---- GPUBuffer ------------------------------------------------------- + Napi::Object MakeBuffer(Napi::Env env, wgpu::Buffer h, uint64_t size, uint32_t usage, bool mapped) + { + Napi::Object o = NewGPUObject(env, "GPUBuffer"); + SetHandle(o, h); + o.Set("size", Napi::Number::New(env, static_cast(size))); + o.Set("usage", Napi::Number::New(env, usage)); + o.Set("mapState", Napi::String::New(env, mapped ? "mapped" : "unmapped")); + + SetMethod(o, "mapAsync", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t mode = ArgU32(info, 0, 0); + uint64_t offset = ArgU64(info, 1, 0); + uint64_t size = ArgIsUndef(info, 2) ? wgpu::kWholeMapSize : ArgU64(info, 2, 0); + auto d = Napi::Promise::Deferred::New(env); + wgpu::Future f = h.MapAsync(wgpu::MapMode(mode), static_cast(offset), + static_cast(size), wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::MapAsyncStatus, wgpu::StringView) {}); + g_state.instance.WaitAny(f, UINT64_MAX); + d.Resolve(env.Undefined()); + return d.Promise(); + }); + SetMethod(o, "getMappedRange", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint64_t offset = ArgU64(info, 0, 0); + uint64_t size = ArgIsUndef(info, 1) + ? (h.GetSize() - offset) : ArgU64(info, 1, 0); + void* ptr = h.GetMappedRange(static_cast(offset), static_cast(size)); + if (ptr == nullptr) + { + return Napi::ArrayBuffer::New(env, static_cast(size)); + } + return Napi::ArrayBuffer::New(env, ptr, static_cast(size)); + }); + SetMethod(o, "unmap", [h, o](const Napi::CallbackInfo& info) -> Napi::Value { + h.Unmap(); + return info.Env().Undefined(); + }); + SetMethod(o, "destroy", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.Destroy(); + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPUTextureView -------------------------------------------------- + Napi::Object MakeTextureView(Napi::Env env, wgpu::TextureView h) + { + Napi::Object o = NewGPUObject(env, "GPUTextureView"); + SetHandle(o, h); + return o; + } + + // ---- GPUTexture ------------------------------------------------------ + Napi::Object MakeTexture(Napi::Env env, wgpu::Texture h, uint32_t w, uint32_t ht, uint32_t depth, + uint32_t mip, uint32_t sample, const std::string& fmt, uint32_t usage, const std::string& dim) + { + Napi::Object o = NewGPUObject(env, "GPUTexture"); + SetHandle(o, h); + o.Set("width", Napi::Number::New(env, w)); + o.Set("height", Napi::Number::New(env, ht)); + o.Set("depthOrArrayLayers", Napi::Number::New(env, depth)); + o.Set("mipLevelCount", Napi::Number::New(env, mip)); + o.Set("sampleCount", Napi::Number::New(env, sample)); + o.Set("format", Napi::String::New(env, fmt)); + o.Set("dimension", Napi::String::New(env, dim.empty() ? "2d" : dim)); + o.Set("usage", Napi::Number::New(env, usage)); + + SetMethod(o, "createView", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::TextureViewDescriptor d{}; + if (info.Length() > 0 && info[0].IsObject()) + { + Napi::Object desc = info[0].As(); + if (PropPresent(desc, "format")) d.format = textureFormat(PropStr(desc, "format")); + if (PropPresent(desc, "dimension")) d.dimension = textureViewDimension(PropStr(desc, "dimension")); + d.baseMipLevel = PropU32(desc, "baseMipLevel", 0); + if (PropPresent(desc, "mipLevelCount")) d.mipLevelCount = PropU32(desc, "mipLevelCount", 0); + d.baseArrayLayer = PropU32(desc, "baseArrayLayer", 0); + if (PropPresent(desc, "arrayLayerCount")) d.arrayLayerCount = PropU32(desc, "arrayLayerCount", 0); + std::string aspect = PropStr(desc, "aspect"); + if (aspect == "stencil-only") d.aspect = wgpu::TextureAspect::StencilOnly; + else if (aspect == "depth-only") d.aspect = wgpu::TextureAspect::DepthOnly; + else d.aspect = wgpu::TextureAspect::All; + if (PropPresent(desc, "usage")) d.usage = wgpu::TextureUsage(PropU32(desc, "usage", 0)); + } + return MakeTextureView(env, h.CreateView(&d)); + }); + SetMethod(o, "destroy", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.Destroy(); + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPUSampler ------------------------------------------------------ + Napi::Object MakeSampler(Napi::Env env, wgpu::Sampler h) + { + Napi::Object o = NewGPUObject(env, "GPUSampler"); + SetHandle(o, h); + return o; + } + + // ---- GPUBindGroupLayout ---------------------------------------------- + Napi::Object MakeBindGroupLayout(Napi::Env env, wgpu::BindGroupLayout h) + { + Napi::Object o = NewGPUObject(env, "GPUBindGroupLayout"); + SetHandle(o, h); + return o; + } + + // ---- GPUBindGroup ---------------------------------------------------- + Napi::Object MakeBindGroup(Napi::Env env, wgpu::BindGroup h) + { + Napi::Object o = NewGPUObject(env, "GPUBindGroup"); + SetHandle(o, h); + return o; + } + + // ---- GPUPipelineLayout ----------------------------------------------- + Napi::Object MakePipelineLayout(Napi::Env env, wgpu::PipelineLayout h) + { + Napi::Object o = NewGPUObject(env, "GPUPipelineLayout"); + SetHandle(o, h); + return o; + } + + // ---- GPUShaderModule ------------------------------------------------- + Napi::Object MakeShaderModule(Napi::Env env, wgpu::ShaderModule h) + { + Napi::Object o = NewGPUObject(env, "GPUShaderModule"); + SetHandle(o, h); + SetMethod(o, "getCompilationInfo", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + Napi::Object res = Napi::Object::New(env); + res.Set("messages", Napi::Array::New(env)); + d.Resolve(res); + return d.Promise(); + }); + return o; + } + + // ---- GPUQuerySet ----------------------------------------------------- + Napi::Object MakeQuerySet(Napi::Env env, wgpu::QuerySet h, uint32_t count) + { + Napi::Object o = NewGPUObject(env, "GPUQuerySet"); + SetHandle(o, h); + o.Set("count", Napi::Number::New(env, count)); + SetMethod(o, "destroy", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.Destroy(); + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPUCommandBuffer ------------------------------------------------ + Napi::Object MakeCommandBuffer(Napi::Env env, wgpu::CommandBuffer h) + { + Napi::Object o = NewGPUObject(env, "GPUCommandBuffer"); + SetHandle(o, h); + return o; + } + + // ---- GPURenderPipeline ----------------------------------------------- + Napi::Object MakeRenderPipeline(Napi::Env env, wgpu::RenderPipeline h) + { + Napi::Object o = NewGPUObject(env, "GPURenderPipeline"); + SetHandle(o, h); + SetMethod(o, "getBindGroupLayout", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t index = ArgU32(info, 0, 0); + return MakeBindGroupLayout(env, h.GetBindGroupLayout(index)); + }); + return o; + } + + // ---- GPUComputePipeline ---------------------------------------------- + Napi::Object MakeComputePipeline(Napi::Env env, wgpu::ComputePipeline h) + { + Napi::Object o = NewGPUObject(env, "GPUComputePipeline"); + SetHandle(o, h); + SetMethod(o, "getBindGroupLayout", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t index = ArgU32(info, 0, 0); + return MakeBindGroupLayout(env, h.GetBindGroupLayout(index)); + }); + return o; + } + + // ---- createRenderPipeline (shared by sync + async) ------------------- + Napi::Object DoCreateRenderPipeline(Napi::Env env, Napi::Value descVal) + { + if (!descVal.IsObject()) + { + throw Napi::Error::New(env, "NativeDawn: createRenderPipeline requires a descriptor"); + } + Napi::Object desc = descVal.As(); + + wgpu::RenderPipelineDescriptor rp{}; + std::string label = PropStr(desc, "label"); + if (!label.empty()) rp.label = label.c_str(); + + // layout (object => explicit; "auto"/absent => auto layout) + { + Napi::Value layoutV = desc.Get("layout"); + wgpu::PipelineLayout* pl = GetH(layoutV); + if (pl != nullptr) rp.layout = *pl; + } + + // vertex + std::string vEntry; + std::vector vBuffers; + std::vector> vAttrs; + { + Napi::Value vtxV = desc.Get("vertex"); + if (!vtxV.IsObject()) + { + throw Napi::Error::New(env, "NativeDawn: createRenderPipeline requires vertex stage"); + } + Napi::Object vtx = vtxV.As(); + wgpu::ShaderModule* mod = GetH(vtx.Get("module")); + if (mod != nullptr) rp.vertex.module = *mod; + vEntry = PropStr(vtx, "entryPoint"); + if (!vEntry.empty()) rp.vertex.entryPoint = vEntry.c_str(); + + Napi::Value buffersV = vtx.Get("buffers"); + if (buffersV.IsArray()) + { + Napi::Array buffers = buffersV.As(); + size_t n = buffers.Length(); + vBuffers.resize(n); + vAttrs.resize(n); + for (size_t i = 0; i < n; ++i) + { + Napi::Value be = buffers.Get(static_cast(i)); + if (!be.IsObject()) + { + vBuffers[i].stepMode = wgpu::VertexStepMode::Undefined; + vBuffers[i].arrayStride = 0; + vBuffers[i].attributeCount = 0; + continue; + } + Napi::Object bo = be.As(); + vBuffers[i].arrayStride = PropU64(bo, "arrayStride", 0); + vBuffers[i].stepMode = PropPresent(bo, "stepMode") + ? vertexStepMode(PropStr(bo, "stepMode")) : wgpu::VertexStepMode::Vertex; + Napi::Value attrsV = bo.Get("attributes"); + if (attrsV.IsArray()) + { + Napi::Array attrs = attrsV.As(); + for (uint32_t j = 0; j < attrs.Length(); ++j) + { + Napi::Value av = attrs.Get(j); + if (!av.IsObject()) continue; + Napi::Object ao = av.As(); + wgpu::VertexAttribute va{}; + va.format = vertexFormat(PropStr(ao, "format")); + va.offset = PropU64(ao, "offset", 0); + va.shaderLocation = PropU32(ao, "shaderLocation", 0); + vAttrs[i].push_back(va); + } + } + vBuffers[i].attributeCount = vAttrs[i].size(); + vBuffers[i].attributes = vAttrs[i].data(); + } + rp.vertex.bufferCount = n; + rp.vertex.buffers = vBuffers.data(); + } + } + + // primitive + { + Napi::Value prV = desc.Get("primitive"); + if (prV.IsObject()) + { + Napi::Object pr = prV.As(); + rp.primitive.topology = PropPresent(pr, "topology") + ? primitiveTopology(PropStr(pr, "topology")) : wgpu::PrimitiveTopology::TriangleList; + if (PropPresent(pr, "stripIndexFormat")) + rp.primitive.stripIndexFormat = indexFormat(PropStr(pr, "stripIndexFormat")); + rp.primitive.frontFace = frontFace(PropStr(pr, "frontFace")); + rp.primitive.cullMode = cullMode(PropStr(pr, "cullMode")); + } + } + + // depthStencil + wgpu::DepthStencilState dss{}; + { + Napi::Value dsV = desc.Get("depthStencil"); + if (dsV.IsObject()) + { + Napi::Object ds = dsV.As(); + dss.format = textureFormat(PropStr(ds, "format")); + dss.depthWriteEnabled = PropBool(ds, "depthWriteEnabled", false) + ? wgpu::OptionalBool::True : wgpu::OptionalBool::False; + if (PropPresent(ds, "depthCompare")) + dss.depthCompare = compareFunction(PropStr(ds, "depthCompare")); + dss.stencilReadMask = PropU32(ds, "stencilReadMask", 0xFFFFFFFF); + dss.stencilWriteMask = PropU32(ds, "stencilWriteMask", 0xFFFFFFFF); + dss.depthBias = PropI32(ds, "depthBias", 0); + dss.depthBiasSlopeScale = static_cast(PropF64(ds, "depthBiasSlopeScale", 0)); + dss.depthBiasClamp = static_cast(PropF64(ds, "depthBiasClamp", 0)); + auto parseFace = [](Napi::Object f, wgpu::StencilFaceState& out) { + if (PropPresent(f, "compare")) out.compare = compareFunction(PropStr(f, "compare")); + if (PropPresent(f, "failOp")) out.failOp = stencilOperation(PropStr(f, "failOp")); + if (PropPresent(f, "depthFailOp")) out.depthFailOp = stencilOperation(PropStr(f, "depthFailOp")); + if (PropPresent(f, "passOp")) out.passOp = stencilOperation(PropStr(f, "passOp")); + }; + if (ds.Get("stencilFront").IsObject()) parseFace(ds.Get("stencilFront").As(), dss.stencilFront); + if (ds.Get("stencilBack").IsObject()) parseFace(ds.Get("stencilBack").As(), dss.stencilBack); + rp.depthStencil = &dss; + } + } + + // multisample + { + Napi::Value msV = desc.Get("multisample"); + if (msV.IsObject()) + { + Napi::Object ms = msV.As(); + rp.multisample.count = PropU32(ms, "count", 1); + rp.multisample.mask = PropU32(ms, "mask", 0xFFFFFFFF); + rp.multisample.alphaToCoverageEnabled = PropBool(ms, "alphaToCoverageEnabled", false); + } + } + + // fragment + std::string fEntry; + wgpu::FragmentState fs{}; + std::vector fTargets; + std::vector fBlends; + { + Napi::Value frV = desc.Get("fragment"); + if (frV.IsObject()) + { + Napi::Object fr = frV.As(); + wgpu::ShaderModule* mod = GetH(fr.Get("module")); + if (mod != nullptr) fs.module = *mod; + fEntry = PropStr(fr, "entryPoint"); + if (!fEntry.empty()) fs.entryPoint = fEntry.c_str(); + + Napi::Value tV = fr.Get("targets"); + if (tV.IsArray()) + { + Napi::Array targets = tV.As(); + size_t n = targets.Length(); + fTargets.resize(n); + fBlends.resize(n); + for (size_t i = 0; i < n; ++i) + { + Napi::Value te = targets.Get(static_cast(i)); + if (!te.IsObject()) continue; + Napi::Object to = te.As(); + fTargets[i].format = textureFormat(PropStr(to, "format")); + fTargets[i].writeMask = wgpu::ColorWriteMask(PropU32(to, "writeMask", 0xF)); + Napi::Value blV = to.Get("blend"); + if (blV.IsObject()) + { + Napi::Object bl = blV.As(); + wgpu::BlendState& bs = fBlends[i]; + Napi::Value cV = bl.Get("color"); + if (cV.IsObject()) + { + Napi::Object c = cV.As(); + if (PropPresent(c, "operation")) bs.color.operation = blendOperation(PropStr(c, "operation")); + if (PropPresent(c, "srcFactor")) bs.color.srcFactor = blendFactor(PropStr(c, "srcFactor")); + if (PropPresent(c, "dstFactor")) bs.color.dstFactor = blendFactor(PropStr(c, "dstFactor")); + } + Napi::Value aV = bl.Get("alpha"); + if (aV.IsObject()) + { + Napi::Object a = aV.As(); + if (PropPresent(a, "operation")) bs.alpha.operation = blendOperation(PropStr(a, "operation")); + if (PropPresent(a, "srcFactor")) bs.alpha.srcFactor = blendFactor(PropStr(a, "srcFactor")); + if (PropPresent(a, "dstFactor")) bs.alpha.dstFactor = blendFactor(PropStr(a, "dstFactor")); + } + fTargets[i].blend = &bs; + } + } + fs.targetCount = n; + fs.targets = fTargets.data(); + } + rp.fragment = &fs; + } + } + + wgpu::RenderPipeline pipe = g_state.device.CreateRenderPipeline(&rp); + return MakeRenderPipeline(env, pipe); + } + + // ---- createComputePipeline ------------------------------------------- + Napi::Object DoCreateComputePipeline(Napi::Env env, Napi::Value descVal) + { + if (!descVal.IsObject()) + { + throw Napi::Error::New(env, "NativeDawn: createComputePipeline requires a descriptor"); + } + Napi::Object desc = descVal.As(); + + wgpu::ComputePipelineDescriptor cp{}; + std::string label = PropStr(desc, "label"); + if (!label.empty()) cp.label = label.c_str(); + + { + Napi::Value layoutV = desc.Get("layout"); + wgpu::PipelineLayout* pl = GetH(layoutV); + if (pl != nullptr) cp.layout = *pl; + } + + std::string cEntry; + Napi::Value coV = desc.Get("compute"); + if (!coV.IsObject()) + { + throw Napi::Error::New(env, "NativeDawn: createComputePipeline requires compute stage"); + } + Napi::Object co = coV.As(); + wgpu::ShaderModule* mod = GetH(co.Get("module")); + if (mod != nullptr) cp.compute.module = *mod; + cEntry = PropStr(co, "entryPoint"); + if (!cEntry.empty()) cp.compute.entryPoint = cEntry.c_str(); + + wgpu::ComputePipeline pipe = g_state.device.CreateComputePipeline(&cp); + return MakeComputePipeline(env, pipe); + } + + // ---- texel copy helpers ---------------------------------------------- + wgpu::TexelCopyBufferInfo ParseTexelCopyBuffer(Napi::Object o) + { + wgpu::TexelCopyBufferInfo info{}; + wgpu::Buffer* b = GetH(o.Get("buffer")); + if (b != nullptr) info.buffer = *b; + info.layout.offset = PropU64(o, "offset", 0); + info.layout.bytesPerRow = PropPresent(o, "bytesPerRow") + ? PropU32(o, "bytesPerRow", 0) : wgpu::kCopyStrideUndefined; + info.layout.rowsPerImage = PropPresent(o, "rowsPerImage") + ? PropU32(o, "rowsPerImage", 0) : wgpu::kCopyStrideUndefined; + return info; + } + wgpu::TexelCopyTextureInfo ParseTexelCopyTexture(Napi::Object o) + { + wgpu::TexelCopyTextureInfo info{}; + wgpu::Texture* t = GetH(o.Get("texture")); + if (t != nullptr) info.texture = *t; + info.mipLevel = PropU32(o, "mipLevel", 0); + if (PropPresent(o, "origin")) info.origin = ParseOrigin3D(o.Get("origin")); + std::string aspect = PropStr(o, "aspect"); + if (aspect == "stencil-only") info.aspect = wgpu::TextureAspect::StencilOnly; + else if (aspect == "depth-only") info.aspect = wgpu::TextureAspect::DepthOnly; + else info.aspect = wgpu::TextureAspect::All; + return info; + } + + std::vector ReadU32Array(Napi::Value v) + { + std::vector out; + if (v.IsArray()) + { + Napi::Array a = v.As(); + for (uint32_t i = 0; i < a.Length(); ++i) + { + Napi::Value e = a.Get(i); + if (e.IsNumber()) out.push_back(e.As().Uint32Value()); + } + } + else if (v.IsTypedArray()) + { + Napi::TypedArray ta = v.As(); + if (ta.TypedArrayType() == napi_uint32_array) + { + Napi::Uint32Array u = v.As(); + for (size_t i = 0; i < u.ElementLength(); ++i) out.push_back(u[i]); + } + } + return out; + } + + // ---- GPURenderPassEncoder -------------------------------------------- + Napi::Object MakeRenderPassEncoder(Napi::Env env, wgpu::RenderPassEncoder h) + { + Napi::Object o = NewGPUObject(env, "GPURenderPassEncoder"); + SetHandle(o, h); + SetMethod(o, "setPipeline", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::RenderPipeline* p = GetH(info[0]); + if (p != nullptr) h.SetPipeline(*p); + return info.Env().Undefined(); + }); + SetMethod(o, "setBindGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + uint32_t index = ArgU32(info, 0, 0); + wgpu::BindGroup bg{}; + wgpu::BindGroup* p = GetH(info[1]); + if (p != nullptr) bg = *p; + std::vector offsets; + if (info.Length() > 2 && !IsNullish(info[2])) offsets = ReadU32Array(info[2]); + h.SetBindGroup(index, bg, offsets.size(), offsets.empty() ? nullptr : offsets.data()); + return info.Env().Undefined(); + }); + SetMethod(o, "setVertexBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + uint32_t slot = ArgU32(info, 0, 0); + wgpu::Buffer buf{}; + wgpu::Buffer* p = GetH(info[1]); + if (p != nullptr) buf = *p; + uint64_t offset = ArgU64(info, 2, 0); + uint64_t size = ArgIsUndef(info, 3) ? wgpu::kWholeSize : ArgU64(info, 3, 0); + h.SetVertexBuffer(slot, buf, offset, size); + return info.Env().Undefined(); + }); + SetMethod(o, "setIndexBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer buf{}; + wgpu::Buffer* p = GetH(info[0]); + if (p != nullptr) buf = *p; + std::string fmt = (info.Length() > 1 && info[1].IsString()) + ? info[1].As().Utf8Value() : std::string{}; + uint64_t offset = ArgU64(info, 2, 0); + uint64_t size = ArgIsUndef(info, 3) ? wgpu::kWholeSize : ArgU64(info, 3, 0); + h.SetIndexBuffer(buf, indexFormat(fmt), offset, size); + return info.Env().Undefined(); + }); + SetMethod(o, "draw", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.Draw(ArgU32(info, 0, 0), ArgU32(info, 1, 1), ArgU32(info, 2, 0), ArgU32(info, 3, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "drawIndexed", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.DrawIndexed(ArgU32(info, 0, 0), ArgU32(info, 1, 1), ArgU32(info, 2, 0), + ArgI32(info, 3, 0), ArgU32(info, 4, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "drawIndirect", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* b = GetH(info[0]); + if (b != nullptr) h.DrawIndirect(*b, ArgU64(info, 1, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "drawIndexedIndirect", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* b = GetH(info[0]); + if (b != nullptr) h.DrawIndexedIndirect(*b, ArgU64(info, 1, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "setViewport", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.SetViewport(static_cast(ArgF64(info, 0, 0)), static_cast(ArgF64(info, 1, 0)), + static_cast(ArgF64(info, 2, 0)), static_cast(ArgF64(info, 3, 0)), + static_cast(ArgF64(info, 4, 0)), static_cast(ArgF64(info, 5, 1))); + return info.Env().Undefined(); + }); + SetMethod(o, "setScissorRect", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.SetScissorRect(ArgU32(info, 0, 0), ArgU32(info, 1, 0), ArgU32(info, 2, 0), ArgU32(info, 3, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "setBlendConstant", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Color c = ParseColor(info[0]); + h.SetBlendConstant(&c); + return info.Env().Undefined(); + }); + SetMethod(o, "setStencilReference", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.SetStencilReference(ArgU32(info, 0, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "beginOcclusionQuery", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.BeginOcclusionQuery(ArgU32(info, 0, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "endOcclusionQuery", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.EndOcclusionQuery(); + return info.Env().Undefined(); + }); + SetMethod(o, "executeBundles", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::vector bundles; + if (info.Length() > 0 && info[0].IsArray()) + { + Napi::Array a = info[0].As(); + for (uint32_t i = 0; i < a.Length(); ++i) + { + wgpu::RenderBundle* b = GetH(a.Get(i)); + if (b != nullptr) bundles.push_back(*b); + } + } + h.ExecuteBundles(bundles.size(), bundles.empty() ? nullptr : bundles.data()); + return info.Env().Undefined(); + }); + SetMethod(o, "pushDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.PushDebugGroup(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "popDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.PopDebugGroup(); + return info.Env().Undefined(); + }); + SetMethod(o, "insertDebugMarker", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.InsertDebugMarker(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "end", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.End(); + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPUComputePassEncoder ------------------------------------------- + Napi::Object MakeComputePassEncoder(Napi::Env env, wgpu::ComputePassEncoder h) + { + Napi::Object o = NewGPUObject(env, "GPUComputePassEncoder"); + SetHandle(o, h); + SetMethod(o, "setPipeline", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::ComputePipeline* p = GetH(info[0]); + if (p != nullptr) h.SetPipeline(*p); + return info.Env().Undefined(); + }); + SetMethod(o, "setBindGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + uint32_t index = ArgU32(info, 0, 0); + wgpu::BindGroup bg{}; + wgpu::BindGroup* p = GetH(info[1]); + if (p != nullptr) bg = *p; + std::vector offsets; + if (info.Length() > 2 && !IsNullish(info[2])) offsets = ReadU32Array(info[2]); + h.SetBindGroup(index, bg, offsets.size(), offsets.empty() ? nullptr : offsets.data()); + return info.Env().Undefined(); + }); + SetMethod(o, "dispatchWorkgroups", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.DispatchWorkgroups(ArgU32(info, 0, 1), ArgU32(info, 1, 1), ArgU32(info, 2, 1)); + return info.Env().Undefined(); + }); + SetMethod(o, "dispatchWorkgroupsIndirect", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* b = GetH(info[0]); + if (b != nullptr) h.DispatchWorkgroupsIndirect(*b, ArgU64(info, 1, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "pushDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.PushDebugGroup(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "popDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.PopDebugGroup(); + return info.Env().Undefined(); + }); + SetMethod(o, "insertDebugMarker", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.InsertDebugMarker(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "end", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.End(); + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPURenderBundle ------------------------------------------------- + Napi::Object MakeRenderBundle(Napi::Env env, wgpu::RenderBundle h) + { + Napi::Object o = NewGPUObject(env, "GPURenderBundle"); + SetHandle(o, h); + return o; + } + + // ---- GPURenderBundleEncoder ------------------------------------------ + Napi::Object MakeRenderBundleEncoder(Napi::Env env, wgpu::RenderBundleEncoder h) + { + Napi::Object o = NewGPUObject(env, "GPURenderBundleEncoder"); + SetHandle(o, h); + SetMethod(o, "setPipeline", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::RenderPipeline* p = GetH(info[0]); + if (p != nullptr) h.SetPipeline(*p); + return info.Env().Undefined(); + }); + SetMethod(o, "setBindGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + uint32_t index = ArgU32(info, 0, 0); + wgpu::BindGroup bg{}; + wgpu::BindGroup* p = GetH(info[1]); + if (p != nullptr) bg = *p; + std::vector offsets; + if (info.Length() > 2 && !IsNullish(info[2])) offsets = ReadU32Array(info[2]); + h.SetBindGroup(index, bg, offsets.size(), offsets.empty() ? nullptr : offsets.data()); + return info.Env().Undefined(); + }); + SetMethod(o, "setVertexBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + uint32_t slot = ArgU32(info, 0, 0); + wgpu::Buffer buf{}; + wgpu::Buffer* p = GetH(info[1]); + if (p != nullptr) buf = *p; + uint64_t offset = ArgU64(info, 2, 0); + uint64_t size = ArgIsUndef(info, 3) ? wgpu::kWholeSize : ArgU64(info, 3, 0); + h.SetVertexBuffer(slot, buf, offset, size); + return info.Env().Undefined(); + }); + SetMethod(o, "setIndexBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer buf{}; + wgpu::Buffer* p = GetH(info[0]); + if (p != nullptr) buf = *p; + std::string fmt = (info.Length() > 1 && info[1].IsString()) + ? info[1].As().Utf8Value() : std::string{}; + uint64_t offset = ArgU64(info, 2, 0); + uint64_t size = ArgIsUndef(info, 3) ? wgpu::kWholeSize : ArgU64(info, 3, 0); + h.SetIndexBuffer(buf, indexFormat(fmt), offset, size); + return info.Env().Undefined(); + }); + SetMethod(o, "draw", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.Draw(ArgU32(info, 0, 0), ArgU32(info, 1, 1), ArgU32(info, 2, 0), ArgU32(info, 3, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "drawIndexed", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.DrawIndexed(ArgU32(info, 0, 0), ArgU32(info, 1, 1), ArgU32(info, 2, 0), + ArgI32(info, 3, 0), ArgU32(info, 4, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "pushDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.PushDebugGroup(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "popDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.PopDebugGroup(); + return info.Env().Undefined(); + }); + SetMethod(o, "insertDebugMarker", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.InsertDebugMarker(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "finish", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::RenderBundleDescriptor d{}; + std::string label; + if (info.Length() > 0 && info[0].IsObject()) + { + label = PropStr(info[0].As(), "label"); + if (!label.empty()) d.label = label.c_str(); + } + return MakeRenderBundle(env, h.Finish(&d)); + }); + return o; + } + + // ---- GPUCommandEncoder ----------------------------------------------- + Napi::Object MakeCommandEncoder(Napi::Env env, wgpu::CommandEncoder h) + { + Napi::Object o = NewGPUObject(env, "GPUCommandEncoder"); + SetHandle(o, h); + + SetMethod(o, "beginRenderPass", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (info.Length() == 0 || !info[0].IsObject()) + { + throw Napi::Error::New(env, "NativeDawn: beginRenderPass requires a descriptor"); + } + Napi::Object desc = info[0].As(); + wgpu::RenderPassDescriptor rpd{}; + std::string label = PropStr(desc, "label"); + if (!label.empty()) rpd.label = label.c_str(); + + std::vector colors; + Napi::Value caV = desc.Get("colorAttachments"); + if (caV.IsArray()) + { + Napi::Array arr = caV.As(); + for (uint32_t i = 0; i < arr.Length(); ++i) + { + wgpu::RenderPassColorAttachment c{}; + Napi::Value ev = arr.Get(i); + if (!ev.IsObject()) { colors.push_back(c); continue; } + Napi::Object ca = ev.As(); + wgpu::TextureView* view = GetH(ca.Get("view")); + if (view != nullptr) c.view = *view; + if (PropPresent(ca, "depthSlice")) c.depthSlice = PropU32(ca, "depthSlice", 0); + wgpu::TextureView* resolve = GetH(ca.Get("resolveTarget")); + if (resolve != nullptr) c.resolveTarget = *resolve; + c.loadOp = loadOp(PropStr(ca, "loadOp")); + c.storeOp = storeOp(PropStr(ca, "storeOp")); + if (PropPresent(ca, "clearValue")) c.clearValue = ParseColor(ca.Get("clearValue")); + colors.push_back(c); + } + } + rpd.colorAttachmentCount = colors.size(); + rpd.colorAttachments = colors.empty() ? nullptr : colors.data(); + + wgpu::RenderPassDepthStencilAttachment dsa{}; + Napi::Value dsV = desc.Get("depthStencilAttachment"); + if (dsV.IsObject()) + { + Napi::Object ds = dsV.As(); + wgpu::TextureView* view = GetH(ds.Get("view")); + if (view != nullptr) dsa.view = *view; + if (PropPresent(ds, "depthLoadOp")) dsa.depthLoadOp = loadOp(PropStr(ds, "depthLoadOp")); + if (PropPresent(ds, "depthStoreOp")) dsa.depthStoreOp = storeOp(PropStr(ds, "depthStoreOp")); + dsa.depthClearValue = static_cast(PropF64(ds, "depthClearValue", 0)); + dsa.depthReadOnly = PropBool(ds, "depthReadOnly", false); + if (PropPresent(ds, "stencilLoadOp")) dsa.stencilLoadOp = loadOp(PropStr(ds, "stencilLoadOp")); + if (PropPresent(ds, "stencilStoreOp")) dsa.stencilStoreOp = storeOp(PropStr(ds, "stencilStoreOp")); + dsa.stencilClearValue = PropU32(ds, "stencilClearValue", 0); + dsa.stencilReadOnly = PropBool(ds, "stencilReadOnly", false); + rpd.depthStencilAttachment = &dsa; + } + + return MakeRenderPassEncoder(env, h.BeginRenderPass(&rpd)); + }); + SetMethod(o, "beginComputePass", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::ComputePassDescriptor d{}; + std::string label; + if (info.Length() > 0 && info[0].IsObject()) + { + label = PropStr(info[0].As(), "label"); + if (!label.empty()) d.label = label.c_str(); + } + return MakeComputePassEncoder(env, h.BeginComputePass(&d)); + }); + SetMethod(o, "copyBufferToBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* src = GetH(info[0]); + wgpu::Buffer* dst = GetH(info[2]); + if (src != nullptr && dst != nullptr) + h.CopyBufferToBuffer(*src, ArgU64(info, 1, 0), *dst, ArgU64(info, 3, 0), ArgU64(info, 4, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "copyBufferToTexture", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::TexelCopyBufferInfo src = ParseTexelCopyBuffer(info[0].As()); + wgpu::TexelCopyTextureInfo dst = ParseTexelCopyTexture(info[1].As()); + wgpu::Extent3D size = ParseExtent3D(info[2]); + h.CopyBufferToTexture(&src, &dst, &size); + return info.Env().Undefined(); + }); + SetMethod(o, "copyTextureToBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::TexelCopyTextureInfo src = ParseTexelCopyTexture(info[0].As()); + wgpu::TexelCopyBufferInfo dst = ParseTexelCopyBuffer(info[1].As()); + wgpu::Extent3D size = ParseExtent3D(info[2]); + h.CopyTextureToBuffer(&src, &dst, &size); + return info.Env().Undefined(); + }); + SetMethod(o, "copyTextureToTexture", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::TexelCopyTextureInfo src = ParseTexelCopyTexture(info[0].As()); + wgpu::TexelCopyTextureInfo dst = ParseTexelCopyTexture(info[1].As()); + wgpu::Extent3D size = ParseExtent3D(info[2]); + h.CopyTextureToTexture(&src, &dst, &size); + return info.Env().Undefined(); + }); + SetMethod(o, "clearBuffer", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* b = GetH(info[0]); + if (b != nullptr) + { + uint64_t offset = ArgU64(info, 1, 0); + uint64_t size = ArgIsUndef(info, 2) ? wgpu::kWholeSize : ArgU64(info, 2, 0); + h.ClearBuffer(*b, offset, size); + } + return info.Env().Undefined(); + }); + SetMethod(o, "resolveQuerySet", [h](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::QuerySet* qs = GetH(info[0]); + wgpu::Buffer* dst = GetH(info[3]); + if (qs != nullptr && dst != nullptr) + h.ResolveQuerySet(*qs, ArgU32(info, 1, 0), ArgU32(info, 2, 0), *dst, ArgU64(info, 4, 0)); + return info.Env().Undefined(); + }); + SetMethod(o, "pushDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.PushDebugGroup(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "popDebugGroup", [h](const Napi::CallbackInfo& info) -> Napi::Value { + h.PopDebugGroup(); + return info.Env().Undefined(); + }); + SetMethod(o, "insertDebugMarker", [h](const Napi::CallbackInfo& info) -> Napi::Value { + std::string s = (info.Length() > 0 && info[0].IsString()) ? info[0].As().Utf8Value() : std::string{}; + h.InsertDebugMarker(s.c_str()); + return info.Env().Undefined(); + }); + SetMethod(o, "finish", [h](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::CommandBufferDescriptor d{}; + std::string label; + if (info.Length() > 0 && info[0].IsObject()) + { + label = PropStr(info[0].As(), "label"); + if (!label.empty()) d.label = label.c_str(); + } + return MakeCommandBuffer(env, h.Finish(&d)); + }); + return o; + } + + // ---- GPUQueue -------------------------------------------------------- + Napi::Object MakeQueue(Napi::Env env) + { + Napi::Object o = NewGPUObject(env, "GPUQueue"); + SetHandle(o, g_state.queue); + SetMethod(o, "submit", [](const Napi::CallbackInfo& info) -> Napi::Value { + std::vector bufs; + if (info.Length() > 0 && info[0].IsArray()) + { + Napi::Array arr = info[0].As(); + for (uint32_t i = 0; i < arr.Length(); ++i) + { + wgpu::CommandBuffer* cb = GetH(arr.Get(i)); + if (cb != nullptr) bufs.push_back(*cb); + } + } + if (!bufs.empty()) g_state.queue.Submit(bufs.size(), bufs.data()); + return info.Env().Undefined(); + }); + SetMethod(o, "writeBuffer", [](const Napi::CallbackInfo& info) -> Napi::Value { + wgpu::Buffer* b = GetH(info[0]); + if (b == nullptr) return info.Env().Undefined(); + uint64_t bufferOffset = ArgU64(info, 1, 0); + Bytes bytes = GetBytes(info[2]); + if (bytes.data == nullptr) return info.Env().Undefined(); + size_t dataOffset = static_cast(ArgU64(info, 3, 0)); + size_t length = (!ArgIsUndef(info, 4)) + ? static_cast(ArgU64(info, 4, 0)) : (bytes.size - dataOffset); + g_state.queue.WriteBuffer(*b, bufferOffset, bytes.data + dataOffset, length); + return info.Env().Undefined(); + }); + SetMethod(o, "writeTexture", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (!info[0].IsObject()) return env.Undefined(); + wgpu::TexelCopyTextureInfo tci = ParseTexelCopyTexture(info[0].As()); + Bytes bytes = GetBytes(info[1]); + Napi::Object layout = info[2].As(); + wgpu::TexelCopyBufferLayout tbl{}; + tbl.offset = PropU64(layout, "offset", 0); + tbl.bytesPerRow = PropPresent(layout, "bytesPerRow") + ? PropU32(layout, "bytesPerRow", 0) : wgpu::kCopyStrideUndefined; + tbl.rowsPerImage = PropPresent(layout, "rowsPerImage") + ? PropU32(layout, "rowsPerImage", 0) : wgpu::kCopyStrideUndefined; + wgpu::Extent3D ext = ParseExtent3D(info[3]); + if (bytes.data != nullptr) + g_state.queue.WriteTexture(&tci, bytes.data, bytes.size, &tbl, &ext); + return env.Undefined(); + }); + SetMethod(o, "copyExternalImageToTexture", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (!info[0].IsObject() || !info[1].IsObject()) + { + throw Napi::Error::New(env, "copyExternalImageToTexture: bad arguments"); + } + Napi::Object srcDesc = info[0].As(); + Napi::Value bmpVal = srcDesc.Get("source"); + if (!bmpVal.IsObject()) + { + throw Napi::Error::New(env, "copyExternalImageToTexture: missing source"); + } + Napi::Object bmp = bmpVal.As(); + Bytes px = GetBytes(bmp.Get("__pixels")); + uint32_t w = PropU32(bmp, "width", 0); + uint32_t h = PropU32(bmp, "height", 0); + if (px.data == nullptr || w == 0 || h == 0) + { + throw Napi::Error::New(env, "copyExternalImageToTexture: source has no decoded pixels"); + } + bool flipY = false; + { + Napi::Value f = srcDesc.Get("flipY"); + if (f.IsBoolean()) flipY = f.As().Value(); + } + + Napi::Object destDesc = info[1].As(); + wgpu::TexelCopyTextureInfo tci = ParseTexelCopyTexture(destDesc); + wgpu::Extent3D ext = ParseExtent3D(info[2]); + if (ext.width == 0) ext.width = w; + if (ext.height == 0) ext.height = h; + if (ext.depthOrArrayLayers == 0) ext.depthOrArrayLayers = 1; + + // Destination texture format (the spec allows the source RGBA8 to + // be converted to the destination format on copy). + std::string fmt; + { + Napi::Value texV = destDesc.Get("texture"); + if (texV.IsObject()) + { + Napi::Value fv = texV.As().Get("format"); + if (fv.IsString()) fmt = fv.As().Utf8Value(); + } + } + + // Access the source RGBA8 row, honoring flipY. + const uint32_t srcRowBytes = w * 4u; + auto srcRow = [&](uint32_t y) -> const uint8_t* { + uint32_t sy = flipY ? (h - 1 - y) : y; + return px.data + static_cast(sy) * srcRowBytes; + }; + + wgpu::TexelCopyBufferLayout tbl{}; + tbl.offset = 0; + tbl.rowsPerImage = h; + + if (fmt == "rgba16float") + { + // Convert 8-bit [0,255] -> normalized [0,1] -> half float. + std::vector half(static_cast(w) * h * 4u); + for (uint32_t y = 0; y < h; ++y) + { + const uint8_t* row = srcRow(y); + uint16_t* dst = &half[static_cast(y) * w * 4u]; + for (uint32_t i = 0; i < w * 4u; ++i) + { + dst[i] = FloatToHalf(static_cast(row[i]) / 255.0f); + } + } + tbl.bytesPerRow = w * 8u; + g_state.queue.WriteTexture(&tci, half.data(), + static_cast(w) * h * 8u, &tbl, &ext); + } + else if (fmt == "rgba32float") + { + std::vector f(static_cast(w) * h * 4u); + for (uint32_t y = 0; y < h; ++y) + { + const uint8_t* row = srcRow(y); + float* dst = &f[static_cast(y) * w * 4u]; + for (uint32_t i = 0; i < w * 4u; ++i) + { + dst[i] = static_cast(row[i]) / 255.0f; + } + } + tbl.bytesPerRow = w * 16u; + g_state.queue.WriteTexture(&tci, f.data(), + static_cast(w) * h * 16u, &tbl, &ext); + } + else if (fmt == "bgra8unorm" || fmt == "bgra8unorm-srgb") + { + // Swap R/B from the RGBA8 source. + std::vector bgra(static_cast(w) * h * 4u); + for (uint32_t y = 0; y < h; ++y) + { + const uint8_t* row = srcRow(y); + uint8_t* dst = &bgra[static_cast(y) * srcRowBytes]; + for (uint32_t x = 0; x < w; ++x) + { + dst[x * 4 + 0] = row[x * 4 + 2]; + dst[x * 4 + 1] = row[x * 4 + 1]; + dst[x * 4 + 2] = row[x * 4 + 0]; + dst[x * 4 + 3] = row[x * 4 + 3]; + } + } + tbl.bytesPerRow = srcRowBytes; + g_state.queue.WriteTexture(&tci, bgra.data(), + static_cast(srcRowBytes) * h, &tbl, &ext); + } + else + { + // rgba8unorm / rgba8unorm-srgb and default: copy as-is (with + // flipY applied per row if needed). + tbl.bytesPerRow = srcRowBytes; + if (flipY) + { + std::vector flipped(static_cast(srcRowBytes) * h); + for (uint32_t y = 0; y < h; ++y) + { + std::memcpy(&flipped[static_cast(y) * srcRowBytes], srcRow(y), srcRowBytes); + } + g_state.queue.WriteTexture(&tci, flipped.data(), + static_cast(srcRowBytes) * h, &tbl, &ext); + } + else + { + g_state.queue.WriteTexture(&tci, px.data, + static_cast(srcRowBytes) * h, &tbl, &ext); + } + } + return env.Undefined(); + }); + SetMethod(o, "onSubmittedWorkDone", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + wgpu::Future f = g_state.queue.OnSubmittedWorkDone(wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {}); + g_state.instance.WaitAny(f, UINT64_MAX); + d.Resolve(env.Undefined()); + return d.Promise(); + }); + o.Set("label", Napi::String::New(env, "")); + return o; + } + + // ---- GPUDevice ------------------------------------------------------- + Napi::Object MakeDevice(Napi::Env env) + { + Napi::Object o = NewGPUObject(env, "GPUDevice"); + SetHandle(o, g_state.device); + o.Set("queue", MakeQueue(env)); + o.Set("label", Napi::String::New(env, "")); + + o.Set("features", MakeFeatureSet(env, [](const std::string& name) { + return name.empty() ? false : static_cast(g_state.device.HasFeature(featureName(name))); + })); + { + wgpu::Limits L{}; + g_state.device.GetLimits(&L); + Napi::Object limits = Napi::Object::New(env); + FillLimits(limits, L); + o.Set("limits", limits); + } + // lost: a promise that never resolves. + { + auto lost = Napi::Promise::Deferred::New(env); + o.Set("lost", lost.Promise()); + } + + SetMethod(o, "createBuffer", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + wgpu::BufferDescriptor bd{}; + bd.size = PropU64(desc, "size", 0); + uint32_t usage = PropU32(desc, "usage", 0); + bd.usage = wgpu::BufferUsage(usage); + bool mapped = PropBool(desc, "mappedAtCreation", false); + bd.mappedAtCreation = mapped; + std::string label = PropStr(desc, "label"); + if (!label.empty()) bd.label = label.c_str(); + wgpu::Buffer buf = g_state.device.CreateBuffer(&bd); + if (!buf) throw Napi::Error::New(env, "NativeDawn: createBuffer failed"); + return MakeBuffer(env, buf, bd.size, usage, mapped); + }); + SetMethod(o, "createTexture", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + wgpu::TextureDescriptor td{}; + td.size = ParseExtent3D(desc.Get("size")); + td.mipLevelCount = PropU32(desc, "mipLevelCount", 1); + td.sampleCount = PropU32(desc, "sampleCount", 1); + std::string fmt = PropStr(desc, "format"); + td.format = textureFormat(fmt); + uint32_t usage = PropU32(desc, "usage", 0); + td.usage = wgpu::TextureUsage(usage); + std::string dim = PropStr(desc, "dimension"); + td.dimension = textureDimension(dim); + std::vector viewFormats; + Napi::Value vf = desc.Get("viewFormats"); + if (vf.IsArray()) + { + Napi::Array a = vf.As(); + for (uint32_t i = 0; i < a.Length(); ++i) + { + Napi::Value e = a.Get(i); + if (e.IsString()) viewFormats.push_back(textureFormat(e.As().Utf8Value())); + } + td.viewFormatCount = viewFormats.size(); + td.viewFormats = viewFormats.data(); + } + std::string label = PropStr(desc, "label"); + if (!label.empty()) td.label = label.c_str(); + wgpu::Texture tex = g_state.device.CreateTexture(&td); + if (!tex) throw Napi::Error::New(env, "NativeDawn: createTexture failed"); + return MakeTexture(env, tex, td.size.width, td.size.height, td.size.depthOrArrayLayers, + td.mipLevelCount, td.sampleCount, fmt, usage, dim); + }); + SetMethod(o, "createSampler", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::SamplerDescriptor sd{}; + if (info.Length() > 0 && info[0].IsObject()) + { + Napi::Object desc = info[0].As(); + sd.addressModeU = addressMode(PropStr(desc, "addressModeU")); + sd.addressModeV = addressMode(PropStr(desc, "addressModeV")); + sd.addressModeW = addressMode(PropStr(desc, "addressModeW")); + sd.magFilter = filterMode(PropStr(desc, "magFilter")); + sd.minFilter = filterMode(PropStr(desc, "minFilter")); + sd.mipmapFilter = mipmapFilterMode(PropStr(desc, "mipmapFilter")); + sd.lodMinClamp = static_cast(PropF64(desc, "lodMinClamp", 0.0)); + sd.lodMaxClamp = static_cast(PropF64(desc, "lodMaxClamp", 32.0)); + if (PropPresent(desc, "compare")) sd.compare = compareFunction(PropStr(desc, "compare")); + sd.maxAnisotropy = static_cast(PropU32(desc, "maxAnisotropy", 1)); + } + wgpu::Sampler s = g_state.device.CreateSampler(&sd); + return MakeSampler(env, s); + }); + SetMethod(o, "createShaderModule", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + std::string code = PropStr(desc, "code"); + wgpu::ShaderSourceWGSL wgsl{}; + wgsl.code = code.c_str(); + wgpu::ShaderModuleDescriptor smd{}; + smd.nextInChain = &wgsl; + std::string label = PropStr(desc, "label"); + if (!label.empty()) smd.label = label.c_str(); + wgpu::ShaderModule m = g_state.device.CreateShaderModule(&smd); + return MakeShaderModule(env, m); + }); + SetMethod(o, "createBindGroupLayout", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + Napi::Value entriesV = desc.Get("entries"); + std::vector entries; + if (entriesV.IsArray()) + { + Napi::Array arr = entriesV.As(); + entries.resize(arr.Length()); + for (uint32_t i = 0; i < arr.Length(); ++i) + { + Napi::Object e = arr.Get(i).As(); + wgpu::BindGroupLayoutEntry& dst = entries[i]; + dst.binding = PropU32(e, "binding", 0); + dst.visibility = wgpu::ShaderStage(PropU32(e, "visibility", 0)); + if (e.Get("buffer").IsObject()) + { + Napi::Object b = e.Get("buffer").As(); + dst.buffer.type = bufferBindingType(PropStr(b, "type").empty() ? "uniform" : PropStr(b, "type")); + dst.buffer.hasDynamicOffset = PropBool(b, "hasDynamicOffset", false); + dst.buffer.minBindingSize = PropU64(b, "minBindingSize", 0); + } + if (e.Get("sampler").IsObject()) + { + Napi::Object s = e.Get("sampler").As(); + dst.sampler.type = samplerBindingType(PropStr(s, "type").empty() ? "filtering" : PropStr(s, "type")); + } + if (e.Get("texture").IsObject()) + { + Napi::Object t = e.Get("texture").As(); + dst.texture.sampleType = textureSampleType(PropStr(t, "sampleType").empty() ? "float" : PropStr(t, "sampleType")); + dst.texture.viewDimension = textureViewDimension(PropStr(t, "viewDimension").empty() ? "2d" : PropStr(t, "viewDimension")); + dst.texture.multisampled = PropBool(t, "multisampled", false); + } + if (e.Get("storageTexture").IsObject()) + { + Napi::Object st = e.Get("storageTexture").As(); + dst.storageTexture.access = storageTextureAccess(PropStr(st, "access").empty() ? "write-only" : PropStr(st, "access")); + dst.storageTexture.format = textureFormat(PropStr(st, "format")); + dst.storageTexture.viewDimension = textureViewDimension(PropStr(st, "viewDimension").empty() ? "2d" : PropStr(st, "viewDimension")); + } + } + } + wgpu::BindGroupLayoutDescriptor bgld{}; + bgld.entryCount = entries.size(); + bgld.entries = entries.empty() ? nullptr : entries.data(); + std::string label = PropStr(desc, "label"); + if (!label.empty()) bgld.label = label.c_str(); + wgpu::BindGroupLayout bgl = g_state.device.CreateBindGroupLayout(&bgld); + return MakeBindGroupLayout(env, bgl); + }); + SetMethod(o, "createBindGroup", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + wgpu::BindGroupLayout* layout = GetH(desc.Get("layout")); + if (layout == nullptr) throw Napi::Error::New(env, "NativeDawn: createBindGroup missing layout"); + Napi::Value entriesV = desc.Get("entries"); + std::vector entries; + if (entriesV.IsArray()) + { + Napi::Array arr = entriesV.As(); + entries.resize(arr.Length()); + for (uint32_t i = 0; i < arr.Length(); ++i) + { + Napi::Object e = arr.Get(i).As(); + wgpu::BindGroupEntry& dst = entries[i]; + dst.binding = PropU32(e, "binding", 0); + Napi::Value resource = e.Get("resource"); + if (!resource.IsObject()) continue; + Napi::Object ro = resource.As(); + if (ro.Has("buffer") && ro.Get("buffer").IsObject()) + { + wgpu::Buffer* b = GetH(ro.Get("buffer")); + if (b != nullptr) + { + dst.buffer = *b; + dst.offset = PropU64(ro, "offset", 0); + dst.size = PropPresent(ro, "size") ? PropU64(ro, "size", 0) : wgpu::kWholeSize; + } + } + else + { + std::string ty = TypeTag(resource); + if (ty == "GPUSampler") + { + wgpu::Sampler* s = GetH(resource); + if (s != nullptr) dst.sampler = *s; + } + else if (ty == "GPUTextureView") + { + wgpu::TextureView* v = GetH(resource); + if (v != nullptr) dst.textureView = *v; + } + } + } + } + wgpu::BindGroupDescriptor bgd{}; + bgd.layout = *layout; + bgd.entryCount = entries.size(); + bgd.entries = entries.empty() ? nullptr : entries.data(); + std::string label = PropStr(desc, "label"); + if (!label.empty()) bgd.label = label.c_str(); + wgpu::BindGroup bg = g_state.device.CreateBindGroup(&bgd); + return MakeBindGroup(env, bg); + }); + SetMethod(o, "createPipelineLayout", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + std::vector layouts; + Napi::Value bglsV = desc.Get("bindGroupLayouts"); + if (bglsV.IsArray()) + { + Napi::Array arr = bglsV.As(); + for (uint32_t i = 0; i < arr.Length(); ++i) + { + wgpu::BindGroupLayout* l = GetH(arr.Get(i)); + if (l != nullptr) layouts.push_back(*l); + } + } + wgpu::PipelineLayoutDescriptor pld{}; + pld.bindGroupLayoutCount = layouts.size(); + pld.bindGroupLayouts = layouts.empty() ? nullptr : layouts.data(); + std::string label = PropStr(desc, "label"); + if (!label.empty()) pld.label = label.c_str(); + wgpu::PipelineLayout pl = g_state.device.CreatePipelineLayout(&pld); + return MakePipelineLayout(env, pl); + }); + SetMethod(o, "createRenderPipeline", [](const Napi::CallbackInfo& info) -> Napi::Value { + return DoCreateRenderPipeline(info.Env(), info[0]); + }); + SetMethod(o, "createRenderPipelineAsync", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(DoCreateRenderPipeline(env, info[0])); + return d.Promise(); + }); + SetMethod(o, "createComputePipeline", [](const Napi::CallbackInfo& info) -> Napi::Value { + return DoCreateComputePipeline(info.Env(), info[0]); + }); + SetMethod(o, "createComputePipelineAsync", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(DoCreateComputePipeline(env, info[0])); + return d.Promise(); + }); + SetMethod(o, "createCommandEncoder", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::CommandEncoderDescriptor ced{}; + std::string label; + if (info.Length() > 0 && info[0].IsObject()) + { + label = PropStr(info[0].As(), "label"); + if (!label.empty()) ced.label = label.c_str(); + } + return MakeCommandEncoder(env, g_state.device.CreateCommandEncoder(&ced)); + }); + SetMethod(o, "createRenderBundleEncoder", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + wgpu::RenderBundleEncoderDescriptor rbed{}; + std::vector cf; + Napi::Value cfV = desc.Get("colorFormats"); + if (cfV.IsArray()) + { + Napi::Array a = cfV.As(); + for (uint32_t i = 0; i < a.Length(); ++i) + { + Napi::Value e = a.Get(i); + cf.push_back(e.IsString() ? textureFormat(e.As().Utf8Value()) + : wgpu::TextureFormat::Undefined); + } + } + rbed.colorFormatCount = cf.size(); + rbed.colorFormats = cf.empty() ? nullptr : cf.data(); + std::string dsFmt = PropStr(desc, "depthStencilFormat"); + if (!dsFmt.empty()) rbed.depthStencilFormat = textureFormat(dsFmt); + rbed.sampleCount = PropU32(desc, "sampleCount", 1); + rbed.depthReadOnly = PropBool(desc, "depthReadOnly", false); + rbed.stencilReadOnly = PropBool(desc, "stencilReadOnly", false); + std::string label = PropStr(desc, "label"); + if (!label.empty()) rbed.label = label.c_str(); + return MakeRenderBundleEncoder(env, g_state.device.CreateRenderBundleEncoder(&rbed)); + }); + SetMethod(o, "createQuerySet", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object desc = info[0].As(); + wgpu::QuerySetDescriptor qsd{}; + qsd.type = queryType(PropStr(desc, "type")); + uint32_t count = PropU32(desc, "count", 0); + qsd.count = count; + return MakeQuerySet(env, g_state.device.CreateQuerySet(&qsd), count); + }); + SetMethod(o, "pushErrorScope", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.Env().Undefined(); + }); + SetMethod(o, "popErrorScope", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(env.Null()); + return d.Promise(); + }); + SetMethod(o, "destroy", [](const Napi::CallbackInfo& info) -> Napi::Value { + // No-op: the shared g_state.device must stay alive. + return info.Env().Undefined(); + }); + SetMethod(o, "addEventListener", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.Env().Undefined(); + }); + SetMethod(o, "removeEventListener", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.Env().Undefined(); + }); + return o; + } + + // ---- GPUAdapter ------------------------------------------------------ + Napi::Object MakeAdapterInfo(Napi::Env env) + { + Napi::Object i = Napi::Object::New(env); + wgpu::AdapterInfo info{}; + g_state.adapter.GetInfo(&info); + i.Set("vendor", Napi::String::New(env, SvToStr(info.vendor))); + i.Set("architecture", Napi::String::New(env, SvToStr(info.architecture))); + i.Set("device", Napi::String::New(env, SvToStr(info.device))); + i.Set("description", Napi::String::New(env, SvToStr(info.description))); + return i; + } + + Napi::Object MakeAdapter(Napi::Env env) + { + Napi::Object o = NewGPUObject(env, "GPUAdapter"); + SetHandle(o, g_state.adapter); + o.Set("isFallbackAdapter", Napi::Boolean::New(env, false)); + + o.Set("features", MakeFeatureSet(env, [](const std::string& name) { + return name.empty() ? false : static_cast(g_state.adapter.HasFeature(featureName(name))); + })); + { + wgpu::Limits L{}; + g_state.adapter.GetLimits(&L); + Napi::Object limits = Napi::Object::New(env); + FillLimits(limits, L); + o.Set("limits", limits); + } + o.Set("info", MakeAdapterInfo(env)); + SetMethod(o, "requestAdapterInfo", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(MakeAdapterInfo(env)); + return d.Promise(); + }); + SetMethod(o, "requestDevice", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(MakeDevice(env)); + return d.Promise(); + }); + return o; + } + + // ---- GPUCanvasContext ------------------------------------------------ + Napi::Object MakeCanvasContext(Napi::Env env) + { + Napi::Object o = NewGPUObject(env, "GPUCanvasContext"); + SetMethod(o, "configure", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (info.Length() == 0 || !info[0].IsObject()) return env.Undefined(); + Napi::Object desc = info[0].As(); + std::string fmt = PropStr(desc, "format"); + wgpu::TextureFormat f = fmt.empty() ? g_state.surfaceFormat : textureFormat(fmt); + g_state.surfaceFormat = f; + wgpu::SurfaceConfiguration cfg{}; + cfg.device = g_state.device; + cfg.format = f; + uint32_t usage = PropU32(desc, "usage", static_cast(wgpu::TextureUsage::RenderAttachment)); + cfg.usage = wgpu::TextureUsage(usage | static_cast(wgpu::TextureUsage::CopySrc)); + cfg.width = g_state.width > 1 ? g_state.width : 1; + cfg.height = g_state.height > 1 ? g_state.height : 1; + std::string am = PropStr(desc, "alphaMode"); + cfg.alphaMode = (am == "premultiplied") + ? wgpu::CompositeAlphaMode::Premultiplied : wgpu::CompositeAlphaMode::Opaque; + cfg.presentMode = wgpu::PresentMode::Fifo; + g_state.surface.Configure(&cfg); + g_surfaceConfigured = true; + return env.Undefined(); + }); + SetMethod(o, "unconfigure", [](const Napi::CallbackInfo& info) -> Napi::Value { + if (g_surfaceConfigured) + { + g_state.surface.Unconfigure(); + g_surfaceConfigured = false; + } + return info.Env().Undefined(); + }); + SetMethod(o, "getCurrentTexture", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + wgpu::SurfaceTexture st{}; + g_state.surface.GetCurrentTexture(&st); + if (!st.texture) throw Napi::Error::New(env, "NativeDawn: getCurrentTexture returned null"); + g_currentTextureAcquired = true; + g_state.currentSurfaceTexture = st.texture; + return MakeTexture(env, st.texture, g_state.width, g_state.height, 1, 1, 1, + textureFormatStr(g_state.surfaceFormat), + static_cast(wgpu::TextureUsage::RenderAttachment), "2d"); + }); + SetMethod(o, "getConfiguration", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object c = Napi::Object::New(env); + c.Set("format", Napi::String::New(env, textureFormatStr(g_state.surfaceFormat))); + return c; + }); + return o; + } + + // __WGPU_BUILDERS__ + + // ---- top-level installation ----------------------------------------- + void InstallWebGPU(Napi::Env env) + { + Napi::Object global = env.Global(); + + Napi::Object navigator; + Napi::Value navV = global.Get("navigator"); + if (navV.IsObject()) + { + navigator = navV.As(); + } + else + { + navigator = Napi::Object::New(env); + global.Set("navigator", navigator); + } + + Napi::Object gpu = Napi::Object::New(env); + SetMethod(gpu, "requestAdapter", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(MakeAdapter(env)); + return d.Promise(); + }); + SetMethod(gpu, "getPreferredCanvasFormat", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::String::New(info.Env(), textureFormatStr(g_state.surfaceFormat)); + }); + Napi::Object wlf = Napi::Object::New(env); + SetMethod(wlf, "has", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), false); + }); + wlf.Set("size", Napi::Number::New(env, 0)); + gpu.Set("wgslLanguageFeatures", wlf); + navigator.Set("gpu", gpu); + + SetMethod(global, "_nativeDawnGetContext", [](const Napi::CallbackInfo& info) -> Napi::Value { + return MakeCanvasContext(info.Env()); + }); + SetMethod(global, "_nativeDawnPresent", [](const Napi::CallbackInfo& info) -> Napi::Value { + if (g_surfaceConfigured && g_currentTextureAcquired) + { + g_state.surface.Present(); + } + g_currentTextureAcquired = false; + if (g_state.instance) + { + g_state.instance.ProcessEvents(); + } + return info.Env().Undefined(); + }); + + // Expose the real Dawn surface (drawing buffer) size so the JS canvas + // can match it exactly (avoids color/depth attachment size mismatch). + SetMethod(global, "_nativeDawnSurfaceSize", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object size = Napi::Object::New(info.Env()); + size.Set("width", Napi::Number::New(info.Env(), g_state.width)); + size.Set("height", Napi::Number::New(info.Env(), g_state.height)); + return size; + }); + + // ---- Validation-harness support -------------------------------------- + // Read back the most recently acquired surface texture as tightly-packed + // top-down RGBA8. Returns {width,height,data(ArrayBuffer)}. Used by the + // Dawn test shim's TestUtils.getFrameBufferData for pixel comparison. + SetMethod(global, "_nativeDawnReadPixels", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (!g_state.currentSurfaceTexture) + { + throw Napi::Error::New(env, "_nativeDawnReadPixels: no surface texture acquired"); + } + const uint32_t w = g_state.width; + const uint32_t h = g_state.height; + const uint32_t unpadded = w * 4u; + const uint32_t padded = (unpadded + 255u) & ~255u; + const uint64_t bufSize = static_cast(padded) * h; + + wgpu::BufferDescriptor bd{}; + bd.size = bufSize; + bd.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead; + wgpu::Buffer buf = g_state.device.CreateBuffer(&bd); + + wgpu::TexelCopyTextureInfo src{}; + src.texture = g_state.currentSurfaceTexture; + src.mipLevel = 0; + src.origin = {0, 0, 0}; + src.aspect = wgpu::TextureAspect::All; + wgpu::TexelCopyBufferInfo dst{}; + dst.buffer = buf; + dst.layout.offset = 0; + dst.layout.bytesPerRow = padded; + dst.layout.rowsPerImage = h; + wgpu::Extent3D ext{w, h, 1}; + + wgpu::CommandEncoder enc = g_state.device.CreateCommandEncoder(); + enc.CopyTextureToBuffer(&src, &dst, &ext); + wgpu::CommandBuffer cmd = enc.Finish(); + g_state.queue.Submit(1, &cmd); + + wgpu::Future f = buf.MapAsync(wgpu::MapMode::Read, 0, bufSize, + wgpu::CallbackMode::WaitAnyOnly, + [](wgpu::MapAsyncStatus, wgpu::StringView) {}); + g_state.instance.WaitAny(f, UINT64_MAX); + + const uint8_t* mapped = static_cast(buf.GetConstMappedRange(0, bufSize)); + const size_t outSize = static_cast(unpadded) * h; + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, outSize); + uint8_t* out = static_cast(ab.Data()); + const bool bgra = (g_state.surfaceFormat == wgpu::TextureFormat::BGRA8Unorm || + g_state.surfaceFormat == wgpu::TextureFormat::BGRA8UnormSrgb); + if (mapped != nullptr) + { + for (uint32_t y = 0; y < h; ++y) + { + const uint8_t* srcRow = mapped + static_cast(y) * padded; + uint8_t* dstRow = out + static_cast(y) * unpadded; + for (uint32_t x = 0; x < w; ++x) + { + const uint8_t c0 = srcRow[x * 4 + 0]; + const uint8_t c1 = srcRow[x * 4 + 1]; + const uint8_t c2 = srcRow[x * 4 + 2]; + const uint8_t c3 = srcRow[x * 4 + 3]; + if (bgra) + { + dstRow[x * 4 + 0] = c2; + dstRow[x * 4 + 1] = c1; + dstRow[x * 4 + 2] = c0; + dstRow[x * 4 + 3] = c3; + } + else + { + dstRow[x * 4 + 0] = c0; + dstRow[x * 4 + 1] = c1; + dstRow[x * 4 + 2] = c2; + dstRow[x * 4 + 3] = c3; + } + } + } + } + buf.Unmap(); + + Napi::Object res = Napi::Object::New(env); + res.Set("width", Napi::Number::New(env, w)); + res.Set("height", Napi::Number::New(env, h)); + res.Set("data", ab); + return res; + }); + + // Resize the window client area + Dawn surface (used by + // TestUtils.updateSize so the framebuffer matches reference-image size). + SetMethod(global, "_nativeDawnResize", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t w = info.Length() > 0 && info[0].IsNumber() ? info[0].As().Uint32Value() : g_state.width; + uint32_t h = info.Length() > 1 && info[1].IsNumber() ? info[1].As().Uint32Value() : g_state.height; + if (w < 1) w = 1; + if (h < 1) h = 1; + g_state.width = w; + g_state.height = h; +#if defined(_WIN32) + if (g_state.hwnd != nullptr) + { + HWND hwnd = static_cast(g_state.hwnd); + RECT rc{0, 0, static_cast(w), static_cast(h)}; + const DWORD style = static_cast(::GetWindowLongPtrW(hwnd, GWL_STYLE)); + const DWORD exStyle = static_cast(::GetWindowLongPtrW(hwnd, GWL_EXSTYLE)); + ::AdjustWindowRectEx(&rc, style, FALSE, exStyle); + ::SetWindowPos(hwnd, nullptr, 0, 0, rc.right - rc.left, rc.bottom - rc.top, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +#endif + if (g_surfaceConfigured) + { + wgpu::SurfaceConfiguration cfg{}; + cfg.device = g_state.device; + cfg.format = g_state.surfaceFormat; + cfg.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; + cfg.width = w; + cfg.height = h; + cfg.presentMode = wgpu::PresentMode::Fifo; + g_state.surface.Configure(&cfg); + } + return env.Undefined(); + }); + + // Set the window title (TestUtils.setTitle). + SetMethod(global, "_nativeDawnSetTitle", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); +#if defined(_WIN32) + if (g_state.hwnd != nullptr && info.Length() > 0 && info[0].IsString()) + { + ::SetWindowTextA(static_cast(g_state.hwnd), info[0].As().Utf8Value().c_str()); + } +#endif + return env.Undefined(); + }); + + // Terminate the process with the given exit code (TestUtils.exit). + SetMethod(global, "_nativeDawnExit", [](const Napi::CallbackInfo& info) -> Napi::Value { + const int code = info.Length() > 0 && info[0].IsNumber() ? info[0].As().Int32Value() : 0; + std::fflush(stdout); + std::fflush(stderr); + std::quick_exit(code); + return info.Env().Undefined(); + }); + + // Read a local file as an ArrayBuffer. Argument is a filesystem path + // (forward or back slashes). Returns the bytes, or null if not found. + // Backs the Dawn test shim's XMLHttpRequest replacement, whose local + // file loads cannot use UrlLib/WinRT (file:// throws there in this app). + SetMethod(global, "_nativeDawnReadFileBytes", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsString()) + { + return env.Null(); + } + const std::string path = info[0].As().Utf8Value(); + std::FILE* f = std::fopen(path.c_str(), "rb"); + if (f == nullptr) + { + return env.Null(); + } + std::fseek(f, 0, SEEK_END); + const long size = std::ftell(f); + std::fseek(f, 0, SEEK_SET); + if (size < 0) + { + std::fclose(f); + return env.Null(); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, static_cast(size)); + if (size > 0) + { + const size_t read = std::fread(ab.Data(), 1, static_cast(size), f); + (void)read; + } + std::fclose(f); + return ab; + }); + + // Decode an encoded image (PNG/JPEG/...) ArrayBuffer/TypedArray to an + // ImageBitmap-like object {width,height,__pixels(ArrayBuffer RGBA8)}. + // Backs the JS createImageBitmap / Image shims so glTF textures work + // in this no-DOM environment. + SetMethod(global, "_nativeDawnDecodeImage", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Bytes in = GetBytes(info.Length() > 0 ? info[0] : env.Undefined()); + if (in.data == nullptr || in.size == 0) + { + throw Napi::Error::New(env, "_nativeDawnDecodeImage: no input bytes"); + } + int w = 0; + int h = 0; + std::vector rgba; + if (!DecodeRGBA(in.data, in.size, rgba, w, h)) + { + throw Napi::Error::New(env, "_nativeDawnDecodeImage: decode failed"); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, rgba.size()); + std::memcpy(ab.Data(), rgba.data(), rgba.size()); + Napi::Object out = Napi::Object::New(env); + out.Set("width", Napi::Number::New(env, w)); + out.Set("height", Napi::Number::New(env, h)); + out.Set("__pixels", ab); + return out; + }); + + std::fprintf(stderr, "[NativeDawn] WebGPU (navigator.gpu) installed\n"); + } + } // namespace (webgpu) + + void Initialize(Napi::Env env, void* window, uint32_t width, uint32_t height) + { + if (!CreateDeviceAndSurface(window, width, height)) + { + std::fprintf(stderr, "[NativeDawn] initialization failed\n"); + return; + } + + // Milestone hook: a global to prove Dawn renders from JS without bgfx. + // navigator.gpu and the full WebGPU surface are added incrementally. + Napi::Object global = env.Global(); + global.Set("_nativeDawnClear", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + float r = info.Length() > 0 ? info[0].ToNumber().FloatValue() : 0.0f; + float g = info.Length() > 1 ? info[1].ToNumber().FloatValue() : 0.0f; + float b = info.Length() > 2 ? info[2].ToNumber().FloatValue() : 0.0f; + ClearToColor(r, g, b); + return info.Env().Undefined(); + }, "_nativeDawnClear")); + + if (g_state.ready) + { + InstallWebGPU(env); + } + } + + void Tick(Napi::Env) + { + if (g_state.ready) + { + g_state.instance.ProcessEvents(); + } + } + + void ResizeSurface(uint32_t width, uint32_t height) + { + if (width < 1) width = 1; + if (height < 1) height = 1; + if (!g_state.ready) + { + return; + } + if (g_state.width == width && g_state.height == height) + { + return; + } + g_state.width = width; + g_state.height = height; + // Drop the cached current texture (it belongs to the old swapchain). + g_state.currentSurfaceTexture = nullptr; + g_currentTextureAcquired = false; + if (g_surfaceConfigured) + { + wgpu::SurfaceConfiguration cfg{}; + cfg.device = g_state.device; + cfg.format = g_state.surfaceFormat; + cfg.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc; + cfg.width = width; + cfg.height = height; + cfg.presentMode = wgpu::PresentMode::Fifo; + g_state.surface.Configure(&cfg); + } + } +} From 2015cf7b6ea071d905f6235c0d90e0c42e81dc56 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:29:25 +0200 Subject: [PATCH 02/11] fix cmake script --- .github/workflows/ci.yml | 4 ++++ CMakeLists.txt | 15 +++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bd11e523..4b8e6b0dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,11 +213,15 @@ jobs: MacOS_NativeDawn: uses: ./.github/workflows/build-macos.yml with: + runs-on: macos-26 + xcode-version: "26.4" nativedawn: true iOS_NativeDawn: uses: ./.github/workflows/build-ios.yml with: + runs-on: macos-26 + xcode-version: "26.4" deployment-target: '18.0' nativedawn: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dc8cdc7d..b19a4b939 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,13 +41,17 @@ FetchContent_Declare(CMakeExtensions GIT_REPOSITORY https://github.com/BabylonJS/CMakeExtensions.git GIT_TAG 631780e42886e5f12bfd1a5568c7395f1d657f43 EXCLUDE_FROM_ALL) -# Dawn (WebGPU) — only populated when BABYLON_NATIVE_PLUGIN_NATIVEDAWN is ON -# (see the guarded FetchContent_MakeAvailable below). Declaring it here is free; -# no clone happens unless the plugin is enabled. +# Dawn (WebGPU) — only populated + built when BABYLON_NATIVE_PLUGIN_NATIVEDAWN is +# ON (see the guarded FetchContent_MakeAvailable below). Declaring it here is +# free; no clone happens unless the plugin is enabled. Pinned to a Dawn dated +# tag (a branch tip) so GIT_SHALLOW works: a raw commit SHA cannot be shallow- +# cloned because CMake's shallow path fetches only branch tips. This tag is the +# 2026-02-09 (Chrome 146) snapshot and contains commit +# a44d7a3d78f23c680491c0fc04f53a1df62e02ff (the reference pin) plus 7 commits. FetchContent_Declare(dawn GIT_REPOSITORY https://github.com/google/dawn.git - GIT_TAG a44d7a3d78f23c680491c0fc04f53a1df62e02ff - GIT_SHALLOW TRUE + GIT_TAG v20260209.194954 + GIT_SHALLOW TRUE EXCLUDE_FROM_ALL) FetchContent_Declare(glslang GIT_REPOSITORY https://github.com/BabylonJS/glslang.git @@ -373,7 +377,6 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) FetchContent_MakeAvailable_With_Message(dawn) endif() - add_subdirectory(Core) add_subdirectory(Plugins) add_subdirectory(Polyfills) From 703e9b5626045b1e87563190a00a53c1a1f85b0c Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:05:22 +0200 Subject: [PATCH 03/11] submodules recurse init --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b19a4b939..c4478739c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,10 +48,16 @@ FetchContent_Declare(CMakeExtensions # cloned because CMake's shallow path fetches only branch tips. This tag is the # 2026-02-09 (Chrome 146) snapshot and contains commit # a44d7a3d78f23c680491c0fc04f53a1df62e02ff (the reference pin) plus 7 commits. +# GIT_SUBMODULES "" disables submodule init: Dawn's .gitmodules declares ~53 +# nested submodules (angle, swiftshader, catapult, dxc, ...), and FetchContent +# would otherwise recursively clone them all (very slow, multi-GB). They are not +# needed — DAWN_FETCH_DEPENDENCIES (below) runs Dawn's fetch script at configure +# time to pull only the deps the enabled backend requires. FetchContent_Declare(dawn GIT_REPOSITORY https://github.com/google/dawn.git GIT_TAG v20260209.194954 GIT_SHALLOW TRUE + GIT_SUBMODULES "" EXCLUDE_FROM_ALL) FetchContent_Declare(glslang GIT_REPOSITORY https://github.com/BabylonJS/glslang.git From 6b6b0c69cfacb781bf6a4944f7c5ff7c8178981b Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:18:57 +0200 Subject: [PATCH 04/11] fix ubuntu --- CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c4478739c..05438c61c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -343,6 +343,14 @@ add_subdirectory(Dependencies) if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) set(DAWN_FETCH_DEPENDENCIES ON CACHE BOOL "" FORCE) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) + set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) + # No windowing/surface toolkit is needed: the NativeDawn plugin creates its + # own surface (Win32-only) and builds surfaceless elsewhere. Disabling GLFW + # (and X11/Wayland) avoids Dawn configuring GLFW, which otherwise requires + # X11 dev packages (libxrandr-dev etc.) on Linux CI runners. + set(DAWN_USE_GLFW OFF CACHE BOOL "" FORCE) + set(DAWN_USE_X11 OFF CACHE BOOL "" FORCE) + set(DAWN_USE_WAYLAND OFF CACHE BOOL "" FORCE) set(TINT_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(TINT_BUILD_CMD_TOOLS OFF CACHE BOOL "" FORCE) From ef5e4b0f75eeab547f0035aca4de30335279b778 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:38:06 +0200 Subject: [PATCH 05/11] dawn desktop gl off --- CMakeLists.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05438c61c..b0986d230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -365,12 +365,13 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) set(TINT_BUILD_IR_BINARY OFF CACHE BOOL "" FORCE) # Start with every GPU backend + shader writer off; enable one set per platform. - set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) - set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) - set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE) - set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) - set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) - set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_DESKTOP_GL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) set(TINT_BUILD_MSL_WRITER OFF CACHE BOOL "" FORCE) set(TINT_BUILD_SPV_WRITER OFF CACHE BOOL "" FORCE) From 8fb57a6617d793c1d0aa7b113223b4f152a7d144 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:44:00 +0200 Subject: [PATCH 06/11] more generic dawn plugin --- Apps/Playground/CMakeLists.txt | 70 +--- Apps/Playground/Scripts/dawn_bootstrap.js | 264 +++++-------- Apps/Playground/Win32/App.cpp | 447 +--------------------- Embedding/CMakeLists.txt | 9 + Embedding/Source/Runtime.cpp | 27 +- Embedding/Source/RuntimeImpl.h | 20 +- Embedding/Source/View.cpp | 126 +++++- Plugins/NativeDawn/Source/NativeDawn.cpp | 56 +++ Polyfills/Window/Source/Window.cpp | 14 +- 9 files changed, 342 insertions(+), 691 deletions(-) diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index f4d144477..858c39ece 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -28,16 +28,6 @@ set(SOURCES "Shared/PlaygroundScripts.cpp" "Shared/PlaygroundScripts.h") -if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN AND WIN32 AND NOT WINDOWS_STORE) - # PlaygroundScripts.cpp is Embedding-specific (LoadBootstrapScripts takes an - # Embedding::Runtime&). The NativeDawn host in Win32/App.cpp bypasses - # Embedding and loads the bootstrap scripts itself, so drop this source to - # avoid pulling the Embedding headers into the Dawn build. - list(REMOVE_ITEM SOURCES - "Shared/PlaygroundScripts.cpp" - "Shared/PlaygroundScripts.h") -endif() - if(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) if(IOS) @@ -155,51 +145,21 @@ endif() target_include_directories(Playground PRIVATE ".") -if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN AND WIN32 AND NOT WINDOWS_STORE) - # NativeDawn (WebGPU-via-Dawn) path. The Embedding layer's View always - # constructs the bgfx Graphics device, which is exactly what this backend - # replaces, so the Dawn host in Win32/App.cpp bypasses Embedding and drives - # a direct AppRuntime + ScriptLoader + NativeDawn pipeline instead. Link the - # polyfills/plugins that host consumes directly (Embedding used to forward - # them transitively). Only the Win32 host has a Dawn code path; other - # platforms keep the Embedding path even when the NativeDawn plugin is built. - target_compile_definitions(Playground PRIVATE BABYLON_NATIVE_PLUGIN_NATIVEDAWN=1) - target_link_libraries(Playground - PRIVATE AppRuntime - PRIVATE ScriptLoader - PRIVATE Console - PRIVATE Window - PRIVATE Performance - PRIVATE Scheduling - PRIVATE XMLHttpRequest - PRIVATE Fetch - PRIVATE Blob - PRIVATE File - PRIVATE TextDecoder - PRIVATE TextEncoder - PRIVATE AbortController - PRIVATE URL - PRIVATE NativeDawn - PRIVATE napi - PRIVATE bx - ${ADDITIONAL_LIBRARIES} - ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) -else() - # The Embedding layer links and initializes the full canonical set of - # polyfills/plugins (Blob, Canvas, Console, File, GraphicsDevice, the - # Native* plugins, ScriptLoader, ShaderCache, the polyfills, etc.) and - # forwards them transitively, so the Playground only needs to list the - # libraries it consumes directly: - # - Embedding : the Runtime + View API the host is built on. - # - bx : used by Shared/Diagnostics.cpp. - # - TestUtils : used by Win32/X11 --test mode. - target_link_libraries(Playground - PRIVATE Embedding - PRIVATE bx - PRIVATE TestUtils - ${ADDITIONAL_LIBRARIES} - ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) -endif() +# The Embedding layer links and initializes the full canonical set of +# polyfills/plugins (Blob, Canvas, Console, File, GraphicsDevice, the +# Native* plugins — including NativeDawn when BABYLON_NATIVE_PLUGIN_NATIVEDAWN +# is ON — ScriptLoader, ShaderCache, the polyfills, etc.) and forwards them +# transitively, so the Playground only needs to list the libraries it consumes +# directly: +# - Embedding : the Runtime + View API the host is built on. +# - bx : used by Shared/Diagnostics.cpp. +# - TestUtils : used by Win32/X11 --test mode. +target_link_libraries(Playground + PRIVATE Embedding + PRIVATE bx + PRIVATE TestUtils + ${ADDITIONAL_LIBRARIES} + ${BABYLON_NATIVE_PLAYGROUND_EXTENSION_LIBRARIES}) # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 # If we can set minimum required to 3.26+, then we can use the `copy -t` syntax instead. diff --git a/Apps/Playground/Scripts/dawn_bootstrap.js b/Apps/Playground/Scripts/dawn_bootstrap.js index cb264d74d..94fccd797 100644 --- a/Apps/Playground/Scripts/dawn_bootstrap.js +++ b/Apps/Playground/Scripts/dawn_bootstrap.js @@ -1,18 +1,24 @@ -// dawn_bootstrap.js — makes the default Playground scene scripts (experience.js +// dawn_bootstrap.js — makes the standard Playground scene scripts (experience.js // and friends, written against BABYLON.NativeEngine) run on the Dawn/WebGPU // backend provided by the NativeDawn plugin. // -// Loaded by Win32/App.cpp (NativeDawn build) after babylon.max.js + addons. -// The native side installs navigator.gpu, _nativeDawnGetContext/Present/ -// SurfaceSize, _nativeDawnDecodeImage, _nativeDawnReadFileBytes, and the -// standard polyfills. This script: +// This script is evaluated by the NativeDawn plugin (Babylon::Plugins:: +// NativeDawn::Initialize) at global scope, BEFORE babylon.max.js and the scene +// scripts load. The native side has already installed navigator.gpu, +// _nativeDawnGetContext/Present/SurfaceSize, _nativeDawnDecodeImage, +// _nativeDawnReadFileBytes, and the standard polyfills. This script: // * installs the no-DOM canvas / image shims WebGPUEngine needs, -// * drives requestAnimationFrame from the native per-frame globalThis.frame(), -// * pre-creates and initializes a WebGPUEngine, then aliases +// * drives requestAnimationFrame from the native per-frame globalThis.frame() +// (pumped by the Embedding View::RenderFrame), +// * installs a __dawnResize hook (called by Embedding View::Resize), +// * defers WebGPUEngine creation until babylon.max.js has defined BABYLON, +// then pre-creates + initializes a WebGPUEngine and aliases // BABYLON.NativeEngine so the scene scripts' synchronous -// `new BABYLON.NativeEngine()` returns the ready WebGPU engine, -// * reads the scene script (default experience.js) natively and evaluates it -// once the engine is ready. +// `new BABYLON.NativeEngine()` returns the ready WebGPU engine. +// +// Input is NOT handled here: it flows through the generic Win32 host +// (App.cpp WndProc -> Embedding View::OnMouse* -> NativeInput -> +// _native.DeviceInputSystem), identical to the bgfx/NativeEngine backend. (function () { "use strict"; @@ -21,34 +27,6 @@ console.log("[dawn-bootstrap] " + Array.prototype.join.call(arguments, " ")); } - // ---- shared DOM event registry ------------------------------------------ - // Babylon's WebDeviceInputSystem attaches its pointer/keyboard listeners to - // whichever element engine.getInputElement() returns and (for some events) to - // window/document. To route native Win32 input regardless of which target and - // event-name prefix ("pointer" vs "mouse") Babylon picks, canvas, document and - // window all funnel their addEventListener calls into this single registry, - // and __dawnInput dispatches to it. - var g_evtListeners = {}; - function addShared(name, cb) { - if (typeof cb !== "function") { return; } - (g_evtListeners[name] = g_evtListeners[name] || []).push(cb); - } - function removeShared(name, cb) { - var a = g_evtListeners[name]; - if (!a) { return; } - var i = a.indexOf(cb); - if (i !== -1) { a.splice(i, 1); } - } - function dispatchShared(evt) { - var a = g_evtListeners[evt.type]; - if (!a) { return; } - var copy = a.slice(); - for (var i = 0; i < copy.length; ++i) { - try { copy[i].call(evt.currentTarget || null, evt); } - catch (e) { console.error("[dawn-bootstrap] listener error: " + (e && e.stack ? e.stack : e)); } - } - } - // ---- no-DOM canvas / DOM shims ------------------------------------------ function makeCanvas(width, height) { var canvas = { @@ -68,10 +46,9 @@ }, setAttribute: function () {}, removeAttribute: function () {}, - addEventListener: function (name, cb) { addShared(name, cb); }, - removeEventListener: function (name, cb) { removeShared(name, cb); }, - dispatchEvent: function (evt) { dispatchShared(evt); return true; }, - // Pointer-capture no-ops (Babylon's input system may call these). + addEventListener: function () {}, + removeEventListener: function () {}, + dispatchEvent: function () { return true; }, setPointerCapture: function () {}, releasePointerCapture: function () {}, hasPointerCapture: function () { return false; }, @@ -88,14 +65,14 @@ if (tag === "img") { return new globalThis.Image(); } return { style: {}, setAttribute: function () {}, appendChild: function () {} }; }, - addEventListener: function (name, cb) { addShared(name, cb); }, - removeEventListener: function (name, cb) { removeShared(name, cb); }, + addEventListener: function () {}, + removeEventListener: function () {}, getElementById: function () { return null; }, body: { appendChild: function () {}, style: {} }, }; } - globalThis.addEventListener = function (name, cb) { addShared(name, cb); }; - globalThis.removeEventListener = function (name, cb) { removeShared(name, cb); }; + globalThis.addEventListener = function () {}; + globalThis.removeEventListener = function () {}; if (typeof globalThis.location === "undefined") { globalThis.location = { @@ -179,9 +156,9 @@ // ---- requestAnimationFrame pump ------------------------------------------ // WebGPUEngine.runRenderLoop schedules its frame via requestAnimationFrame. - // The native App.cpp frame loop calls globalThis.frame() once per iteration; - // we flush queued rAF callbacks (which run the engine's begin/render/end, - // submitting GPU work), then present. + // The Embedding View::RenderFrame calls globalThis.frame() once per host + // frame (on the JS thread); we flush queued rAF callbacks (which run the + // engine's begin/render/end, submitting GPU work), then present. var rafQueue = []; globalThis.requestAnimationFrame = function (cb) { rafQueue.push(cb); return rafQueue.length; }; globalThis.cancelAnimationFrame = function () {}; @@ -195,12 +172,9 @@ if (typeof _nativeDawnPresent === "function") { _nativeDawnPresent(); } }; - // The Window polyfill provides `window` (addEventListener no-op, atob, ...) - // but not the timer / animation methods and its addEventListener drops - // handlers. The scene scripts call window.setTimeout / window.requestAnimation - // Frame, and Babylon's input system attaches some listeners to window; forward - // timers to the globals (setTimeout via the Scheduling polyfill) and route - // window/global event registration into the shared registry. + // The Window polyfill provides `window` but not the timer / animation + // methods; forward them to the globals so scene scripts that call + // window.setTimeout / window.requestAnimationFrame work. if (globalThis.window) { var w = globalThis.window; try { w.setTimeout = globalThis.setTimeout; } catch (e) {} @@ -209,15 +183,13 @@ try { w.clearInterval = globalThis.clearInterval; } catch (e) {} try { w.requestAnimationFrame = globalThis.requestAnimationFrame; } catch (e) {} try { w.cancelAnimationFrame = globalThis.cancelAnimationFrame; } catch (e) {} - try { w.addEventListener = function (name, cb) { addShared(name, cb); }; } catch (e) {} - try { w.removeEventListener = function (name, cb) { removeShared(name, cb); }; } catch (e) {} - try { w.dispatchEvent = function (evt) { dispatchShared(evt); return true; }; } catch (e) {} + try { w.addEventListener = function () {}; } catch (e) {} + try { w.removeEventListener = function () {}; } catch (e) {} + try { w.dispatchEvent = function () { return true; }; } catch (e) {} } - // Advertise pointer/mouse/wheel event constructors. Babylon's - // Scene.simulatePointer* build events via `new PointerEvent(type, init)`, and - // its input system uses `window.PointerEvent` for feature detection. Provide - // real constructors that copy the init dictionary and expose the event API. + // Some Babylon input feature-detection reads window.PointerEvent etc. + // Provide inert constructors so detection succeeds without a DOM. function makeEventClass() { return function (type, init) { init = init || {}; @@ -228,7 +200,7 @@ if (typeof this.stopImmediatePropagation !== "function") { this.stopImmediatePropagation = function () {}; } }; } - globalThis.PointerEvent = makeEventClass(); + globalThis.PointerEvent = globalThis.PointerEvent || makeEventClass(); globalThis.MouseEvent = globalThis.MouseEvent || makeEventClass(); globalThis.WheelEvent = globalThis.WheelEvent || makeEventClass(); if (typeof globalThis.Event === "undefined") { globalThis.Event = makeEventClass(); } @@ -238,122 +210,68 @@ try { globalThis.window.WheelEvent = globalThis.WheelEvent; } catch (e) {} } - // ---- engine + scene load ------------------------------------------------- - var surf = (typeof _nativeDawnSurfaceSize === "function") ? _nativeDawnSurfaceSize() : { width: 1280, height: 720 }; - var canvas = makeCanvas(surf.width, surf.height); - globalThis.__dawnCanvas = canvas; - - log("creating WebGPUEngine " + surf.width + "x" + surf.height); - var engine = new BABYLON.WebGPUEngine(canvas, { - antialias: false, stencil: true, premultipliedAlpha: false, enableAllFeatures: false, - }); - engine.enableOfflineSupport = false; - engine.disableManifestCheck = true; - - // ---- native input + resize bridge --------------------------------------- - // Win32/App.cpp drains the message-thread pointer/resize queue each frame - // (on the JS thread) and calls these globals. Pointer input is injected - // through Scene.simulatePointer* which drives scene.onPointerObservable (and - // hence the camera controls) directly, independent of the DOM/device-input - // system selection (which doesn't attach in this no-NativeInput WebGPU host). - var pointerIdSeq = 1; - var lastX = 0; - var lastY = 0; - var PET = (typeof BABYLON !== "undefined" && BABYLON.PointerEventTypes) || {}; - function buildPointerEventInit(name, x, y, button, buttons, deltaY, movementX, movementY) { - return { - type: name, - clientX: x, clientY: y, - offsetX: x, offsetY: y, - x: x, y: y, pageX: x, pageY: y, screenX: x, screenY: y, - movementX: movementX, movementY: movementY, - pointerId: pointerIdSeq, - pointerType: "mouse", - button: button, - buttons: buttons, - deltaX: 0, deltaY: deltaY, deltaZ: 0, deltaMode: 0, - detail: 0, - ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, - preventDefault: function () {}, stopPropagation: function () {}, - stopImmediatePropagation: function () {}, - }; - } - globalThis.__dawnInput = function (type, x, y, button, buttons, deltaY) { - var sc = globalThis.__dawnEngine && globalThis.__dawnEngine.scenes && globalThis.__dawnEngine.scenes[0]; - if (!sc) { return; } - var movementX = x - lastX; - var movementY = y - lastY; - lastX = x; - lastY = y; - if (type === "pointerdown") { pointerIdSeq++; } - try { - var pick = new BABYLON.PickingInfo(); - if (type === "pointerdown") { - sc.simulatePointerDown(pick, buildPointerEventInit("pointerdown", x, y, button, buttons, deltaY, movementX, movementY)); - } else if (type === "pointermove") { - sc.simulatePointerMove(pick, buildPointerEventInit("pointermove", x, y, button, buttons, deltaY, movementX, movementY)); - } else if (type === "pointerup") { - sc.simulatePointerUp(pick, buildPointerEventInit("pointerup", x, y, button, buttons, deltaY, movementX, movementY)); - } else if (type === "wheel" && sc.onPointerObservable && PET.POINTERWHEEL !== undefined) { - var wevt = buildPointerEventInit("wheel", x, y, button, buttons, deltaY, movementX, movementY); - var pi = new BABYLON.PointerInfo(PET.POINTERWHEEL, wevt, pick); - sc.onPointerObservable.notifyObservers(pi, PET.POINTERWHEEL); - } - } catch (e) { - console.error("[dawn-bootstrap] input error: " + (e && e.stack ? e.stack : e)); + // ---- resize bridge ------------------------------------------------------- + // Embedding View::Resize dispatches NativeDawn::ResizeSurface then calls this + // (on the JS thread) so the engine's drawing buffer matches the surface. + var g_canvas = null; + globalThis.__dawnResize = function (width, height) { + if (g_canvas) { + g_canvas.width = width; + g_canvas.height = height; + g_canvas.clientWidth = width; + g_canvas.clientHeight = height; } - }; - - globalThis.__dawnResize = function (w, h) { - canvas.width = w; - canvas.height = h; - canvas.clientWidth = w; - canvas.clientHeight = h; if (globalThis.__dawnEngine) { - try { globalThis.__dawnEngine.setSize(w, h, true); } + try { globalThis.__dawnEngine.setSize(width, height, true); } catch (e) { console.error("[dawn-bootstrap] setSize error: " + (e && e.stack ? e.stack : e)); } } - // Some Babylon code listens for the window "resize" event to re-read size. - if (globalThis.window && typeof globalThis.window.dispatchEvent === "function") { - try { globalThis.window.dispatchEvent({ type: "resize" }); } catch (e) {} - } }; - function loadSceneSource() { - var path = globalThis.__sceneFsPath; - if (!path || typeof _nativeDawnReadFileBytes !== "function") { - return null; - } - var bytes = _nativeDawnReadFileBytes(path); - if (!bytes) { - return null; - } - return new TextDecoder("utf-8").decode(new Uint8Array(bytes)); - } + // ---- deferred WebGPUEngine creation -------------------------------------- + // This script runs before babylon.max.js, so BABYLON.WebGPUEngine does not + // exist yet. Hook the global BABYLON assignment (babylon.max.js's UMD does + // `global.BABYLON = factory()`); when it lands, pre-create + initialize a + // WebGPUEngine and alias BABYLON.NativeEngine so the scene scripts' + // synchronous `new BABYLON.NativeEngine()` returns the ready WebGPU engine. + var g_started = false; + function onBabylonReady(BABYLON) { + if (g_started || !BABYLON || !BABYLON.WebGPUEngine) { return; } + g_started = true; - engine.initAsync().then(function () { - globalThis.__dawnEngine = engine; - // The scene scripts construct their engine with `new BABYLON.NativeEngine()`; - // return the ready WebGPU engine instead so they run unmodified on WebGPU. - BABYLON.NativeEngine = function () { return globalThis.__dawnEngine; }; - log("WebGPUEngine ready (" + engine.getCaps().maxTextureSize + " max tex); running scene"); - var code = loadSceneSource(); - if (code === null) { - console.error("[dawn-bootstrap] could not read scene source at " + globalThis.__sceneFsPath); - return; - } - (0, eval)(code + "\n//# sourceURL=" + (globalThis.__sceneName || "scene.js") + "\n"); + var surf = (typeof _nativeDawnSurfaceSize === "function") ? _nativeDawnSurfaceSize() : { width: 1280, height: 720 }; + g_canvas = makeCanvas(surf.width, surf.height); + globalThis.__dawnCanvas = g_canvas; - // Ensure each scene's InputManager is attached so simulatePointer* input - // (from __dawnInput) is processed. The scene scripts attach camera - // controls but don't necessarily call scene.attachControl(). - setTimeout(function () { - (engine.scenes || []).forEach(function (sc) { - try { if (sc.attachControl) { sc.attachControl(); } } - catch (e) { console.error("[dawn-bootstrap] attachControl error: " + e); } - }); - }, 0); - }).catch(function (e) { - console.error("[dawn-bootstrap] init/scene load failed: " + (e && e.stack ? e.stack : e)); - }); + log("creating WebGPUEngine " + surf.width + "x" + surf.height); + var engine = new BABYLON.WebGPUEngine(g_canvas, { + antialias: false, stencil: true, premultipliedAlpha: false, enableAllFeatures: false, + }); + engine.enableOfflineSupport = false; + engine.disableManifestCheck = true; + + engine.initAsync().then(function () { + globalThis.__dawnEngine = engine; + // Scene scripts construct their engine with `new BABYLON.NativeEngine()`; + // return the ready WebGPU engine instead so they run unmodified. + BABYLON.NativeEngine = function () { return globalThis.__dawnEngine; }; + log("WebGPUEngine ready (" + engine.getCaps().maxTextureSize + " max tex)"); + }).catch(function (e) { + console.error("[dawn-bootstrap] engine init failed: " + (e && e.stack ? e.stack : e)); + }); + } + + var _BABYLON = globalThis.BABYLON; + if (_BABYLON && _BABYLON.WebGPUEngine) { + onBabylonReady(_BABYLON); + } else { + Object.defineProperty(globalThis, "BABYLON", { + configurable: true, + get: function () { return _BABYLON; }, + set: function (v) { + _BABYLON = v; + try { onBabylonReady(v); } + catch (e) { console.error("[dawn-bootstrap] onBabylonReady error: " + (e && e.stack ? e.stack : e)); } + }, + }); + } })(); diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 523b61943..aa77ad878 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,444 +1,13 @@ // App.cpp : Defines the entry point for the application. // -// Two mutually-exclusive build configurations, selected by the CMake option -// BABYLON_NATIVE_PLUGIN_NATIVEDAWN: +// Single, backend-agnostic Win32 host built on Babylon::Embedding. The +// cross-platform Runtime + View API handles plugin/polyfill setup, GPU device +// construction, frame rendering, resize and input forwarding. // -// * Default (bgfx / NativeEngine): built on Babylon::Embedding — the -// cross-platform Runtime + View API handles plugin/polyfill setup, GPU -// device construction, frame rendering, and input forwarding. -// -// * NativeDawn (WebGPU via Dawn): the Embedding View always constructs the -// bgfx Graphics device, which this backend replaces, so the Dawn host -// below bypasses Embedding and drives a direct AppRuntime + ScriptLoader + -// NativeDawn pipeline. It installs navigator.gpu, loads the same Babylon -// bootstrap scripts, then runs the default scene (experience.js) on -// WebGPU via dawn_bootstrap.js. - -#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN - -#define WIN32_LEAN_AND_MEAN -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#include -#include -#pragma comment(lib, "Shlwapi.lib") -#pragma comment(lib, "Shell32.lib") - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - std::optional g_runtime; - std::optional g_loader; - std::atomic g_frameInFlight{false}; - HWND g_hwnd{}; - - // Win32 input + resize are produced on the host (message) thread and consumed - // on the JS thread once per frame. A small mutex-protected queue marshals - // pointer events; resize is coalesced to the latest pending size. - struct PointerEvent - { - std::string type; // "pointerdown" | "pointermove" | "pointerup" | "wheel" - int x = 0; - int y = 0; - int button = -1; // DOM button: 0 left, 1 middle, 2 right, -1 none - int buttons = 0; // DOM buttons bitmask: 1 left, 2 right, 4 middle - double deltaY = 0.0; - }; - std::mutex g_inputMutex; - std::vector g_pointerEvents; - bool g_resizePending = false; - uint32_t g_pendingWidth = 0; - uint32_t g_pendingHeight = 0; - - int DomButtonsFromWParam(WPARAM wParam) - { - int buttons = 0; - if (wParam & MK_LBUTTON) buttons |= 1; - if (wParam & MK_RBUTTON) buttons |= 2; - if (wParam & MK_MBUTTON) buttons |= 4; - return buttons; - } - - void PushPointerEvent(const char* type, int x, int y, int button, int buttons, double deltaY) - { - std::lock_guard lock{g_inputMutex}; - g_pointerEvents.push_back(PointerEvent{type, x, y, button, buttons, deltaY}); - } - - std::filesystem::path ExeDir() - { - wchar_t buf[MAX_PATH]{}; - ::GetModuleFileNameW(nullptr, buf, MAX_PATH); - return std::filesystem::path(buf).parent_path(); - } - - std::string FileUrl(const std::filesystem::path& path) - { - char url[2048]; - DWORD length = ARRAYSIZE(url); - if (FAILED(::UrlCreateFromPathA( - reinterpret_cast(path.u8string().c_str()), url, &length, 0))) - { - return {}; - } - return std::string(url, length); - } - - // JSON/JS-escape a Windows filesystem path (backslashes) so it can be - // embedded in a double-quoted JS string literal. - std::string JsEscapePath(const std::filesystem::path& path) - { - std::string s = path.string(); - std::string out; - for (char c : s) - { - if (c == '\\' || c == '"') out += '\\'; - out += c; - } - return out; - } - - LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) - { - switch (message) - { - case WM_DESTROY: - ::PostQuitMessage(0); - return 0; - - case WM_SIZE: - { - if (wParam != SIZE_MINIMIZED) - { - std::lock_guard lock{g_inputMutex}; - g_pendingWidth = static_cast(LOWORD(lParam)); - g_pendingHeight = static_cast(HIWORD(lParam)); - g_resizePending = true; - } - return 0; - } - - case WM_MOUSEMOVE: - PushPointerEvent("pointermove", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - -1, DomButtonsFromWParam(wParam), 0.0); - return 0; - - case WM_LBUTTONDOWN: - ::SetCapture(hWnd); - PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 0, DomButtonsFromWParam(wParam), 0.0); - return 0; - case WM_LBUTTONUP: - ::ReleaseCapture(); - PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 0, DomButtonsFromWParam(wParam), 0.0); - return 0; - - case WM_RBUTTONDOWN: - ::SetCapture(hWnd); - PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 2, DomButtonsFromWParam(wParam), 0.0); - return 0; - case WM_RBUTTONUP: - ::ReleaseCapture(); - PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 2, DomButtonsFromWParam(wParam), 0.0); - return 0; - - case WM_MBUTTONDOWN: - ::SetCapture(hWnd); - PushPointerEvent("pointerdown", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 1, DomButtonsFromWParam(wParam), 0.0); - return 0; - case WM_MBUTTONUP: - ::ReleaseCapture(); - PushPointerEvent("pointerup", GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), - 1, DomButtonsFromWParam(wParam), 0.0); - return 0; - - case WM_MOUSEWHEEL: - { - // Wheel coords are in screen space; convert to client space. - POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; - ::ScreenToClient(hWnd, &pt); - const int delta = GET_WHEEL_DELTA_WPARAM(wParam); - // DOM wheel deltaY is positive when scrolling down (away from user), - // opposite sign to Win32's WHEEL_DELTA. - PushPointerEvent("wheel", pt.x, pt.y, -1, 0, -static_cast(delta)); - return 0; - } - - default: - return ::DefWindowProcW(hWnd, message, wParam, lParam); - } - } -} - -int APIENTRY wWinMain(_In_ HINSTANCE hInstance, - _In_opt_ HINSTANCE, - _In_ LPWSTR, - _In_ int nCmdShow) -{ - ::SetConsoleOutputCP(CP_UTF8); - std::setvbuf(stdout, nullptr, _IONBF, 0); - std::setvbuf(stderr, nullptr, _IONBF, 0); - - Diagnostics::Initialize(); - std::fprintf(stderr, "[Playground] starting (NativeDawn / WebGPU, no bgfx)\n"); - - // Window. - WNDCLASSEXW wc{}; - wc.cbSize = sizeof(wc); - wc.lpfnWndProc = WndProc; - wc.hInstance = hInstance; - wc.hCursor = ::LoadCursor(nullptr, IDC_ARROW); - wc.lpszClassName = L"PlaygroundDawnWindow"; - ::RegisterClassExW(&wc); - - const uint32_t width = 1280; - const uint32_t height = 720; - g_hwnd = ::CreateWindowW(wc.lpszClassName, L"BabylonNative Playground (NativeDawn / WebGPU)", - WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, - static_cast(width), static_cast(height), - nullptr, nullptr, hInstance, nullptr); - if (!g_hwnd) - { - std::fprintf(stderr, "[Playground] CreateWindow failed\n"); - return 1; - } - ::ShowWindow(g_hwnd, nCmdShow); - ::UpdateWindow(g_hwnd); - - RECT rc{}; - ::GetClientRect(g_hwnd, &rc); - const uint32_t clientW = static_cast(rc.right - rc.left); - const uint32_t clientH = static_cast(rc.bottom - rc.top); - - // Runtime (V8). Uncaught JS exceptions go to the diagnostics banner. - Babylon::AppRuntime::Options runtimeOptions{}; - runtimeOptions.UnhandledExceptionHandler = [](const Napi::Error& error) { - const std::string message = Napi::GetErrorString(error); - Diagnostics::DumpFailure("UNCAUGHT JS ERROR", nullptr, 0, 0, "%s", message.c_str()); - Diagnostics::SetExitCode(1); - Diagnostics::PrintFinishLine(); - std::quick_exit(1); - }; - g_runtime.emplace(std::move(runtimeOptions)); - g_loader.emplace(*g_runtime); - - // Install polyfills + NativeDawn on the JS thread (the Dawn device is - // created here, on the JS thread, so all later Dawn calls stay on it). - void* hwnd = g_hwnd; - g_loader->Dispatch([hwnd, clientW, clientH](Napi::Env env) { - // The JS thread needs a properly winrt-initialized apartment for - // UrlLib's XMLHttpRequest backend (Windows.Foundation.Uri / HTTP); - // the bare CoInitializeEx done by AppRuntime is not sufficient. - try - { - winrt::init_apartment(winrt::apartment_type::single_threaded); - } - catch (const winrt::hresult_error&) - { - } - - Babylon::Polyfills::Console::Initialize(env, - [](const char* message, Babylon::Polyfills::Console::LogLevel level) { - std::FILE* out = level == Babylon::Polyfills::Console::LogLevel::Error ? stderr : stdout; - std::fprintf(out, "%s", message ? message : ""); - }); - // Pre-define devicePixelRatio so the Window polyfill's Graphics(bgfx)- - // backed accessor isn't the only source (it throws without a device). - env.Global().Set("devicePixelRatio", Napi::Number::New(env, 1)); - Babylon::Polyfills::Window::Initialize(env); - Babylon::Polyfills::Scheduling::Initialize(env); - Babylon::Polyfills::Performance::Initialize(env); - Babylon::Polyfills::TextDecoder::Initialize(env); - Babylon::Polyfills::TextEncoder::Initialize(env); - Babylon::Polyfills::Blob::Initialize(env); - Babylon::Polyfills::File::Initialize(env); - Babylon::Polyfills::URL::Initialize(env); - Babylon::Polyfills::AbortController::Initialize(env); - Babylon::Polyfills::XMLHttpRequest::Initialize(env); - Babylon::Polyfills::Fetch::Initialize(env); - Babylon::Plugins::NativeDawn::Initialize(env, hwnd, clientW, clientH); - }); - - // Resolve the scene script: default experience.js, or the first command-line - // argument (a filesystem path) if provided. - const std::filesystem::path scripts = ExeDir() / "Scripts"; - std::filesystem::path scenePath = scripts / "experience.js"; - { - int argc = 0; - LPWSTR* argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv != nullptr) - { - if (argc > 1) - { - std::filesystem::path arg(argv[1]); - scenePath = arg.is_relative() ? (scripts / arg.filename()) : arg; - } - ::LocalFree(argv); - } - } - - // Load the Babylon bootstrap scripts (same set as the Embedding Playground), - // skipping any that aren't staged. recast.js is intentionally omitted (asm.js - // incompatible with v8jsi). - const char* bootstrap[] = { - "ammo.js", - "babylon.max.js", - "babylonjs.addons.js", - "babylonjs.loaders.js", - "babylonjs.materials.js", - "babylon.gui.js", - "meshwriter.min.js", - "babylonjs.serializers.js", - }; - for (const char* name : bootstrap) - { - const std::filesystem::path p = scripts / name; - if (std::filesystem::exists(p)) - { - g_loader->LoadScript(FileUrl(p)); - } - } - - // Expose roots + the scene path (read natively by dawn_bootstrap.js so it can - // eval the scene once the WebGPU engine is ready). - { - std::string scriptsRoot = FileUrl(scripts); - if (!scriptsRoot.empty() && scriptsRoot.back() != '/') scriptsRoot += '/'; - std::string js = "globalThis.__scriptsRoot = \"" + scriptsRoot + "\";\n" - "globalThis.__sceneFsPath = \"" + JsEscapePath(scenePath) + "\";\n" - "globalThis.__sceneName = \"" + scenePath.filename().string() + "\";\n"; - g_loader->Eval(js, "Playground-dawn-roots.js"); - } - - // dawn_bootstrap.js pre-inits WebGPUEngine, aliases BABYLON.NativeEngine, and - // evaluates the scene script once ready. - g_loader->LoadScript(FileUrl(scripts / "dawn_bootstrap.js")); - - // Main loop: pump Win32 messages and dispatch one JS frame at a time - // (throttled so the JS queue doesn't flood). The frame callback runs on the - // JS thread, calls the JS frame() (which flushes requestAnimationFrame - // callbacks and presents) and ticks Dawn. - MSG msg{}; - while (msg.message != WM_QUIT) - { - if (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) - { - ::TranslateMessage(&msg); - ::DispatchMessageW(&msg); - continue; - } - - if (!g_frameInFlight.exchange(true)) - { - g_loader->Dispatch([](Napi::Env env) { - // Apply a coalesced resize (from WM_SIZE) before rendering so the - // Dawn swapchain and the engine's back buffer match this frame. - bool resized = false; - uint32_t newW = 0; - uint32_t newH = 0; - std::vector events; - { - std::lock_guard lock{g_inputMutex}; - if (g_resizePending) - { - g_resizePending = false; - resized = true; - newW = g_pendingWidth; - newH = g_pendingHeight; - } - events.swap(g_pointerEvents); - } - - if (resized) - { - Babylon::Plugins::NativeDawn::ResizeSurface(newW, newH); - Napi::Value resizeFn = env.Global().Get("__dawnResize"); - if (resizeFn.IsFunction()) - { - resizeFn.As().Call({ - Napi::Number::New(env, newW), - Napi::Number::New(env, newH)}); - } - } - - if (!events.empty()) - { - Napi::Value inputFn = env.Global().Get("__dawnInput"); - if (inputFn.IsFunction()) - { - Napi::Function fn = inputFn.As(); - for (const PointerEvent& e : events) - { - fn.Call({ - Napi::String::New(env, e.type), - Napi::Number::New(env, e.x), - Napi::Number::New(env, e.y), - Napi::Number::New(env, e.button), - Napi::Number::New(env, e.buttons), - Napi::Number::New(env, e.deltaY)}); - } - } - } - - Napi::Value frame = env.Global().Get("frame"); - if (frame.IsFunction()) - { - frame.As().Call({}); - } - Babylon::Plugins::NativeDawn::Tick(env); - g_frameInFlight.store(false); - }); - } - else - { - ::Sleep(1); - } - } - - g_loader.reset(); - g_runtime.reset(); - Diagnostics::SetExitCode(static_cast(msg.wParam)); - Diagnostics::PrintFinishLine(); - return static_cast(msg.wParam); -} - -#else // BABYLON_NATIVE_PLUGIN_NATIVEDAWN +// The graphics backend is selected at CMake configure time and is invisible +// here: BABYLON_NATIVE_PLUGIN_NATIVEENGINE (bgfx) or BABYLON_NATIVE_PLUGIN_ +// NATIVEDAWN (WebGPU via Dawn). The only place that differs is the plugin +// Initialize call inside the Embedding layer's RunFirstAttachInit. #include "App.h" @@ -994,5 +563,3 @@ INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) } return (INT_PTR)FALSE; } - -#endif // BABYLON_NATIVE_PLUGIN_NATIVEDAWN diff --git a/Embedding/CMakeLists.txt b/Embedding/CMakeLists.txt index 20715f0bb..45e2231de 100644 --- a/Embedding/CMakeLists.txt +++ b/Embedding/CMakeLists.txt @@ -87,6 +87,15 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) target_link_libraries(Embedding PRIVATE NativeEngine) endif() +# NativeDawn (experimental WebGPU-via-Dawn backend). Mutually exclusive with +# NativeEngine (the root CMake forces NativeEngine OFF when this is ON). When +# enabled, the View/Runtime route the graphics-device lifecycle to the NativeDawn +# plugin instead of the bgfx Graphics::Device. +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEDAWN=1) + target_link_libraries(Embedding PRIVATE NativeDawn) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEINPUT=1) target_link_libraries(Embedding PRIVATE NativeInput) diff --git a/Embedding/Source/Runtime.cpp b/Embedding/Source/Runtime.cpp index 852528288..b23efe43f 100644 --- a/Embedding/Source/Runtime.cpp +++ b/Embedding/Source/Runtime.cpp @@ -5,6 +5,9 @@ #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN +#include +#endif #if BABYLON_NATIVE_PLUGIN_NATIVECAMERA #include #endif @@ -171,7 +174,7 @@ namespace Babylon::Embedding // completes m_initTcs to unblock host calls that were queued before the // first attach. Post-init, those host calls fire their continuation // synchronously via inline_scheduler and submit straight to ScriptLoader. - void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) + void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window, uint32_t width, uint32_t height) { #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Enable + hydrate before any JS-thread shader compilation. Both @@ -181,7 +184,7 @@ namespace Babylon::Embedding LoadShaderCache(); #endif - m_appRuntime->Dispatch([implPtr = this, window](Napi::Env env) { + m_appRuntime->Dispatch([implPtr = this, window, width, height](Napi::Env env) { // 0. Install the ES2020 `globalThis` self-reference. V8/JSC/Chakra // provide it intrinsically, but the embedded Hermes runtime does // not, and Hermes evaluates eval()'d code as indirect (global @@ -190,7 +193,9 @@ namespace Babylon::Embedding env.Global().Set("globalThis", env.Global()); // 1. Make the Device available to JS. +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE implPtr->m_device->AddToJavaScript(env); +#endif // 2. Polyfills (always-on). Babylon::Polyfills::Blob::Initialize(env); @@ -269,6 +274,12 @@ namespace Babylon::Embedding #endif #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE Babylon::Plugins::NativeEngine::Initialize(env); +#elif BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn replaces the bgfx NativeEngine: it creates the Dawn + // (WebGPU) device + surface bound to `window`, installs navigator.gpu + // and the WebGPU globals, and (via its bootstrap) drives the scene on + // a WebGPUEngine. `width`/`height` are the initial surface size. + Babylon::Plugins::NativeDawn::Initialize(env, window, width, height); #endif #if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS Babylon::Plugins::NativeOptimizations::Initialize(env); @@ -298,11 +309,21 @@ namespace Babylon::Embedding }); } #endif -#if BABYLON_NATIVE_PLUGIN_TESTUTILS +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // TestUtils is bgfx-only (its constructor acquires the bgfx + // Graphics::DeviceContext, which the NativeDawn backend does not + // create). `window`/`width`/`height` are consumed by + // NativeDawn::Initialize above. +#elif BABYLON_NATIVE_PLUGIN_TESTUTILS Babylon::Plugins::TestUtils::Initialize(env, window); #else (void)window; #endif +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // Initial surface size is only consumed by the NativeDawn plugin init. + (void)width; + (void)height; +#endif // 4. Fire any host calls queued before the first View attach. implPtr->m_initTcs.complete(); diff --git a/Embedding/Source/RuntimeImpl.h b/Embedding/Source/RuntimeImpl.h index 6f2260b3b..2395b9638 100644 --- a/Embedding/Source/RuntimeImpl.h +++ b/Embedding/Source/RuntimeImpl.h @@ -19,6 +19,10 @@ #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN +#include +#endif + #include #include @@ -50,6 +54,16 @@ namespace Babylon::Embedding std::optional m_device; std::optional m_deviceUpdate; +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn (WebGPU) backend state. The bgfx Graphics::Device above is + // never constructed in this configuration; the NativeDawn plugin owns + // the device/surface/present lifecycle instead. m_dawnInitialized latches + // the first-attach init; m_dawnFrameInFlight throttles the per-frame + // JS-thread dispatch (host RenderFrame → JS frame() + NativeDawn::Tick). + bool m_dawnInitialized{false}; + std::atomic m_dawnFrameInFlight{false}; +#endif + #if BABYLON_NATIVE_POLYFILL_CANVAS std::optional m_canvas; #endif @@ -96,8 +110,10 @@ namespace Babylon::Embedding // Dispatched onto the JS thread by the very first View attach. // Runs all plugin/polyfill Initialize calls, wires NativeXr // callbacks, and completes m_initTcs. `window` is forwarded to - // TestUtils::Initialize; ignored otherwise. - void RunFirstAttachInit(Babylon::Graphics::WindowT window); + // TestUtils::Initialize (and, for NativeDawn, to the plugin's + // Initialize along with the initial surface `width`/`height`); + // ignored by the bgfx backend otherwise. + void RunFirstAttachInit(Babylon::Graphics::WindowT window, uint32_t width, uint32_t height); #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Persistent shader cache. No-ops when `m_options.shaderCachePath` diff --git a/Embedding/Source/View.cpp b/Embedding/Source/View.cpp index 916afc9d0..985403561 100644 --- a/Embedding/Source/View.cpp +++ b/Embedding/Source/View.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -41,6 +43,19 @@ namespace Babylon::Embedding throw std::runtime_error{std::string{operation} + " requires non-zero width and height."}; } } + + // Current device-pixel ratio: the bgfx Graphics::Device's stored value + // when present, otherwise the window's (the NativeDawn backend has no + // Graphics::Device, so input/coordinate conversion falls back to the + // window DPR). Only referenced by the NativeInput forwarding methods. + [[maybe_unused]] float CurrentDpr(RuntimeImpl& impl, Babylon::Graphics::WindowT window) + { + if (impl.m_device) + { + return impl.m_device->GetDevicePixelRatio(); + } + return Babylon::Graphics::GetDevicePixelRatio(window); + } } View::View(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) @@ -62,6 +77,10 @@ namespace Babylon::Embedding ViewImpl::~ViewImpl() { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn presents each frame inside its per-frame Tick, so there is + // no host-held in-flight frame to close on teardown. +#else // Close the in-flight frame iff one is currently open: i.e. the // view is initialized and the Runtime is not externally suspended // (Suspend already closed the frame). The Device persists on the @@ -71,6 +90,7 @@ namespace Babylon::Embedding { m_runtime.m_device->FinishRenderingCurrentFrame(); } +#endif m_runtime.m_currentView = nullptr; } @@ -86,7 +106,9 @@ namespace Babylon::Embedding { return; } +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN m_runtime.m_device->FinishRenderingCurrentFrame(); +#endif } void ViewImpl::Resume() @@ -98,7 +120,9 @@ namespace Babylon::Embedding InitializeIfReady(); return; } +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN m_runtime.m_device->StartRenderingCurrentFrame(); +#endif } // Single recipe for binding the Device to this view's window and @@ -117,6 +141,28 @@ namespace Babylon::Embedding } const auto [lw, lh] = *m_size; +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn owns the device/surface lifecycle; there is no bgfx + // Graphics::Device to construct here. The plugin's Initialize (creating + // the Dawn device + surface bound to m_window at the current size) runs + // inside RunFirstAttachInit on the JS thread. + const bool firstAttachEver = !m_runtime.m_dawnInitialized; + if (!firstAttachEver) + { + // Re-attach to an existing Runtime: reconfigure the Dawn surface to + // the current size on the JS thread (where the Dawn device lives). + m_runtime.m_appRuntime->Dispatch([lw, lh](Napi::Env) { + Babylon::Plugins::NativeDawn::ResizeSurface(lw, lh); + }); + } + m_initialized = true; + + if (firstAttachEver) + { + m_runtime.m_dawnInitialized = true; + m_runtime.RunFirstAttachInit(m_window, lw, lh); + } +#else const bool firstAttachEver = !m_runtime.m_device; if (firstAttachEver) { @@ -146,8 +192,9 @@ namespace Babylon::Embedding if (firstAttachEver) { - m_runtime.RunFirstAttachInit(m_window); + m_runtime.RunFirstAttachInit(m_window, lw, lh); } +#endif } void View::RenderFrame() @@ -169,10 +216,29 @@ namespace Babylon::Embedding return; } +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // WebGPUEngine drives its render loop from requestAnimationFrame, pumped + // by the JS-thread global frame(). Dispatch one frame at a time (throttled + // so the JS queue can't flood): flush rAF (runs the engine's render + + // GPU submit) and present via NativeDawn::Tick. + if (!impl.m_dawnFrameInFlight.exchange(true)) + { + impl.m_appRuntime->Dispatch([implPtr = &impl](Napi::Env env) { + Napi::Value frame = env.Global().Get("frame"); + if (frame.IsFunction()) + { + frame.As().Call({}); + } + Babylon::Plugins::NativeDawn::Tick(env); + implPtr->m_dawnFrameInFlight.store(false, std::memory_order_relaxed); + }); + } +#else // Babylon's JS render loop runs between Start and Finish, scheduled // via DeviceUpdate's SafeTimespanGuarantor onto the JS thread. impl.m_device->FinishRenderingCurrentFrame(); impl.m_device->StartRenderingCurrentFrame(); +#endif } // Stores the host-supplied size on the ViewImpl, then either pushes @@ -187,10 +253,21 @@ namespace Babylon::Embedding // DPR source: before init, query the window directly (Device // doesn't exist or is still bound to the previous attach). After - // init, the Device's stored DPR is authoritative. - const float dpr = !m_impl->m_initialized - ? Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window) - : impl.m_device->GetDevicePixelRatio(); + // init, the Device's stored DPR is authoritative (bgfx). NativeDawn + // has no Device, so keep querying the window. + float dpr; + if (!m_impl->m_initialized) + { + dpr = Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window); + } + else + { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + dpr = Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window); +#else + dpr = impl.m_device->GetDevicePixelRatio(); +#endif + } const auto [lw, lh] = ToLogicalSize(width, height, units, dpr); ValidateNonZeroSize(lw, lh, "View::Resize logical size"); @@ -198,7 +275,22 @@ namespace Babylon::Embedding if (m_impl->m_initialized) { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // Reconfigure the Dawn surface and the JS engine's drawing buffer on + // the JS thread (where the Dawn device lives). + impl.m_appRuntime->Dispatch([lw, lh](Napi::Env env) { + Babylon::Plugins::NativeDawn::ResizeSurface(lw, lh); + Napi::Value resizeFn = env.Global().Get("__dawnResize"); + if (resizeFn.IsFunction()) + { + resizeFn.As().Call({ + Napi::Number::New(env, lw), + Napi::Number::New(env, lh)}); + } + }); +#else impl.m_device->UpdateSize(lw, lh); +#endif } else { @@ -212,10 +304,10 @@ namespace Babylon::Embedding void View::OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchDown(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -225,10 +317,10 @@ namespace Babylon::Embedding void View::OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchMove(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -238,10 +330,10 @@ namespace Babylon::Embedding void View::OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchUp(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -251,10 +343,10 @@ namespace Babylon::Embedding void View::OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseDown(buttonIndex, static_cast(lx), static_cast(ly)); @@ -264,10 +356,10 @@ namespace Babylon::Embedding void View::OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseUp(buttonIndex, static_cast(lx), static_cast(ly)); @@ -277,10 +369,10 @@ namespace Babylon::Embedding void View::OnMouseMove(float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseMove(static_cast(lx), static_cast(ly)); } diff --git a/Plugins/NativeDawn/Source/NativeDawn.cpp b/Plugins/NativeDawn/Source/NativeDawn.cpp index bbd33cd65..b66ca874a 100644 --- a/Plugins/NativeDawn/Source/NativeDawn.cpp +++ b/Plugins/NativeDawn/Source/NativeDawn.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -2657,6 +2659,59 @@ namespace Babylon::Plugins::NativeDawn } } // namespace (webgpu) + namespace + { + // Locate the directory containing the running executable so the plugin can + // read Scripts/dawn_bootstrap.js staged next to it by the build. + std::filesystem::path ExeDir() + { +#if defined(_WIN32) + wchar_t buf[MAX_PATH]{}; + ::GetModuleFileNameW(nullptr, buf, MAX_PATH); + return std::filesystem::path(buf).parent_path(); +#else + return std::filesystem::current_path(); +#endif + } + + // Evaluate the Dawn bootstrap glue (dawn_bootstrap.js) at global scope. + // Runs before babylon.max.js and the scene scripts: it installs the no-DOM + // canvas/image shims, the requestAnimationFrame pump + globalThis.frame(), + // the __dawnResize hook, and a deferred WebGPUEngine creation that aliases + // BABYLON.NativeEngine once babylon.max.js defines BABYLON. Evaluating via a + // non-identifier reference to `eval` is an indirect eval, so the script runs + // in the global scope (globalThis assignments and the BABYLON accessor hook + // take effect globally). + void InstallBootstrap(Napi::Env env) + { + const std::filesystem::path scriptPath = ExeDir() / "Scripts" / "dawn_bootstrap.js"; + std::ifstream stream{scriptPath, std::ios::binary}; + if (!stream.good()) + { + std::fprintf(stderr, "[NativeDawn] bootstrap not found: %s\n", + scriptPath.string().c_str()); + return; + } + std::string source{std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; + source += "\n//# sourceURL=dawn_bootstrap.js\n"; + + Napi::Value evalVal = env.Global().Get("eval"); + if (!evalVal.IsFunction()) + { + std::fprintf(stderr, "[NativeDawn] global eval unavailable; cannot run bootstrap\n"); + return; + } + try + { + evalVal.As().Call({Napi::String::New(env, source)}); + } + catch (const Napi::Error& e) + { + std::fprintf(stderr, "[NativeDawn] bootstrap eval threw: %s\n", e.what()); + } + } + } // namespace (bootstrap) + void Initialize(Napi::Env env, void* window, uint32_t width, uint32_t height) { if (!CreateDeviceAndSurface(window, width, height)) @@ -2679,6 +2734,7 @@ namespace Babylon::Plugins::NativeDawn if (g_state.ready) { InstallWebGPU(env); + InstallBootstrap(env); } } diff --git a/Polyfills/Window/Source/Window.cpp b/Polyfills/Window/Source/Window.cpp index faa99a764..408dcfff0 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -92,7 +92,19 @@ namespace Babylon::Polyfills::Internal Napi::Value Window::GetDevicePixelRatio(const Napi::CallbackInfo& info) { auto env{info.Env()}; - return Napi::Value::From(env, Graphics::DeviceContext::GetFromJavaScript(env).GetDevicePixelRatio()); + try + { + return Napi::Value::From(env, Graphics::DeviceContext::GetFromJavaScript(env).GetDevicePixelRatio()); + } + catch (const Napi::Error&) + { + // No graphics device context is registered (e.g. the NativeDawn / + // WebGPU backend, which does not create a bgfx Graphics device). + // Default to a 1.0 device-pixel ratio so windowing consumers — e.g. + // WebGPUEngine's constructor, which reads devicePixelRatio — work + // without a bgfx device rather than throwing "Invalid argument". + return Napi::Value::From(env, 1.0f); + } } } From a760f91ed6a11eeb655e99a4e40ecf0f82313c0d Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:09:48 +0200 Subject: [PATCH 07/11] bootstrap --- Apps/Playground/Scripts/dawn_bootstrap.js | 277 ----------- Plugins/NativeDawn/Source/NativeDawn.cpp | 581 ++++++++++++++++++++-- 2 files changed, 545 insertions(+), 313 deletions(-) delete mode 100644 Apps/Playground/Scripts/dawn_bootstrap.js diff --git a/Apps/Playground/Scripts/dawn_bootstrap.js b/Apps/Playground/Scripts/dawn_bootstrap.js deleted file mode 100644 index 94fccd797..000000000 --- a/Apps/Playground/Scripts/dawn_bootstrap.js +++ /dev/null @@ -1,277 +0,0 @@ -// dawn_bootstrap.js — makes the standard Playground scene scripts (experience.js -// and friends, written against BABYLON.NativeEngine) run on the Dawn/WebGPU -// backend provided by the NativeDawn plugin. -// -// This script is evaluated by the NativeDawn plugin (Babylon::Plugins:: -// NativeDawn::Initialize) at global scope, BEFORE babylon.max.js and the scene -// scripts load. The native side has already installed navigator.gpu, -// _nativeDawnGetContext/Present/SurfaceSize, _nativeDawnDecodeImage, -// _nativeDawnReadFileBytes, and the standard polyfills. This script: -// * installs the no-DOM canvas / image shims WebGPUEngine needs, -// * drives requestAnimationFrame from the native per-frame globalThis.frame() -// (pumped by the Embedding View::RenderFrame), -// * installs a __dawnResize hook (called by Embedding View::Resize), -// * defers WebGPUEngine creation until babylon.max.js has defined BABYLON, -// then pre-creates + initializes a WebGPUEngine and aliases -// BABYLON.NativeEngine so the scene scripts' synchronous -// `new BABYLON.NativeEngine()` returns the ready WebGPU engine. -// -// Input is NOT handled here: it flows through the generic Win32 host -// (App.cpp WndProc -> Embedding View::OnMouse* -> NativeInput -> -// _native.DeviceInputSystem), identical to the bgfx/NativeEngine backend. - -(function () { - "use strict"; - - function log() { - console.log("[dawn-bootstrap] " + Array.prototype.join.call(arguments, " ")); - } - - // ---- no-DOM canvas / DOM shims ------------------------------------------ - function makeCanvas(width, height) { - var canvas = { - width: width, - height: height, - clientWidth: width, - clientHeight: height, - style: {}, - getContext: function (type) { - if (type === "webgpu") { - return _nativeDawnGetContext(); - } - return null; - }, - getBoundingClientRect: function () { - return { x: 0, y: 0, left: 0, top: 0, right: this.width, bottom: this.height, width: this.width, height: this.height }; - }, - setAttribute: function () {}, - removeAttribute: function () {}, - addEventListener: function () {}, - removeEventListener: function () {}, - dispatchEvent: function () { return true; }, - setPointerCapture: function () {}, - releasePointerCapture: function () {}, - hasPointerCapture: function () { return false; }, - focus: function () {}, - getRootNode: function () { return this; }, - }; - return canvas; - } - - if (typeof globalThis.document === "undefined") { - globalThis.document = { - createElement: function (tag) { - if (tag === "canvas") { return makeCanvas(1280, 720); } - if (tag === "img") { return new globalThis.Image(); } - return { style: {}, setAttribute: function () {}, appendChild: function () {} }; - }, - addEventListener: function () {}, - removeEventListener: function () {}, - getElementById: function () { return null; }, - body: { appendChild: function () {}, style: {} }, - }; - } - globalThis.addEventListener = function () {}; - globalThis.removeEventListener = function () {}; - - if (typeof globalThis.location === "undefined") { - globalThis.location = { - href: "file:///", origin: "file://", protocol: "file:", host: "", hostname: "", - pathname: "/", search: "", hash: "", - }; - } - - // ---- image decoding via the native bimg decoder -------------------------- - (function installImageShim() { - var blobRegistry = {}; - var blobSeq = 0; - if (typeof URL !== "undefined") { - if (typeof URL.createObjectURL !== "function") { - URL.createObjectURL = function (blob) { - var id = "blob:nativedawn/" + (++blobSeq); - blobRegistry[id] = blob; - return id; - }; - } - if (typeof URL.revokeObjectURL !== "function") { - URL.revokeObjectURL = function (url) { delete blobRegistry[url]; }; - } - } - function toArrayBuffer(src) { - if (src instanceof ArrayBuffer) return Promise.resolve(src); - if (ArrayBuffer.isView(src)) return Promise.resolve(src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)); - if (src && typeof src.arrayBuffer === "function") return src.arrayBuffer(); - if (typeof src === "string") { - var blob = blobRegistry[src]; - if (blob) return toArrayBuffer(blob); - return fetch(src).then(function (r) { return r.arrayBuffer(); }); - } - return Promise.reject(new Error("toArrayBuffer: unsupported source")); - } - if (typeof globalThis.createImageBitmap === "undefined") { - globalThis.createImageBitmap = function (src) { - return Promise.resolve().then(function () { - if (src && src.__pixels) return src; - return toArrayBuffer(src).then(function (ab) { - if (!ab || ab.byteLength === 0) { - return { width: 1, height: 1, __pixels: new ArrayBuffer(4), close: function () {} }; - } - var bmp = _nativeDawnDecodeImage(ab); - bmp.close = function () {}; - return bmp; - }); - }); - }; - } - function ImageShim() { - var img = { - width: 0, height: 0, naturalWidth: 0, naturalHeight: 0, - __pixels: null, onload: null, onerror: null, crossOrigin: null, - complete: false, _src: "", - decode: function () { return Promise.resolve(); }, - addEventListener: function (name, cb) { if (name === "load") img.onload = cb; else if (name === "error") img.onerror = cb; }, - removeEventListener: function () {}, - }; - Object.defineProperty(img, "src", { - get: function () { return img._src; }, - set: function (v) { - img._src = v; - toArrayBuffer(v).then(function (ab) { - var d = _nativeDawnDecodeImage(ab); - img.width = img.naturalWidth = d.width; - img.height = img.naturalHeight = d.height; - img.__pixels = d.__pixels; - img.complete = true; - if (typeof img.onload === "function") img.onload({ target: img }); - }).catch(function (e) { - if (typeof img.onerror === "function") img.onerror(e); - }); - }, - }); - return img; - } - if (typeof globalThis.Image === "undefined") { globalThis.Image = ImageShim; } - if (typeof globalThis.HTMLImageElement === "undefined") { globalThis.HTMLImageElement = ImageShim; } - })(); - - // ---- requestAnimationFrame pump ------------------------------------------ - // WebGPUEngine.runRenderLoop schedules its frame via requestAnimationFrame. - // The Embedding View::RenderFrame calls globalThis.frame() once per host - // frame (on the JS thread); we flush queued rAF callbacks (which run the - // engine's begin/render/end, submitting GPU work), then present. - var rafQueue = []; - globalThis.requestAnimationFrame = function (cb) { rafQueue.push(cb); return rafQueue.length; }; - globalThis.cancelAnimationFrame = function () {}; - globalThis.frame = function () { - var q = rafQueue; - rafQueue = []; - for (var i = 0; i < q.length; ++i) { - try { q[i](performance.now()); } - catch (e) { console.error("[dawn-bootstrap] rAF error: " + (e && e.stack ? e.stack : e)); } - } - if (typeof _nativeDawnPresent === "function") { _nativeDawnPresent(); } - }; - - // The Window polyfill provides `window` but not the timer / animation - // methods; forward them to the globals so scene scripts that call - // window.setTimeout / window.requestAnimationFrame work. - if (globalThis.window) { - var w = globalThis.window; - try { w.setTimeout = globalThis.setTimeout; } catch (e) {} - try { w.clearTimeout = globalThis.clearTimeout; } catch (e) {} - try { w.setInterval = globalThis.setInterval; } catch (e) {} - try { w.clearInterval = globalThis.clearInterval; } catch (e) {} - try { w.requestAnimationFrame = globalThis.requestAnimationFrame; } catch (e) {} - try { w.cancelAnimationFrame = globalThis.cancelAnimationFrame; } catch (e) {} - try { w.addEventListener = function () {}; } catch (e) {} - try { w.removeEventListener = function () {}; } catch (e) {} - try { w.dispatchEvent = function () { return true; }; } catch (e) {} - } - - // Some Babylon input feature-detection reads window.PointerEvent etc. - // Provide inert constructors so detection succeeds without a DOM. - function makeEventClass() { - return function (type, init) { - init = init || {}; - this.type = type; - for (var k in init) { if (Object.prototype.hasOwnProperty.call(init, k)) { this[k] = init[k]; } } - if (typeof this.preventDefault !== "function") { this.preventDefault = function () {}; } - if (typeof this.stopPropagation !== "function") { this.stopPropagation = function () {}; } - if (typeof this.stopImmediatePropagation !== "function") { this.stopImmediatePropagation = function () {}; } - }; - } - globalThis.PointerEvent = globalThis.PointerEvent || makeEventClass(); - globalThis.MouseEvent = globalThis.MouseEvent || makeEventClass(); - globalThis.WheelEvent = globalThis.WheelEvent || makeEventClass(); - if (typeof globalThis.Event === "undefined") { globalThis.Event = makeEventClass(); } - if (globalThis.window) { - try { globalThis.window.PointerEvent = globalThis.PointerEvent; } catch (e) {} - try { globalThis.window.MouseEvent = globalThis.MouseEvent; } catch (e) {} - try { globalThis.window.WheelEvent = globalThis.WheelEvent; } catch (e) {} - } - - // ---- resize bridge ------------------------------------------------------- - // Embedding View::Resize dispatches NativeDawn::ResizeSurface then calls this - // (on the JS thread) so the engine's drawing buffer matches the surface. - var g_canvas = null; - globalThis.__dawnResize = function (width, height) { - if (g_canvas) { - g_canvas.width = width; - g_canvas.height = height; - g_canvas.clientWidth = width; - g_canvas.clientHeight = height; - } - if (globalThis.__dawnEngine) { - try { globalThis.__dawnEngine.setSize(width, height, true); } - catch (e) { console.error("[dawn-bootstrap] setSize error: " + (e && e.stack ? e.stack : e)); } - } - }; - - // ---- deferred WebGPUEngine creation -------------------------------------- - // This script runs before babylon.max.js, so BABYLON.WebGPUEngine does not - // exist yet. Hook the global BABYLON assignment (babylon.max.js's UMD does - // `global.BABYLON = factory()`); when it lands, pre-create + initialize a - // WebGPUEngine and alias BABYLON.NativeEngine so the scene scripts' - // synchronous `new BABYLON.NativeEngine()` returns the ready WebGPU engine. - var g_started = false; - function onBabylonReady(BABYLON) { - if (g_started || !BABYLON || !BABYLON.WebGPUEngine) { return; } - g_started = true; - - var surf = (typeof _nativeDawnSurfaceSize === "function") ? _nativeDawnSurfaceSize() : { width: 1280, height: 720 }; - g_canvas = makeCanvas(surf.width, surf.height); - globalThis.__dawnCanvas = g_canvas; - - log("creating WebGPUEngine " + surf.width + "x" + surf.height); - var engine = new BABYLON.WebGPUEngine(g_canvas, { - antialias: false, stencil: true, premultipliedAlpha: false, enableAllFeatures: false, - }); - engine.enableOfflineSupport = false; - engine.disableManifestCheck = true; - - engine.initAsync().then(function () { - globalThis.__dawnEngine = engine; - // Scene scripts construct their engine with `new BABYLON.NativeEngine()`; - // return the ready WebGPU engine instead so they run unmodified. - BABYLON.NativeEngine = function () { return globalThis.__dawnEngine; }; - log("WebGPUEngine ready (" + engine.getCaps().maxTextureSize + " max tex)"); - }).catch(function (e) { - console.error("[dawn-bootstrap] engine init failed: " + (e && e.stack ? e.stack : e)); - }); - } - - var _BABYLON = globalThis.BABYLON; - if (_BABYLON && _BABYLON.WebGPUEngine) { - onBabylonReady(_BABYLON); - } else { - Object.defineProperty(globalThis, "BABYLON", { - configurable: true, - get: function () { return _BABYLON; }, - set: function (v) { - _BABYLON = v; - try { onBabylonReady(v); } - catch (e) { console.error("[dawn-bootstrap] onBabylonReady error: " + (e && e.stack ? e.stack : e)); } - }, - }); - } -})(); diff --git a/Plugins/NativeDawn/Source/NativeDawn.cpp b/Plugins/NativeDawn/Source/NativeDawn.cpp index b66ca874a..94e3b36e0 100644 --- a/Plugins/NativeDawn/Source/NativeDawn.cpp +++ b/Plugins/NativeDawn/Source/NativeDawn.cpp @@ -14,8 +14,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -2661,53 +2660,563 @@ namespace Babylon::Plugins::NativeDawn namespace { - // Locate the directory containing the running executable so the plugin can - // read Scripts/dawn_bootstrap.js staged next to it by the build. - std::filesystem::path ExeDir() + // ---- Dawn bootstrap glue, implemented in C++ (Napi) ------------------- + // Formerly dawn_bootstrap.js. Installs, at global scope and before + // babylon.max.js loads, everything WebGPUEngine needs to run the standard + // (bgfx-oriented) Playground scene scripts unmodified on Dawn/WebGPU: + // * no-DOM canvas / document / window / location shims, + // * image decoding shims (createImageBitmap / Image / URL.createObjectURL) + // backed by the native bimg decoder, + // * a requestAnimationFrame pump driven by globalThis.frame() + // (called each host frame by the Embedding View::RenderFrame), + // * a __dawnResize hook (called by View::Resize), + // * a deferred WebGPUEngine creation that aliases BABYLON.NativeEngine + // once babylon.max.js defines BABYLON, so the scene scripts' + // synchronous `new BABYLON.NativeEngine()` returns the ready engine. + // Input is NOT handled here (it flows through NativeInput, like bgfx). + // + // All state is JS-thread-only (the plugin only ever runs on the JS thread). + + std::vector g_rafQueue; + Napi::ObjectReference g_blobRegistry; + Napi::ObjectReference g_bootstrapCanvas; + Napi::Reference g_babylon; + bool g_engineStarted = false; + uint32_t g_blobSeq = 0; + + Napi::Value Noop(const Napi::CallbackInfo& info) { return info.Env().Undefined(); } + + // Build a no-DOM canvas whose getContext("webgpu") returns the Dawn context. + Napi::Object MakeCanvas(Napi::Env env, uint32_t width, uint32_t height) + { + Napi::Object canvas = Napi::Object::New(env); + canvas.Set("width", Napi::Number::New(env, width)); + canvas.Set("height", Napi::Number::New(env, height)); + canvas.Set("clientWidth", Napi::Number::New(env, width)); + canvas.Set("clientHeight", Napi::Number::New(env, height)); + canvas.Set("style", Napi::Object::New(env)); + SetMethod(canvas, "getContext", [](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() > 0 && info[0].IsString() && info[0].As().Utf8Value() == "webgpu") + { + return MakeCanvasContext(info.Env()); + } + return info.Env().Null(); + }); + SetMethod(canvas, "getBoundingClientRect", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object self = info.This().As(); + double w = self.Get("width").ToNumber().DoubleValue(); + double h = self.Get("height").ToNumber().DoubleValue(); + Napi::Object r = Napi::Object::New(env); + r.Set("x", Napi::Number::New(env, 0)); + r.Set("y", Napi::Number::New(env, 0)); + r.Set("left", Napi::Number::New(env, 0)); + r.Set("top", Napi::Number::New(env, 0)); + r.Set("right", Napi::Number::New(env, w)); + r.Set("bottom", Napi::Number::New(env, h)); + r.Set("width", Napi::Number::New(env, w)); + r.Set("height", Napi::Number::New(env, h)); + return r; + }); + SetMethod(canvas, "setAttribute", Noop); + SetMethod(canvas, "removeAttribute", Noop); + SetMethod(canvas, "addEventListener", Noop); + SetMethod(canvas, "removeEventListener", Noop); + SetMethod(canvas, "dispatchEvent", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), true); + }); + SetMethod(canvas, "setPointerCapture", Noop); + SetMethod(canvas, "releasePointerCapture", Noop); + SetMethod(canvas, "hasPointerCapture", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), false); + }); + SetMethod(canvas, "focus", Noop); + SetMethod(canvas, "getRootNode", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.This(); + }); + return canvas; + } + + // Decode an encoded-image ArrayBuffer/TypedArray into an ImageBitmap-like + // object {width,height,__pixels(RGBA8 ArrayBuffer),close} via bimg. + Napi::Object DecodeToBitmap(Napi::Env env, Napi::Value abVal) { -#if defined(_WIN32) - wchar_t buf[MAX_PATH]{}; - ::GetModuleFileNameW(nullptr, buf, MAX_PATH); - return std::filesystem::path(buf).parent_path(); -#else - return std::filesystem::current_path(); -#endif + Napi::Object bmp = Napi::Object::New(env); + SetMethod(bmp, "close", Noop); + + Bytes in = GetBytes(abVal); + int w = 0; + int h = 0; + std::vector rgba; + if (in.data == nullptr || in.size == 0 || !DecodeRGBA(in.data, in.size, rgba, w, h)) + { + bmp.Set("width", Napi::Number::New(env, 1)); + bmp.Set("height", Napi::Number::New(env, 1)); + bmp.Set("__pixels", Napi::ArrayBuffer::New(env, 4)); + return bmp; + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, rgba.size()); + std::memcpy(ab.Data(), rgba.data(), rgba.size()); + bmp.Set("width", Napi::Number::New(env, w)); + bmp.Set("height", Napi::Number::New(env, h)); + bmp.Set("__pixels", ab); + return bmp; } - // Evaluate the Dawn bootstrap glue (dawn_bootstrap.js) at global scope. - // Runs before babylon.max.js and the scene scripts: it installs the no-DOM - // canvas/image shims, the requestAnimationFrame pump + globalThis.frame(), - // the __dawnResize hook, and a deferred WebGPUEngine creation that aliases - // BABYLON.NativeEngine once babylon.max.js defines BABYLON. Evaluating via a - // non-identifier reference to `eval` is an indirect eval, so the script runs - // in the global scope (globalThis assignments and the BABYLON accessor hook - // take effect globally). - void InstallBootstrap(Napi::Env env) + // Resolve `src` (ArrayBuffer / TypedArray view / {arrayBuffer()} / blob URL + // string / fetchable URL string) to an ArrayBuffer, returning a Promise. + Napi::Value ToArrayBuffer(Napi::Env env, Napi::Value src) { - const std::filesystem::path scriptPath = ExeDir() / "Scripts" / "dawn_bootstrap.js"; - std::ifstream stream{scriptPath, std::ios::binary}; - if (!stream.good()) + if (src.IsArrayBuffer()) { - std::fprintf(stderr, "[NativeDawn] bootstrap not found: %s\n", - scriptPath.string().c_str()); - return; + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(src); + return d.Promise(); + } + if (src.IsTypedArray() || src.IsDataView()) + { + Napi::Object o = src.As(); + Napi::Value buffer = o.Get("buffer"); + double offset = o.Get("byteOffset").ToNumber().DoubleValue(); + double length = o.Get("byteLength").ToNumber().DoubleValue(); + Napi::Value sliced = buffer.As().Get("slice").As().Call( + buffer, {Napi::Number::New(env, offset), Napi::Number::New(env, offset + length)}); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(sliced); + return d.Promise(); } - std::string source{std::istreambuf_iterator{stream}, std::istreambuf_iterator{}}; - source += "\n//# sourceURL=dawn_bootstrap.js\n"; + if (src.IsObject() && src.As().Get("arrayBuffer").IsFunction()) + { + Napi::Object o = src.As(); + return o.Get("arrayBuffer").As().Call(o, {}); + } + if (src.IsString()) + { + const std::string id = src.As().Utf8Value(); + Napi::Value blob = g_blobRegistry.Value().Get(id); + if (!blob.IsUndefined()) + { + return ToArrayBuffer(env, blob); + } + Napi::Value fetchVal = env.Global().Get("fetch"); + if (fetchVal.IsFunction()) + { + Napi::Value p = fetchVal.As().Call({src}); + Napi::Function toAb = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object r = info[0].As(); + return r.Get("arrayBuffer").As().Call(r, {}); + }); + return p.As().Get("then").As().Call(p, {toAb}); + } + } + auto d = Napi::Promise::Deferred::New(env); + d.Reject(Napi::Error::New(env, "toArrayBuffer: unsupported source").Value()); + return d.Promise(); + } - Napi::Value evalVal = env.Global().Get("eval"); - if (!evalVal.IsFunction()) + // Create + initialize the WebGPUEngine once babylon.max.js has defined + // BABYLON, then alias BABYLON.NativeEngine to return it. + void OnBabylonReady(Napi::Env env, Napi::Value babylonVal) + { + if (g_engineStarted || !babylonVal.IsObject()) + { + return; + } + Napi::Object babylon = babylonVal.As(); + Napi::Value wgpuCtor = babylon.Get("WebGPUEngine"); + if (!wgpuCtor.IsFunction()) { - std::fprintf(stderr, "[NativeDawn] global eval unavailable; cannot run bootstrap\n"); return; } - try + g_engineStarted = true; + + Napi::Object canvas = MakeCanvas(env, g_state.width, g_state.height); + g_bootstrapCanvas = Napi::Persistent(canvas); + env.Global().Set("__dawnCanvas", canvas); + + Napi::Object opts = Napi::Object::New(env); + opts.Set("antialias", Napi::Boolean::New(env, false)); + opts.Set("stencil", Napi::Boolean::New(env, true)); + opts.Set("premultipliedAlpha", Napi::Boolean::New(env, false)); + opts.Set("enableAllFeatures", Napi::Boolean::New(env, false)); + + Napi::Object engine = wgpuCtor.As().New({canvas, opts}); + engine.Set("enableOfflineSupport", Napi::Boolean::New(env, false)); + engine.Set("disableManifestCheck", Napi::Boolean::New(env, true)); + // Stash the engine so the async init callback can promote it without + // capturing handles; the alias is only installed once ready. + env.Global().Set("__dawnPendingEngine", engine); + + Napi::Value initPromise = engine.Get("initAsync").As().Call(engine, {}); + Napi::Function onReady = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object global = env.Global(); + global.Set("__dawnEngine", global.Get("__dawnPendingEngine")); + Napi::Value babylon = global.Get("BABYLON"); + if (babylon.IsObject()) + { + babylon.As().Set("NativeEngine", + Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.Env().Global().Get("__dawnEngine"); + })); + } + std::fprintf(stderr, "[NativeDawn] WebGPUEngine ready\n"); + return env.Undefined(); + }); + Napi::Function onErr = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + std::string msg = info.Length() > 0 ? info[0].ToString().Utf8Value() : "?"; + std::fprintf(stderr, "[NativeDawn] engine init failed: %s\n", msg.c_str()); + return info.Env().Undefined(); + }); + initPromise.As().Get("then").As().Call(initPromise, {onReady, onErr}); + } + + // Install all of the above onto the global object. Replaces the eval of + // dawn_bootstrap.js. + void InstallBootstrap(Napi::Env env) + { + Napi::Object global = env.Global(); + + g_rafQueue.clear(); + g_engineStarted = false; + g_blobSeq = 0; + g_blobRegistry = Napi::Persistent(Napi::Object::New(env)); + + // ---- canvas / document / window / location shims ----------------- + if (global.Get("document").IsUndefined()) + { + Napi::Object document = Napi::Object::New(env); + SetMethod(document, "createElement", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + std::string tag = info.Length() > 0 && info[0].IsString() ? info[0].As().Utf8Value() : ""; + if (tag == "canvas") + { + return MakeCanvas(env, 1280, 720); + } + if (tag == "img") + { + return env.Global().Get("Image").As().New({}); + } + Napi::Object el = Napi::Object::New(env); + el.Set("style", Napi::Object::New(env)); + SetMethod(el, "setAttribute", Noop); + SetMethod(el, "appendChild", Noop); + return el; + }); + SetMethod(document, "addEventListener", Noop); + SetMethod(document, "removeEventListener", Noop); + SetMethod(document, "getElementById", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.Env().Null(); + }); + Napi::Object body = Napi::Object::New(env); + SetMethod(body, "appendChild", Noop); + body.Set("style", Napi::Object::New(env)); + document.Set("body", body); + global.Set("document", document); + } + SetMethod(global, "addEventListener", Noop); + SetMethod(global, "removeEventListener", Noop); + + if (global.Get("location").IsUndefined()) + { + Napi::Object location = Napi::Object::New(env); + location.Set("href", Napi::String::New(env, "file:///")); + location.Set("origin", Napi::String::New(env, "file://")); + location.Set("protocol", Napi::String::New(env, "file:")); + location.Set("host", Napi::String::New(env, "")); + location.Set("hostname", Napi::String::New(env, "")); + location.Set("pathname", Napi::String::New(env, "/")); + location.Set("search", Napi::String::New(env, "")); + location.Set("hash", Napi::String::New(env, "")); + global.Set("location", location); + } + + // ---- image decoding shims ---------------------------------------- + Napi::Value urlVal = global.Get("URL"); + if (urlVal.IsObject() || urlVal.IsFunction()) + { + Napi::Object url = urlVal.As(); + if (!url.Get("createObjectURL").IsFunction()) + { + SetMethod(url, "createObjectURL", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + std::string id = "blob:nativedawn/" + std::to_string(++g_blobSeq); + g_blobRegistry.Value().Set(id, info.Length() > 0 ? info[0] : env.Undefined()); + return Napi::String::New(env, id); + }); + } + if (!url.Get("revokeObjectURL").IsFunction()) + { + SetMethod(url, "revokeObjectURL", [](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() > 0 && info[0].IsString()) + { + g_blobRegistry.Value().Delete(info[0].As().Utf8Value()); + } + return info.Env().Undefined(); + }); + } + } + + if (global.Get("createImageBitmap").IsUndefined()) + { + SetMethod(global, "createImageBitmap", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Value src = info.Length() > 0 ? info[0] : env.Undefined(); + auto deferred = Napi::Promise::Deferred::New(env); + if (src.IsObject() && src.As().Get("__pixels").IsArrayBuffer()) + { + deferred.Resolve(src); + return deferred.Promise(); + } + Napi::Value p = ToArrayBuffer(env, src); + Napi::Function onAb = Napi::Function::New(env, [deferred](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + deferred.Resolve(DecodeToBitmap(env, info.Length() > 0 ? info[0] : env.Undefined())); + return env.Undefined(); + }); + Napi::Function onErr = Napi::Function::New(env, [deferred](const Napi::CallbackInfo& info) -> Napi::Value { + deferred.Reject(info.Length() > 0 ? info[0] : info.Env().Undefined()); + return info.Env().Undefined(); + }); + p.As().Get("then").As().Call(p, {onAb, onErr}); + return deferred.Promise(); + }); + } + + if (global.Get("Image").IsUndefined()) + { + Napi::Function imageCtor = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object img = Napi::Object::New(env); + img.Set("width", Napi::Number::New(env, 0)); + img.Set("height", Napi::Number::New(env, 0)); + img.Set("naturalWidth", Napi::Number::New(env, 0)); + img.Set("naturalHeight", Napi::Number::New(env, 0)); + img.Set("__pixels", env.Null()); + img.Set("onload", env.Null()); + img.Set("onerror", env.Null()); + img.Set("crossOrigin", env.Null()); + img.Set("complete", Napi::Boolean::New(env, false)); + img.Set("_src", Napi::String::New(env, "")); + SetMethod(img, "decode", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Promise::Deferred::New(info.Env()).Promise(); + }); + SetMethod(img, "addEventListener", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object self = info.This().As(); + std::string name = info.Length() > 0 && info[0].IsString() ? info[0].As().Utf8Value() : ""; + if (name == "load") self.Set("onload", info[1]); + else if (name == "error") self.Set("onerror", info[1]); + return info.Env().Undefined(); + }); + SetMethod(img, "removeEventListener", Noop); + + // Define the async-decoding `src` accessor. + auto imgRef = std::make_shared(Napi::Persistent(img)); + Napi::Object desc = Napi::Object::New(env); + desc.Set("configurable", Napi::Boolean::New(env, true)); + desc.Set("get", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.This().As().Get("_src"); + })); + desc.Set("set", Napi::Function::New(env, [imgRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Value v = info.Length() > 0 ? info[0] : env.Undefined(); + imgRef->Value().Set("_src", v); + Napi::Value p = ToArrayBuffer(env, v); + Napi::Function onAb = Napi::Function::New(env, [imgRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object img = imgRef->Value(); + Napi::Object bmp = DecodeToBitmap(env, info.Length() > 0 ? info[0] : env.Undefined()); + img.Set("width", bmp.Get("width")); + img.Set("naturalWidth", bmp.Get("width")); + img.Set("height", bmp.Get("height")); + img.Set("naturalHeight", bmp.Get("height")); + img.Set("__pixels", bmp.Get("__pixels")); + img.Set("complete", Napi::Boolean::New(env, true)); + Napi::Value onload = img.Get("onload"); + if (onload.IsFunction()) + { + Napi::Object ev = Napi::Object::New(env); + ev.Set("target", img); + onload.As().Call(img, {ev}); + } + return env.Undefined(); + }); + Napi::Function onErr = Napi::Function::New(env, [imgRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Value onerror = imgRef->Value().Get("onerror"); + if (onerror.IsFunction()) + { + onerror.As().Call(imgRef->Value(), {info.Length() > 0 ? info[0] : env.Undefined()}); + } + return env.Undefined(); + }); + p.As().Get("then").As().Call(p, {onAb, onErr}); + return env.Undefined(); + })); + Napi::Object objectCtor = env.Global().Get("Object").As(); + objectCtor.Get("defineProperty").As().Call(objectCtor, {img, Napi::String::New(env, "src"), desc}); + return img; + }); + global.Set("Image", imageCtor); + if (global.Get("HTMLImageElement").IsUndefined()) + { + global.Set("HTMLImageElement", imageCtor); + } + } + + // ---- requestAnimationFrame pump ---------------------------------- + SetMethod(global, "requestAnimationFrame", [](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() > 0 && info[0].IsFunction()) + { + g_rafQueue.push_back(Napi::Persistent(info[0].As())); + } + return Napi::Number::New(info.Env(), static_cast(g_rafQueue.size())); + }); + SetMethod(global, "cancelAnimationFrame", Noop); + SetMethod(global, "frame", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + double now = 0.0; + Napi::Value perf = env.Global().Get("performance"); + if (perf.IsObject()) + { + Napi::Value nowFn = perf.As().Get("now"); + if (nowFn.IsFunction()) + { + now = nowFn.As().Call(perf, {}).ToNumber().DoubleValue(); + } + } + std::vector queue; + queue.swap(g_rafQueue); + for (auto& cb : queue) + { + cb.Value().Call({Napi::Number::New(env, now)}); + } + // Present the frame. + if (g_surfaceConfigured && g_currentTextureAcquired) + { + g_state.surface.Present(); + } + g_currentTextureAcquired = false; + if (g_state.instance) + { + g_state.instance.ProcessEvents(); + } + return env.Undefined(); + }); + + // Forward timer/animation methods onto `window` (Window polyfill + // provides `window` but not these). + Napi::Value windowVal = global.Get("window"); + if (windowVal.IsObject()) + { + Napi::Object w = windowVal.As(); + for (const char* name : {"setTimeout", "clearTimeout", "setInterval", "clearInterval", + "requestAnimationFrame", "cancelAnimationFrame"}) + { + Napi::Value fn = global.Get(name); + if (!fn.IsUndefined()) + { + w.Set(name, fn); + } + } + SetMethod(w, "addEventListener", Noop); + SetMethod(w, "removeEventListener", Noop); + SetMethod(w, "dispatchEvent", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), true); + }); + } + + // ---- inert event constructors ------------------------------------ + auto makeEventClass = [](Napi::Env env) { + return Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object self = info.This().As(); + self.Set("type", info.Length() > 0 ? info[0] : env.Undefined()); + if (info.Length() > 1 && info[1].IsObject()) + { + Napi::Object init = info[1].As(); + Napi::Array keys = init.GetPropertyNames(); + for (uint32_t i = 0; i < keys.Length(); ++i) + { + Napi::Value k = keys.Get(i); + self.Set(k, init.Get(k)); + } + } + SetMethod(self, "preventDefault", Noop); + SetMethod(self, "stopPropagation", Noop); + SetMethod(self, "stopImmediatePropagation", Noop); + return env.Undefined(); + }); + }; + for (const char* name : {"PointerEvent", "MouseEvent", "WheelEvent", "Event"}) + { + if (global.Get(name).IsUndefined()) + { + global.Set(name, makeEventClass(env)); + } + } + if (windowVal.IsObject()) { - evalVal.As().Call({Napi::String::New(env, source)}); + Napi::Object w = windowVal.As(); + for (const char* name : {"PointerEvent", "MouseEvent", "WheelEvent"}) + { + w.Set(name, global.Get(name)); + } } - catch (const Napi::Error& e) + + // ---- resize bridge (called by Embedding View::Resize) ------------ + SetMethod(global, "__dawnResize", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + double width = info.Length() > 0 ? info[0].ToNumber().DoubleValue() : 0.0; + double height = info.Length() > 1 ? info[1].ToNumber().DoubleValue() : 0.0; + if (!g_bootstrapCanvas.IsEmpty()) + { + Napi::Object canvas = g_bootstrapCanvas.Value(); + canvas.Set("width", Napi::Number::New(env, width)); + canvas.Set("height", Napi::Number::New(env, height)); + canvas.Set("clientWidth", Napi::Number::New(env, width)); + canvas.Set("clientHeight", Napi::Number::New(env, height)); + } + Napi::Value engine = env.Global().Get("__dawnEngine"); + if (engine.IsObject()) + { + Napi::Value setSize = engine.As().Get("setSize"); + if (setSize.IsFunction()) + { + setSize.As().Call(engine, { + Napi::Number::New(env, width), + Napi::Number::New(env, height), + Napi::Boolean::New(env, true)}); + } + } + return env.Undefined(); + }); + + // ---- deferred WebGPUEngine creation via the BABYLON global hook -- + g_babylon.Reset(); + Napi::Value existing = global.Get("BABYLON"); + if (existing.IsObject() && existing.As().Get("WebGPUEngine").IsFunction()) + { + g_babylon = Napi::Persistent(existing); + OnBabylonReady(env, existing); + } + else { - std::fprintf(stderr, "[NativeDawn] bootstrap eval threw: %s\n", e.what()); + Napi::Object desc = Napi::Object::New(env); + desc.Set("configurable", Napi::Boolean::New(env, true)); + desc.Set("get", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + return g_babylon.IsEmpty() ? info.Env().Undefined() : g_babylon.Value(); + })); + desc.Set("set", Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Value v = info.Length() > 0 ? info[0] : env.Undefined(); + g_babylon = Napi::Persistent(v); + OnBabylonReady(env, v); + return env.Undefined(); + })); + Napi::Object objectCtor = global.Get("Object").As(); + objectCtor.Get("defineProperty").As().Call(objectCtor, + {global, Napi::String::New(env, "BABYLON"), desc}); } } } // namespace (bootstrap) From 442264e29bd24676c2cdb4b1b866785cfb0b4d5a Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:21:29 +0200 Subject: [PATCH 08/11] fix pg cmake --- Apps/Playground/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 858c39ece..2f39b3699 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -17,7 +17,6 @@ set(SCRIPTS "Scripts/experience.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" - "Scripts/dawn_bootstrap.js" "Scripts/config.json") set(SOURCES From a527cfb387871e4ed5939ce3b3d3a446be0f2b63 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:57:52 +0200 Subject: [PATCH 09/11] iteration --- Apps/Playground/Scripts/validation_native.js | 59 ++- Plugins/NativeDawn/CMakeLists.txt | 3 +- Plugins/NativeDawn/Source/NativeDawn.cpp | 517 ++++++++++++++++++- 3 files changed, 554 insertions(+), 25 deletions(-) diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 1553ce29a..46def2d92 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -105,6 +105,17 @@ globalThis.engine = engine; engine.getCaps().parallelShaderCompile = undefined; + // The default HTML loading screen pokes at DOM nodes (document head, an + // logo with a network fetch) that don't meaningfully exist in this + // headless host. It's a pure overlay and never part of the captured 3D + // frame, so disable it. Prevents SceneLoader (glTF imports) from calling + // engine.displayLoadingUI(). + if (BABYLON.SceneLoader) { + BABYLON.SceneLoader.ShowLoadingScreen = false; + } + engine.displayLoadingUI = function () { }; + engine.hideLoadingUI = function () { }; + // Broaden Babylon's default retry strategy for the test framework: in addition to // network drops (status 0, the default trigger), also retry transient HTTP errors // (5xx) and rate limits (429). Applies to every BABYLON.Tools.LoadFile request @@ -570,28 +581,36 @@ } } - OffscreenCanvas = function (width, height) { - return { - width: width - , height: height - , getContext: function (type) { - return { - fillRect: function (x, y, w, h) { } - , measureText: function (text) { return 8; } - , fillText: function (text, x, y) { } - }; - } - }; + // Only define no-op DOM stubs if the host hasn't already provided functional + // ones. The NativeDawn (WebGPU) backend installs a real 2D canvas + document + // (needed for WebGPU texture upload); the bgfx backend provides neither, so + // these fallbacks apply there. + if (typeof OffscreenCanvas === "undefined") { + OffscreenCanvas = function (width, height) { + return { + width: width + , height: height + , getContext: function (type) { + return { + fillRect: function (x, y, w, h) { } + , measureText: function (text) { return 8; } + , fillText: function (text, x, y) { } + }; + } + }; + } } - document = { - createElement: function (type) { - if (type === "canvas") { - return new OffscreenCanvas(64, 64); - } - return {}; - }, - removeEventListener: function () { } + if (typeof document === "undefined") { + document = { + createElement: function (type) { + if (type === "canvas") { + return new OffscreenCanvas(64, 64); + } + return {}; + }, + removeEventListener: function () { } + } } const xhr = new XMLHttpRequest(); diff --git a/Plugins/NativeDawn/CMakeLists.txt b/Plugins/NativeDawn/CMakeLists.txt index c8a60c331..6f42cd764 100644 --- a/Plugins/NativeDawn/CMakeLists.txt +++ b/Plugins/NativeDawn/CMakeLists.txt @@ -14,7 +14,8 @@ target_link_libraries(NativeDawn PRIVATE webgpu_dawn PRIVATE bx PRIVATE bimg - PRIVATE bimg_decode) + PRIVATE bimg_decode + PRIVATE bimg_encode) if(WIN32) target_compile_definitions(NativeDawn diff --git a/Plugins/NativeDawn/Source/NativeDawn.cpp b/Plugins/NativeDawn/Source/NativeDawn.cpp index 94e3b36e0..3e109af3b 100644 --- a/Plugins/NativeDawn/Source/NativeDawn.cpp +++ b/Plugins/NativeDawn/Source/NativeDawn.cpp @@ -10,10 +10,19 @@ #include +#include +#include +#include +#include +#include + #include #include #include #include +#include +#include +#include #include #include #include @@ -2684,9 +2693,301 @@ namespace Babylon::Plugins::NativeDawn bool g_engineStarted = false; uint32_t g_blobSeq = 0; + // Deferred framebuffer readback for the validation harness. The harness + // calls TestUtils.getFrameBufferData() from inside the WebGPU render + // callback, BEFORE the engine's endFrame() submits the GPU commands. We + // stash the callback and perform the readback in frame(), after the rAF + // flush (which runs endFrame) but before present, so the surface texture + // holds the freshly-submitted render. + bool g_readbackPending = false; + Napi::FunctionReference g_readbackCallback; + Napi::Value Noop(const Napi::CallbackInfo& info) { return info.Env().Undefined(); } - // Build a no-DOM canvas whose getContext("webgpu") returns the Dawn context. + // ---- minimal 2D canvas raster (enough for WebGPU texture upload) ----- + // Babylon's WebGPU texture path sometimes draws a decoded image onto a 2D + // canvas (e.g. for invert-Y or resize) and uses that canvas as the + // copyExternalImageToTexture source. We back the 2D context with the + // canvas's `__pixels` RGBA8 buffer so the canvas is a valid image source. + struct Ctm { float sx{1}, sy{1}, tx{0}, ty{0}; std::vector> stack; }; + + Napi::ArrayBuffer EnsureCanvasBuffer(Napi::Env env, Napi::Object canvas) + { + uint32_t w = canvas.Get("width").ToNumber().Uint32Value(); + uint32_t h = canvas.Get("height").ToNumber().Uint32Value(); + if (w == 0) w = 1; + if (h == 0) h = 1; + const size_t need = static_cast(w) * h * 4u; + Napi::Value pv = canvas.Get("__pixels"); + if (pv.IsArrayBuffer() && pv.As().ByteLength() == need) + { + return pv.As(); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, need); + std::memset(ab.Data(), 0, need); + canvas.Set("__pixels", ab); + return ab; + } + + Napi::Object Make2DContext(Napi::Env env, Napi::Object canvas) + { + auto canvasRef = std::make_shared(Napi::Persistent(canvas)); + auto ctm = std::make_shared(); + Napi::Object ctx = Napi::Object::New(env); + ctx.Set("canvas", canvas); + ctx.Set("fillStyle", Napi::String::New(env, "#000000")); + ctx.Set("strokeStyle", Napi::String::New(env, "#000000")); + ctx.Set("globalAlpha", Napi::Number::New(env, 1)); + ctx.Set("imageSmoothingEnabled", Napi::Boolean::New(env, true)); + + SetMethod(ctx, "save", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->stack.push_back({ctm->sx, ctm->sy, ctm->tx, ctm->ty}); + return info.Env().Undefined(); + }); + SetMethod(ctx, "restore", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + if (!ctm->stack.empty()) { auto a = ctm->stack.back(); ctm->stack.pop_back(); ctm->sx = a[0]; ctm->sy = a[1]; ctm->tx = a[2]; ctm->ty = a[3]; } + return info.Env().Undefined(); + }); + SetMethod(ctx, "translate", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->tx += ctm->sx * info[0].ToNumber().FloatValue(); + ctm->ty += ctm->sy * info[1].ToNumber().FloatValue(); + return info.Env().Undefined(); + }); + SetMethod(ctx, "scale", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->sx *= info[0].ToNumber().FloatValue(); + ctm->sy *= info[1].ToNumber().FloatValue(); + return info.Env().Undefined(); + }); + SetMethod(ctx, "setTransform", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() >= 6) + { + ctm->sx = info[0].ToNumber().FloatValue(); + ctm->sy = info[3].ToNumber().FloatValue(); + ctm->tx = info[4].ToNumber().FloatValue(); + ctm->ty = info[5].ToNumber().FloatValue(); + } + return info.Env().Undefined(); + }); + SetMethod(ctx, "resetTransform", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->sx = 1; ctm->sy = 1; ctm->tx = 0; ctm->ty = 0; + return info.Env().Undefined(); + }); + SetMethod(ctx, "transform", Noop); + SetMethod(ctx, "rotate", Noop); + SetMethod(ctx, "beginPath", Noop); + SetMethod(ctx, "closePath", Noop); + SetMethod(ctx, "fill", Noop); + SetMethod(ctx, "stroke", Noop); + SetMethod(ctx, "moveTo", Noop); + SetMethod(ctx, "lineTo", Noop); + SetMethod(ctx, "rect", Noop); + SetMethod(ctx, "clip", Noop); + SetMethod(ctx, "fillText", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + // Text rasterization is not supported on the WebGPU backend (the + // bgfx/nanovg Canvas polyfill is unavailable). Ensure the backing + // buffer exists so the canvas is still a valid texture source. + EnsureCanvasBuffer(info.Env(), canvasRef->Value()); + return info.Env().Undefined(); + }); + SetMethod(ctx, "strokeText", Noop); + SetMethod(ctx, "setLineDash", Noop); + SetMethod(ctx, "measureText", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object o = Napi::Object::New(info.Env()); + o.Set("width", Napi::Number::New(info.Env(), 8)); + return o; + }); + SetMethod(ctx, "getContextAttributes", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Object::New(info.Env()); + }); + SetMethod(ctx, "fillRect", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + EnsureCanvasBuffer(info.Env(), canvasRef->Value()); + return info.Env().Undefined(); + }); + + SetMethod(ctx, "clearRect", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + int x = info[0].ToNumber().Int32Value(); + int y = info[1].ToNumber().Int32Value(); + int w = info[2].ToNumber().Int32Value(); + int h = info[3].ToNumber().Int32Value(); + uint8_t* buf = static_cast(ab.Data()); + for (int yy = y; yy < y + h && yy < ch; ++yy) + { + if (yy < 0) continue; + for (int xx = x; xx < x + w && xx < cw; ++xx) + { + if (xx < 0) continue; + uint8_t* p = buf + (static_cast(yy) * cw + xx) * 4; + p[0] = p[1] = p[2] = p[3] = 0; + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "drawImage", [canvasRef, ctm](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (info.Length() < 3 || !info[0].IsObject()) return env.Undefined(); + Napi::Object img = info[0].As(); + Bytes src = GetBytes(img.Get("__pixels")); + uint32_t iw = PropU32(img, "width", 0); + uint32_t ih = PropU32(img, "height", 0); + if (iw == 0) iw = PropU32(img, "naturalWidth", 0); + if (ih == 0) ih = PropU32(img, "naturalHeight", 0); + if (src.data == nullptr || iw == 0 || ih == 0) + { + // Source not yet decoded (e.g. an unrendered DynamicTexture + // label). Leave the destination buffer as-is. + return env.Undefined(); + } + + double sx = 0, sy = 0, sw = iw, sh = ih, dx, dy, dw, dh; + if (info.Length() >= 9) + { + sx = info[1].ToNumber().DoubleValue(); sy = info[2].ToNumber().DoubleValue(); + sw = info[3].ToNumber().DoubleValue(); sh = info[4].ToNumber().DoubleValue(); + dx = info[5].ToNumber().DoubleValue(); dy = info[6].ToNumber().DoubleValue(); + dw = info[7].ToNumber().DoubleValue(); dh = info[8].ToNumber().DoubleValue(); + } + else if (info.Length() >= 5) + { + dx = info[1].ToNumber().DoubleValue(); dy = info[2].ToNumber().DoubleValue(); + dw = info[3].ToNumber().DoubleValue(); dh = info[4].ToNumber().DoubleValue(); + } + else + { + dx = info[1].ToNumber().DoubleValue(); dy = info[2].ToNumber().DoubleValue(); + dw = iw; dh = ih; + } + + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + uint8_t* dst = static_cast(ab.Data()); + const int idw = static_cast(std::lround(dw)); + const int idh = static_cast(std::lround(dh)); + for (int ddy = 0; ddy < idh; ++ddy) + { + int syi = static_cast(sy + ((ddy + 0.5) / dh) * sh); + if (syi < 0) syi = 0; + if (syi >= static_cast(ih)) syi = ih - 1; + for (int ddx = 0; ddx < idw; ++ddx) + { + int sxi = static_cast(sx + ((ddx + 0.5) / dw) * sw); + if (sxi < 0) sxi = 0; + if (sxi >= static_cast(iw)) sxi = iw - 1; + const uint8_t* sp = src.data + (static_cast(syi) * iw + sxi) * 4; + const double px = dx + ddx + 0.5; + const double py = dy + ddy + 0.5; + const int bx = static_cast(std::floor(ctm->sx * px + ctm->tx)); + const int by = static_cast(std::floor(ctm->sy * py + ctm->ty)); + if (bx < 0 || by < 0 || bx >= cw || by >= ch) continue; + uint8_t* dp = dst + (static_cast(by) * cw + bx) * 4; + dp[0] = sp[0]; dp[1] = sp[1]; dp[2] = sp[2]; dp[3] = sp[3]; + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "getImageData", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + int x = info[0].ToNumber().Int32Value(); + int y = info[1].ToNumber().Int32Value(); + int w = info[2].ToNumber().Int32Value(); + int h = info[3].ToNumber().Int32Value(); + if (w <= 0 || h <= 0) { w = cw; h = ch; x = 0; y = 0; } + Napi::ArrayBuffer out = Napi::ArrayBuffer::New(env, static_cast(w) * h * 4u); + uint8_t* od = static_cast(out.Data()); + std::memset(od, 0, static_cast(w) * h * 4u); + const uint8_t* sd = static_cast(ab.Data()); + for (int yy = 0; yy < h; ++yy) + { + const int syy = y + yy; + if (syy < 0 || syy >= ch) continue; + for (int xx = 0; xx < w; ++xx) + { + const int sxx = x + xx; + if (sxx < 0 || sxx >= cw) continue; + std::memcpy(od + (static_cast(yy) * w + xx) * 4, sd + (static_cast(syy) * cw + sxx) * 4, 4); + } + } + Napi::Function u8c = env.Global().Get("Uint8ClampedArray").As(); + Napi::Object dataArr = u8c.New({out, Napi::Number::New(env, 0), Napi::Number::New(env, static_cast(w) * h * 4)}).As(); + Napi::Object res = Napi::Object::New(env); + res.Set("data", dataArr); + res.Set("width", Napi::Number::New(env, w)); + res.Set("height", Napi::Number::New(env, h)); + return res; + }); + + SetMethod(ctx, "putImageData", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (!info[0].IsObject()) return env.Undefined(); + Napi::Object imgData = info[0].As(); + const int dx = info.Length() > 1 ? info[1].ToNumber().Int32Value() : 0; + const int dy = info.Length() > 2 ? info[2].ToNumber().Int32Value() : 0; + const uint32_t iw = PropU32(imgData, "width", 0); + const uint32_t ih = PropU32(imgData, "height", 0); + Bytes src = GetBytes(imgData.Get("data")); + if (src.data == nullptr || iw == 0 || ih == 0) return env.Undefined(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + uint8_t* dst = static_cast(ab.Data()); + for (uint32_t yy = 0; yy < ih; ++yy) + { + const int by = dy + static_cast(yy); + if (by < 0 || by >= ch) continue; + for (uint32_t xx = 0; xx < iw; ++xx) + { + const int bx = dx + static_cast(xx); + if (bx < 0 || bx >= cw) continue; + std::memcpy(dst + (static_cast(by) * cw + bx) * 4, src.data + (static_cast(yy) * iw + xx) * 4, 4); + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "createImageData", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t w = 1; + uint32_t h = 1; + if (info.Length() >= 2 && info[0].IsNumber()) + { + w = info[0].ToNumber().Uint32Value(); + h = info[1].ToNumber().Uint32Value(); + } + else if (info.Length() >= 1 && info[0].IsObject()) + { + Napi::Object o = info[0].As(); + w = PropU32(o, "width", 1); + h = PropU32(o, "height", 1); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, static_cast(w) * h * 4u); + std::memset(ab.Data(), 0, static_cast(w) * h * 4u); + Napi::Function u8c = env.Global().Get("Uint8ClampedArray").As(); + Napi::Object dataArr = u8c.New({ab, Napi::Number::New(env, 0), Napi::Number::New(env, static_cast(w) * h * 4)}).As(); + Napi::Object res = Napi::Object::New(env); + res.Set("data", dataArr); + res.Set("width", Napi::Number::New(env, w)); + res.Set("height", Napi::Number::New(env, h)); + return res; + }); + + return ctx; + } + + // Build a no-DOM canvas whose getContext("webgpu") returns the Dawn context + // and getContext("2d") returns the raster context above. Napi::Object MakeCanvas(Napi::Env env, uint32_t width, uint32_t height) { Napi::Object canvas = Napi::Object::New(env); @@ -2696,11 +2997,25 @@ namespace Babylon::Plugins::NativeDawn canvas.Set("clientHeight", Napi::Number::New(env, height)); canvas.Set("style", Napi::Object::New(env)); SetMethod(canvas, "getContext", [](const Napi::CallbackInfo& info) -> Napi::Value { - if (info.Length() > 0 && info[0].IsString() && info[0].As().Utf8Value() == "webgpu") + Napi::Env env = info.Env(); + const std::string type = info.Length() > 0 && info[0].IsString() ? info[0].As().Utf8Value() : ""; + if (type == "webgpu") { - return MakeCanvasContext(info.Env()); + return MakeCanvasContext(env); } - return info.Env().Null(); + if (type == "2d") + { + Napi::Object self = info.This().As(); + Napi::Value existing = self.Get("__ctx2d"); + if (existing.IsObject()) + { + return existing; + } + Napi::Object c = Make2DContext(env, self); + self.Set("__ctx2d", c); + return c; + } + return env.Null(); }); SetMethod(canvas, "getBoundingClientRect", [](const Napi::CallbackInfo& info) -> Napi::Value { Napi::Env env = info.Env(); @@ -2734,6 +3049,18 @@ namespace Babylon::Plugins::NativeDawn SetMethod(canvas, "getRootNode", [](const Napi::CallbackInfo& info) -> Napi::Value { return info.This(); }); + // The default Babylon loading screen appends its overlay div to + // `renderingCanvas.parentNode`; provide a DOM-node-like parent so + // displayLoadingUI()/hideLoadingUI() don't dereference undefined. + { + Napi::Object parent = Napi::Object::New(env); + SetMethod(parent, "appendChild", Noop); + SetMethod(parent, "removeChild", Noop); + SetMethod(parent, "insertBefore", Noop); + parent.Set("style", Napi::Object::New(env)); + canvas.Set("parentNode", parent); + canvas.Set("parentElement", parent); + } return canvas; } @@ -2901,6 +3228,10 @@ namespace Babylon::Plugins::NativeDawn el.Set("style", Napi::Object::New(env)); SetMethod(el, "setAttribute", Noop); SetMethod(el, "appendChild", Noop); + SetMethod(el, "removeChild", Noop); + SetMethod(el, "insertBefore", Noop); + SetMethod(el, "addEventListener", Noop); + SetMethod(el, "removeEventListener", Noop); return el; }); SetMethod(document, "addEventListener", Noop); @@ -2908,12 +3239,59 @@ namespace Babylon::Plugins::NativeDawn SetMethod(document, "getElementById", [](const Napi::CallbackInfo& info) -> Napi::Value { return info.Env().Null(); }); + // Return an empty array-like for the query methods some Babylon + // paths call (e.g. glTF loaders probing for