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..4b8e6b0dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,3 +198,44 @@ 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: + 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 + + 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..05438c61c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,24 @@ FetchContent_Declare(CMakeExtensions GIT_REPOSITORY https://github.com/BabylonJS/CMakeExtensions.git GIT_TAG 631780e42886e5f12bfd1a5568c7395f1d657f43 EXCLUDE_FROM_ALL) +# 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. +# 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 GIT_TAG 284e4301e5a6b44b279635276588db7cdd942624 @@ -117,6 +135,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 +162,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 +334,63 @@ 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(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) + + # 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); + } + } +}