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..658182a26 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,42 @@ 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 + # NativeDawn (WebGPU) validation. Runs the same harness on the Dawn + # backend; tests that don't yet render correctly on WebGPU are marked + # `excludedGraphicsApis: ["WebGPU"]` in config.json and are skipped, so + # this gates on the currently-passing WebGPU subset. --headless routes + # all logs to stdout for CI diagnostics. + - name: Validation Tests (NativeDawn) + if: ${{ inputs.nativedawn }} + shell: cmd + run: | + cd build\${{ steps.vars.outputs.solution_name }}\Apps\Playground\RelWithDebInfo + Playground --headless app:///Scripts/validation_native.js + + - name: Upload Rendered Pictures (NativeDawn) + if: ${{ inputs.nativedawn && !cancelled() }} + uses: actions/upload-artifact@v6 + with: + name: ${{ steps.vars.outputs.solution_name }}-NativeDawn-rendered-pictures + path: build/${{ steps.vars.outputs.solution_name }}/Apps/Playground/RelWithDebInfo/Results + if-no-files-found: ignore + + - name: Upload Error Pictures (NativeDawn) + if: ${{ inputs.nativedawn && failure() }} + uses: actions/upload-artifact@v6 + with: + name: ${{ steps.vars.outputs.solution_name }}-NativeDawn-error-pictures + path: build/${{ steps.vars.outputs.solution_name }}/Apps/Playground/RelWithDebInfo/Errors + if-no-files-found: ignore + - 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 +177,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..777ad6d2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,3 +198,51 @@ 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. + # + # The Win32 job runs the WebGPU validation tests, so it must use the V8 JS + # engine: the NativeDawn bootstrap (navigator.gpu + WebGPUEngine init + + # NativeEngine aliasing, and the glslang/twgsl WASM shader-translation helpers) + # relies on V8 semantics and fails on the Win32 default (ChakraCore) with + # "TypeError: Object doesn't support this action". + Win32_x64_NativeDawn: + uses: ./.github/workflows/build-win32.yml + with: + platform: x64 + napi-type: V8 + 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..2f39b3699 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -146,9 +146,10 @@ 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: +# Native* plugins — including NativeDawn when BABYLON_NATIVE_PLUGIN_NATIVEDAWN +# is ON — ScriptLoader, ShaderCache, the polyfills, etc.) and forwards them +# transitively, so the Playground only needs to list the libraries it consumes +# directly: # - Embedding : the Runtime + View API the host is built on. # - bx : used by Shared/Diagnostics.cpp. # - TestUtils : used by Win32/X11 --test mode. @@ -164,6 +165,15 @@ target_link_libraries(Playground 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/config.json b/Apps/Playground/Scripts/config.json index f6d345f30..d37d3e0b3 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2,18 +2,21 @@ "root": "https://cdn.babylonjs.com", "tests": [ { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting Compressed ply SH", "playgroundId": "#U8O4EP#1", "renderCount": 15, "referenceImage": "gsplat-compressedply-sh.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting Update Data", "playgroundId": "#Q0LBM8#2", "renderCount": 15, "referenceImage": "gsplat-update-data.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Native Canvas", "playgroundId": "#TKVFSA#8", "referenceImage": "native-canvas.png" @@ -47,18 +50,21 @@ "referenceImage": "gltfMSFTLOD.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Texture Linear Interpolation Test", "playgroundId": "#WGZLGJ#3009", "referenceImage": "gltfTextureLinearInterpolation.png", "renderCount": 10 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Extension KHR_materials_volume with attenuation", "playgroundId": "#YG3BBF#18", "referenceImage": "gltfExtensionKhrMaterialsVolumeAttenuation.png", "renderCount": 10 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF ClearCoat", "playgroundId": "#YG3BBF#33", "referenceImage": "glTFClearCoat.png" @@ -69,6 +75,7 @@ "referenceImage": "iridescence_gltf.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "EXT_texture_webp", "playgroundId": "#LSAUH2#6", "referenceImage": "webp.png" @@ -87,26 +94,31 @@ "referenceImage": "thinInstances.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Sheen", "playgroundId": "#YG3BBF#2", "referenceImage": "glTFSheen.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Serializer multimaterial with raw texture", "playgroundId": "#KU72PX", "referenceImage": "glTFSerializerMultimaterial.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Extension KHR_materials_specular", "playgroundId": "#RNT7K4#9", "referenceImage": "gltfExtensionKhrMaterialsSpecular.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Extension KHR_texture_transform", "playgroundId": "#RNT7K4#2", "referenceImage": "gltfExtensionKhrTextureTransform.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Extension KHR_materials_variants", "playgroundId": "#BKGTKL#3", "referenceImage": "gltfExtensionKhrMaterialsVariants.png" @@ -133,17 +145,20 @@ "referenceImage": "glowlayerandlods.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Nested BBG", "playgroundId": "#ZG0C8B#6", "renderCount": 10, "referenceImage": "nested_BBG.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Dynamic Texture context clip", "playgroundId": "#FU0ES5#47", "referenceImage": "dynamicTextureClip.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GUI3D SpherePanel", "playgroundId": "#HB4C01#9", "renderCount": 50, @@ -155,6 +170,7 @@ "referenceImage": "KernelBlur.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Mesh Primitive Attribute", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Mesh_PrimitiveAttribute, __page__, 0, __disableSRGBBuffers__, 0", @@ -168,6 +184,7 @@ "referenceImage": "gltfMeshPrimitiveVertexColor.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Alpha Blend", "playgroundId": "#SYQW69#579", "referenceImage": "glTFAlphaBlend.png", @@ -217,6 +234,7 @@ "referenceImage": "sharpen.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF BoomBox with Unlit Material", "playgroundId": "#GYM97C#2", "referenceImage": "gltfUnlit.png" @@ -235,6 +253,7 @@ "errorRatio": 6.0 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "LightFalloff", "playgroundId": "#20OAV9#11218", "referenceImage": "lightFalloff.png" @@ -300,6 +319,7 @@ "referenceImage": "multiCameraRenderingWithScissors.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Scissor test", "playgroundId": "#W7E7CF#34", "referenceImage": "scissor-test.png" @@ -309,21 +329,24 @@ "playgroundId": "#W7E7CF#34", "replace": "//options//, hardwareScalingLevel = 0.9;", "referenceImage": "scissor-test-2.png", - "excludedGraphicsApis": ["OpenGL"], + "excludedGraphicsApis": ["OpenGL", "WebGPU"], "reason": "State-leak cascade fixed in validation_native.js (stencil/scissor reset between tests); pixel diff fail on Linux OpenGL (56223 px) under 0.9 hardware scaling; passes on Win32 D3D11." }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Scissor test with 1.5 hardware scaling", "playgroundId": "#W7E7CF#34", "replace": "//options//, hardwareScalingLevel = 1.5;", "referenceImage": "scissor-test-3.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Scissor test with negative x and y", "playgroundId": "#3L5BCD#2", "referenceImage": "scissorTestNegativeXandY.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Scissor test with out of bounds width and height", "playgroundId": "#3L5BCD#3", "referenceImage": "scissorTestOobWidthAndHeight.png" @@ -345,11 +368,13 @@ "referenceImage": "upVector.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Normals", "playgroundId": "#SMEAEB#0", "referenceImage": "gltfnormals.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Animation Node", "renderCount": 2, "playgroundId": "#DS8AA7#27", @@ -357,6 +382,7 @@ "referenceImage": "gltfAnimationNode.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Animation Node Misc", "renderCount": 2, "playgroundId": "#DS8AA7#27", @@ -378,48 +404,56 @@ "referenceImage": "gltfAnimationSkin1.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Buffer Interleaved", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Buffer_Interleaved, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfBufferInterleaved.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterial.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Alpha Blend", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_AlphaBlend, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialAlphaBlend.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Alpha Mask", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_AlphaMask, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialAlphaMask.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Double Sided (Front)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_DoubleSided, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialDoubleSidedFront.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Double Sided (Back)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_DoubleSided, __page__, 0, alpha = Math.PI / 2, alpha = -Math.PI / 2, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialDoubleSidedBack.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Metallic Roughness (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_MetallicRoughness, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialMetallicRoughness0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Metallic Roughness (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_MetallicRoughness, __page__, 1, __disableSRGBBuffers__, 0", @@ -432,12 +466,14 @@ "referenceImage": "gltfMaterialMixed.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Specular Glossiness (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_SpecularGlossiness, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMaterialSpecularGlossiness0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Material Specular Glossiness (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Material_SpecularGlossiness, __page__, 1, __disableSRGBBuffers__, 0", @@ -445,6 +481,7 @@ "errorRatio": 5.0 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Mesh Primitive Mode (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Mesh_PrimitiveMode, __page__, 0, __disableSRGBBuffers__, 0", @@ -452,6 +489,7 @@ "errorRatio": 8.0 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Mesh Primitive Mode (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Mesh_PrimitiveMode, __page__, 1, __disableSRGBBuffers__, 0", @@ -465,48 +503,56 @@ "referenceImage": "gltfMeshPrimitives.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Mesh Primitives UV (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Mesh_PrimitivesUV, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfMeshPrimitivesUV0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Mesh Primitives UV (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Mesh_PrimitivesUV, __page__, 1, __disableSRGBBuffers__, 0", "referenceImage": "gltfMeshPrimitivesUV1.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Node Attribute (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Node_Attribute, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfNodeAttribute0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Node NegativeScale (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Node_NegativeScale, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfNodeNegativeScale0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Node NegativeScale (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Node_NegativeScale, __page__, 1, __disableSRGBBuffers__, 0", "referenceImage": "gltfNodeNegativeScale1.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Texture Sampler (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Texture_Sampler, __page__, 0, __disableSRGBBuffers__, 0", "referenceImage": "gltfTextureSampler0.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Texture Sampler (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Texture_Sampler, __page__, 1, __disableSRGBBuffers__, 0", "referenceImage": "gltfTextureSampler1.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Alien", "playgroundId": "#XN37SR#5", "referenceImage": "gltfAlien.png" @@ -536,6 +582,7 @@ "renderCount": 2 }, { + "excludedGraphicsApis": ["WebGPU"], "title": "CesiumMan from Khronos Sample Assets", "playgroundId": "#4GWX8M", "referenceImage": "CesiumMan.png" @@ -561,6 +608,7 @@ "referenceImage": "gsplat-splat-modify.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting PLY SH Reload", "playgroundId": "#YMXPPW#1", "referenceImage": "gsplat-ply-sh-reload.png" @@ -574,6 +622,7 @@ "referenceImage": "gsplat-splat-bake-transforms.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting Depth", "playgroundId": "#V80DRL#12", "renderCount": 15, @@ -604,6 +653,7 @@ "referenceImage": "gsplat-viewports.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting Shadows", "playgroundId": "#OE54M5#16", "renderCount": 15, @@ -638,6 +688,7 @@ "referenceImage": "gsplat-part-test.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "RH billboard", "playgroundId": "#PDO1L6#1", "referenceImage": "rh-billboard.png" @@ -790,6 +841,7 @@ "referenceImage": "nme-loop.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gaussian Splatting Loading", "playgroundId": "#CID4NN#204", "renderCount": 15, @@ -1095,7 +1147,7 @@ "title": "GUI Near Menu", "playgroundId": "#2YZFA0#302", "renderCount": 60, - "excludedGraphicsApis": ["OpenGL"], + "excludedGraphicsApis": ["OpenGL", "WebGPU"], "reason": "OpenGL: BGFX FATAL shader compile error in GUI fragment shader ('=' : cannot convert from 'highp float' to 'bool'). Re-enabled on D3D11 post BabylonJS/BabylonNative#1695 (original 'V8 D3D11 crash' no longer reproduces under Chakra; original 'hangs on OpenGL' is now this clean shader-compile failure surfaced by the BabylonJS/BabylonNative#1688 BgfxCallback).", "referenceImage": "guiNearMenu.png" }, @@ -1142,6 +1194,7 @@ "referenceImage": "shadowOnlyMaterial.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Gizmos", "playgroundId": "#8GY6J8#196", "renderCount": 50, @@ -1196,7 +1249,7 @@ { "title": "Area Lights - Standard Material", "playgroundId": "#T7FXR8#67", - "excludedGraphicsApis": ["OpenGL"], + "excludedGraphicsApis": ["OpenGL", "WebGPU"], "reason": "Renders black on Linux desktop OpenGL (area-light LTC path unsupported); passes on Win32 D3D11.", "referenceImage": "areaLightsStandardMaterial.png" }, @@ -1346,22 +1399,26 @@ "referenceImage": "glTFSerializerSharedBufferConversions.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Serializer Skinning and Animation", "playgroundId": "#DMZBX1#1", "referenceImage": "gltfSerializerSkinningAndAnimation.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Serializer Skinning and Animation (Right Handed)", "playgroundId": "#DMZBX1#2", "referenceImage": "gltfSerializerSkinningAndAnimation.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Serializer Morph Target Animation", "playgroundId": "#84M2SR#107", "renderCount": 2, "referenceImage": "gltfSerializerMorphTargetAnimation.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Serializer Morph Target Animation Group", "playgroundId": "#T087A8#29", "referenceImage": "gltfSerializerMorphTargetAnimationGroup.png" @@ -1514,6 +1571,7 @@ "referenceImage": "gltfExtensionKhrMaterialsVolumeHemiLight.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "GLTF Node visibility test", "playgroundId": "#80DN99#6", "referenceImage": "gltfNodeVisibility.png" @@ -1524,6 +1582,7 @@ "referenceImage": "assetContainer.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Enable disable post process", "playgroundId": "#1VI6WV#19", "renderCount": 50, @@ -1662,6 +1721,7 @@ "reason": "Pixel comparison fails on Linux (125368 px diff)" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "TGA", "playgroundId": "#ZI77S7#4", "renderCount": 5, @@ -1675,6 +1735,7 @@ "referenceImage": "dds.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "DDS2D", "playgroundId": "#I2MSBE#0", "referenceImage": "dds2d.png" @@ -2041,10 +2102,11 @@ "playgroundId": "#W7E7CF#38", "replace": "//options//, hardwareScalingLevel = 0.9;", "referenceImage": "scissor-test-2.png", - "excludedGraphicsApis": ["OpenGL"], + "excludedGraphicsApis": ["OpenGL", "WebGPU"], "reason": "State-leak cascade fixed in validation_native.js (stencil/scissor reset between tests); pixel diff fail on Linux OpenGL (56223 px) under 0.9 hardware scaling; passes on Win32 D3D11." }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Scissor test with 1.5 hardware scaling", "playgroundId": "#W7E7CF#38", "replace": "//options//, hardwareScalingLevel = 1.5;", @@ -2257,6 +2319,7 @@ "referenceImage": "serializeWithoutMaterials.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "UI unaffected by post-process", "playgroundId": "#XJVHWQ", "referenceImage": "uiUnaffected.png" @@ -2267,11 +2330,13 @@ "referenceImage": "billboardParentScale.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Load GUI snippet with unicode", "playgroundId": "#YS93KY#1", "referenceImage": "loadGuiSnippetWithUnicode.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Parse GUI json with unicode", "playgroundId": "#ERVGT5#1", "referenceImage": "parseGuiJsonWithUnicode.png" @@ -2370,6 +2435,7 @@ "referenceImage": "Asset-Container-Instantiate-to-Scene.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Asset Container Instantiate to Scene 2", "playgroundId": "#5NFRVE#161", "referenceImage": "Asset-Container-Instantiate-to-Scene-2.png" @@ -2456,6 +2522,7 @@ "referenceImage": "PBR-Debug-Modes.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "convertToFlatShadedMesh", "playgroundId": "#UFDL9T#4", "referenceImage": "convertToFlatShadedMesh.png" @@ -2466,6 +2533,7 @@ "referenceImage": "SerializeMesh-with-hierarchy.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "MeshDebugPluginMaterial", "playgroundId": "#TFDNZI#2", "referenceImage": "MeshDebugPluginMaterial.png" @@ -2760,6 +2828,7 @@ "referenceImage": "computeMaxExtents-MorphStressTest.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "glTF Loader Capabilities", "playgroundId": "#EBO3J1#10", "referenceImage": "glTFLoaderCapabilities.png" @@ -3156,6 +3225,7 @@ "referenceImage": "IBL-radiance-roughness.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Synchronous Effect", "playgroundId": "#D9XXL3#1", "referenceImage": "Synchronous-Effect.png" @@ -3278,6 +3348,7 @@ "referenceImage": "sponza-clustered-lighting-viewports.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Test code inlining", "playgroundId": "#YG3BBF#51", "referenceImage": "Test-code-inlining.png" @@ -4461,6 +4532,7 @@ "referenceImage": "vertexPullingIsUnIndexed.png" }, { + "excludedGraphicsApis": ["WebGPU"], "title": "Vertex Pulling - UV Channels 1-6", "playgroundId": "#0SA2NH#4", "referenceImage": "vertexPullingUVChannels.png" diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 1553ce29a..46def2d92 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -105,6 +105,17 @@ globalThis.engine = engine; engine.getCaps().parallelShaderCompile = undefined; + // The default HTML loading screen pokes at DOM nodes (document head, an + // logo with a network fetch) that don't meaningfully exist in this + // headless host. It's a pure overlay and never part of the captured 3D + // frame, so disable it. Prevents SceneLoader (glTF imports) from calling + // engine.displayLoadingUI(). + if (BABYLON.SceneLoader) { + BABYLON.SceneLoader.ShowLoadingScreen = false; + } + engine.displayLoadingUI = function () { }; + engine.hideLoadingUI = function () { }; + // Broaden Babylon's default retry strategy for the test framework: in addition to // network drops (status 0, the default trigger), also retry transient HTTP errors // (5xx) and rate limits (429). Applies to every BABYLON.Tools.LoadFile request @@ -570,28 +581,36 @@ } } - OffscreenCanvas = function (width, height) { - return { - width: width - , height: height - , getContext: function (type) { - return { - fillRect: function (x, y, w, h) { } - , measureText: function (text) { return 8; } - , fillText: function (text, x, y) { } - }; - } - }; + // Only define no-op DOM stubs if the host hasn't already provided functional + // ones. The NativeDawn (WebGPU) backend installs a real 2D canvas + document + // (needed for WebGPU texture upload); the bgfx backend provides neither, so + // these fallbacks apply there. + if (typeof OffscreenCanvas === "undefined") { + OffscreenCanvas = function (width, height) { + return { + width: width + , height: height + , getContext: function (type) { + return { + fillRect: function (x, y, w, h) { } + , measureText: function (text) { return 8; } + , fillText: function (text, x, y) { } + }; + } + }; + } } - document = { - createElement: function (type) { - if (type === "canvas") { - return new OffscreenCanvas(64, 64); - } - return {}; - }, - removeEventListener: function () { } + if (typeof document === "undefined") { + document = { + createElement: function (type) { + if (type === "canvas") { + return new OffscreenCanvas(64, 64); + } + return {}; + }, + removeEventListener: function () { } + } } const xhr = new XMLHttpRequest(); diff --git a/Apps/Playground/Win32/App.cpp b/Apps/Playground/Win32/App.cpp index 1a3640d27..aa77ad878 100644 --- a/Apps/Playground/Win32/App.cpp +++ b/Apps/Playground/Win32/App.cpp @@ -1,8 +1,13 @@ // 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. +// Single, backend-agnostic Win32 host built on Babylon::Embedding. The +// cross-platform Runtime + View API handles plugin/polyfill setup, GPU device +// construction, frame rendering, resize and input forwarding. +// +// The graphics backend is selected at CMake configure time and is invisible +// here: BABYLON_NATIVE_PLUGIN_NATIVEENGINE (bgfx) or BABYLON_NATIVE_PLUGIN_ +// NATIVEDAWN (WebGPU via Dawn). The only place that differs is the plugin +// Initialize call inside the Embedding layer's RunFirstAttachInit. #include "App.h" diff --git a/CMakeLists.txt b/CMakeLists.txt index d37aac35e..b0986d230 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,64 @@ 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_DESKTOP_GL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_MSL_WRITER OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_SPV_WRITER OFF CACHE BOOL "" FORCE) + + 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/Embedding/CMakeLists.txt b/Embedding/CMakeLists.txt index 20715f0bb..45e2231de 100644 --- a/Embedding/CMakeLists.txt +++ b/Embedding/CMakeLists.txt @@ -87,6 +87,15 @@ if(BABYLON_NATIVE_PLUGIN_NATIVEENGINE) target_link_libraries(Embedding PRIVATE NativeEngine) endif() +# NativeDawn (experimental WebGPU-via-Dawn backend). Mutually exclusive with +# NativeEngine (the root CMake forces NativeEngine OFF when this is ON). When +# enabled, the View/Runtime route the graphics-device lifecycle to the NativeDawn +# plugin instead of the bgfx Graphics::Device. +if(BABYLON_NATIVE_PLUGIN_NATIVEDAWN) + target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEDAWN=1) + target_link_libraries(Embedding PRIVATE NativeDawn) +endif() + if(BABYLON_NATIVE_PLUGIN_NATIVEINPUT) target_compile_definitions(Embedding PUBLIC BABYLON_NATIVE_PLUGIN_NATIVEINPUT=1) target_link_libraries(Embedding PRIVATE NativeInput) diff --git a/Embedding/Source/Runtime.cpp b/Embedding/Source/Runtime.cpp index 852528288..b23efe43f 100644 --- a/Embedding/Source/Runtime.cpp +++ b/Embedding/Source/Runtime.cpp @@ -5,6 +5,9 @@ #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN +#include +#endif #if BABYLON_NATIVE_PLUGIN_NATIVECAMERA #include #endif @@ -171,7 +174,7 @@ namespace Babylon::Embedding // completes m_initTcs to unblock host calls that were queued before the // first attach. Post-init, those host calls fire their continuation // synchronously via inline_scheduler and submit straight to ScriptLoader. - void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window) + void RuntimeImpl::RunFirstAttachInit(Babylon::Graphics::WindowT window, uint32_t width, uint32_t height) { #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Enable + hydrate before any JS-thread shader compilation. Both @@ -181,7 +184,7 @@ namespace Babylon::Embedding LoadShaderCache(); #endif - m_appRuntime->Dispatch([implPtr = this, window](Napi::Env env) { + m_appRuntime->Dispatch([implPtr = this, window, width, height](Napi::Env env) { // 0. Install the ES2020 `globalThis` self-reference. V8/JSC/Chakra // provide it intrinsically, but the embedded Hermes runtime does // not, and Hermes evaluates eval()'d code as indirect (global @@ -190,7 +193,9 @@ namespace Babylon::Embedding env.Global().Set("globalThis", env.Global()); // 1. Make the Device available to JS. +#if BABYLON_NATIVE_PLUGIN_NATIVEENGINE implPtr->m_device->AddToJavaScript(env); +#endif // 2. Polyfills (always-on). Babylon::Polyfills::Blob::Initialize(env); @@ -269,6 +274,12 @@ namespace Babylon::Embedding #endif #if BABYLON_NATIVE_PLUGIN_NATIVEENGINE Babylon::Plugins::NativeEngine::Initialize(env); +#elif BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn replaces the bgfx NativeEngine: it creates the Dawn + // (WebGPU) device + surface bound to `window`, installs navigator.gpu + // and the WebGPU globals, and (via its bootstrap) drives the scene on + // a WebGPUEngine. `width`/`height` are the initial surface size. + Babylon::Plugins::NativeDawn::Initialize(env, window, width, height); #endif #if BABYLON_NATIVE_PLUGIN_NATIVEOPTIMIZATIONS Babylon::Plugins::NativeOptimizations::Initialize(env); @@ -298,11 +309,21 @@ namespace Babylon::Embedding }); } #endif -#if BABYLON_NATIVE_PLUGIN_TESTUTILS +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // TestUtils is bgfx-only (its constructor acquires the bgfx + // Graphics::DeviceContext, which the NativeDawn backend does not + // create). `window`/`width`/`height` are consumed by + // NativeDawn::Initialize above. +#elif BABYLON_NATIVE_PLUGIN_TESTUTILS Babylon::Plugins::TestUtils::Initialize(env, window); #else (void)window; #endif +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // Initial surface size is only consumed by the NativeDawn plugin init. + (void)width; + (void)height; +#endif // 4. Fire any host calls queued before the first View attach. implPtr->m_initTcs.complete(); diff --git a/Embedding/Source/RuntimeImpl.h b/Embedding/Source/RuntimeImpl.h index 6f2260b3b..2395b9638 100644 --- a/Embedding/Source/RuntimeImpl.h +++ b/Embedding/Source/RuntimeImpl.h @@ -19,6 +19,10 @@ #include #endif +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN +#include +#endif + #include #include @@ -50,6 +54,16 @@ namespace Babylon::Embedding std::optional m_device; std::optional m_deviceUpdate; +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn (WebGPU) backend state. The bgfx Graphics::Device above is + // never constructed in this configuration; the NativeDawn plugin owns + // the device/surface/present lifecycle instead. m_dawnInitialized latches + // the first-attach init; m_dawnFrameInFlight throttles the per-frame + // JS-thread dispatch (host RenderFrame → JS frame() + NativeDawn::Tick). + bool m_dawnInitialized{false}; + std::atomic m_dawnFrameInFlight{false}; +#endif + #if BABYLON_NATIVE_POLYFILL_CANVAS std::optional m_canvas; #endif @@ -96,8 +110,10 @@ namespace Babylon::Embedding // Dispatched onto the JS thread by the very first View attach. // Runs all plugin/polyfill Initialize calls, wires NativeXr // callbacks, and completes m_initTcs. `window` is forwarded to - // TestUtils::Initialize; ignored otherwise. - void RunFirstAttachInit(Babylon::Graphics::WindowT window); + // TestUtils::Initialize (and, for NativeDawn, to the plugin's + // Initialize along with the initial surface `width`/`height`); + // ignored by the bgfx backend otherwise. + void RunFirstAttachInit(Babylon::Graphics::WindowT window, uint32_t width, uint32_t height); #if BABYLON_NATIVE_PLUGIN_SHADERCACHE // Persistent shader cache. No-ops when `m_options.shaderCachePath` diff --git a/Embedding/Source/View.cpp b/Embedding/Source/View.cpp index 916afc9d0..985403561 100644 --- a/Embedding/Source/View.cpp +++ b/Embedding/Source/View.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -41,6 +43,19 @@ namespace Babylon::Embedding throw std::runtime_error{std::string{operation} + " requires non-zero width and height."}; } } + + // Current device-pixel ratio: the bgfx Graphics::Device's stored value + // when present, otherwise the window's (the NativeDawn backend has no + // Graphics::Device, so input/coordinate conversion falls back to the + // window DPR). Only referenced by the NativeInput forwarding methods. + [[maybe_unused]] float CurrentDpr(RuntimeImpl& impl, Babylon::Graphics::WindowT window) + { + if (impl.m_device) + { + return impl.m_device->GetDevicePixelRatio(); + } + return Babylon::Graphics::GetDevicePixelRatio(window); + } } View::View(Runtime& runtime, Babylon::Graphics::WindowT nativeWindow) @@ -62,6 +77,10 @@ namespace Babylon::Embedding ViewImpl::~ViewImpl() { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn presents each frame inside its per-frame Tick, so there is + // no host-held in-flight frame to close on teardown. +#else // Close the in-flight frame iff one is currently open: i.e. the // view is initialized and the Runtime is not externally suspended // (Suspend already closed the frame). The Device persists on the @@ -71,6 +90,7 @@ namespace Babylon::Embedding { m_runtime.m_device->FinishRenderingCurrentFrame(); } +#endif m_runtime.m_currentView = nullptr; } @@ -86,7 +106,9 @@ namespace Babylon::Embedding { return; } +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN m_runtime.m_device->FinishRenderingCurrentFrame(); +#endif } void ViewImpl::Resume() @@ -98,7 +120,9 @@ namespace Babylon::Embedding InitializeIfReady(); return; } +#if !BABYLON_NATIVE_PLUGIN_NATIVEDAWN m_runtime.m_device->StartRenderingCurrentFrame(); +#endif } // Single recipe for binding the Device to this view's window and @@ -117,6 +141,28 @@ namespace Babylon::Embedding } const auto [lw, lh] = *m_size; +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // NativeDawn owns the device/surface lifecycle; there is no bgfx + // Graphics::Device to construct here. The plugin's Initialize (creating + // the Dawn device + surface bound to m_window at the current size) runs + // inside RunFirstAttachInit on the JS thread. + const bool firstAttachEver = !m_runtime.m_dawnInitialized; + if (!firstAttachEver) + { + // Re-attach to an existing Runtime: reconfigure the Dawn surface to + // the current size on the JS thread (where the Dawn device lives). + m_runtime.m_appRuntime->Dispatch([lw, lh](Napi::Env) { + Babylon::Plugins::NativeDawn::ResizeSurface(lw, lh); + }); + } + m_initialized = true; + + if (firstAttachEver) + { + m_runtime.m_dawnInitialized = true; + m_runtime.RunFirstAttachInit(m_window, lw, lh); + } +#else const bool firstAttachEver = !m_runtime.m_device; if (firstAttachEver) { @@ -146,8 +192,9 @@ namespace Babylon::Embedding if (firstAttachEver) { - m_runtime.RunFirstAttachInit(m_window); + m_runtime.RunFirstAttachInit(m_window, lw, lh); } +#endif } void View::RenderFrame() @@ -169,10 +216,29 @@ namespace Babylon::Embedding return; } +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // WebGPUEngine drives its render loop from requestAnimationFrame, pumped + // by the JS-thread global frame(). Dispatch one frame at a time (throttled + // so the JS queue can't flood): flush rAF (runs the engine's render + + // GPU submit) and present via NativeDawn::Tick. + if (!impl.m_dawnFrameInFlight.exchange(true)) + { + impl.m_appRuntime->Dispatch([implPtr = &impl](Napi::Env env) { + Napi::Value frame = env.Global().Get("frame"); + if (frame.IsFunction()) + { + frame.As().Call({}); + } + Babylon::Plugins::NativeDawn::Tick(env); + implPtr->m_dawnFrameInFlight.store(false, std::memory_order_relaxed); + }); + } +#else // Babylon's JS render loop runs between Start and Finish, scheduled // via DeviceUpdate's SafeTimespanGuarantor onto the JS thread. impl.m_device->FinishRenderingCurrentFrame(); impl.m_device->StartRenderingCurrentFrame(); +#endif } // Stores the host-supplied size on the ViewImpl, then either pushes @@ -187,10 +253,21 @@ namespace Babylon::Embedding // DPR source: before init, query the window directly (Device // doesn't exist or is still bound to the previous attach). After - // init, the Device's stored DPR is authoritative. - const float dpr = !m_impl->m_initialized - ? Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window) - : impl.m_device->GetDevicePixelRatio(); + // init, the Device's stored DPR is authoritative (bgfx). NativeDawn + // has no Device, so keep querying the window. + float dpr; + if (!m_impl->m_initialized) + { + dpr = Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window); + } + else + { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + dpr = Babylon::Graphics::GetDevicePixelRatio(m_impl->m_window); +#else + dpr = impl.m_device->GetDevicePixelRatio(); +#endif + } const auto [lw, lh] = ToLogicalSize(width, height, units, dpr); ValidateNonZeroSize(lw, lh, "View::Resize logical size"); @@ -198,7 +275,22 @@ namespace Babylon::Embedding if (m_impl->m_initialized) { +#if BABYLON_NATIVE_PLUGIN_NATIVEDAWN + // Reconfigure the Dawn surface and the JS engine's drawing buffer on + // the JS thread (where the Dawn device lives). + impl.m_appRuntime->Dispatch([lw, lh](Napi::Env env) { + Babylon::Plugins::NativeDawn::ResizeSurface(lw, lh); + Napi::Value resizeFn = env.Global().Get("__dawnResize"); + if (resizeFn.IsFunction()) + { + resizeFn.As().Call({ + Napi::Number::New(env, lw), + Napi::Number::New(env, lh)}); + } + }); +#else impl.m_device->UpdateSize(lw, lh); +#endif } else { @@ -212,10 +304,10 @@ namespace Babylon::Embedding void View::OnPointerDown(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchDown(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -225,10 +317,10 @@ namespace Babylon::Embedding void View::OnPointerMove(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchMove(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -238,10 +330,10 @@ namespace Babylon::Embedding void View::OnPointerUp(int32_t pointerId, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->TouchUp(static_cast(pointerId), static_cast(lx), static_cast(ly)); @@ -251,10 +343,10 @@ namespace Babylon::Embedding void View::OnMouseDown(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseDown(buttonIndex, static_cast(lx), static_cast(ly)); @@ -264,10 +356,10 @@ namespace Babylon::Embedding void View::OnMouseUp(uint32_t buttonIndex, float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseUp(buttonIndex, static_cast(lx), static_cast(ly)); @@ -277,10 +369,10 @@ namespace Babylon::Embedding void View::OnMouseMove(float x, float y, CoordinateUnits units) { RuntimeImpl& impl = m_impl->m_runtime; - if (impl.m_input && impl.m_device) + if (impl.m_input) { const auto [lx, ly] = ToLogicalCoords(x, y, units, - impl.m_device->GetDevicePixelRatio()); + CurrentDpr(impl, m_impl->m_window)); impl.m_input->MouseMove(static_cast(lx), static_cast(ly)); } diff --git a/Plugins/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..6f42cd764 --- /dev/null +++ b/Plugins/NativeDawn/CMakeLists.txt @@ -0,0 +1,26 @@ +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 + PRIVATE bimg_encode) + +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..12b5efcef --- /dev/null +++ b/Plugins/NativeDawn/Source/NativeDawn.cpp @@ -0,0 +1,4021 @@ +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#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")); + + // Outstanding getMappedRange() shadow buffers for this GPUBuffer. + // getMappedRange must NOT hand V8 a raw pointer into Dawn's mapped + // memory: that memory is freed/invalidated by unmap(), leaving a + // dangling external ArrayBuffer that corrupts the V8 heap and crashes + // a later GC. Instead each getMappedRange returns a V8-owned + // ArrayBuffer (seeded from Dawn's current bytes) and unmap() copies + // those bytes back into Dawn before unmapping. + struct MappedRange + { + uint64_t offset; + uint64_t size; + Napi::Reference ab; + }; + auto ranges = std::make_shared>(); + + 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, ranges](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); + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, static_cast(size)); + // Seed from Dawn's current mapped bytes so read-maps work. For a + // write/mappedAtCreation map the source is writable too. + const void* src = h.GetConstMappedRange(static_cast(offset), static_cast(size)); + if (src != nullptr && size > 0) + { + std::memcpy(ab.Data(), src, static_cast(size)); + } + ranges->push_back({offset, size, Napi::Persistent(ab)}); + return ab; + }); + SetMethod(o, "unmap", [h, ranges](const Napi::CallbackInfo& info) -> Napi::Value { + // Copy each outstanding shadow buffer back into Dawn's mapped + // memory before unmapping (write-through), then release the JS + // references so the ArrayBuffers can be collected. + for (auto& r : *ranges) + { + void* dst = h.GetMappedRange(static_cast(r.offset), static_cast(r.size)); + if (dst != nullptr && !r.ab.IsEmpty()) + { + Napi::ArrayBuffer ab = r.ab.Value(); + if (ab.ByteLength() >= r.size && r.size > 0) + { + std::memcpy(dst, ab.Data(), static_cast(r.size)); + } + } + r.ab.Reset(); + } + ranges->clear(); + h.Unmap(); + return info.Env().Undefined(); + }); + SetMethod(o, "destroy", [h, ranges](const Napi::CallbackInfo& info) -> Napi::Value { + for (auto& r : *ranges) { r.ab.Reset(); } + ranges->clear(); + 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) + + namespace + { + // ---- Dawn bootstrap glue, implemented in C++ (Napi) ------------------- + // Formerly dawn_bootstrap.js. Installs, at global scope and before + // babylon.max.js loads, everything WebGPUEngine needs to run the standard + // (bgfx-oriented) Playground scene scripts unmodified on Dawn/WebGPU: + // * no-DOM canvas / document / window / location shims, + // * image decoding shims (createImageBitmap / Image / URL.createObjectURL) + // backed by the native bimg decoder, + // * a requestAnimationFrame pump driven by globalThis.frame() + // (called each host frame by the Embedding View::RenderFrame), + // * a __dawnResize hook (called by View::Resize), + // * a deferred WebGPUEngine creation that aliases BABYLON.NativeEngine + // once babylon.max.js defines BABYLON, so the scene scripts' + // synchronous `new BABYLON.NativeEngine()` returns the ready engine. + // Input is NOT handled here (it flows through NativeInput, like bgfx). + // + // All state is JS-thread-only (the plugin only ever runs on the JS thread). + + std::vector g_rafQueue; + Napi::ObjectReference g_blobRegistry; + Napi::ObjectReference g_bootstrapCanvas; + Napi::Reference g_babylon; + bool g_engineStarted = false; + uint32_t g_blobSeq = 0; + + // Deferred framebuffer readback for the validation harness. The harness + // calls TestUtils.getFrameBufferData() from inside the WebGPU render + // callback, BEFORE the engine's endFrame() submits the GPU commands. We + // stash the callback and perform the readback in frame(), after the rAF + // flush (which runs endFrame) but before present, so the surface texture + // holds the freshly-submitted render. + bool g_readbackPending = false; + Napi::FunctionReference g_readbackCallback; + + Napi::Value Noop(const Napi::CallbackInfo& info) { return info.Env().Undefined(); } + + // ---- minimal 2D canvas raster (enough for WebGPU texture upload) ----- + // Babylon's WebGPU texture path sometimes draws a decoded image onto a 2D + // canvas (e.g. for invert-Y or resize) and uses that canvas as the + // copyExternalImageToTexture source. We back the 2D context with the + // canvas's `__pixels` RGBA8 buffer so the canvas is a valid image source. + struct Ctm { float sx{1}, sy{1}, tx{0}, ty{0}; std::vector> stack; }; + + Napi::ArrayBuffer EnsureCanvasBuffer(Napi::Env env, Napi::Object canvas) + { + uint32_t w = canvas.Get("width").ToNumber().Uint32Value(); + uint32_t h = canvas.Get("height").ToNumber().Uint32Value(); + if (w == 0) w = 1; + if (h == 0) h = 1; + const size_t need = static_cast(w) * h * 4u; + Napi::Value pv = canvas.Get("__pixels"); + if (pv.IsArrayBuffer() && pv.As().ByteLength() == need) + { + return pv.As(); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, need); + std::memset(ab.Data(), 0, need); + canvas.Set("__pixels", ab); + return ab; + } + + Napi::Object Make2DContext(Napi::Env env, Napi::Object canvas) + { + auto canvasRef = std::make_shared(Napi::Persistent(canvas)); + auto ctm = std::make_shared(); + Napi::Object ctx = Napi::Object::New(env); + ctx.Set("canvas", canvas); + ctx.Set("fillStyle", Napi::String::New(env, "#000000")); + ctx.Set("strokeStyle", Napi::String::New(env, "#000000")); + ctx.Set("globalAlpha", Napi::Number::New(env, 1)); + ctx.Set("imageSmoothingEnabled", Napi::Boolean::New(env, true)); + + SetMethod(ctx, "save", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->stack.push_back({ctm->sx, ctm->sy, ctm->tx, ctm->ty}); + return info.Env().Undefined(); + }); + SetMethod(ctx, "restore", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + if (!ctm->stack.empty()) { auto a = ctm->stack.back(); ctm->stack.pop_back(); ctm->sx = a[0]; ctm->sy = a[1]; ctm->tx = a[2]; ctm->ty = a[3]; } + return info.Env().Undefined(); + }); + SetMethod(ctx, "translate", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->tx += ctm->sx * info[0].ToNumber().FloatValue(); + ctm->ty += ctm->sy * info[1].ToNumber().FloatValue(); + return info.Env().Undefined(); + }); + SetMethod(ctx, "scale", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->sx *= info[0].ToNumber().FloatValue(); + ctm->sy *= info[1].ToNumber().FloatValue(); + return info.Env().Undefined(); + }); + SetMethod(ctx, "setTransform", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + if (info.Length() >= 6) + { + ctm->sx = info[0].ToNumber().FloatValue(); + ctm->sy = info[3].ToNumber().FloatValue(); + ctm->tx = info[4].ToNumber().FloatValue(); + ctm->ty = info[5].ToNumber().FloatValue(); + } + return info.Env().Undefined(); + }); + SetMethod(ctx, "resetTransform", [ctm](const Napi::CallbackInfo& info) -> Napi::Value { + ctm->sx = 1; ctm->sy = 1; ctm->tx = 0; ctm->ty = 0; + return info.Env().Undefined(); + }); + SetMethod(ctx, "transform", Noop); + SetMethod(ctx, "rotate", Noop); + SetMethod(ctx, "beginPath", Noop); + SetMethod(ctx, "closePath", Noop); + SetMethod(ctx, "fill", Noop); + SetMethod(ctx, "stroke", Noop); + SetMethod(ctx, "moveTo", Noop); + SetMethod(ctx, "lineTo", Noop); + SetMethod(ctx, "rect", Noop); + SetMethod(ctx, "clip", Noop); + SetMethod(ctx, "fillText", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + // Text rasterization is not supported on the WebGPU backend (the + // bgfx/nanovg Canvas polyfill is unavailable). Ensure the backing + // buffer exists so the canvas is still a valid texture source. + EnsureCanvasBuffer(info.Env(), canvasRef->Value()); + return info.Env().Undefined(); + }); + SetMethod(ctx, "strokeText", Noop); + SetMethod(ctx, "setLineDash", Noop); + SetMethod(ctx, "measureText", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object o = Napi::Object::New(info.Env()); + o.Set("width", Napi::Number::New(info.Env(), 8)); + return o; + }); + SetMethod(ctx, "getContextAttributes", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Object::New(info.Env()); + }); + SetMethod(ctx, "fillRect", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + EnsureCanvasBuffer(info.Env(), canvasRef->Value()); + return info.Env().Undefined(); + }); + + SetMethod(ctx, "clearRect", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + int x = info[0].ToNumber().Int32Value(); + int y = info[1].ToNumber().Int32Value(); + int w = info[2].ToNumber().Int32Value(); + int h = info[3].ToNumber().Int32Value(); + uint8_t* buf = static_cast(ab.Data()); + for (int yy = y; yy < y + h && yy < ch; ++yy) + { + if (yy < 0) continue; + for (int xx = x; xx < x + w && xx < cw; ++xx) + { + if (xx < 0) continue; + uint8_t* p = buf + (static_cast(yy) * cw + xx) * 4; + p[0] = p[1] = p[2] = p[3] = 0; + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "drawImage", [canvasRef, ctm](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (info.Length() < 3 || !info[0].IsObject()) return env.Undefined(); + Napi::Object img = info[0].As(); + Bytes src = GetBytes(img.Get("__pixels")); + uint32_t iw = PropU32(img, "width", 0); + uint32_t ih = PropU32(img, "height", 0); + if (iw == 0) iw = PropU32(img, "naturalWidth", 0); + if (ih == 0) ih = PropU32(img, "naturalHeight", 0); + if (src.data == nullptr || iw == 0 || ih == 0) + { + // Source not yet decoded (e.g. an unrendered DynamicTexture + // label). Leave the destination buffer as-is. + return env.Undefined(); + } + + double sx = 0, sy = 0, sw = iw, sh = ih, dx, dy, dw, dh; + if (info.Length() >= 9) + { + sx = info[1].ToNumber().DoubleValue(); sy = info[2].ToNumber().DoubleValue(); + sw = info[3].ToNumber().DoubleValue(); sh = info[4].ToNumber().DoubleValue(); + dx = info[5].ToNumber().DoubleValue(); dy = info[6].ToNumber().DoubleValue(); + dw = info[7].ToNumber().DoubleValue(); dh = info[8].ToNumber().DoubleValue(); + } + else if (info.Length() >= 5) + { + dx = info[1].ToNumber().DoubleValue(); dy = info[2].ToNumber().DoubleValue(); + dw = info[3].ToNumber().DoubleValue(); dh = info[4].ToNumber().DoubleValue(); + } + else + { + dx = info[1].ToNumber().DoubleValue(); dy = info[2].ToNumber().DoubleValue(); + dw = iw; dh = ih; + } + + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + uint8_t* dst = static_cast(ab.Data()); + const int idw = static_cast(std::lround(dw)); + const int idh = static_cast(std::lround(dh)); + for (int ddy = 0; ddy < idh; ++ddy) + { + int syi = static_cast(sy + ((ddy + 0.5) / dh) * sh); + if (syi < 0) syi = 0; + if (syi >= static_cast(ih)) syi = ih - 1; + for (int ddx = 0; ddx < idw; ++ddx) + { + int sxi = static_cast(sx + ((ddx + 0.5) / dw) * sw); + if (sxi < 0) sxi = 0; + if (sxi >= static_cast(iw)) sxi = iw - 1; + const uint8_t* sp = src.data + (static_cast(syi) * iw + sxi) * 4; + const double px = dx + ddx + 0.5; + const double py = dy + ddy + 0.5; + const int bx = static_cast(std::floor(ctm->sx * px + ctm->tx)); + const int by = static_cast(std::floor(ctm->sy * py + ctm->ty)); + if (bx < 0 || by < 0 || bx >= cw || by >= ch) continue; + uint8_t* dp = dst + (static_cast(by) * cw + bx) * 4; + dp[0] = sp[0]; dp[1] = sp[1]; dp[2] = sp[2]; dp[3] = sp[3]; + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "getImageData", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + int x = info[0].ToNumber().Int32Value(); + int y = info[1].ToNumber().Int32Value(); + int w = info[2].ToNumber().Int32Value(); + int h = info[3].ToNumber().Int32Value(); + if (w <= 0 || h <= 0) { w = cw; h = ch; x = 0; y = 0; } + Napi::ArrayBuffer out = Napi::ArrayBuffer::New(env, static_cast(w) * h * 4u); + uint8_t* od = static_cast(out.Data()); + std::memset(od, 0, static_cast(w) * h * 4u); + const uint8_t* sd = static_cast(ab.Data()); + for (int yy = 0; yy < h; ++yy) + { + const int syy = y + yy; + if (syy < 0 || syy >= ch) continue; + for (int xx = 0; xx < w; ++xx) + { + const int sxx = x + xx; + if (sxx < 0 || sxx >= cw) continue; + std::memcpy(od + (static_cast(yy) * w + xx) * 4, sd + (static_cast(syy) * cw + sxx) * 4, 4); + } + } + Napi::Function u8c = env.Global().Get("Uint8ClampedArray").As(); + Napi::Object dataArr = u8c.New({out, Napi::Number::New(env, 0), Napi::Number::New(env, static_cast(w) * h * 4)}).As(); + Napi::Object res = Napi::Object::New(env); + res.Set("data", dataArr); + res.Set("width", Napi::Number::New(env, w)); + res.Set("height", Napi::Number::New(env, h)); + return res; + }); + + SetMethod(ctx, "putImageData", [canvasRef](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + if (!info[0].IsObject()) return env.Undefined(); + Napi::Object imgData = info[0].As(); + const int dx = info.Length() > 1 ? info[1].ToNumber().Int32Value() : 0; + const int dy = info.Length() > 2 ? info[2].ToNumber().Int32Value() : 0; + const uint32_t iw = PropU32(imgData, "width", 0); + const uint32_t ih = PropU32(imgData, "height", 0); + Bytes src = GetBytes(imgData.Get("data")); + if (src.data == nullptr || iw == 0 || ih == 0) return env.Undefined(); + Napi::Object canvas = canvasRef->Value(); + Napi::ArrayBuffer ab = EnsureCanvasBuffer(env, canvas); + const int cw = static_cast(canvas.Get("width").ToNumber().Uint32Value()); + const int ch = static_cast(canvas.Get("height").ToNumber().Uint32Value()); + uint8_t* dst = static_cast(ab.Data()); + for (uint32_t yy = 0; yy < ih; ++yy) + { + const int by = dy + static_cast(yy); + if (by < 0 || by >= ch) continue; + for (uint32_t xx = 0; xx < iw; ++xx) + { + const int bx = dx + static_cast(xx); + if (bx < 0 || bx >= cw) continue; + std::memcpy(dst + (static_cast(by) * cw + bx) * 4, src.data + (static_cast(yy) * iw + xx) * 4, 4); + } + } + return env.Undefined(); + }); + + SetMethod(ctx, "createImageData", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + uint32_t w = 1; + uint32_t h = 1; + if (info.Length() >= 2 && info[0].IsNumber()) + { + w = info[0].ToNumber().Uint32Value(); + h = info[1].ToNumber().Uint32Value(); + } + else if (info.Length() >= 1 && info[0].IsObject()) + { + Napi::Object o = info[0].As(); + w = PropU32(o, "width", 1); + h = PropU32(o, "height", 1); + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, static_cast(w) * h * 4u); + std::memset(ab.Data(), 0, static_cast(w) * h * 4u); + Napi::Function u8c = env.Global().Get("Uint8ClampedArray").As(); + Napi::Object dataArr = u8c.New({ab, Napi::Number::New(env, 0), Napi::Number::New(env, static_cast(w) * h * 4)}).As(); + Napi::Object res = Napi::Object::New(env); + res.Set("data", dataArr); + res.Set("width", Napi::Number::New(env, w)); + res.Set("height", Napi::Number::New(env, h)); + return res; + }); + + return ctx; + } + + // Build a no-DOM canvas whose getContext("webgpu") returns the Dawn context + // and getContext("2d") returns the raster context above. + Napi::Object MakeCanvas(Napi::Env env, uint32_t width, uint32_t height) + { + Napi::Object canvas = Napi::Object::New(env); + canvas.Set("width", Napi::Number::New(env, width)); + canvas.Set("height", Napi::Number::New(env, height)); + canvas.Set("clientWidth", Napi::Number::New(env, width)); + canvas.Set("clientHeight", Napi::Number::New(env, height)); + canvas.Set("style", Napi::Object::New(env)); + // Some scenes stash metadata on canvas.dataset (e.g. Babylon-Lite sets + // canvas.dataset.ready = "true"); provide a plain object so those + // assignments don't throw. + canvas.Set("dataset", Napi::Object::New(env)); + SetMethod(canvas, "getContext", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + const std::string type = info.Length() > 0 && info[0].IsString() ? info[0].As().Utf8Value() : ""; + if (type == "webgpu") + { + return MakeCanvasContext(env); + } + if (type == "2d") + { + Napi::Object self = info.This().As(); + Napi::Value existing = self.Get("__ctx2d"); + if (existing.IsObject()) + { + return existing; + } + Napi::Object c = Make2DContext(env, self); + self.Set("__ctx2d", c); + return c; + } + return env.Null(); + }); + SetMethod(canvas, "getBoundingClientRect", [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Env env = info.Env(); + Napi::Object self = info.This().As(); + double w = self.Get("width").ToNumber().DoubleValue(); + double h = self.Get("height").ToNumber().DoubleValue(); + Napi::Object r = Napi::Object::New(env); + r.Set("x", Napi::Number::New(env, 0)); + r.Set("y", Napi::Number::New(env, 0)); + r.Set("left", Napi::Number::New(env, 0)); + r.Set("top", Napi::Number::New(env, 0)); + r.Set("right", Napi::Number::New(env, w)); + r.Set("bottom", Napi::Number::New(env, h)); + r.Set("width", Napi::Number::New(env, w)); + r.Set("height", Napi::Number::New(env, h)); + return r; + }); + SetMethod(canvas, "setAttribute", Noop); + SetMethod(canvas, "removeAttribute", Noop); + SetMethod(canvas, "addEventListener", Noop); + SetMethod(canvas, "removeEventListener", Noop); + SetMethod(canvas, "dispatchEvent", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), true); + }); + SetMethod(canvas, "setPointerCapture", Noop); + SetMethod(canvas, "releasePointerCapture", Noop); + SetMethod(canvas, "hasPointerCapture", [](const Napi::CallbackInfo& info) -> Napi::Value { + return Napi::Boolean::New(info.Env(), false); + }); + SetMethod(canvas, "focus", Noop); + SetMethod(canvas, "getRootNode", [](const Napi::CallbackInfo& info) -> Napi::Value { + return info.This(); + }); + // The default Babylon loading screen appends its overlay div to + // `renderingCanvas.parentNode`; provide a DOM-node-like parent so + // displayLoadingUI()/hideLoadingUI() don't dereference undefined. + { + Napi::Object parent = Napi::Object::New(env); + SetMethod(parent, "appendChild", Noop); + SetMethod(parent, "removeChild", Noop); + SetMethod(parent, "insertBefore", Noop); + parent.Set("style", Napi::Object::New(env)); + canvas.Set("parentNode", parent); + canvas.Set("parentElement", parent); + } + return canvas; + } + + // Decode an encoded-image ArrayBuffer/TypedArray into an ImageBitmap-like + // object {width,height,__pixels(RGBA8 ArrayBuffer),close} via bimg. + Napi::Object DecodeToBitmap(Napi::Env env, Napi::Value abVal) + { + Napi::Object bmp = Napi::Object::New(env); + SetMethod(bmp, "close", Noop); + + Bytes in = GetBytes(abVal); + int w = 0; + int h = 0; + std::vector rgba; + if (in.data == nullptr || in.size == 0 || !DecodeRGBA(in.data, in.size, rgba, w, h)) + { + bmp.Set("width", Napi::Number::New(env, 1)); + bmp.Set("height", Napi::Number::New(env, 1)); + bmp.Set("__pixels", Napi::ArrayBuffer::New(env, 4)); + return bmp; + } + Napi::ArrayBuffer ab = Napi::ArrayBuffer::New(env, rgba.size()); + std::memcpy(ab.Data(), rgba.data(), rgba.size()); + bmp.Set("width", Napi::Number::New(env, w)); + bmp.Set("height", Napi::Number::New(env, h)); + bmp.Set("__pixels", ab); + return bmp; + } + + // Resolve `src` (ArrayBuffer / TypedArray view / {arrayBuffer()} / blob URL + // string / fetchable URL string) to an ArrayBuffer, returning a Promise. + Napi::Value ToArrayBuffer(Napi::Env env, Napi::Value src) + { + if (src.IsArrayBuffer()) + { + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(src); + return d.Promise(); + } + if (src.IsTypedArray() || src.IsDataView()) + { + Napi::Object o = src.As(); + Napi::Value buffer = o.Get("buffer"); + double offset = o.Get("byteOffset").ToNumber().DoubleValue(); + double length = o.Get("byteLength").ToNumber().DoubleValue(); + Napi::Value sliced = buffer.As().Get("slice").As().Call( + buffer, {Napi::Number::New(env, offset), Napi::Number::New(env, offset + length)}); + auto d = Napi::Promise::Deferred::New(env); + d.Resolve(sliced); + return d.Promise(); + } + if (src.IsObject() && src.As().Get("arrayBuffer").IsFunction()) + { + Napi::Object o = src.As(); + return o.Get("arrayBuffer").As().Call(o, {}); + } + if (src.IsString()) + { + const std::string id = src.As().Utf8Value(); + Napi::Value blob = g_blobRegistry.Value().Get(id); + if (!blob.IsUndefined()) + { + return ToArrayBuffer(env, blob); + } + Napi::Value fetchVal = env.Global().Get("fetch"); + if (fetchVal.IsFunction()) + { + Napi::Value p = fetchVal.As().Call({src}); + Napi::Function toAb = Napi::Function::New(env, [](const Napi::CallbackInfo& info) -> Napi::Value { + Napi::Object r = info[0].As(); + return r.Get("arrayBuffer").As().Call(r, {}); + }); + return p.As().Get("then").As().Call(p, {toAb}); + } + } + auto d = Napi::Promise::Deferred::New(env); + d.Reject(Napi::Error::New(env, "toArrayBuffer: unsupported source").Value()); + return d.Promise(); + } + + // Create + initialize the WebGPUEngine once babylon.max.js has defined + // BABYLON, then alias BABYLON.NativeEngine to return it. + void OnBabylonReady(Napi::Env env, Napi::Value babylonVal) + { + if (g_engineStarted || !babylonVal.IsObject()) + { + return; + } + Napi::Object babylon = babylonVal.As(); + Napi::Value wgpuCtor = babylon.Get("WebGPUEngine"); + if (!wgpuCtor.IsFunction()) + { + return; + } + g_engineStarted = true; + + // Babylon assigns Tools.LoadScript = _LoadScriptWeb when the `_native` + // global is absent -- which it is in the Dawn build (NativeEngine is + // disabled). _LoadScriptWeb injects a