diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 2add0bd2cb..62fa7a85b6 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -25,7 +25,7 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: Android diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 8f76b391b2..923961c79e 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -57,7 +57,7 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: ${{ matrix.platform }} @@ -71,20 +71,20 @@ jobs: - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: build-type: ${{ matrix.build_type }} cmake-args: ${{ matrix.cmake_args }} - name: Build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 with: target: install - name: DiligentCoreTest if: ${{ success() && matrix.platform == 'MacOS' }} - uses: DiligentGraphics/github-action/run-core-tests@v27 + uses: DiligentGraphics/github-action/run-core-tests@v28 - name: Upload artifact uses: actions/upload-artifact@v7 diff --git a/.github/workflows/build-emscripten.yml b/.github/workflows/build-emscripten.yml index 2ec6e33a31..4a1919fcff 100644 --- a/.github/workflows/build-emscripten.yml +++ b/.github/workflows/build-emscripten.yml @@ -38,17 +38,17 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: Web - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: build-type: ${{ matrix.build_type }} cmake-args: ${{ matrix.cmake_args }} - name: Build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 6e57137005..302462a752 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -101,7 +101,7 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: Linux cmake-generator: ${{ matrix.cmake_generator }} @@ -116,7 +116,7 @@ jobs: - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: cc: ${{ matrix.cc }} cxx: ${{ matrix.cxx }} @@ -129,38 +129,38 @@ jobs: - name: Build id: build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 with: target: install - name: DiligentCoreTest if: success() - uses: DiligentGraphics/github-action/run-core-tests@v27 + uses: DiligentGraphics/github-action/run-core-tests@v28 - name: DiligentCoreAPITest VK # NB: it is essential to include failure() to override the default status check of success() # that is automatically applied to if conditions that don't contain a status check function. if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Clang' || matrix.name == 'GCC' || matrix.name == 'Clang-ASAN' || matrix.name == 'Clang-TSAN') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: vk_sw - name: DiligentCoreAPITest VK Compatibility if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Clang' || matrix.name == 'GCC' || matrix.name == 'Clang-ASAN' || matrix.name == 'Clang-TSAN') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: vk_sw vk-compatibility: true - name: DiligentCoreAPITest GL if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Clang' || matrix.name == 'GCC' || matrix.name == 'Clang-ASAN' || matrix.name == 'Clang-TSAN') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: gl - name: DiligentCoreAPITest GL with Non-Separable Programs if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Clang' || matrix.name == 'GCC' || matrix.name == 'Clang-ASAN' || matrix.name == 'Clang-TSAN') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: gl non-separable-progs: true diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b7081c6eaa..7bb771aee9 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -77,7 +77,7 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: ${{ matrix.platform }} cmake-generator: ${{ matrix.cmake_generator }} @@ -93,7 +93,7 @@ jobs: - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: generator: ${{ matrix.cmake_generator }} vs-arch: ${{ matrix.toolset }} @@ -103,25 +103,25 @@ jobs: - name: Build id: build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 with: target: install - name: DiligentCoreTest if: ${{ success() && matrix.name != 'UWP'}} - uses: DiligentGraphics/github-action/run-core-tests@v27 + uses: DiligentGraphics/github-action/run-core-tests@v28 - name: DiligentCoreAPITest D3D11 # NB: it is essential to include failure() to override the default status check of success() # that is automatically applied to if conditions that don't contain a status check function. if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Win10' || matrix.name == 'Win10-Ninja') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: d3d11_sw - name: DiligentCoreAPITest D3D12 if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Win10' || matrix.name == 'Win10-Ninja') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: d3d12_sw # Temporarily skip flaky RT2/RT4 until WARP and reference AS synchronization are fixed. @@ -129,7 +129,7 @@ jobs: - name: DiligentCoreAPITest D3D12 DXC if: ${{ (success() || failure() && steps.build.outcome == 'success') && (matrix.name == 'Win10' || matrix.name == 'Win10-Ninja') }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: d3d12_sw use-dxc: true @@ -139,7 +139,7 @@ jobs: - name: DiligentCoreAPITest WebGPU if: ${{ (success() || failure() && steps.build.outcome == 'success') && matrix.name == 'Win10' }} - uses: DiligentGraphics/github-action/run-core-gpu-tests@v27 + uses: DiligentGraphics/github-action/run-core-gpu-tests@v28 with: mode: wgpu diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5a30fd913c..d5f71ed851 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,14 +41,14 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: ${{ matrix.platform }} cmake-generator: ${{ matrix.cmake_generator }} - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: cc: ${{ matrix.cc }} cxx: ${{ matrix.cxx }} @@ -65,7 +65,7 @@ jobs: - name: Build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/msvc_analysis.yml b/.github/workflows/msvc_analysis.yml index 3ccfe2ebc6..27ba82e306 100644 --- a/.github/workflows/msvc_analysis.yml +++ b/.github/workflows/msvc_analysis.yml @@ -32,14 +32,14 @@ jobs: - name: Set up build environment if: success() - uses: DiligentGraphics/github-action/setup-build-env@v27 + uses: DiligentGraphics/github-action/setup-build-env@v28 with: platform: ${{ matrix.platform }} cmake-generator: ${{ matrix.cmake_generator }} - name: Configure CMake if: success() - uses: DiligentGraphics/github-action/configure-cmake@v27 + uses: DiligentGraphics/github-action/configure-cmake@v28 with: generator: ${{ matrix.cmake_generator }} vs-arch: ${{ matrix.toolset }} @@ -48,7 +48,7 @@ jobs: - name: Build if: success() - uses: DiligentGraphics/github-action/build@v27 + uses: DiligentGraphics/github-action/build@v28 - name: Run MSVC Code Analysis uses: DiligentGraphics/msvc-code-analysis-action@main diff --git a/BuildTools/CMake/BuildUtils.cmake b/BuildTools/CMake/BuildUtils.cmake index 4c8c6f7488..55fddd2415 100644 --- a/BuildTools/CMake/BuildUtils.cmake +++ b/BuildTools/CMake/BuildUtils.cmake @@ -110,11 +110,12 @@ if(PLATFORM_WIN32) set(DXCOMPILER_REQUIRED NO) set(DXCOMPILER_FOR_SPIRV_REQUIRED NO) - if (D3D11_SUPPORTED OR D3D12_SUPPORTED) + # Dawn falls back to FXC on adapters that do not support shader model 6.0. + if (D3D11_SUPPORTED OR D3D12_SUPPORTED OR WEBGPU_SUPPORTED) set(D3D_COMPILER_REQUIRED YES) endif() - # Dawn uses DXC, so we need to copy the DXC dlls even if DXC_REQUIRED is not set + # Dawn uses DXC, so copy the DXC dlls even if DXC_REQUIRED is not set # to get consistent shader compilation results if((arg_DXC_REQUIRED AND D3D12_SUPPORTED) OR WEBGPU_SUPPORTED) set(DXCOMPILER_REQUIRED YES) @@ -522,6 +523,12 @@ endfunction() # clone. This macro takes care of it. macro(FetchContent_DeclareShallowGit Name GIT_REPOSITORY GitRepository GIT_TAG GitTag) include(FetchContent) + cmake_parse_arguments(_fetch_args "" "" "GIT_SUBMODULES" ${ARGN}) + set(_fetch_submodules_command) + if(_fetch_args_GIT_SUBMODULES) + set(_fetch_submodules_command + && git submodule update --init --depth=1 -- ${_fetch_args_GIT_SUBMODULES}) + endif() FetchContent_Declare( "${Name}" @@ -536,6 +543,7 @@ macro(FetchContent_DeclareShallowGit Name GIT_REPOSITORY GitRepository GIT_TAG G git init && git fetch --depth=1 "${GitRepository}" "${GitTag}" && git reset --hard FETCH_HEAD + ${_fetch_submodules_command} ) endmacro() diff --git a/Graphics/Archiver/CMakeLists.txt b/Graphics/Archiver/CMakeLists.txt index 4e20f8de5b..c09fd7ddf0 100644 --- a/Graphics/Archiver/CMakeLists.txt +++ b/Graphics/Archiver/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.10...3.22) include(../../BuildTools/CMake/BuildUtils.cmake) diff --git a/Graphics/Archiver/src/Archiver_WebGPU.cpp b/Graphics/Archiver/src/Archiver_WebGPU.cpp index 3d86912e34..9cf55a80e8 100644 --- a/Graphics/Archiver/src/Archiver_WebGPU.cpp +++ b/Graphics/Archiver/src/Archiver_WebGPU.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,7 +203,7 @@ void SerializedPipelineStateImpl::PatchShadersWebGPU(const CreateInfoType& Creat ShaderCI.Source = WGSL.c_str(); ShaderCI.SourceLength = WGSL.length(); ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; - ShaderCI.EntryPoint = ShaderStagesWebGPU[i].pShader->GetEntryPoint(); + ShaderCI.EntryPoint = ShaderStagesWebGPU[i].GetEntryPoint(); ShaderCI.FilePath = nullptr; ShaderCI.Macros = {}; ShaderCI.ByteCode = nullptr; diff --git a/Graphics/GraphicsEngine/include/ShaderResourceVariableBase.hpp b/Graphics/GraphicsEngine/include/ShaderResourceVariableBase.hpp index 0469d518f3..d02fde3f06 100644 --- a/Graphics/GraphicsEngine/include/ShaderResourceVariableBase.hpp +++ b/Graphics/GraphicsEngine/include/ShaderResourceVariableBase.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2019-2025 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * Copyright 2015-2019 Egor Yusov * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -840,7 +840,7 @@ struct ShaderVariableBase : public ResourceVariableBaseInterface const PipelineResourceDesc& ResDesc = pThis->GetDesc(); const SHADER_RESOURCE_VARIABLE_TYPE_FLAGS VarTypeFlag = static_cast(1u << ResDesc.VarType); - if ((Flags & VarTypeFlag) == 0) + if ((Flags & static_cast(VarTypeFlag)) == 0) return; // This variable type is not being processed if ((StaleVarTypes & VarTypeFlag) != 0) diff --git a/Graphics/GraphicsEngine/interface/SwapChain.h b/Graphics/GraphicsEngine/interface/SwapChain.h index 10bc8b8e84..5c5ffdba09 100644 --- a/Graphics/GraphicsEngine/interface/SwapChain.h +++ b/Graphics/GraphicsEngine/interface/SwapChain.h @@ -1,5 +1,5 @@ /* - * Copyright 2019-2025 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * Copyright 2015-2019 Egor Yusov * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -107,6 +107,12 @@ DILIGENT_BEGIN_INTERFACE(ISwapChain, IObject) /// backend it always returns the same pointer. For OpenGL/GLES backends /// the method returns null. /// + /// For WebGPU, the back buffer is acquired on demand and is valid until Present() + /// or Resize(). The method may return null if no surface texture is available; + /// the application should skip rendering and retry on a subsequent frame. + /// On the Web, acquire, render, and present within the same browser animation + /// frame, since canvas textures expire when control returns to the browser. + /// /// The method does **NOT** increment the reference counter of the returned object, /// so Release() **must not** be called. VIRTUAL ITextureView* METHOD(GetCurrentBackBufferRTV)(THIS) PURE; diff --git a/Graphics/GraphicsEngineWebGPU/CMakeLists.txt b/Graphics/GraphicsEngineWebGPU/CMakeLists.txt index 518e0a80fa..e229ab14e6 100644 --- a/Graphics/GraphicsEngineWebGPU/CMakeLists.txt +++ b/Graphics/GraphicsEngineWebGPU/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.10...3.22) project(Diligent-GraphicsEngineWebGPU CXX) @@ -33,10 +33,10 @@ set(INCLUDE include/SyncPointWebGPU.hpp include/TextureViewWebGPUImpl.hpp include/TextureWebGPUImpl.hpp + include/WebGPUDefinitions.hpp include/WebGPUObjectWrappers.hpp include/WebGPUTypeConversions.hpp include/WebGPUResourceBase.hpp - include/WebGPUStubs.hpp ) set(INTERFACE @@ -96,6 +96,9 @@ set(DLL_SOURCE add_library(Diligent-GraphicsEngineWebGPUInterface INTERFACE) target_link_libraries (Diligent-GraphicsEngineWebGPUInterface INTERFACE Diligent-GraphicsEngineInterface) target_include_directories(Diligent-GraphicsEngineWebGPUInterface INTERFACE interface) +if(PLATFORM_WEB) + target_link_libraries(Diligent-GraphicsEngineWebGPUInterface INTERFACE emdawnwebgpu_c_include) +endif() add_library(Diligent-GraphicsEngineWebGPU-static STATIC ${SRC} ${INTERFACE} ${INCLUDE} @@ -134,7 +137,9 @@ else() list(APPEND PRIVATE_DEPENDENCIES SPIRV-Tools-opt) endif() -if (NOT PLATFORM_WEB) +if (PLATFORM_WEB) + list(APPEND PRIVATE_DEPENDENCIES emdawnwebgpu_c) +else() list(APPEND PRIVATE_DEPENDENCIES dawn_native dawn_proc) endif() @@ -166,7 +171,7 @@ else() endif() set_common_target_properties(Diligent-GraphicsEngineWebGPU-shared) -set_common_target_properties(Diligent-GraphicsEngineWebGPU-static) +set_common_target_properties(Diligent-GraphicsEngineWebGPU-static 20) if (${DILIGENT_NO_GLSLANG}) message("GLSLang is not being built. WebGPU backend will only be able to consume SPIRV byte code.") @@ -179,7 +184,7 @@ PRIVATE ) if (PLATFORM_WEB) - target_link_options(Diligent-GraphicsEngineWebGPU-static PUBLIC "SHELL: -s USE_WEBGPU=1 -s USE_PTHREADS=1") + target_link_options(Diligent-GraphicsEngineWebGPU-static PUBLIC "-pthread") endif() target_compile_definitions(Diligent-GraphicsEngineWebGPU-shared PUBLIC DILIGENT_WEBGPU_SHARED=1) diff --git a/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp index 7a18d19cb0..18d5692329 100644 --- a/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp @@ -84,12 +84,14 @@ class PipelineStateWebGPUImpl final : public PipelineStateBase SpecConstEntries; + std::vector SpecConstKeys; ShaderStageInfo(ShaderWebGPUImpl* _pShader) : Type{_pShader->GetDesc().ShaderType}, @@ -101,6 +103,11 @@ class PipelineStateWebGPUImpl final : public PipelineStateBaseGetWGSL() : PatchedWGSL; } + const char* GetEntryPoint() const + { + return PatchedEntryPoint.empty() ? pShader->GetEntryPoint() : PatchedEntryPoint.c_str(); + } + friend SHADER_TYPE GetShaderStageType(const ShaderStageInfo& Stage) { return Stage.Type; } friend std::vector GetStageShaders(const ShaderStageInfo& Stage) { return {Stage.pShader}; } diff --git a/Graphics/GraphicsEngineWebGPU/include/SwapChainWebGPUImpl.hpp b/Graphics/GraphicsEngineWebGPU/include/SwapChainWebGPUImpl.hpp index 40a2783143..9586e7d5c8 100644 --- a/Graphics/GraphicsEngineWebGPU/include/SwapChainWebGPUImpl.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/SwapChainWebGPUImpl.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,6 @@ /// \file /// Declaration of Diligent::SwapChainWebGPUImpl class -#include - #include "SwapChainBase.hpp" #include "SwapChainWebGPU.h" #include "EngineWebGPUImplTraits.hpp" @@ -55,10 +53,10 @@ class SwapChainWebGPUImpl final : public SwapChainBase IMPLEMENT_QUERY_INTERFACE_IN_PLACE(IID_SwapChainWebGPU, TSwapChainBase) - /// Implementation of ISwapChain::Present() in Direct3D11 backend. + /// Implementation of ISwapChain::Present() in WebGPU backend. void DILIGENT_CALL_TYPE Present(Uint32 SyncInterval) override final; - /// Implementation of ISwapChain::Resize() in Direct3D11 backend. + /// Implementation of ISwapChain::Resize() in WebGPU backend. void DILIGENT_CALL_TYPE Resize(Uint32 NewWidth, Uint32 NewHeight, SURFACE_TRANSFORM NewPreTransform) override final; /// Implementation of ISwapChain::SetFullscreenMode() in WebGPU backend. @@ -68,7 +66,7 @@ class SwapChainWebGPUImpl final : public SwapChainBase void DILIGENT_CALL_TYPE SetWindowedMode() override final; /// Implementation of ISwapChainWebGPU::GetCurrentBackBufferRTV() in WebGPU backend. - ITextureViewWebGPU* DILIGENT_CALL_TYPE GetCurrentBackBufferRTV() override final { return m_pBackBufferRTV; } + ITextureViewWebGPU* DILIGENT_CALL_TYPE GetCurrentBackBufferRTV() override final; /// Implementation of ISwapChainWebGPU::GetDepthBufferDSV() in WebGPU backend. ITextureViewWebGPU* DILIGENT_CALL_TYPE GetDepthBufferDSV() override final { return m_pDepthBufferDSV; } @@ -77,26 +75,21 @@ class SwapChainWebGPUImpl final : public SwapChainBase WGPUSurface DILIGENT_CALL_TYPE GetWebGPUSurface() override final { return m_wgpuSurface.Get(); } private: - void CreateSurface(); - - void ConfigureSurface(); - - void CreateBuffersAndViews(); - + bool CreateSurface(); + bool ConfigureSurface(); + bool AcquireSurfaceTexture(); + bool CreateDepthBufferView(); void ReleaseSwapChainResources(); - - void RecreateSwapChain(); - -private: - class PresentCommand; + bool RecreateSwapChain(); NativeWindow m_NativeWindow; WebGPUSurfaceWrapper m_wgpuSurface; + WebGPUTextureWrapper m_wgpuCurrentTexture; RefCntAutoPtr m_pBackBufferRTV; - RefCntAutoPtr m_pBackBufferSRV; RefCntAutoPtr m_pDepthBufferDSV; - std::unique_ptr m_pCmdPresent; - bool m_VSyncEnabled = true; + bool m_VSyncEnabled = true; + bool m_SurfaceConfigured = false; + bool m_ReconfigureSurface = false; }; } // namespace Diligent diff --git a/Graphics/GraphicsEngineWebGPU/include/WebGPUStubs.hpp b/Graphics/GraphicsEngineWebGPU/include/WebGPUDefinitions.hpp similarity index 55% rename from Graphics/GraphicsEngineWebGPU/include/WebGPUStubs.hpp rename to Graphics/GraphicsEngineWebGPU/include/WebGPUDefinitions.hpp index d4dec4d798..51c34abb24 100644 --- a/Graphics/GraphicsEngineWebGPU/include/WebGPUStubs.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/WebGPUDefinitions.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,19 +26,24 @@ #pragma once +#include + #if PLATFORM_WEB -inline constexpr WGPUFeatureName WGPUFeatureName_ChromiumExperimentalTimestampQueryInsidePasses = static_cast(0x000003EE); -inline constexpr WGPUFeatureName WGPUFeatureName_Unorm16TextureFormats = static_cast(0x000003FB); -inline constexpr WGPUFeatureName WGPUFeatureName_Snorm16TextureFormats = static_cast(0x000003FC); +// This Dawn-only feature is not exposed by Emdawnwebgpu. +inline constexpr WGPUFeatureName WGPUFeatureName_ChromiumExperimentalTimestampQueryInsidePasses = static_cast(0x00050003); -inline constexpr WGPUTextureFormat WGPUTextureFormat_R16Unorm = static_cast(0x00000060); -inline constexpr WGPUTextureFormat WGPUTextureFormat_R16Snorm = static_cast(0x00000063); -inline constexpr WGPUTextureFormat WGPUTextureFormat_RG16Unorm = static_cast(0x00000061); -inline constexpr WGPUTextureFormat WGPUTextureFormat_RG16Snorm = static_cast(0x00000064); -inline constexpr WGPUTextureFormat WGPUTextureFormat_RGBA16Unorm = static_cast(0x00000062); -inline constexpr WGPUTextureFormat WGPUTextureFormat_RGBA16Snorm = static_cast(0x00000065); +#endif -inline constexpr WGPUSurfaceGetCurrentTextureStatus WGPUSurfaceGetCurrentTextureStatus_Error = static_cast(0x00000007); +namespace Diligent +{ +#if PLATFORM_WEB +// Let the browser event loop deliver completion without explicit polling. +constexpr WGPUCallbackMode DefaultWGPUCallbackMode = WGPUCallbackMode_AllowSpontaneous; +#else +// Keep callback execution on the thread that processes instance events. +constexpr WGPUCallbackMode DefaultWGPUCallbackMode = WGPUCallbackMode_AllowProcessEvents; #endif + +} // namespace Diligent diff --git a/Graphics/GraphicsEngineWebGPU/include/WebGPUTypeConversions.hpp b/Graphics/GraphicsEngineWebGPU/include/WebGPUTypeConversions.hpp index 0949427259..61d611daca 100644 --- a/Graphics/GraphicsEngineWebGPU/include/WebGPUTypeConversions.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/WebGPUTypeConversions.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ #include "DepthStencilState.h" #include "RasterizerState.h" #include "InputLayout.h" +#include "TextureView.h" namespace Diligent { @@ -81,6 +82,10 @@ WGPUShaderStage ShaderStagesToWGPUShaderStageFlags(SHADER_TYPE Stages); WGPUVertexStepMode InputElementFrequencyToWGPUVertexStepMode(INPUT_ELEMENT_FREQUENCY StepRate); +WGPUComponentSwizzle TextureComponentSwizzleToWGPUComponentSwizzle(TEXTURE_COMPONENT_SWIZZLE Swizzle); + +WGPUTextureComponentSwizzle TextureComponentMappingToWGPUTextureComponentSwizzle(const TextureComponentMapping& Mapping); + inline WGPUOptionalBool BoolToWGPUOptionalBool(bool Value) { return Value ? WGPUOptionalBool_True : WGPUOptionalBool_False; diff --git a/Graphics/GraphicsEngineWebGPU/include/pch.h b/Graphics/GraphicsEngineWebGPU/include/pch.h index 8ace9d3b41..1468242e47 100644 --- a/Graphics/GraphicsEngineWebGPU/include/pch.h +++ b/Graphics/GraphicsEngineWebGPU/include/pch.h @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,47 +35,8 @@ #include -#if PLATFORM_WEB +#include "WebGPUDefinitions.hpp" -using WGPUOptionalBool = bool; -using WGPUShaderSourceWGSL = WGPUShaderModuleWGSLDescriptor; -using WGPUStringView = const char*; -using WGPUSurfaceSourceCanvasHTMLSelector_Emscripten = WGPUSurfaceDescriptorFromCanvasHTMLSelector; - -constexpr bool WGPUOptionalBool_True = true; -constexpr bool WGPUOptionalBool_False = false; - -constexpr WGPUSType WGPUSType_ShaderSourceWGSL = WGPUSType_ShaderModuleWGSLDescriptor; -constexpr WGPUSType WGPUSType_SurfaceSourceCanvasHTMLSelector_Emscripten = WGPUSType_SurfaceDescriptorFromCanvasHTMLSelector; - -constexpr WGPUBufferBindingType WGPUBufferBindingType_BindingNotUsed = WGPUBufferBindingType_Undefined; -constexpr WGPUSamplerBindingType WGPUSamplerBindingType_BindingNotUsed = WGPUSamplerBindingType_Undefined; -constexpr WGPUTextureSampleType WGPUTextureSampleType_BindingNotUsed = WGPUTextureSampleType_Undefined; -constexpr WGPUStorageTextureAccess WGPUStorageTextureAccess_BindingNotUsed = WGPUStorageTextureAccess_Undefined; - -constexpr WGPUFeatureName WGPUFeatureName_DualSourceBlending = static_cast(0x00050008); - -inline bool WGPUStringViewValid(WGPUStringView Str) -{ - return Str != nullptr && Str[0] != '\0'; -} - -inline const char* WGPUStringViewToString(WGPUStringView Str) -{ - return Str; -} - -inline WGPUStringView GetWGPUStringView(const std::string& Str) -{ - return Str.c_str(); -} - -inline WGPUStringView GetWGPUStringView(const char* Str) -{ - return Str; -} - -#else inline bool WGPUStringViewValid(const WGPUStringView& Str) { @@ -102,5 +63,3 @@ inline WGPUStringView GetWGPUStringView(const char* Str) { return {Str, Str ? strlen(Str) : 0}; } - -#endif diff --git a/Graphics/GraphicsEngineWebGPU/interface/EngineFactoryWebGPU.h b/Graphics/GraphicsEngineWebGPU/interface/EngineFactoryWebGPU.h index 83711b9fcd..9848738389 100644 --- a/Graphics/GraphicsEngineWebGPU/interface/EngineFactoryWebGPU.h +++ b/Graphics/GraphicsEngineWebGPU/interface/EngineFactoryWebGPU.h @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,9 @@ DILIGENT_BEGIN_INTERFACE(IEngineFactoryWebGPU, IEngineFactory) { /// Creates a render device and device contexts for WebGPU-based engine implementation. + /// This synchronous method is not supported on the Web. Create the device asynchronously + /// in JavaScript and use IEngineFactoryWebGPU::AttachToWebGPUDevice() instead. + /// \param [in] EngineCI - Engine creation info. /// \param [out] ppDevice - Address of the memory location where pointer to /// the created device will be written. diff --git a/Graphics/GraphicsEngineWebGPU/src/DeviceContextWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/DeviceContextWebGPUImpl.cpp index ed0e66e838..0deee5dd31 100644 --- a/Graphics/GraphicsEngineWebGPU/src/DeviceContextWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/DeviceContextWebGPUImpl.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -919,7 +919,7 @@ void DeviceContextWebGPUImpl::UpdateTexture(ITexture* pText BufferWebGPUImpl* const pSrcBufferWebGPU = ClassPtrCast(SubresData.pSrcBuffer); const BufferDesc& SrcBuffDesc = pSrcBufferWebGPU->GetDesc(); - WGPUImageCopyTexture wgpuImageCopyDst{}; + WGPUTexelCopyTextureInfo wgpuImageCopyDst{}; wgpuImageCopyDst.texture = pDstTextureWebGPU->GetWebGPUTexture(); wgpuImageCopyDst.aspect = WGPUTextureAspect_All; wgpuImageCopyDst.origin.x = DstBox.MinX; @@ -951,7 +951,7 @@ void DeviceContextWebGPUImpl::UpdateTexture(ITexture* pText } } - WGPUImageCopyBuffer wgpuImageCopySrc{}; + WGPUTexelCopyBufferInfo wgpuImageCopySrc{}; wgpuImageCopySrc.buffer = pSrcStagingBuffer != nullptr ? pSrcStagingBuffer->wgpuBuffer : pSrcBufferWebGPU->GetWebGPUBuffer(); wgpuImageCopySrc.layout.offset = SubresData.SrcOffset; wgpuImageCopySrc.layout.bytesPerRow = static_cast(SubresData.Stride); @@ -988,13 +988,13 @@ void DeviceContextWebGPUImpl::UpdateTexture(ITexture* pText } } - WGPUImageCopyBuffer wgpuImageCopySrc{}; + WGPUTexelCopyBufferInfo wgpuImageCopySrc{}; wgpuImageCopySrc.buffer = UploadAlloc.wgpuBuffer; wgpuImageCopySrc.layout.offset = UploadAlloc.Offset; wgpuImageCopySrc.layout.bytesPerRow = static_cast(CopyInfo.RowStride); wgpuImageCopySrc.layout.rowsPerImage = static_cast(CopyInfo.DepthStride / CopyInfo.RowStride); - WGPUImageCopyTexture wgpuImageCopyDst{}; + WGPUTexelCopyTextureInfo wgpuImageCopyDst{}; wgpuImageCopyDst.texture = pTextureWebGPU->GetWebGPUTexture(); wgpuImageCopyDst.aspect = WGPUTextureAspect_All; wgpuImageCopyDst.origin.x = DstBox.MinX; @@ -1056,7 +1056,7 @@ void DeviceContextWebGPUImpl::CopyTexture(const CopyTextureAttribs& CopyAttribs) else wgpuAspectMask = WGPUTextureAspect_All; - WGPUImageCopyTexture wgpuImageCopySrc{}; + WGPUTexelCopyTextureInfo wgpuImageCopySrc{}; wgpuImageCopySrc.texture = pSrcTexWebGPU->GetWebGPUTexture(); wgpuImageCopySrc.aspect = wgpuAspectMask; wgpuImageCopySrc.origin.x = pSrcBox->MinX; @@ -1064,7 +1064,7 @@ void DeviceContextWebGPUImpl::CopyTexture(const CopyTextureAttribs& CopyAttribs) wgpuImageCopySrc.origin.z = CopyAttribs.SrcSlice != 0 ? CopyAttribs.SrcSlice : pSrcBox->MinZ; wgpuImageCopySrc.mipLevel = CopyAttribs.SrcMipLevel; - WGPUImageCopyTexture wgpuImageCopyDst{}; + WGPUTexelCopyTextureInfo wgpuImageCopyDst{}; wgpuImageCopyDst.texture = pDstTexWebGPU->GetWebGPUTexture(); wgpuImageCopyDst.aspect = wgpuAspectMask; wgpuImageCopyDst.origin.x = CopyAttribs.DstX; @@ -1106,13 +1106,13 @@ void DeviceContextWebGPUImpl::CopyTexture(const CopyTextureAttribs& CopyAttribs) return; } - WGPUImageCopyBuffer wgpuImageCopySrc{}; + WGPUTexelCopyBufferInfo wgpuImageCopySrc{}; wgpuImageCopySrc.buffer = pSrcStagingBuffer->wgpuBuffer; wgpuImageCopySrc.layout.offset = SrcBufferOffset; wgpuImageCopySrc.layout.bytesPerRow = static_cast(AlignUp(SrcMipLevelAttribs.RowSize, TextureWebGPUImpl::ImageCopyBufferRowAlignment)); wgpuImageCopySrc.layout.rowsPerImage = SrcMipLevelAttribs.StorageHeight / DstFmtAttribs.BlockHeight; - WGPUImageCopyTexture wgpuImageCopyDst{}; + WGPUTexelCopyTextureInfo wgpuImageCopyDst{}; wgpuImageCopyDst.texture = pDstTexWebGPU->GetWebGPUTexture(); wgpuImageCopyDst.aspect = wgpuAspectMask; wgpuImageCopyDst.origin.x = CopyAttribs.DstX; @@ -1156,7 +1156,7 @@ void DeviceContextWebGPUImpl::CopyTexture(const CopyTextureAttribs& CopyAttribs) return; } - WGPUImageCopyTexture wgpuImageCopySrc{}; + WGPUTexelCopyTextureInfo wgpuImageCopySrc{}; wgpuImageCopySrc.texture = pSrcTexWebGPU->GetWebGPUTexture(); wgpuImageCopySrc.aspect = wgpuAspectMask; wgpuImageCopySrc.origin.x = pSrcBox->MinX; @@ -1164,7 +1164,7 @@ void DeviceContextWebGPUImpl::CopyTexture(const CopyTextureAttribs& CopyAttribs) wgpuImageCopySrc.origin.z = CopyAttribs.SrcSlice != 0 ? CopyAttribs.SrcSlice : pSrcBox->MinZ; wgpuImageCopySrc.mipLevel = CopyAttribs.SrcMipLevel; - WGPUImageCopyBuffer wgpuImageCopyDst{}; + WGPUTexelCopyBufferInfo wgpuImageCopyDst{}; wgpuImageCopyDst.buffer = pDstStagingBuffer->wgpuBuffer; wgpuImageCopyDst.layout.offset = DstBufferOffset; wgpuImageCopyDst.layout.bytesPerRow = static_cast(AlignUp(DstMipLevelAttribs.RowSize, TextureWebGPUImpl::ImageCopyBufferRowAlignment)); @@ -1300,13 +1300,13 @@ void DeviceContextWebGPUImpl::UnmapTextureSubresource(ITexture* pTexture, Uint32 const UploadMemoryManagerWebGPU::Allocation& Allocation = UploadSpaceIt->second.Allocation; const BufferToTextureCopyInfo& CopyInfo = UploadSpaceIt->second.CopyInfo; - WGPUImageCopyBuffer wgpuImageCopySrc{}; + WGPUTexelCopyBufferInfo wgpuImageCopySrc{}; wgpuImageCopySrc.buffer = Allocation.wgpuBuffer; wgpuImageCopySrc.layout.offset = Allocation.Offset; wgpuImageCopySrc.layout.bytesPerRow = static_cast(CopyInfo.RowStride); wgpuImageCopySrc.layout.rowsPerImage = static_cast(CopyInfo.DepthStride / CopyInfo.RowStride); - WGPUImageCopyTexture wgpuImageCopyDst{}; + WGPUTexelCopyTextureInfo wgpuImageCopyDst{}; wgpuImageCopyDst.texture = pTextureWebGPU->GetWebGPUTexture(); wgpuImageCopyDst.aspect = WGPUTextureAspect_All; wgpuImageCopyDst.origin.x = CopyInfo.Region.MinX; @@ -1499,7 +1499,7 @@ void DeviceContextWebGPUImpl::Flush() if (m_wgpuCommandEncoder || !m_SignaledFences.empty()) { - auto WorkDoneCallback = [](WGPUQueueWorkDoneStatus Status, void* pUserData) { + auto WorkDoneCallback = [](WGPUQueueWorkDoneStatus Status, WGPUStringView Message, void* pUserData, void* pUserData2) { VERIFY_EXPR(pUserData != nullptr); SyncPointWebGPUImpl* pSyncPoint = static_cast(pUserData); pSyncPoint->Trigger(); @@ -1526,7 +1526,11 @@ void DeviceContextWebGPUImpl::Flush() DEV_CHECK_ERR(wgpuCmdBuffer != nullptr, "Failed to finish command encoder"); wgpuQueueSubmit(m_wgpuQueue, 1, &wgpuCmdBuffer.Get()); - wgpuQueueOnSubmittedWorkDone(m_wgpuQueue, WorkDoneCallback, pWorkDoneSyncPoint.Detach()); + WGPUQueueWorkDoneCallbackInfo CallbackInfo{}; + CallbackInfo.mode = DefaultWGPUCallbackMode; + CallbackInfo.callback = WorkDoneCallback; + CallbackInfo.userdata1 = pWorkDoneSyncPoint.Detach(); + wgpuQueueOnSubmittedWorkDone(m_wgpuQueue, CallbackInfo); m_wgpuCommandEncoder.Reset(nullptr); for (auto& PendingReadIt : m_PendingStagingReads) diff --git a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp index abd299ed49..8f8ca736e8 100644 --- a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp @@ -35,7 +35,7 @@ #include "ShaderResourceCacheWebGPU.hpp" #include "FenceWebGPUImpl.hpp" #include "DearchiverWebGPUImpl.hpp" -#include "WebGPUStubs.hpp" +#include "WebGPUDefinitions.hpp" #include "StringTools.hpp" #include "GraphicsAccessories.hpp" @@ -45,10 +45,6 @@ # include "dawn/dawn_proc.h" #endif -#if PLATFORM_WEB -# include -#endif - namespace Diligent { @@ -99,19 +95,15 @@ class EngineFactoryWebGPUImpl final : public EngineFactoryBase(&wgpuDawnTogglesDesc); } WebGPUInstanceWrapper wgpuInstance{wgpuCreateInstance(&wgpuInstanceDesc)}; -#endif if (!wgpuInstance) LOG_ERROR_AND_THROW("Failed to create WebGPU instance"); return wgpuInstance; @@ -153,7 +144,7 @@ std::vector FindCompatibleAdapters(WGPUInstance wgpuInstan bool IsReady = {}; }; - auto OnAdapterRequestEnded = [](WGPURequestAdapterStatus Status, WGPUAdapter Adapter, WGPUStringView Message, void* pCallbackUserData) { + auto OnAdapterRequestEnded = [](WGPURequestAdapterStatus Status, WGPUAdapter Adapter, WGPUStringView Message, void* pCallbackUserData, void* pCallbackUserData2) { if (pCallbackUserData != nullptr) { CallbackUserData* pUserData = static_cast(pCallbackUserData); @@ -177,8 +168,12 @@ std::vector FindCompatibleAdapters(WGPUInstance wgpuInstan Options.powerPreference = powerPreference; Options.backendType = WGPUBackendType_Undefined; Options.forceFallbackAdapter = false; - Options.compatibilityMode = false; - wgpuInstanceRequestAdapter(wgpuInstance, &Options, OnAdapterRequestEnded, &UserData); + Options.featureLevel = WGPUFeatureLevel_Core; + WGPURequestAdapterCallbackInfo CallbackInfo{}; + CallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; + CallbackInfo.callback = OnAdapterRequestEnded; + CallbackInfo.userdata1 = &UserData; + wgpuInstanceRequestAdapter(wgpuInstance, &Options, CallbackInfo); while (!UserData.IsReady) InstancePoolEvents(wgpuInstance); @@ -200,46 +195,35 @@ std::vector FindCompatibleAdapters(WGPUInstance wgpuInstan return wgpuAdapters; } -static void DeviceLostCallback(WGPUDeviceLostReason Reason, +static void DeviceLostCallback(WGPUDevice const* device, + WGPUDeviceLostReason Reason, WGPUStringView Message, - void* userdata) + void* userdata1, + void* userdata2) { - bool Expression = Reason != WGPUDeviceLostReason_Destroyed; -#if !PLATFORM_WEB - Expression &= (Reason != WGPUDeviceLostReason_InstanceDropped); -#endif - if (Expression && WGPUStringViewValid(Message)) + if (Reason != WGPUDeviceLostReason_Destroyed && + Reason != WGPUDeviceLostReason_CallbackCancelled && + WGPUStringViewValid(Message)) { LOG_DEBUG_MESSAGE(DEBUG_MESSAGE_SEVERITY_ERROR, "WebGPU: ", WGPUStringViewToString(Message)); } } -#if !PLATFORM_WEB -static void DeviceLostCallback2(WGPUDevice const* device, - WGPUDeviceLostReason Reason, - WGPUStringView Message, - void* userdata1, - void* userdata2) -{ - DeviceLostCallback(Reason, Message, userdata1); -} - -static void UncapturedErrorCallback2(WGPUDevice const* device, - WGPUErrorType MessageType, - WGPUStringView Message, - void* userdata1, - void* userdata2) +static void UncapturedErrorCallback(WGPUDevice const* device, + WGPUErrorType MessageType, + WGPUStringView Message, + void* userdata1, + void* userdata2) { if (WGPUStringViewValid(Message)) { LOG_DEBUG_MESSAGE(DEBUG_MESSAGE_SEVERITY_ERROR, "WebGPU: ", WGPUStringViewToString(Message)); } } -#endif WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUInstance wgpuInstance, WGPUAdapter wgpuAdapter) { - WGPUSupportedLimits SupportedLimits{}; + WGPULimits SupportedLimits{}; wgpuAdapterGetLimits(wgpuAdapter, &SupportedLimits); std::vector wgpuFeatures{}; @@ -255,6 +239,7 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI AddWGPUFeature(Features.TextureCompressionBC, WGPUFeatureName_TextureCompressionBC); AddWGPUFeature(Features.TextureCompressionETC2, WGPUFeatureName_TextureCompressionETC2); AddWGPUFeature(Features.ShaderFloat16, WGPUFeatureName_ShaderF16); + AddWGPUFeature(Features.TextureComponentSwizzle, WGPUFeatureName_TextureComponentSwizzle); if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_Depth32FloatStencil8)) wgpuFeatures.push_back(WGPUFeatureName_Depth32FloatStencil8); @@ -262,6 +247,9 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_Float32Filterable)) wgpuFeatures.push_back(WGPUFeatureName_Float32Filterable); + if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_Float32Blendable)) + wgpuFeatures.push_back(WGPUFeatureName_Float32Blendable); + if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_IndirectFirstInstance)) wgpuFeatures.push_back(WGPUFeatureName_IndirectFirstInstance); @@ -274,8 +262,11 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_Unorm16TextureFormats)) wgpuFeatures.push_back(WGPUFeatureName_Unorm16TextureFormats); - if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_Snorm16TextureFormats)) - wgpuFeatures.push_back(WGPUFeatureName_Snorm16TextureFormats); + if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_TextureFormatsTier1)) + wgpuFeatures.push_back(WGPUFeatureName_TextureFormatsTier1); + + if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_TextureFormatsTier2)) + wgpuFeatures.push_back(WGPUFeatureName_TextureFormatsTier2); } struct CallbackUserData @@ -286,7 +277,7 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI bool IsReady = {}; } UserData; - auto OnDeviceRequestEnded = [](WGPURequestDeviceStatus Status, WGPUDevice Device, WGPUStringView Message, void* pCallbackUserData) { + auto OnDeviceRequestEnded = [](WGPURequestDeviceStatus Status, WGPUDevice Device, WGPUStringView Message, void* pCallbackUserData, void* pCallbackUserData2) { if (pCallbackUserData != nullptr) { CallbackUserData* pUserData = static_cast(pCallbackUserData); @@ -298,32 +289,34 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI } }; -#if !PLATFORM_WEB - const char* ToggleNames[] = { - "disable_timestamp_query_conversion", + // Keep timestamp conversion enabled: QueryWebGPUImpl reports a nanosecond frequency. + const char* EnabledToggles[] = { "use_dxc", }; + // Preserve native timestamp precision so short GPU workloads remain measurable. + const char* DisabledToggles[] = { + "timestamp_quantization", + }; WGPUDawnTogglesDescriptor wgpuDawnTogglesDesc = {}; wgpuDawnTogglesDesc.chain.sType = WGPUSType_DawnTogglesDescriptor; - wgpuDawnTogglesDesc.enabledToggleCount = _countof(ToggleNames); - wgpuDawnTogglesDesc.enabledToggles = ToggleNames; -#endif - - WGPURequiredLimits RequiredLimits{nullptr, SupportedLimits.limits}; + wgpuDawnTogglesDesc.enabledToggleCount = _countof(EnabledToggles); + wgpuDawnTogglesDesc.enabledToggles = EnabledToggles; + wgpuDawnTogglesDesc.disabledToggleCount = _countof(DisabledToggles); + wgpuDawnTogglesDesc.disabledToggles = DisabledToggles; WGPUDeviceDescriptor DeviceDesc{}; - DeviceDesc.requiredLimits = &RequiredLimits; - DeviceDesc.requiredFeatureCount = wgpuFeatures.size(); - DeviceDesc.requiredFeatures = wgpuFeatures.data(); -#if PLATFORM_WEB - DeviceDesc.deviceLostCallback = DeviceLostCallback; -#else - DeviceDesc.deviceLostCallbackInfo2 = {nullptr, WGPUCallbackMode_AllowSpontaneous, DeviceLostCallback2}; - DeviceDesc.uncapturedErrorCallbackInfo2 = {nullptr, UncapturedErrorCallback2}; - DeviceDesc.nextInChain = reinterpret_cast(&wgpuDawnTogglesDesc); -#endif - wgpuAdapterRequestDevice(wgpuAdapter, &DeviceDesc, OnDeviceRequestEnded, &UserData); + DeviceDesc.requiredLimits = &SupportedLimits; + DeviceDesc.requiredFeatureCount = wgpuFeatures.size(); + DeviceDesc.requiredFeatures = wgpuFeatures.data(); + DeviceDesc.deviceLostCallbackInfo = {nullptr, WGPUCallbackMode_AllowSpontaneous, DeviceLostCallback}; + DeviceDesc.uncapturedErrorCallbackInfo = {nullptr, UncapturedErrorCallback}; + DeviceDesc.nextInChain = reinterpret_cast(&wgpuDawnTogglesDesc); + WGPURequestDeviceCallbackInfo CallbackInfo{}; + CallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; + CallbackInfo.callback = OnDeviceRequestEnded; + CallbackInfo.userdata1 = &UserData; + wgpuAdapterRequestDevice(wgpuAdapter, &DeviceDesc, CallbackInfo); while (!UserData.IsReady) InstancePoolEvents(wgpuInstance); @@ -334,6 +327,8 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI return WebGPUDeviceWrapper{UserData.Device}; } +#endif // !PLATFORM_WEB + bool FeatureSupported(WGPUAdapter wgpuAdapter, WGPUDevice wgpuDevice, WGPUFeatureName Feature) { if (wgpuAdapter != nullptr) @@ -398,7 +393,7 @@ DeviceFeatures GetSupportedFeatures(WGPUAdapter wgpuAdapter, WGPUDevice wgpuDevi Features.VariableRateShading = DEVICE_FEATURE_STATE_DISABLED; Features.SparseResources = DEVICE_FEATURE_STATE_DISABLED; Features.SubpassFramebufferFetch = DEVICE_FEATURE_STATE_DISABLED; - Features.TextureComponentSwizzle = DEVICE_FEATURE_STATE_DISABLED; + Features.TextureComponentSwizzle = CheckFeature(WGPUFeatureName_TextureComponentSwizzle); Features.TextureSubresourceViews = DEVICE_FEATURE_STATE_ENABLED; Features.NativeMultiDraw = DEVICE_FEATURE_STATE_DISABLED; Features.AsyncShaderCompilation = DEVICE_FEATURE_STATE_ENABLED; @@ -457,7 +452,7 @@ GraphicsAdapterInfo GetGraphicsAdapterInfo(WGPUAdapter wgpuAdapter, WGPUDevice w AdapterInfo.Features = GetSupportedFeatures(wgpuAdapter, wgpuDevice); - WGPUSupportedLimits wgpuSupportedLimits{}; + WGPULimits wgpuSupportedLimits{}; if (wgpuAdapter) wgpuAdapterGetLimits(wgpuAdapter, &wgpuSupportedLimits); else @@ -494,16 +489,16 @@ GraphicsAdapterInfo GetGraphicsAdapterInfo(WGPUAdapter wgpuAdapter, WGPUDevice w { ComputeShaderProperties& ComputeShaderInfo{AdapterInfo.ComputeShader}; - ComputeShaderInfo.MaxThreadGroupSizeX = wgpuSupportedLimits.limits.maxComputeWorkgroupSizeX; - ComputeShaderInfo.MaxThreadGroupSizeY = wgpuSupportedLimits.limits.maxComputeWorkgroupSizeY; - ComputeShaderInfo.MaxThreadGroupSizeZ = wgpuSupportedLimits.limits.maxComputeWorkgroupSizeZ; + ComputeShaderInfo.MaxThreadGroupSizeX = wgpuSupportedLimits.maxComputeWorkgroupSizeX; + ComputeShaderInfo.MaxThreadGroupSizeY = wgpuSupportedLimits.maxComputeWorkgroupSizeY; + ComputeShaderInfo.MaxThreadGroupSizeZ = wgpuSupportedLimits.maxComputeWorkgroupSizeZ; - ComputeShaderInfo.MaxThreadGroupCountX = wgpuSupportedLimits.limits.maxComputeWorkgroupsPerDimension; - ComputeShaderInfo.MaxThreadGroupCountY = wgpuSupportedLimits.limits.maxComputeWorkgroupsPerDimension; - ComputeShaderInfo.MaxThreadGroupCountZ = wgpuSupportedLimits.limits.maxComputeWorkgroupsPerDimension; + ComputeShaderInfo.MaxThreadGroupCountX = wgpuSupportedLimits.maxComputeWorkgroupsPerDimension; + ComputeShaderInfo.MaxThreadGroupCountY = wgpuSupportedLimits.maxComputeWorkgroupsPerDimension; + ComputeShaderInfo.MaxThreadGroupCountZ = wgpuSupportedLimits.maxComputeWorkgroupsPerDimension; - ComputeShaderInfo.SharedMemorySize = wgpuSupportedLimits.limits.maxComputeWorkgroupStorageSize; - ComputeShaderInfo.MaxThreadGroupInvocations = wgpuSupportedLimits.limits.maxComputeInvocationsPerWorkgroup; + ComputeShaderInfo.SharedMemorySize = wgpuSupportedLimits.maxComputeWorkgroupStorageSize; + ComputeShaderInfo.MaxThreadGroupInvocations = wgpuSupportedLimits.maxComputeInvocationsPerWorkgroup; } // Set texture info @@ -511,11 +506,11 @@ GraphicsAdapterInfo GetGraphicsAdapterInfo(WGPUAdapter wgpuAdapter, WGPUDevice w TextureProperties& TextureInfo{AdapterInfo.Texture}; TextureInfo.MaxTexture1DArraySlices = 0; // Not supported in WebGPU - TextureInfo.MaxTexture2DArraySlices = wgpuSupportedLimits.limits.maxTextureArrayLayers; + TextureInfo.MaxTexture2DArraySlices = wgpuSupportedLimits.maxTextureArrayLayers; - TextureInfo.MaxTexture1DDimension = wgpuSupportedLimits.limits.maxTextureDimension1D; - TextureInfo.MaxTexture2DDimension = wgpuSupportedLimits.limits.maxTextureDimension2D; - TextureInfo.MaxTexture3DDimension = wgpuSupportedLimits.limits.maxTextureDimension3D; + TextureInfo.MaxTexture1DDimension = wgpuSupportedLimits.maxTextureDimension1D; + TextureInfo.MaxTexture2DDimension = wgpuSupportedLimits.maxTextureDimension2D; + TextureInfo.MaxTexture3DDimension = wgpuSupportedLimits.maxTextureDimension3D; TextureInfo.Texture2DMSSupported = True; TextureInfo.Texture2DMSArraySupported = False; @@ -527,8 +522,8 @@ GraphicsAdapterInfo GetGraphicsAdapterInfo(WGPUAdapter wgpuAdapter, WGPUDevice w // Set buffer info { BufferProperties& BufferInfo{AdapterInfo.Buffer}; - BufferInfo.ConstantBufferOffsetAlignment = wgpuSupportedLimits.limits.minUniformBufferOffsetAlignment; - BufferInfo.StructuredBufferOffsetAlignment = wgpuSupportedLimits.limits.minStorageBufferOffsetAlignment; + BufferInfo.ConstantBufferOffsetAlignment = wgpuSupportedLimits.minUniformBufferOffsetAlignment; + BufferInfo.StructuredBufferOffsetAlignment = wgpuSupportedLimits.minStorageBufferOffsetAlignment; BufferInfo.TextureUpdateOffsetAlignment = 256; // Where is this specified? BufferInfo.TextureUpdateStrideAlignment = 256; // From spec ASSERT_SIZEOF(BufferInfo, 16, "Did you add a new member to BufferProperties? Please initialize it here."); @@ -550,6 +545,10 @@ void EngineFactoryWebGPUImpl::EnumerateAdapters(Version MinVersion, Uint32& NumAdapters, GraphicsAdapterInfo* Adapters) const { +#if PLATFORM_WEB + NumAdapters = 0; + LOG_ERROR_MESSAGE("Synchronous adapter enumeration is not supported on the Web. Request an adapter asynchronously in JavaScript."); +#else WebGPUInstanceWrapper wgpuInstance = InitializeWebGPUInstance(true); std::vector wgpuAdapters = FindCompatibleAdapters(wgpuInstance.Get(), MinVersion); @@ -564,6 +563,7 @@ void EngineFactoryWebGPUImpl::EnumerateAdapters(Version MinVersion, Adapters[AdapterId] = GetGraphicsAdapterInfo(wgpuAdapter.Get()); } } +#endif } void EngineFactoryWebGPUImpl::CreateDearchiver(const DearchiverCreateInfo& CreateInfo, @@ -583,6 +583,9 @@ void EngineFactoryWebGPUImpl::CreateDeviceAndContextsWebGPU(const EngineWebGPUCr *ppDevice = nullptr; *ppImmediateContext = nullptr; +#if PLATFORM_WEB + LOG_ERROR_MESSAGE("Synchronous WebGPU device creation is not supported on the Web. Create the device asynchronously in JavaScript and use AttachToWebGPUDevice()."); +#else try { WebGPUInstanceWrapper wgpuInstance = InitializeWebGPUInstance(true); @@ -607,6 +610,7 @@ void EngineFactoryWebGPUImpl::CreateDeviceAndContextsWebGPU(const EngineWebGPUCr catch (const std::runtime_error&) { } +#endif } void EngineFactoryWebGPUImpl::CreateSwapChainWebGPU(IRenderDevice* pDevice, diff --git a/Graphics/GraphicsEngineWebGPU/src/GenerateMipsHelperWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/GenerateMipsHelperWebGPU.cpp index bb72acf8b5..638c67709e 100644 --- a/Graphics/GraphicsEngineWebGPU/src/GenerateMipsHelperWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/GenerateMipsHelperWebGPU.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -245,6 +245,25 @@ std::string_view ConvertWebGPUFormatToString(WGPUTextureFormat TexFmt) case WGPUTextureFormat_BGRA8Unorm: return "bgra8unorm"; + case WGPUTextureFormat_RGBA16Unorm: + return "rgba16unorm"; + case WGPUTextureFormat_RG16Unorm: + return "rg16unorm"; + case WGPUTextureFormat_R16Unorm: + return "r16unorm"; + + case WGPUTextureFormat_RGBA16Snorm: + return "rgba16snorm"; + case WGPUTextureFormat_RG16Snorm: + return "rg16snorm"; + case WGPUTextureFormat_R16Snorm: + return "r16snorm"; + + case WGPUTextureFormat_RGB10A2Unorm: + return "rgb10a2unorm"; + case WGPUTextureFormat_RG11B10Ufloat: + return "rg11b10ufloat"; + case WGPUTextureFormat_RGBA16Float: return "rgba16float"; case WGPUTextureFormat_RG16Float: diff --git a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp index 112209d205..22ff112e08 100644 --- a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp @@ -103,6 +103,9 @@ void BuildSpecializationDataWebGPU(PipelineStateWebGPUImpl::TShaderStages& Shade if (!pShaderResources) continue; + // Reserve before taking c_str() pointers, including for short strings. + Stage.SpecConstKeys.reserve(pShaderResources->GetNumSpecConstants()); + Stage.SpecConstEntries.reserve(pShaderResources->GetNumSpecConstants()); for (Uint32 r = 0; r < pShaderResources->GetNumSpecConstants(); ++r) { const WGSLSpecializationConstantAttribs& ReflectedSC = pShaderResources->GetSpecConstant(r); @@ -141,7 +144,18 @@ void BuildSpecializationDataWebGPU(PipelineStateWebGPUImpl::TShaderStages& Shade } WGPUConstantEntry Entry{}; - Entry.key = GetWGPUStringView(ReflectedSC.Name); + // WGSL overrides with @id are addressed by decimal ID, not name. + // Tint's IR rewrite materializes IDs even for originally named-only + // overrides; it preserves the IDs assigned by the original parser. + if (!Stage.PatchedWGSL.empty() || ReflectedSC.IsIdSpecified) + { + Stage.SpecConstKeys.emplace_back(std::to_string(ReflectedSC.OverrideId)); + Entry.key = GetWGPUStringView(Stage.SpecConstKeys.back().c_str()); + } + else + { + Entry.key = GetWGPUStringView(ReflectedSC.Name); + } Entry.value = ConvertSpecConstantToDouble(pUserConst->pData, pUserConst->Size, ReflectedType); Stage.SpecConstEntries.push_back(Entry); @@ -328,7 +342,13 @@ void PipelineStateWebGPUImpl::RemapOrVerifyShaderResources( if (!bVerifyOnly) { - PatchedWGSL = RemapWGSLResourceBindings(pShader->GetWGSL(), ResMapping, pShader->GetEmulatedArrayIndexSuffix()); + PatchedWGSL = RemapWGSLResourceBindings(pShader->GetWGSL(), ResMapping, pShader->GetEmulatedArrayIndexSuffix(), + pShader->GetEntryPoint(), &ShaderStage.PatchedEntryPoint); + if (PatchedWGSL.empty()) + { + LOG_ERROR_AND_THROW("Failed to remap resources for shader '", pShader->GetDesc().Name, + "' in pipeline '", PipelineName, "'."); + } } } } @@ -422,35 +442,19 @@ struct PipelineStateWebGPUImpl::AsyncPipelineBuilder : public ObjectBase(pUserData)->InitializePipelines(Status, Pipeline, nullptr, Message); - } - - static void CreateRenderPipelineCallback2(WGPUCreatePipelineAsyncStatus Status, - WGPURenderPipeline Pipeline, - WGPUStringView Message, - void* pUserData1, - void* pUserData2) - { - CreateRenderPipelineCallback(Status, Pipeline, Message, pUserData1); + static_cast(pUserData1)->InitializePipelines(Status, Pipeline, nullptr, Message); } static void CreateComputePipelineCallback(WGPUCreatePipelineAsyncStatus Status, WGPUComputePipeline Pipeline, WGPUStringView Message, - void* pUserData) - { - static_cast(pUserData)->InitializePipelines(Status, nullptr, Pipeline, Message); - } - - static void CreateComputePipelineCallback2(WGPUCreatePipelineAsyncStatus Status, - WGPUComputePipeline Pipeline, - WGPUStringView Message, - void* pUserData1, - void* pUserData2) + void* pUserData1, + void* pUserData2) { - CreateComputePipelineCallback(Status, Pipeline, Message, pUserData1); + static_cast(pUserData1)->InitializePipelines(Status, nullptr, Pipeline, Message); } }; @@ -534,7 +538,7 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages case SHADER_TYPE_VERTEX: VERIFY(wgpuRenderPipelineDesc.vertex.module == nullptr, "Only one vertex shader is allowed"); wgpuRenderPipelineDesc.vertex.module = wgpuShaderModules[ShaderIdx].Get(); - wgpuRenderPipelineDesc.vertex.entryPoint = GetWGPUStringView(Stage.pShader->GetEntryPoint()); + wgpuRenderPipelineDesc.vertex.entryPoint = GetWGPUStringView(Stage.GetEntryPoint()); wgpuRenderPipelineDesc.vertex.constantCount = Stage.SpecConstEntries.size(); wgpuRenderPipelineDesc.vertex.constants = Stage.SpecConstEntries.empty() ? nullptr : Stage.SpecConstEntries.data(); break; @@ -542,7 +546,7 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages case SHADER_TYPE_PIXEL: VERIFY(wgpuFragmentState.module == nullptr, "Only one vertex shader is allowed"); wgpuFragmentState.module = wgpuShaderModules[ShaderIdx].Get(); - wgpuFragmentState.entryPoint = GetWGPUStringView(Stage.pShader->GetEntryPoint()); + wgpuFragmentState.entryPoint = GetWGPUStringView(Stage.GetEntryPoint()); wgpuFragmentState.constantCount = Stage.SpecConstEntries.size(); wgpuFragmentState.constants = Stage.SpecConstEntries.empty() ? nullptr : Stage.SpecConstEntries.data(); wgpuRenderPipelineDesc.fragment = &wgpuFragmentState; @@ -579,7 +583,7 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages for (size_t Idx = 0; Idx < MaxBufferSlot + 1; ++Idx) { - wgpuVertexBufferLayouts[Idx].stepMode = !wgpuVertexAttributes[Idx].empty() ? wgpuVertexBufferLayouts[Idx].stepMode : WGPUVertexStepMode_VertexBufferNotUsed; + wgpuVertexBufferLayouts[Idx].stepMode = !wgpuVertexAttributes[Idx].empty() ? wgpuVertexBufferLayouts[Idx].stepMode : WGPUVertexStepMode_Undefined; wgpuVertexBufferLayouts[Idx].attributeCount = static_cast(wgpuVertexAttributes[Idx].size()); wgpuVertexBufferLayouts[Idx].attributes = wgpuVertexAttributes[Idx].data(); } @@ -655,9 +659,6 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages wgpuRenderPipelineDesc.depthStencil = &wgpuDepthStencilState; } -#if PLATFORM_WEB - WGPUPrimitiveDepthClipControl wgpuDepthClipControl{}; -#endif { const RasterizerStateDesc& RasterizerDesc = GraphicsPipeline.RasterizerDesc; @@ -679,13 +680,7 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages { if (m_pDevice->GetDeviceInfo().Features.DepthClamp) { -#if PLATFORM_WEB - wgpuDepthClipControl.chain.sType = WGPUSType_PrimitiveDepthClipControl; - wgpuDepthClipControl.unclippedDepth = true; - wgpuPrimitiveState.nextInChain = reinterpret_cast(&wgpuDepthClipControl); -#else wgpuPrimitiveState.unclippedDepth = true; -#endif } else { @@ -706,18 +701,14 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages { // The reference will be released from the callback. AsyncBuilder->AddRef(); -#if PLATFORM_WEB - wgpuDeviceCreateRenderPipelineAsync(m_pDevice->GetWebGPUDevice(), &wgpuRenderPipelineDesc, AsyncPipelineBuilder::CreateRenderPipelineCallback, AsyncBuilder); -#else - wgpuDeviceCreateRenderPipelineAsync2(m_pDevice->GetWebGPUDevice(), &wgpuRenderPipelineDesc, - { - nullptr, - WGPUCallbackMode_AllowSpontaneous, - AsyncPipelineBuilder::CreateRenderPipelineCallback2, - AsyncBuilder, - nullptr, - }); -#endif + wgpuDeviceCreateRenderPipelineAsync(m_pDevice->GetWebGPUDevice(), &wgpuRenderPipelineDesc, + { + nullptr, + WGPUCallbackMode_AllowSpontaneous, + AsyncPipelineBuilder::CreateRenderPipelineCallback, + AsyncBuilder, + nullptr, + }); } else { @@ -747,7 +738,7 @@ void PipelineStateWebGPUImpl::InitializeWebGPUComputePipeline(const TShaderStage WGPUComputePipelineDescriptor wgpuComputePipelineDesc{}; wgpuComputePipelineDesc.label = GetWGPUStringView(m_Desc.Name); wgpuComputePipelineDesc.compute.module = wgpuShaderModule.Get(); - wgpuComputePipelineDesc.compute.entryPoint = GetWGPUStringView(pShaderWebGPU->GetEntryPoint()); + wgpuComputePipelineDesc.compute.entryPoint = GetWGPUStringView(ShaderStages[0].GetEntryPoint()); wgpuComputePipelineDesc.layout = m_PipelineLayout.GetWebGPUPipelineLayout(); const std::vector& SpecConstEntries = ShaderStages[0].SpecConstEntries; @@ -758,18 +749,14 @@ void PipelineStateWebGPUImpl::InitializeWebGPUComputePipeline(const TShaderStage { // The reference will be released from the callback. AsyncBuilder->AddRef(); -#if PLATFORM_WEB - wgpuDeviceCreateComputePipelineAsync(m_pDevice->GetWebGPUDevice(), &wgpuComputePipelineDesc, AsyncPipelineBuilder::CreateComputePipelineCallback, AsyncBuilder); -#else - wgpuDeviceCreateComputePipelineAsync2(m_pDevice->GetWebGPUDevice(), &wgpuComputePipelineDesc, - { - nullptr, - WGPUCallbackMode_AllowSpontaneous, - AsyncPipelineBuilder::CreateComputePipelineCallback2, - AsyncBuilder, - nullptr, - }); -#endif + wgpuDeviceCreateComputePipelineAsync(m_pDevice->GetWebGPUDevice(), &wgpuComputePipelineDesc, + { + nullptr, + WGPUCallbackMode_AllowSpontaneous, + AsyncPipelineBuilder::CreateComputePipelineCallback, + AsyncBuilder, + nullptr, + }); } else { diff --git a/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp index 2f264be81d..5d0ea2ab25 100644 --- a/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp @@ -42,7 +42,7 @@ #include "FenceWebGPUImpl.hpp" #include "QueryWebGPUImpl.hpp" #include "AttachmentCleanerWebGPU.hpp" -#include "WebGPUStubs.hpp" +#include "WebGPUDefinitions.hpp" #if !DILIGENT_NO_GLSLANG # include "GLSLangUtils.hpp" @@ -68,13 +68,55 @@ class DeviceMemoryWebGPUImpl {}; #if PLATFORM_WEB -static void DebugMessengerCallback(WGPUErrorType MessageType, const char* Message, void* pUserData) +namespace { - if (Message != nullptr) - LOG_DEBUG_MESSAGE(DEBUG_MESSAGE_SEVERITY_ERROR, "WebGPU: ", Message); + +using WebGPUErrorCallbackType = void (*)(const char*); + +void ReportWebGPUUncapturedError(const char* Message) +{ + LOG_DEBUG_MESSAGE(DEBUG_MESSAGE_SEVERITY_ERROR, "WebGPU: ", Message); } + +EM_JS_DEPS(WebGPUErrorCallbackDeps, "$WebGPU,$stringToNewUTF8,$getWasmTableEntry,free"); + +// Imported devices have no creation descriptor through which to register a C callback. +// Keep one listener per JavaScript device, even when multiple Diligent wrappers share it. +// clang-format off +EM_JS(void, AddWebGPUUncapturedErrorCallback, (WGPUDevice wgpuDevice, WebGPUErrorCallbackType Callback), { + var device = WebGPU.getJsObject(wgpuDevice); + var registration = device['diligentUncapturedErrorCallback']; + if (registration) { + ++registration.refCount; + return; + } + + var listener = (event) => { + var message = stringToNewUTF8(event.error.message); + try { + getWasmTableEntry(Callback)(message); + } finally { + _free(message); + } + }; + device.addEventListener('uncapturederror', listener); + device['diligentUncapturedErrorCallback'] = {listener: listener, refCount: 1}; +}); + +EM_JS(void, RemoveWebGPUUncapturedErrorCallback, (WGPUDevice wgpuDevice), { + var device = WebGPU.getJsObject(wgpuDevice); + var registration = device['diligentUncapturedErrorCallback']; + if (--registration.refCount == 0) { + device.removeEventListener('uncapturederror', registration.listener); + delete device['diligentUncapturedErrorCallback']; + } +}); +// clang-format on + +} // namespace #endif + RenderDeviceWebGPUImpl::RenderDeviceWebGPUImpl(IReferenceCounters* pRefCounters, const CreateInfo& CI) : @@ -92,13 +134,8 @@ RenderDeviceWebGPUImpl::RenderDeviceWebGPUImpl(IReferenceCounters* pRefCounters, m_wgpuDevice{CI.wgpuDevice} // clang-format on { - WGPUSupportedLimits wgpuSupportedLimits{}; - wgpuDeviceGetLimits(m_wgpuDevice, &wgpuSupportedLimits); - m_wgpuLimits = wgpuSupportedLimits.limits; + wgpuDeviceGetLimits(m_wgpuDevice, &m_wgpuLimits); -#if PLATFORM_WEB - wgpuDeviceSetUncapturedErrorCallback(m_wgpuDevice, DebugMessengerCallback, nullptr); -#endif FindSupportedTextureFormats(); m_DeviceInfo.Type = RENDER_DEVICE_TYPE_WEBGPU; @@ -119,10 +156,17 @@ RenderDeviceWebGPUImpl::RenderDeviceWebGPUImpl(IReferenceCounters* pRefCounters, #endif InitShaderCompilationThreadPool(EngineCI.pAsyncShaderCompilationThreadPool, EngineCI.NumAsyncShaderCompilationThreads); + +#if PLATFORM_WEB + AddWebGPUUncapturedErrorCallback(m_wgpuDevice, ReportWebGPUUncapturedError); +#endif } RenderDeviceWebGPUImpl::~RenderDeviceWebGPUImpl() { +#if PLATFORM_WEB + RemoveWebGPUUncapturedErrorCallback(m_wgpuDevice); +#endif #if !DILIGENT_NO_GLSLANG GLSLangUtils::FinalizeGlslang(); #endif @@ -367,7 +411,7 @@ DynamicMemoryManagerWebGPU::Page RenderDeviceWebGPUImpl::GetDynamicMemoryPage(si void RenderDeviceWebGPUImpl::DeviceTick() { #if !PLATFORM_WEB - wgpuDeviceTick(m_wgpuDevice); + wgpuInstanceProcessEvents(m_wgpuInstance); #endif } @@ -437,34 +481,42 @@ void RenderDeviceWebGPUImpl::FindSupportedTextureFormats() const bool Depth32FloatStencil8Supported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Depth32FloatStencil8); const bool TextureCompressionBCSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureCompressionBC); const bool TextureCompressionETC2Supported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureCompressionETC2); - const bool R16UnormSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Unorm16TextureFormats); - const bool R16SnormSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Snorm16TextureFormats); + const bool TextureFormatsTier1Supported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureFormatsTier1); + const bool Unorm16TextureFormatsSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Unorm16TextureFormats); + + // Tier 1 enables normalized 16-bit formats without guaranteeing filtering support. + bool Norm16FilterableSupported = Unorm16TextureFormatsSupported; +#if !PLATFORM_WEB + Norm16FilterableSupported = Norm16FilterableSupported || wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Unorm16Filterable); +#endif + const bool R16UnormSupported = TextureFormatsTier1Supported || Unorm16TextureFormatsSupported; + const bool R16SnormSupported = TextureFormatsTier1Supported; // https://www.w3.org/TR/webgpu/#texture-format-caps // Color formats with 8-bits per channel - SetTexFormatInfo({TEX_FORMAT_R8_TYPELESS, TEX_FORMAT_R8_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_R8_SNORM}, BIND_S, FMT_FLAG_FILTER); - SetTexFormatInfo({TEX_FORMAT_R8_UINT, TEX_FORMAT_R8_SINT}, BIND_SR, FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R8_TYPELESS, TEX_FORMAT_R8_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R8_SNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_S, TextureFormatsTier1Supported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_FILTER); + SetTexFormatInfo({TEX_FORMAT_R8_UINT, TEX_FORMAT_R8_SINT}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG8_TYPELESS, TEX_FORMAT_RG8_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG8_SNORM}, BIND_S, FMT_FLAG_FILTER); - SetTexFormatInfo({TEX_FORMAT_RG8_UINT, TEX_FORMAT_RG8_SINT}, BIND_SR, FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG8_TYPELESS, TEX_FORMAT_RG8_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG8_SNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_S, TextureFormatsTier1Supported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_FILTER); + SetTexFormatInfo({TEX_FORMAT_RG8_UINT, TEX_FORMAT_RG8_SINT}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_RGBA8_TYPELESS, TEX_FORMAT_RGBA8_UNORM}, BIND_SRU, FMT_FLAG_FILTER | FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_RGBA8_UNORM_SRGB}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGBA8_SNORM}, BIND_SU, FMT_FLAG_FILTER); + SetTexFormatInfo({TEX_FORMAT_RGBA8_SNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SU, TextureFormatsTier1Supported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_FILTER); SetTexFormatInfo({TEX_FORMAT_RGBA8_UINT, TEX_FORMAT_RGBA8_SINT}, BIND_SRU, FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_BGRA8_TYPELESS, TEX_FORMAT_BGRA8_UNORM}, BGRA8UnormStorageSupported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_BGRA8_UNORM_SRGB}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); // Color formats with 16-bits per channel - SetTexFormatInfo({TEX_FORMAT_R16_UINT, TEX_FORMAT_R16_SINT}, BIND_SR, FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_R16_FLOAT, TEX_FORMAT_R16_TYPELESS}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R16_UINT, TEX_FORMAT_R16_SINT}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R16_FLOAT, TEX_FORMAT_R16_TYPELESS}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_UINT, TEX_FORMAT_RG16_SINT}, BIND_SR, FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_FLOAT, TEX_FORMAT_RG16_TYPELESS}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_UINT, TEX_FORMAT_RG16_SINT}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_FLOAT, TEX_FORMAT_RG16_TYPELESS}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_RGBA16_UINT, TEX_FORMAT_RGBA16_SINT}, BIND_SRU, FMT_FLAG_MSAA); SetTexFormatInfo({TEX_FORMAT_RGBA16_FLOAT, TEX_FORMAT_RGBA16_TYPELESS}, BIND_SRU, FMT_FLAG_FILTER | FMT_FLAG_MSAA); @@ -480,10 +532,10 @@ void RenderDeviceWebGPUImpl::FindSupportedTextureFormats() SetTexFormatInfo({TEX_FORMAT_RGBA32_FLOAT}, BIND_SRU, Float32FilterableSupported ? FMT_FLAG_FILTER : FMT_FLAG_NONE); // Color formats with mixed width - SetTexFormatInfo({TEX_FORMAT_RGB10A2_TYPELESS, TEX_FORMAT_RGB10A2_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGB10A2_UINT}, BIND_SR, FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGB10A2_TYPELESS, TEX_FORMAT_RGB10A2_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGB10A2_UINT}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_R11G11B10_FLOAT}, RG11B10UfloatRenderableSupported ? BIND_SR : BIND_S, RG11B10UfloatRenderableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_FILTER); + SetTexFormatInfo({TEX_FORMAT_R11G11B10_FLOAT}, TextureFormatsTier1Supported ? BIND_SRU : (RG11B10UfloatRenderableSupported ? BIND_SR : BIND_S), RG11B10UfloatRenderableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_FILTER); // Depth-stencil formats SetTexFormatInfo({TEX_FORMAT_D16_UNORM}, BIND_D, FMT_FLAG_FILTER | FMT_FLAG_MSAA); @@ -523,16 +575,16 @@ void RenderDeviceWebGPUImpl::FindSupportedTextureFormats() if (R16UnormSupported) { - SetTexFormatInfo({TEX_FORMAT_R16_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGBA16_UNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R16_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGBA16_UNORM}, TextureFormatsTier1Supported ? BIND_SRU : BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); } if (R16SnormSupported) { - SetTexFormatInfo({TEX_FORMAT_R16_SNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_SNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGBA16_SNORM}, BIND_SR, FMT_FLAG_FILTER | FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_R16_SNORM}, BIND_SRU, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_SNORM}, BIND_SRU, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGBA16_SNORM}, BIND_SRU, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); } if (TextureCompressionETC2Supported) diff --git a/Graphics/GraphicsEngineWebGPU/src/ShaderWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/ShaderWebGPUImpl.cpp index eca349ba04..17c6a7b0c6 100644 --- a/Graphics/GraphicsEngineWebGPU/src/ShaderWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/ShaderWebGPUImpl.cpp @@ -206,7 +206,7 @@ void ShaderWebGPUImpl::Initialize(const ShaderCreateInfo& ShaderCI, LOG_ERROR_AND_THROW("Shader source must be provided through one of the 'Source', 'FilePath' or 'ByteCode' members"); } - m_WGSL = ConvertSPIRVtoWGSL(SPIRV); + m_WGSL = ConvertSPIRVtoWGSL(SPIRV, ShaderCI.LoadConstantBufferReflection); if (m_WGSL.empty()) { LOG_ERROR_AND_THROW("Failed to convert SPIRV to WGSL for shader '", m_Desc.Name, '\''); diff --git a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp index 90a7d74fca..a788c22d69 100644 --- a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,10 @@ #include "SwapChainWebGPUImpl.hpp" #include "RenderDeviceWebGPUImpl.hpp" #include "DeviceContextWebGPUImpl.hpp" -#include "TextureViewWebGPU.h" +#include "TextureWebGPUImpl.hpp" #include "WebGPUTypeConversions.hpp" -#include "WebGPUStubs.hpp" -#ifdef PLATFORM_WIN32 +#if PLATFORM_WIN32 # include #endif @@ -44,327 +43,13 @@ namespace Diligent { -namespace -{ - -constexpr char VSSource[] = R"( -struct VertexOutput -{ - @builtin(position) Position: vec4f, -} - -@vertex -fn VSMain(@builtin(vertex_index) VertexId: u32) -> VertexOutput -{ - let Texcoord: vec2f = vec2f(f32((VertexId << 1u) & 2u), f32(VertexId & 2u)); - let Position: vec4f = vec4f(Texcoord * vec2f(2.0f, -2.0f) + vec2f(-1.0f, 1.0f), 1.0f, 1.0f); - return VertexOutput(Position); -} -)"; - -constexpr char PSSource[] = R"( -@group(0) @binding(0) var TextureSrc: texture_2d; - -struct VertexOutput -{ - @builtin(position) Position: vec4f, -} - -@fragment -fn PSMain(Input: VertexOutput) -> @location(0) vec4f -{ - return textureLoad(TextureSrc, vec2i(Input.Position.xy), 0); -} -)"; - -constexpr char PSSourceGamma[] = R"( -@group(0) @binding(0) var TextureSrc: texture_2d; - -struct VertexOutput -{ - @builtin(position) Position: vec4f, -} - -fn LinearToSRGB(RGB: vec3) -> vec3 { - let threshold = vec3(0.0031308); - let bGreater = step(threshold, RGB); - let linearPart = RGB * 12.92; - let sRGBPart = (pow(RGB, vec3(1.0 / 2.4)) * 1.055) - vec3(0.055); - return mix(linearPart, sRGBPart, bGreater); -} - -@fragment -fn PSMain(Input: VertexOutput) -> @location(0) vec4f -{ - var Color: vec4f = textureLoad(TextureSrc, vec2i(Input.Position.xy), 0); - return vec4f(LinearToSRGB(Color.rgb), Color.a); -} -)"; - -WGPUTextureFormat WGPUConvertUnormToSRGB(WGPUTextureFormat Format) -{ - switch (Format) - { - case WGPUTextureFormat_RGBA8Unorm: - return WGPUTextureFormat_RGBA8UnormSrgb; - case WGPUTextureFormat_BGRA8Unorm: - return WGPUTextureFormat_BGRA8UnormSrgb; - default: - UNEXPECTED("Unexpected texture format"); - return Format; - } -} - -} // namespace - -class SwapChainWebGPUImpl::PresentCommand -{ -public: - PresentCommand(IRenderDeviceWebGPU* pRenderDevice) : - m_pRenderDevice{pRenderDevice} - { - } - - bool InitializePipelineState(WGPUTextureFormat wgpuFormat, bool ConvertToGamma) - { - if (m_wgpuRenderPipeline) - return true; - - WGPUDevice wgpuDevice = m_pRenderDevice->GetWebGPUDevice(); - - WebGPUShaderModuleWrapper wgpuVSShaderModule{}; - { - WGPUShaderSourceWGSL wgpuShaderCodeDesc{}; - wgpuShaderCodeDesc.chain.sType = WGPUSType_ShaderSourceWGSL; - wgpuShaderCodeDesc.code = GetWGPUStringView(VSSource); - - WGPUShaderModuleDescriptor wgpuShaderModuleDesc{}; - wgpuShaderModuleDesc.nextInChain = reinterpret_cast(&wgpuShaderCodeDesc); - wgpuVSShaderModule.Reset(wgpuDeviceCreateShaderModule(wgpuDevice, &wgpuShaderModuleDesc)); - if (!wgpuVSShaderModule) - { - LOG_ERROR_MESSAGE("Failed to create shader module for swap chain present command"); - return false; - } - } - - WebGPUShaderModuleWrapper wgpuPSShaderModule{}; - { - WGPUShaderSourceWGSL wgpuShaderCodeDesc{}; - wgpuShaderCodeDesc.chain.sType = WGPUSType_ShaderSourceWGSL; - wgpuShaderCodeDesc.code = GetWGPUStringView(ConvertToGamma ? PSSourceGamma : PSSource); - - WGPUShaderModuleDescriptor wgpuShaderModuleDesc{}; - wgpuShaderModuleDesc.nextInChain = reinterpret_cast(&wgpuShaderCodeDesc); - wgpuPSShaderModule.Reset(wgpuDeviceCreateShaderModule(wgpuDevice, &wgpuShaderModuleDesc)); - if (!wgpuPSShaderModule) - { - LOG_ERROR_MESSAGE("Failed to create shader module for swap chain present command"); - return false; - } - } - - WGPUBindGroupLayoutEntry wgpuBindGroupLayoutEntries[1]{}; - wgpuBindGroupLayoutEntries[0].binding = 0; - wgpuBindGroupLayoutEntries[0].visibility = WGPUShaderStage_Fragment; - wgpuBindGroupLayoutEntries[0].texture.sampleType = WGPUTextureSampleType_Float; - wgpuBindGroupLayoutEntries[0].texture.viewDimension = WGPUTextureViewDimension_2D; - - WGPUBindGroupLayoutDescriptor wgpuBindGroupLayoutDesc{}; - wgpuBindGroupLayoutDesc.entryCount = _countof(wgpuBindGroupLayoutEntries); - wgpuBindGroupLayoutDesc.entries = wgpuBindGroupLayoutEntries; - m_wgpuBindGroupLayout.Reset(wgpuDeviceCreateBindGroupLayout(wgpuDevice, &wgpuBindGroupLayoutDesc)); - if (!m_wgpuBindGroupLayout) - { - LOG_ERROR_MESSAGE("Failed to create bind group layout for swap chain present command"); - return false; - } - - WGPUPipelineLayoutDescriptor wgpuPipelineLayoutDesc{}; - wgpuPipelineLayoutDesc.bindGroupLayoutCount = 1; - wgpuPipelineLayoutDesc.bindGroupLayouts = &m_wgpuBindGroupLayout.Get(); - m_wgpuPipelineLayout.Reset(wgpuDeviceCreatePipelineLayout(wgpuDevice, &wgpuPipelineLayoutDesc)); - if (!m_wgpuPipelineLayout) - { - LOG_ERROR_MESSAGE("Failed to create pipeline layout for swap chain present command"); - return false; - } - - WGPUColorTargetState wgpuColorTargetState{}; - wgpuColorTargetState.format = wgpuFormat; - wgpuColorTargetState.blend = nullptr; - wgpuColorTargetState.writeMask = WGPUColorWriteMask_All; - - WGPUFragmentState wgpuFragmentState{}; - wgpuFragmentState.module = wgpuPSShaderModule; - wgpuFragmentState.entryPoint = GetWGPUStringView("PSMain"); - wgpuFragmentState.targets = &wgpuColorTargetState; - wgpuFragmentState.targetCount = 1; - - WGPURenderPipelineDescriptor wgpuRenderPipelineDesc{}; - wgpuRenderPipelineDesc.label = GetWGPUStringView("SwapChainPresentPSO"); - wgpuRenderPipelineDesc.layout = m_wgpuPipelineLayout; - wgpuRenderPipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList; - wgpuRenderPipelineDesc.primitive.cullMode = WGPUCullMode_None; - wgpuRenderPipelineDesc.vertex.module = wgpuVSShaderModule; - wgpuRenderPipelineDesc.vertex.entryPoint = GetWGPUStringView("VSMain"); - wgpuRenderPipelineDesc.fragment = &wgpuFragmentState; - wgpuRenderPipelineDesc.multisample.count = 1; - wgpuRenderPipelineDesc.multisample.mask = 0xFFFFFFFF; - m_wgpuRenderPipeline.Reset(wgpuDeviceCreateRenderPipeline(wgpuDevice, &wgpuRenderPipelineDesc)); - if (!m_wgpuPipelineLayout) - { - LOG_ERROR_MESSAGE("Failed to create render pipeline for swap chain present command"); - return false; - } - - return true; - } - - WGPUSurfaceGetCurrentTextureStatus Execute(ITextureViewWebGPU* pTexture, ISwapChainWebGPU* pSwapChain, IDeviceContextWebGPU* pDeviceContext) - { - WGPUSurfaceTexture wgpuSurfaceTexture{}; - wgpuSurfaceGetCurrentTexture(pSwapChain->GetWebGPUSurface(), &wgpuSurfaceTexture); - WebGPUTextureWrapper wgpuTexture{wgpuSurfaceTexture.texture}; - - switch (wgpuSurfaceTexture.status) - { - case WGPUSurfaceGetCurrentTextureStatus_Success: - case WGPUSurfaceGetCurrentTextureStatus_Outdated: - break; - - case WGPUSurfaceGetCurrentTextureStatus_Timeout: - break; - - case WGPUSurfaceGetCurrentTextureStatus_Lost: - LOG_WARNING_MESSAGE("Unable to present: swap chain surface is lost"); - return wgpuSurfaceTexture.status; - - case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory: - LOG_ERROR_MESSAGE("Unable to present: out of memory"); - return wgpuSurfaceTexture.status; - - case WGPUSurfaceGetCurrentTextureStatus_DeviceLost: - LOG_ERROR_MESSAGE("Unable to present: device is lost"); - return wgpuSurfaceTexture.status; - - case WGPUSurfaceGetCurrentTextureStatus_Error: - LOG_ERROR_MESSAGE("Unable to present: unknown error"); - return wgpuSurfaceTexture.status; - - default: - UNEXPECTED("Unexpected status"); - return wgpuSurfaceTexture.status; - } - - WGPUTextureFormat ViewFormat = wgpuTextureGetFormat(wgpuTexture); - - // Simplify this code once the bug for sRGB texture view is fixed in Dawn - bool ConvertToGamma = false; -#if !PLATFORM_WEB - if (IsSRGBFormat(pSwapChain->GetDesc().ColorBufferFormat)) - ViewFormat = WGPUConvertUnormToSRGB(ViewFormat); -#else - ConvertToGamma = IsSRGBFormat(pSwapChain->GetDesc().ColorBufferFormat); -#endif - if (!InitializePipelineState(ViewFormat, ConvertToGamma)) - return WGPUSurfaceGetCurrentTextureStatus_Error; - - WGPUTextureViewDescriptor wgpuTextureViewDesc{}; - wgpuTextureViewDesc.nextInChain = nullptr; - wgpuTextureViewDesc.label = GetWGPUStringView("SwapChainPresentTextureView"); - wgpuTextureViewDesc.format = ViewFormat; - wgpuTextureViewDesc.dimension = WGPUTextureViewDimension_2D; - wgpuTextureViewDesc.baseMipLevel = 0; - wgpuTextureViewDesc.mipLevelCount = 1; - wgpuTextureViewDesc.baseArrayLayer = 0; - wgpuTextureViewDesc.arrayLayerCount = 1; - wgpuTextureViewDesc.aspect = WGPUTextureAspect_All; - - WebGPUTextureViewWrapper wgpuTextureView{wgpuTextureCreateView(wgpuTexture, &wgpuTextureViewDesc)}; - if (!wgpuTextureView) - { - LOG_ERROR_MESSAGE("Failed to create texture view for WGPU surface texture"); - return WGPUSurfaceGetCurrentTextureStatus_Error; - } - - if (!m_wgpuBindGroup) - { - WGPUBindGroupEntry wgpuBindGroupEntries[1]{}; - wgpuBindGroupEntries[0].binding = 0; - wgpuBindGroupEntries[0].textureView = pTexture->GetWebGPUTextureView(); - - WGPUBindGroupDescriptor wgpuBindGroupDesc{}; - wgpuBindGroupDesc.entries = wgpuBindGroupEntries; - wgpuBindGroupDesc.entryCount = _countof(wgpuBindGroupEntries); - wgpuBindGroupDesc.layout = m_wgpuBindGroupLayout; - - m_wgpuBindGroup.Reset(wgpuDeviceCreateBindGroup(m_pRenderDevice->GetWebGPUDevice(), &wgpuBindGroupDesc)); - } - - WGPUCommandEncoderDescriptor wgpuCmdEncoderDesc{}; - WebGPUCommandEncoderWrapper wgpuCmdEncoder{wgpuDeviceCreateCommandEncoder(m_pRenderDevice->GetWebGPUDevice(), &wgpuCmdEncoderDesc)}; - - WGPURenderPassColorAttachment wgpuRenderPassColorAttachments[1]{}; - wgpuRenderPassColorAttachments[0].clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0}; - wgpuRenderPassColorAttachments[0].loadOp = WGPULoadOp_Clear; - wgpuRenderPassColorAttachments[0].storeOp = WGPUStoreOp_Store; - wgpuRenderPassColorAttachments[0].view = wgpuTextureView; - wgpuRenderPassColorAttachments[0].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; - - WGPURenderPassDescriptor wgpuRenderPassDesc{}; - wgpuRenderPassDesc.colorAttachmentCount = _countof(wgpuRenderPassColorAttachments); - wgpuRenderPassDesc.colorAttachments = wgpuRenderPassColorAttachments; - - WebGPURenderPassEncoderWrapper wgpuRenderPassEncoder{wgpuCommandEncoderBeginRenderPass(wgpuCmdEncoder, &wgpuRenderPassDesc)}; - wgpuRenderPassEncoderSetPipeline(wgpuRenderPassEncoder, m_wgpuRenderPipeline); - wgpuRenderPassEncoderSetBindGroup(wgpuRenderPassEncoder, 0, m_wgpuBindGroup, 0, nullptr); - wgpuRenderPassEncoderDraw(wgpuRenderPassEncoder, 3, 1, 0, 0); - wgpuRenderPassEncoderEnd(wgpuRenderPassEncoder); - - WGPUCommandBufferDescriptor wgpuCmdBufferDesc{}; - WebGPUCommandBufferWrapper wgpuCmdBuffer{wgpuCommandEncoderFinish(wgpuCmdEncoder, &wgpuCmdBufferDesc)}; - - wgpuQueueSubmit(pDeviceContext->GetWebGPUQueue(), 1, &wgpuCmdBuffer.Get()); - -#if PLATFORM_WEB - emscripten_request_animation_frame([](double Time, void* pUserData) -> EM_BOOL { return EM_FALSE; }, nullptr); -#else - wgpuSurfacePresent(pSwapChain->GetWebGPUSurface()); -#endif - - return wgpuSurfaceTexture.status; - } - - void ReleaseSwapChainResources() - { - m_wgpuBindGroup.Reset(); - } - -private: - RefCntAutoPtr m_pRenderDevice; - WebGPUBindGroupLayoutWrapper m_wgpuBindGroupLayout; - WebGPUBindGroupWrapper m_wgpuBindGroup; - WebGPUPipelineLayoutWrapper m_wgpuPipelineLayout; - WebGPURenderPipelineWrapper m_wgpuRenderPipeline; -}; - SwapChainWebGPUImpl::SwapChainWebGPUImpl(IReferenceCounters* pRefCounters, const SwapChainDesc& SCDesc, RenderDeviceWebGPUImpl* pRenderDevice, DeviceContextWebGPUImpl* pDeviceContext, const NativeWindow& Window) : - // clang-format off - TSwapChainBase - { - pRefCounters, - pRenderDevice, - pDeviceContext, - SCDesc - }, - m_NativeWindow{Window}, - m_pCmdPresent{std::make_unique(pRenderDevice)} -// clang-format on + TSwapChainBase{pRefCounters, pRenderDevice, pDeviceContext, SCDesc}, + m_NativeWindow{Window} { if (m_DesiredPreTransform != SURFACE_TRANSFORM_OPTIMAL && m_DesiredPreTransform != SURFACE_TRANSFORM_IDENTITY) { @@ -375,12 +60,40 @@ SwapChainWebGPUImpl::SwapChainWebGPUImpl(IReferenceCounters* pRefCounters, m_DesiredPreTransform = SURFACE_TRANSFORM_OPTIMAL; m_SwapChainDesc.PreTransform = SURFACE_TRANSFORM_IDENTITY; - CreateSurface(); - ConfigureSurface(); - CreateBuffersAndViews(); + if (!CreateSurface() || !ConfigureSurface() || !CreateDepthBufferView()) + LOG_ERROR_AND_THROW("Failed to initialize WebGPU swap chain"); + + // Acquire lazily: a browser canvas texture expires at the end of its rendering frame. +} + +SwapChainWebGPUImpl::~SwapChainWebGPUImpl() +{ + ReleaseSwapChainResources(); + if (m_wgpuSurface) + wgpuSurfaceUnconfigure(m_wgpuSurface); } -SwapChainWebGPUImpl::~SwapChainWebGPUImpl() = default; +ITextureViewWebGPU* SwapChainWebGPUImpl::GetCurrentBackBufferRTV() +{ + if (m_pBackBufferRTV) + return m_pBackBufferRTV; + + try + { + if ((!m_SurfaceConfigured || m_ReconfigureSurface) && !RecreateSwapChain()) + return nullptr; + + if (!AcquireSurfaceTexture()) + return nullptr; + } + catch (const std::exception& e) + { + LOG_ERROR_MESSAGE("Failed to acquire WebGPU swap chain texture: ", e.what()); + return nullptr; + } + + return m_pBackBufferRTV; +} void SwapChainWebGPUImpl::Present(Uint32 SyncInterval) { @@ -388,31 +101,40 @@ void SwapChainWebGPUImpl::Present(Uint32 SyncInterval) LOG_WARNING_MESSAGE_ONCE("WebGPU only supports 0 and 1 present intervals"); RefCntAutoPtr pDeviceContext = m_wpDeviceContext.Lock(); - RenderDeviceWebGPUImpl* pRenderDevice = m_pRenderDevice.RawPtr(); if (!pDeviceContext) { LOG_ERROR_MESSAGE("Immediate context has been released"); return; } - DeviceContextWebGPUImpl* pImmediateCtxWebGPU = pDeviceContext.RawPtr(); + DeviceContextWebGPUImpl* pImmediateCtx = pDeviceContext.RawPtr(); + pImmediateCtx->Flush(); - pImmediateCtxWebGPU->Flush(); - WGPUSurfaceGetCurrentTextureStatus SurfaceStatus = m_pCmdPresent->Execute(m_pBackBufferSRV, this, pImmediateCtxWebGPU); + if (m_pBackBufferRTV) + { + pImmediateCtx->UnbindTextureFromFramebuffer(ClassPtrCast(m_pBackBufferRTV->GetTexture()), false); +#if !PLATFORM_WEB + if (wgpuSurfacePresent(m_wgpuSurface) != WGPUStatus_Success) + m_ReconfigureSurface = true; +#endif + } + + // The browser presents when control returns to its event loop. Do not acquire the + // next texture or reconfigure the canvas here, as that would invalidate this frame. + m_pBackBufferRTV.Release(); + m_wgpuCurrentTexture.Reset(); if (m_SwapChainDesc.IsPrimary) { - pImmediateCtxWebGPU->FinishFrame(); - pRenderDevice->ReleaseStaleResources(); + pImmediateCtx->FinishFrame(); + m_pRenderDevice->ReleaseStaleResources(); } const bool EnableVSync = SyncInterval != 0; - if (SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Outdated || - SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Lost || - m_VSyncEnabled != EnableVSync) + if (m_VSyncEnabled != EnableVSync) { - m_VSyncEnabled = EnableVSync; - RecreateSwapChain(); + m_VSyncEnabled = EnableVSync; + m_ReconfigureSurface = true; } } @@ -434,7 +156,7 @@ void SwapChainWebGPUImpl::SetWindowedMode() UNSUPPORTED("WebGPU does not support switching to the windowed mode"); } -void SwapChainWebGPUImpl::CreateSurface() +bool SwapChainWebGPUImpl::CreateSurface() { const RenderDeviceWebGPUImpl* pRenderDeviceWebGPU = m_pRenderDevice.RawPtr(); @@ -445,17 +167,17 @@ void SwapChainWebGPUImpl::CreateSurface() wgpuSurfaceNativeDesc.hinstance = GetModuleHandle(nullptr); #elif PLATFORM_LINUX WGPUSurfaceSourceXCBWindow wgpuSurfaceNativeDesc{}; - wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceXCBWindow}; + wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceXCBWindow}; wgpuSurfaceNativeDesc.connection = m_NativeWindow.pXCBConnection; - wgpuSurfaceNativeDesc.window = m_NativeWindow.WindowId; -#elif PLATFROM_MACOS + wgpuSurfaceNativeDesc.window = m_NativeWindow.WindowId; +#elif PLATFORM_MACOS WGPUSurfaceSourceMetalLayer wgpuSurfaceNativeDesc{}; - wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceMetalLayer}; - wgpuSurfaceNativeDesc.window = m_NativeWindow.MetalLayer; + wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceMetalLayer}; + wgpuSurfaceNativeDesc.layer = m_NativeWindow.GetLayer(); #elif PLATFORM_WEB - WGPUSurfaceSourceCanvasHTMLSelector_Emscripten wgpuSurfaceNativeDesc{}; - wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceCanvasHTMLSelector_Emscripten}; - wgpuSurfaceNativeDesc.selector = m_NativeWindow.pCanvasId; + WGPUEmscriptenSurfaceSourceCanvasHTMLSelector wgpuSurfaceNativeDesc{}; + wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector}; + wgpuSurfaceNativeDesc.selector = GetWGPUStringView(m_NativeWindow.pCanvasId); #endif WGPUSurfaceDescriptor wgpuSurfaceDesc{}; @@ -465,166 +187,266 @@ void SwapChainWebGPUImpl::CreateSurface() if (!m_wgpuSurface) { LOG_ERROR_MESSAGE("Failed to create OS-specific surface"); + return false; } + return true; } -void SwapChainWebGPUImpl::ConfigureSurface() +bool SwapChainWebGPUImpl::ConfigureSurface() { - const RenderDeviceWebGPUImpl* pRenderDeviceWebGPU = m_pRenderDevice.RawPtr(); - - WGPUSurfaceCapabilities wgpuSurfaceCapabilities{}; - wgpuSurfaceGetCapabilities(m_wgpuSurface.Get(), pRenderDeviceWebGPU->GetWebGPUAdapter(), &wgpuSurfaceCapabilities); + m_SurfaceConfigured = false; + const RenderDeviceWebGPUImpl* pDevice = m_pRenderDevice.RawPtr(); - auto SelectPresentMode = [&](bool IsVSyncEnabled) -> WGPUPresentMode { - WGPUPresentMode Result = WGPUPresentMode_Fifo; + struct SurfaceCapabilities + { + WGPUSurfaceCapabilities Caps{}; + ~SurfaceCapabilities() { wgpuSurfaceCapabilitiesFreeMembers(Caps); } + } Capabilities; + WGPUSurfaceCapabilities& Caps = Capabilities.Caps; + if (wgpuSurfaceGetCapabilities(m_wgpuSurface, pDevice->GetWebGPUAdapter(), &Caps) != WGPUStatus_Success || + Caps.formatCount == 0 || Caps.presentModeCount == 0 || Caps.alphaModeCount == 0) + { + LOG_ERROR_MESSAGE("Failed to query WebGPU surface capabilities"); + return false; + } - std::vector PreferredPresentModes; - if (IsVSyncEnabled) - { - PreferredPresentModes.push_back(WGPUPresentMode_Fifo); - } - else + const bool IsSRGB = IsSRGBFormat(m_SwapChainDesc.ColorBufferFormat); + WGPUTextureFormat SurfaceFormat = TextureFormatToWGPUFormat(SRGBFormatToUnorm(m_SwapChainDesc.ColorBufferFormat)); + auto SupportsFormat = [&](WGPUTextureFormat Format) { + return std::find(Caps.formats, Caps.formats + Caps.formatCount, Format) != Caps.formats + Caps.formatCount; + }; + if (!SupportsFormat(SurfaceFormat)) + { + // Match Vulkan's RGBA/BGRA fallback without changing precision or sRGB encoding. + WGPUTextureFormat Replacement = WGPUTextureFormat_Undefined; + if (SurfaceFormat == WGPUTextureFormat_RGBA8Unorm) + Replacement = WGPUTextureFormat_BGRA8Unorm; + else if (SurfaceFormat == WGPUTextureFormat_BGRA8Unorm) + Replacement = WGPUTextureFormat_RGBA8Unorm; + + if (Replacement == WGPUTextureFormat_Undefined || !SupportsFormat(Replacement)) { - PreferredPresentModes.push_back(WGPUPresentMode_Mailbox); - PreferredPresentModes.push_back(WGPUPresentMode_Immediate); - PreferredPresentModes.push_back(WGPUPresentMode_Fifo); + LOG_ERROR_MESSAGE("Requested color buffer format ", GetTextureFormatAttribs(m_SwapChainDesc.ColorBufferFormat).Name, + " is not supported by the WebGPU surface"); + return false; } - const WGPUPresentMode* FindBegin = wgpuSurfaceCapabilities.presentModes; - const WGPUPresentMode* FindEnd = wgpuSurfaceCapabilities.presentModes + wgpuSurfaceCapabilities.presentModeCount; + SurfaceFormat = Replacement; + TEXTURE_FORMAT NewFormat = WGPUFormatToTextureFormat(Replacement); + if (IsSRGB) + NewFormat = UnormFormatToSRGB(NewFormat); + LOG_INFO_MESSAGE("Requested color buffer format ", GetTextureFormatAttribs(m_SwapChainDesc.ColorBufferFormat).Name, + " is not supported by the surface and will be replaced with ", GetTextureFormatAttribs(NewFormat).Name); + m_SwapChainDesc.ColorBufferFormat = NewFormat; + } - for (WGPUPresentMode PreferredMode : PreferredPresentModes) + static_assert(SWAP_CHAIN_USAGE_LAST == 8, "Please update this function to handle the new swap chain usage"); + if ((m_SwapChainDesc.Usage & SWAP_CHAIN_USAGE_RENDER_TARGET) == 0 || + (m_SwapChainDesc.Usage & SWAP_CHAIN_USAGE_INPUT_ATTACHMENT) != 0) + { + LOG_ERROR_MESSAGE("WebGPU swap chains require render target usage and do not support input attachments"); + return false; + } + + WGPUTextureUsage Usage = WGPUTextureUsage_RenderAttachment; + if (m_SwapChainDesc.Usage & SWAP_CHAIN_USAGE_SHADER_RESOURCE) + Usage |= WGPUTextureUsage_TextureBinding; + if (m_SwapChainDesc.Usage & SWAP_CHAIN_USAGE_COPY_SOURCE) + Usage |= WGPUTextureUsage_CopySrc; +#if !PLATFORM_WEB + // Emdawnwebgpu does not populate the surface usage capabilities on the Web. + if ((Usage & Caps.usages) != Usage) + { + LOG_ERROR_MESSAGE("Requested swap chain usage is not supported by the WebGPU surface"); + return false; + } +#endif + // Preserve copy-destination support where available without requiring it to present. +#if !PLATFORM_WEB + if ((Caps.usages & WGPUTextureUsage_CopyDst) != 0) +#endif + Usage |= WGPUTextureUsage_CopyDst; + + WGPUPresentMode PresentMode = WGPUPresentMode_Fifo; + const WGPUPresentMode PreferredModes[] = {WGPUPresentMode_Mailbox, WGPUPresentMode_Immediate, WGPUPresentMode_Fifo}; + if (!m_VSyncEnabled) + { + for (WGPUPresentMode Mode : PreferredModes) { - if (std::find(FindBegin, FindEnd, PreferredMode) != FindEnd) + if (std::find(Caps.presentModes, Caps.presentModes + Caps.presentModeCount, Mode) != Caps.presentModes + Caps.presentModeCount) { - Result = PreferredMode; + PresentMode = Mode; break; } } - - return Result; - }; - - auto SelectUsage = [&](SWAP_CHAIN_USAGE_FLAGS Flags) -> WGPUTextureUsage { - WGPUTextureUsage Result = {}; - - DEV_CHECK_ERR(Flags != 0, "No swap chain usage flags defined"); - static_assert(SWAP_CHAIN_USAGE_LAST == 8, "Please update this function to handle the new swap chain usage"); - - if (Flags & SWAP_CHAIN_USAGE_RENDER_TARGET) - Result = static_cast(Result | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopyDst); - if (Flags & SWAP_CHAIN_USAGE_SHADER_RESOURCE) - Result = static_cast(Result | WGPUTextureUsage_TextureBinding); - if (Flags & SWAP_CHAIN_USAGE_COPY_SOURCE) - Result = static_cast(Result | WGPUTextureUsage_CopySrc); - - return Result; - }; + } if (m_SwapChainDesc.Width == 0 || m_SwapChainDesc.Height == 0) { #if PLATFORM_WIN32 - RECT WindowRect; + RECT WindowRect{}; GetClientRect(static_cast(m_NativeWindow.hWnd), &WindowRect); - m_SwapChainDesc.Width = WindowRect.right - WindowRect.left; m_SwapChainDesc.Height = WindowRect.bottom - WindowRect.top; +#elif PLATFORM_MACOS + m_NativeWindow.GetBackingSize(m_SwapChainDesc.Width, m_SwapChainDesc.Height); #elif PLATFORM_WEB - int32_t CanvasWidth = 0; - int32_t CanvasHeight = 0; + int CanvasWidth = 0; + int CanvasHeight = 0; emscripten_get_canvas_element_size(m_NativeWindow.pCanvasId, &CanvasWidth, &CanvasHeight); - - m_SwapChainDesc.Width = static_cast(CanvasWidth); + m_SwapChainDesc.Width = static_cast(CanvasWidth); m_SwapChainDesc.Height = static_cast(CanvasHeight); #endif - m_SwapChainDesc.Width = (std::max)(m_SwapChainDesc.Width, 1u); m_SwapChainDesc.Height = (std::max)(m_SwapChainDesc.Height, 1u); } - const WGPUTextureFormat wgpuPreferredFormat = wgpuSurfaceCapabilities.formats[0]; - - WGPUTextureFormat wgpuRTVFormats[] = { - wgpuPreferredFormat, - WGPUConvertUnormToSRGB(wgpuPreferredFormat), - }; - - WGPUSurfaceConfiguration wgpuSurfaceConfig{}; - wgpuSurfaceConfig.nextInChain = nullptr; - wgpuSurfaceConfig.device = pRenderDeviceWebGPU->GetWebGPUDevice(); - wgpuSurfaceConfig.usage = SelectUsage(m_SwapChainDesc.Usage); - wgpuSurfaceConfig.width = m_SwapChainDesc.Width; - wgpuSurfaceConfig.height = m_SwapChainDesc.Height; - wgpuSurfaceConfig.format = wgpuPreferredFormat; - wgpuSurfaceConfig.presentMode = SelectPresentMode(m_VSyncEnabled); - wgpuSurfaceConfig.alphaMode = WGPUCompositeAlphaMode_Auto; - wgpuSurfaceConfig.viewFormats = wgpuRTVFormats; - wgpuSurfaceConfig.viewFormatCount = _countof(wgpuRTVFormats); - - wgpuSurfaceConfigure(m_wgpuSurface, &wgpuSurfaceConfig); - wgpuSurfaceCapabilitiesFreeMembers(wgpuSurfaceCapabilities); + WGPUCompositeAlphaMode AlphaMode = Caps.alphaModes[0]; + if (std::find(Caps.alphaModes, Caps.alphaModes + Caps.alphaModeCount, WGPUCompositeAlphaMode_Opaque) != Caps.alphaModes + Caps.alphaModeCount) + AlphaMode = WGPUCompositeAlphaMode_Opaque; + + const WGPUTextureFormat ViewFormat = TextureFormatToWGPUFormat(m_SwapChainDesc.ColorBufferFormat); + WGPUSurfaceConfiguration Config{}; + Config.device = pDevice->GetWebGPUDevice(); + Config.usage = Usage; + Config.width = m_SwapChainDesc.Width; + Config.height = m_SwapChainDesc.Height; + Config.format = SurfaceFormat; + Config.presentMode = PresentMode; + Config.alphaMode = AlphaMode; + Config.viewFormatCount = IsSRGB ? 1 : 0; + Config.viewFormats = IsSRGB ? &ViewFormat : nullptr; + wgpuSurfaceConfigure(m_wgpuSurface, &Config); + m_SurfaceConfigured = true; + return true; } -void SwapChainWebGPUImpl::CreateBuffersAndViews() +bool SwapChainWebGPUImpl::AcquireSurfaceTexture() { - TextureDesc BackBufferDesc{}; - BackBufferDesc.Type = RESOURCE_DIM_TEX_2D; - BackBufferDesc.Width = m_SwapChainDesc.Width; - BackBufferDesc.Height = m_SwapChainDesc.Height; - BackBufferDesc.Format = m_SwapChainDesc.ColorBufferFormat; - BackBufferDesc.SampleCount = 1; - BackBufferDesc.Usage = USAGE_DEFAULT; - BackBufferDesc.BindFlags = BIND_SHADER_RESOURCE | BIND_RENDER_TARGET; - BackBufferDesc.Name = "Main back buffer"; - - RefCntAutoPtr pBackBufferTex; - m_pRenderDevice->CreateTexture(BackBufferDesc, nullptr, &pBackBufferTex); - m_pBackBufferRTV = RefCntAutoPtr{pBackBufferTex->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET), IID_TextureViewWebGPU}; - m_pBackBufferSRV = RefCntAutoPtr{pBackBufferTex->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE), IID_TextureViewWebGPU}; - - if (m_SwapChainDesc.DepthBufferFormat != TEX_FORMAT_UNKNOWN) + // Retry once after recovering an outdated or lost surface. A timeout skips this + // acquisition instead of blocking; the next GetCurrentBackBufferRTV() can retry. + for (Uint32 Attempt = 0; Attempt < 2; ++Attempt) { - TextureDesc DepthBufferDesc{}; - DepthBufferDesc.Type = RESOURCE_DIM_TEX_2D; - DepthBufferDesc.Width = BackBufferDesc.Width; - DepthBufferDesc.Height = BackBufferDesc.Height; - DepthBufferDesc.Format = m_SwapChainDesc.DepthBufferFormat; - DepthBufferDesc.SampleCount = 1; - DepthBufferDesc.Usage = USAGE_DEFAULT; - DepthBufferDesc.BindFlags = BIND_DEPTH_STENCIL; - - DepthBufferDesc.ClearValue.Format = DepthBufferDesc.Format; - DepthBufferDesc.ClearValue.DepthStencil.Depth = m_SwapChainDesc.DefaultDepthValue; - DepthBufferDesc.ClearValue.DepthStencil.Stencil = m_SwapChainDesc.DefaultStencilValue; - DepthBufferDesc.Name = "Main depth buffer"; - RefCntAutoPtr pDepthBufferTex; - m_pRenderDevice->CreateTexture(DepthBufferDesc, nullptr, &pDepthBufferTex); - m_pDepthBufferDSV = RefCntAutoPtr{pDepthBufferTex->GetDefaultView(TEXTURE_VIEW_DEPTH_STENCIL), IID_TextureViewWebGPU}; + WGPUSurfaceTexture SurfaceTexture{}; + wgpuSurfaceGetCurrentTexture(m_wgpuSurface, &SurfaceTexture); + WebGPUTextureWrapper Texture{SurfaceTexture.texture}; + + switch (SurfaceTexture.status) + { + case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal: + case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: + break; + + case WGPUSurfaceGetCurrentTextureStatus_Timeout: + return false; + + case WGPUSurfaceGetCurrentTextureStatus_Outdated: + case WGPUSurfaceGetCurrentTextureStatus_Lost: + Texture.Reset(); + m_ReconfigureSurface = true; + if (Attempt == 0 && RecreateSwapChain()) + continue; + return false; + + default: + LOG_ERROR_MESSAGE("Failed to acquire WebGPU surface texture"); + return false; + } + + if (!Texture) + { + LOG_ERROR_MESSAGE("WebGPU surface returned a null texture"); + return false; + } + + TextureDesc Desc; + Desc.Name = "Swap chain back buffer"; + Desc.Type = RESOURCE_DIM_TEX_2D; + Desc.Width = wgpuTextureGetWidth(Texture); + Desc.Height = wgpuTextureGetHeight(Texture); + Desc.Format = m_SwapChainDesc.ColorBufferFormat; + Desc.BindFlags = BIND_RENDER_TARGET; + if (m_SwapChainDesc.Usage & SWAP_CHAIN_USAGE_SHADER_RESOURCE) + Desc.BindFlags |= BIND_SHADER_RESOURCE; + + RefCntAutoPtr pBackBuffer; + m_pRenderDevice.RawPtr()->CreateTextureFromWebGPUTexture(Texture, Desc, RESOURCE_STATE_UNDEFINED, &pBackBuffer); + if (!pBackBuffer) + return false; + RefCntAutoPtr pRTV{pBackBuffer->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET), IID_TextureViewWebGPU}; + if (!pRTV) + { + LOG_ERROR_MESSAGE("Failed to create WebGPU surface render target view"); + return false; + } + + // Imported textures borrow their native handle. Retain the acquired reference + // until all rendering for this frame has been submitted and presented. + m_wgpuCurrentTexture = std::move(Texture); + m_pBackBufferRTV = std::move(pRTV); + m_ReconfigureSurface = SurfaceTexture.status == WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal; + return true; } + return false; } -void SwapChainWebGPUImpl::ReleaseSwapChainResources() +bool SwapChainWebGPUImpl::CreateDepthBufferView() { - if (!m_wgpuSurface) - return; + if (m_SwapChainDesc.DepthBufferFormat == TEX_FORMAT_UNKNOWN) + return true; + + TextureDesc Desc; + Desc.Name = "Main depth buffer"; + Desc.Type = RESOURCE_DIM_TEX_2D; + Desc.Width = m_SwapChainDesc.Width; + Desc.Height = m_SwapChainDesc.Height; + Desc.Format = m_SwapChainDesc.DepthBufferFormat; + Desc.BindFlags = BIND_DEPTH_STENCIL; + Desc.ClearValue.Format = Desc.Format; + Desc.ClearValue.DepthStencil.Depth = m_SwapChainDesc.DefaultDepthValue; + Desc.ClearValue.DepthStencil.Stencil = m_SwapChainDesc.DefaultStencilValue; + RefCntAutoPtr pDepthBuffer; + m_pRenderDevice->CreateTexture(Desc, nullptr, &pDepthBuffer); + if (!pDepthBuffer) + return false; + m_pDepthBufferDSV = RefCntAutoPtr{pDepthBuffer->GetDefaultView(TEXTURE_VIEW_DEPTH_STENCIL), IID_TextureViewWebGPU}; + return m_pDepthBufferDSV != nullptr; +} - m_pCmdPresent->ReleaseSwapChainResources(); - m_pBackBufferSRV.Release(); +void SwapChainWebGPUImpl::ReleaseSwapChainResources() +{ + if (RefCntAutoPtr pContext = m_wpDeviceContext.Lock()) + { + // Reconfiguration invalidates acquired textures. Submit any pending clears + // and draws before releasing them, and remove stale framebuffer bindings. + DeviceContextWebGPUImpl* pImmediateCtx = pContext.RawPtr(); + if (m_pBackBufferRTV || m_pDepthBufferDSV) + pImmediateCtx->Flush(); + if (m_pBackBufferRTV) + pImmediateCtx->UnbindTextureFromFramebuffer(ClassPtrCast(m_pBackBufferRTV->GetTexture()), false); + if (m_pDepthBufferDSV) + pImmediateCtx->UnbindTextureFromFramebuffer(ClassPtrCast(m_pDepthBufferDSV->GetTexture()), false); + } m_pBackBufferRTV.Release(); + m_wgpuCurrentTexture.Reset(); m_pDepthBufferDSV.Release(); } -void SwapChainWebGPUImpl::RecreateSwapChain() +bool SwapChainWebGPUImpl::RecreateSwapChain() { + m_ReconfigureSurface = true; try { ReleaseSwapChainResources(); - ConfigureSurface(); - CreateBuffersAndViews(); + if (!ConfigureSurface() || !CreateDepthBufferView()) + return false; + m_ReconfigureSurface = false; + return true; } - catch (const std::runtime_error&) + catch (const std::exception& e) { - LOG_ERROR("Failed to recreate the swap chain"); + LOG_ERROR_MESSAGE("Failed to recreate WebGPU swap chain: ", e.what()); + return false; } } diff --git a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp index 3162d2d9c3..1d13983d75 100644 --- a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,7 +138,6 @@ WGPUTextureDescriptor TextureDescToWGPUTextureDescriptor(const TextureDesc& return wgpuTextureDesc; } -#if !PLATFORM_EMSCRIPTEN static WGPUTextureUsage TextureViewTypeToWGPUTextureUsage(TEXTURE_VIEW_TYPE ViewType) { static_assert(TEXTURE_VIEW_NUM_VIEWS == 7, "Please update the switch below to handle the new view type"); @@ -149,10 +148,8 @@ static WGPUTextureUsage TextureViewTypeToWGPUTextureUsage(TEXTURE_VIEW_TYPE View case TEXTURE_VIEW_RENDER_TARGET: case TEXTURE_VIEW_DEPTH_STENCIL: - return WGPUTextureUsage_RenderAttachment; - case TEXTURE_VIEW_READ_ONLY_DEPTH_STENCIL: - return WGPUTextureUsage_TextureBinding; + return WGPUTextureUsage_RenderAttachment; case TEXTURE_VIEW_UNORDERED_ACCESS: return WGPUTextureUsage_StorageBinding; @@ -166,7 +163,6 @@ static WGPUTextureUsage TextureViewTypeToWGPUTextureUsage(TEXTURE_VIEW_TYPE View return WGPUTextureUsage_None; } } -#endif WGPUTextureViewDescriptor TextureViewDescToWGPUTextureViewDescriptor(const TextureDesc& TexDesc, TextureViewDesc& ViewDesc, @@ -229,14 +225,12 @@ WGPUTextureViewDescriptor TextureViewDescToWGPUTextureViewDescriptor(const Textu wgpuTextureViewDesc.aspect = WGPUTextureAspect_All; } - // TODO: enable this as soon as Emscripten adds the usage member to WGPUTextureViewDescriptor - // https://github.com/emscripten-core/emscripten/issues/23945 -#if !PLATFORM_EMSCRIPTEN - if (ViewDesc.Format != TexDesc.Format) + if (ViewDesc.Format != TexDesc.Format || (TexDesc.MiscFlags & MISC_TEXTURE_FLAG_GENERATE_MIPS)) { + // Mip generation may use an UNORM storage texture underneath an sRGB texture. + // Restrict view usage even when the Diligent texture and view formats match. wgpuTextureViewDesc.usage = TextureViewTypeToWGPUTextureUsage(ViewDesc.ViewType); } -#endif return wgpuTextureViewDesc; } @@ -385,13 +379,13 @@ TextureWebGPUImpl::TextureWebGPUImpl(IReferenceCounters* pRefCounters, DstDepthStride // DstDepthStride ); - WGPUImageCopyBuffer wgpuSourceCopyInfo{}; + WGPUTexelCopyBufferInfo wgpuSourceCopyInfo{}; wgpuSourceCopyInfo.layout.offset = DstSubResOffset; wgpuSourceCopyInfo.layout.bytesPerRow = static_cast(DstRawStride); wgpuSourceCopyInfo.layout.rowsPerImage = static_cast(DstDepthStride / DstRawStride); wgpuSourceCopyInfo.buffer = wgpuUploadBuffer.Get(); - WGPUImageCopyTexture wgpuDestinationCopyInfo{}; + WGPUTexelCopyTextureInfo wgpuDestinationCopyInfo{}; wgpuDestinationCopyInfo.texture = m_wgpuTexture.Get(); wgpuDestinationCopyInfo.mipLevel = MipIdx; wgpuDestinationCopyInfo.origin = {0, 0, LayerIdx}; @@ -527,6 +521,20 @@ void TextureWebGPUImpl::CreateViewInternal(const TextureViewDesc& ViewDesc, ITex ValidatedAndCorrectTextureViewDesc(m_Desc, UpdatedViewDesc); WGPUTextureViewDescriptor wgpuTextureViewDesc = TextureViewDescToWGPUTextureViewDescriptor(m_Desc, UpdatedViewDesc, m_pDevice); + WGPUTextureComponentSwizzleDescriptor wgpuSwizzleDesc{}; + if (!IsIdentityComponentMapping(UpdatedViewDesc.Swizzle)) + { + if (!m_pDevice->GetDeviceInfo().Features.TextureComponentSwizzle) + { + LOG_ERROR_MESSAGE("Texture component swizzle is not enabled for this device"); + return; + } + + wgpuSwizzleDesc.chain.sType = WGPUSType_TextureComponentSwizzleDescriptor; + wgpuSwizzleDesc.swizzle = TextureComponentMappingToWGPUTextureComponentSwizzle(UpdatedViewDesc.Swizzle); + wgpuTextureViewDesc.nextInChain = &wgpuSwizzleDesc.chain; + } + WebGPUTextureViewWrapper wgpuTextureView{wgpuTextureCreateView(m_wgpuTexture.Get(), &wgpuTextureViewDesc)}; if (!wgpuTextureView) LOG_ERROR_AND_THROW("Failed to create WebGPU texture view ", " '", ViewDesc.Name ? ViewDesc.Name : "", '\''); diff --git a/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp index 588d660410..41af3874ad 100644 --- a/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,11 +183,11 @@ void WebGPUResourceBase::FlushPendingWrites(StagingBufferInfo& Buffer) void WebGPUResourceBase::ProcessAsyncReadback(StagingBufferInfo& Buffer) { - auto MapAsyncCallback = [](WGPUBufferMapAsyncStatus MapStatus, void* pUserData) { + auto MapAsyncCallback = [](WGPUMapAsyncStatus MapStatus, WGPUStringView Message, void* pUserData, void* pUserData2) { VERIFY_EXPR(pUserData != nullptr); StagingBufferInfo& BufferInfo = *static_cast(pUserData); - if (MapStatus == WGPUBufferMapAsyncStatus_Success) + if (MapStatus == WGPUMapAsyncStatus_Success) { // Do NOT use WGPU_WHOLE_MAP_SIZE due to https://github.com/emscripten-core/emscripten/issues/20538 const size_t MappedDataSize = BufferInfo.Resource.m_MappedData.size(); @@ -201,6 +201,10 @@ void WebGPUResourceBase::ProcessAsyncReadback(StagingBufferInfo& Buffer) } wgpuBufferUnmap(BufferInfo.wgpuBuffer.Get()); } + else + { + LOG_DEBUG_MESSAGE(DEBUG_MESSAGE_SEVERITY_ERROR, "WebGPU: Failed to map staging readback buffer: ", WGPUStringViewToString(Message)); + } BufferInfo.pSyncPoint->Trigger(); @@ -211,8 +215,12 @@ void WebGPUResourceBase::ProcessAsyncReadback(StagingBufferInfo& Buffer) // Keep the resource alive until the callback is called m_Owner.AddRef(); // Do NOT use WGPU_WHOLE_MAP_SIZE due to https://github.com/emscripten-core/emscripten/issues/20538 - const size_t MappedDataSize = Buffer.Resource.m_MappedData.size(); - wgpuBufferMapAsync(Buffer.wgpuBuffer, WGPUMapMode_Read, 0, AlignUp(MappedDataSize, MappedRangeAlignment), MapAsyncCallback, &Buffer); + const size_t MappedDataSize = Buffer.Resource.m_MappedData.size(); + WGPUBufferMapCallbackInfo CallbackInfo{}; + CallbackInfo.mode = DefaultWGPUCallbackMode; + CallbackInfo.callback = MapAsyncCallback; + CallbackInfo.userdata1 = &Buffer; + wgpuBufferMapAsync(Buffer.wgpuBuffer, WGPUMapMode_Read, 0, AlignUp(MappedDataSize, MappedRangeAlignment), CallbackInfo); } } // namespace Diligent diff --git a/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp index ffbff1b0bc..051fe5d182 100644 --- a/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,12 @@ #include "pch.h" #include "WebGPUTypeConversions.hpp" -#include "WebGPUStubs.hpp" +#include "WebGPUDefinitions.hpp" #include "DebugUtilities.hpp" #include "GraphicsAccessories.hpp" -#define WEBGPU_FORMAT_RANGE_SIZE (WGPUTextureFormat_RGBA16Snorm - WGPUTextureFormat_Undefined + 1) +// Normalized 16-bit formats precede the compressed formats in the current WebGPU enum. +#define WEBGPU_FORMAT_RANGE_SIZE (WGPUTextureFormat_ASTC12x12UnormSrgb - WGPUTextureFormat_Undefined + 1) namespace Diligent { @@ -954,4 +955,29 @@ WGPUVertexStepMode InputElementFrequencyToWGPUVertexStepMode(INPUT_ELEMENT_FREQU } } +WGPUComponentSwizzle TextureComponentSwizzleToWGPUComponentSwizzle(TEXTURE_COMPONENT_SWIZZLE Swizzle) +{ + static_assert(TEXTURE_COMPONENT_SWIZZLE_COUNT == 7, "Did you add a new swizzle type? Please handle it here."); + // clang-format off + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_IDENTITY) == WGPUComponentSwizzle_Undefined, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_IDENTITY enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_ZERO) == WGPUComponentSwizzle_Zero, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_ZERO enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_ONE) == WGPUComponentSwizzle_One, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_ONE enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_R) == WGPUComponentSwizzle_R, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_R enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_G) == WGPUComponentSwizzle_G, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_G enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_B) == WGPUComponentSwizzle_B, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_B enum."); + static_assert(static_cast(TEXTURE_COMPONENT_SWIZZLE_A) == WGPUComponentSwizzle_A, "Unexpected value of TEXTURE_COMPONENT_SWIZZLE_A enum."); + // clang-format on + return static_cast(Swizzle); +} + +WGPUTextureComponentSwizzle TextureComponentMappingToWGPUTextureComponentSwizzle(const TextureComponentMapping& Mapping) +{ + return WGPUTextureComponentSwizzle{ + TextureComponentSwizzleToWGPUComponentSwizzle(Mapping.R), + TextureComponentSwizzleToWGPUComponentSwizzle(Mapping.G), + TextureComponentSwizzleToWGPUComponentSwizzle(Mapping.B), + TextureComponentSwizzleToWGPUComponentSwizzle(Mapping.A) // + }; +} + } // namespace Diligent diff --git a/Graphics/GraphicsTools/CMakeLists.txt b/Graphics/GraphicsTools/CMakeLists.txt index 5feb961cb5..153c44cf31 100644 --- a/Graphics/GraphicsTools/CMakeLists.txt +++ b/Graphics/GraphicsTools/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.10...3.22) project(Diligent-GraphicsTools CXX) diff --git a/Graphics/ShaderTools/CMakeLists.txt b/Graphics/ShaderTools/CMakeLists.txt index 1d18300072..a411e8df6a 100644 --- a/Graphics/ShaderTools/CMakeLists.txt +++ b/Graphics/ShaderTools/CMakeLists.txt @@ -49,8 +49,8 @@ if(ENABLE_HLSL) endif() if(ENABLE_WGSL) - list(APPEND SOURCE src/WGSLUtils.cpp src/WGSLShaderResources.cpp) - list(APPEND INCLUDE include/WGSLUtils.hpp include/WGSLShaderResources.hpp) + list(APPEND SOURCE src/WGSLUtils.cpp src/WGSLShaderResources.cpp src/WGSLShaderReflection.cpp) + list(APPEND INCLUDE include/WGSLUtils.hpp include/WGSLShaderResources.hpp include/WGSLShaderReflection.hpp) endif() if(D3D11_SUPPORTED OR D3D12_SUPPORTED) @@ -218,6 +218,8 @@ if(ENABLE_WGSL) target_link_libraries(Diligent-ShaderTools PRIVATE libtint + # Tint's internal headers require Dawn's utility build configuration. + dawn_utils_config # We include this library because when building for Emscripten, libtint does not include this library in the dependency list tint_lang_wgsl_inspector ) @@ -249,7 +251,11 @@ add_custom_command(OUTPUT ${HLSL_DEFINITIONS_INC} # We must use full path here! VERBATIM ) -set_common_target_properties(Diligent-ShaderTools) +if(ENABLE_WGSL) + set_common_target_properties(Diligent-ShaderTools 20) +else() + set_common_target_properties(Diligent-ShaderTools) +endif() source_group("src" FILES ${SOURCE}) source_group("include" FILES ${INCLUDE}) diff --git a/Graphics/ShaderTools/include/ShaderToolsCommon.hpp b/Graphics/ShaderTools/include/ShaderToolsCommon.hpp index 5a66191a06..b1be79a7bc 100644 --- a/Graphics/ShaderTools/include/ShaderToolsCommon.hpp +++ b/Graphics/ShaderTools/include/ShaderToolsCommon.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2019-2025 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * Copyright 2015-2019 Egor Yusov * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -237,6 +237,11 @@ struct ShaderCodeVariableDescX : ShaderCodeVariableDesc return Members[idx]; } + const ShaderCodeVariableDescX& GetMember(size_t idx) const + { + return Members[idx]; + } + template void ProcessMembers(HandlerType&& Handler) { @@ -334,6 +339,11 @@ struct ShaderCodeBufferDescX : ShaderCodeBufferDesc return Variables[idx]; } + const ShaderCodeVariableDescX& GetVariable(size_t idx) const + { + return Variables[idx]; + } + void AssignVariables(std::vector&& _Variables) { Variables = std::move(_Variables); diff --git a/Graphics/ShaderTools/include/WGSLShaderReflection.hpp b/Graphics/ShaderTools/include/WGSLShaderReflection.hpp new file mode 100644 index 0000000000..a1e29a2d22 --- /dev/null +++ b/Graphics/ShaderTools/include/WGSLShaderReflection.hpp @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#pragma once + +#include +#include "ShaderToolsCommon.hpp" + +namespace Diligent +{ + +// Source-level information that cannot be recovered reliably from Tint-generated WGSL. +// Binding points identify resources independently of generated variable and type names. +struct WGSLShaderReflection +{ + struct BindingPoint + { + Uint32 Group = 0; + Uint32 Binding = 0; + + bool operator<(const BindingPoint& Rhs) const + { + return Group < Rhs.Group || (Group == Rhs.Group && Binding < Rhs.Binding); + } + }; + struct Resource + { + std::string Name; + std::unique_ptr UniformBuffer; + }; + std::map Resources; + + // Tint preserves override IDs through the IR rewrite, but may rename their + // identifiers. Keep the application-facing names independently of those names. + std::map SpecializationConstants; + + Resource* Find(Uint32 Group, Uint32 Binding) + { + auto It = Resources.find({Group, Binding}); + return It != Resources.end() ? &It->second : nullptr; + } + + const Resource* Find(Uint32 Group, Uint32 Binding) const + { + auto It = Resources.find({Group, Binding}); + return It != Resources.end() ? &It->second : nullptr; + } +}; + +// Keep metadata with saved WGSL so GetBytecode(), archives, and compilation-free +// clients retain the original reflection. The payload contains no SPIR-V bytecode. +void AppendWGSLShaderReflection(std::string& WGSL, const WGSLShaderReflection& Reflection); + +// Missing metadata is valid for native WGSL and shaders produced by older versions. +// Returns false for an unsupported version or malformed metadata. +bool ParseWGSLShaderReflection(const std::string& WGSL, WGSLShaderReflection& Reflection); + +} // namespace Diligent diff --git a/Graphics/ShaderTools/include/WGSLShaderResources.hpp b/Graphics/ShaderTools/include/WGSLShaderResources.hpp index 9266b31436..3b3b4b1f44 100644 --- a/Graphics/ShaderTools/include/WGSLShaderResources.hpp +++ b/Graphics/ShaderTools/include/WGSLShaderResources.hpp @@ -166,17 +166,19 @@ struct WGSLSpecializationConstantAttribs /* 0 */const char* const Name; /* 8 */const Uint16 OverrideId; /* 10 */const Uint8 Type; // SHADER_CODE_BASIC_TYPE -/* 11 */ +/* 11 */const bool IsIdSpecified; /* 16 */ // End of structure // clang-format on WGSLSpecializationConstantAttribs(const char* _Name, Uint16 _OverrideId, - SHADER_CODE_BASIC_TYPE _Type) noexcept : + SHADER_CODE_BASIC_TYPE _Type, + bool _IsIdSpecified) noexcept : Name{_Name}, OverrideId{_OverrideId}, - Type{static_cast(_Type)} + Type{static_cast(_Type)}, + IsIdSpecified{_IsIdSpecified} {} SHADER_CODE_BASIC_TYPE GetType() const { return static_cast(Type); } diff --git a/Graphics/ShaderTools/include/WGSLUtils.hpp b/Graphics/ShaderTools/include/WGSLUtils.hpp index e4304a2a12..b0eed0d6a6 100644 --- a/Graphics/ShaderTools/include/WGSLUtils.hpp +++ b/Graphics/ShaderTools/include/WGSLUtils.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,9 @@ struct ResourceBinding; namespace Diligent { -std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV); +// Preserve original resource names and optionally constant-buffer member descriptions +// in the generated source, independently of Tint's type and identifier rewrites. +std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV, bool LoadUniformBufferReflection = false); struct WGSLResourceBindingInfo { @@ -52,9 +54,21 @@ struct WGSLResourceBindingInfo }; using WGSLResourceMapping = std::unordered_map; +// Remap the selected entry point and discard declarations used only by other +// entry points. A null EntryPoint remaps the whole module. The IR round trip +// gives every retained override an explicit @id with its original Tint ID. +// Metadata preserves original resource and specialization-constant names across +// rewrites, including native WGSL shaders that initially have no metadata. +// On success, pRemappedEntryPoint receives the generated entry-point name when +// EntryPoint is selected, or an empty string for a whole-module rewrite. The +// output is unchanged on failure and may also supply the input entry-point name. +// Returns an empty string if an active resource cannot be mapped unambiguously, +// including distinct resource names sharing a destination across entry points. std::string RemapWGSLResourceBindings(const std::string& WGSL, const WGSLResourceMapping& ResMapping, - const char* EmulatedArrayIndexSuffix); + const char* EmulatedArrayIndexSuffix, + const char* EntryPoint = nullptr, + std::string* pRemappedEntryPoint = nullptr); /// When WGSL is generated from SPIR-V, the names of resources may be mangled diff --git a/Graphics/ShaderTools/src/SPIRVUtils.cpp b/Graphics/ShaderTools/src/SPIRVUtils.cpp index 642acab8ec..619c80ecf5 100644 --- a/Graphics/ShaderTools/src/SPIRVUtils.cpp +++ b/Graphics/ShaderTools/src/SPIRVUtils.cpp @@ -108,20 +108,31 @@ std::vector PatchImageFormats(const std::vector& diligent_spirv_cross::ShaderResources resources = Compiler.get_shader_resources(); - std::unordered_map ImageTypeIdToFormat; - for (uint32_t i = 0; i < SPIRV.size(); ++i) + std::unordered_map ImageTypeIdToFormat; + // Skip the header and walk instruction boundaries: operands may have the same + // low bits as OpTypeImage and must not be interpreted as instruction words. + constexpr size_t SPIRVHeaderSize = 5; + for (size_t i = SPIRVHeaderSize; i < SPIRV.size();) { + const uint32_t WordCount = SPIRV[i] >> 16; + if (WordCount == 0 || WordCount > SPIRV.size() - i) + { + LOG_ERROR_MESSAGE("Invalid SPIR-V instruction size at word ", i); + return {}; + } + // OpTypeImage // 0 1 2 3 4 5 6 7 8 9 // | OpCode | Result | Sampled Type | Dim | Depth | Arrayed | MS | Sampled | Image Format | Access Qualifier constexpr uint32_t ImageFormatOffset = 8; - uint32_t OpCode = SPIRV[i] & 0xFFFF; - if (OpCode == spv::OpTypeImage && i + ImageFormatOffset < SPIRV.size()) + const uint32_t OpCode = SPIRV[i] & 0xFFFF; + if (OpCode == spv::OpTypeImage && WordCount > ImageFormatOffset) { const uint32_t ImageTypeId = SPIRV[i + 1]; ImageTypeIdToFormat[ImageTypeId] = i + ImageFormatOffset; } + i += WordCount; } std::vector PatchedSPIRV = SPIRV; @@ -141,8 +152,8 @@ std::vector PatchImageFormats(const std::vector& auto FormatOffsetIt = ImageTypeIdToFormat.find(Img.base_type_id); if (FormatOffsetIt != ImageTypeIdToFormat.end()) { - const uint32_t ImageFormatOffset = FormatOffsetIt->second; - uint32_t& FormatWord = PatchedSPIRV[ImageFormatOffset]; + const size_t ImageFormatOffset = FormatOffsetIt->second; + uint32_t& FormatWord = PatchedSPIRV[ImageFormatOffset]; if (FormatWord != static_cast(type.image.format) && FormatWord != static_cast(spvFormat)) { diff --git a/Graphics/ShaderTools/src/WGSLShaderReflection.cpp b/Graphics/ShaderTools/src/WGSLShaderReflection.cpp new file mode 100644 index 0000000000..2bc9d9bc68 --- /dev/null +++ b/Graphics/ShaderTools/src/WGSLShaderReflection.cpp @@ -0,0 +1,291 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include "WGSLShaderReflection.hpp" +#include "ParsingTools.hpp" + +#include +#include +#include + +namespace Diligent +{ +namespace +{ + +constexpr char ReflectionMarker[] = "/*$DILIGENT_WGSL_REFLECTION="; + +// Encode directly into the output so arbitrary SPIR-V names cannot terminate +// the WGSL comment, without allocating a temporary string for each name. +void WriteString(std::string& Output, const char* Str) +{ + if (Str == nullptr || *Str == '\0') + { + Output += "- "; + return; + } + constexpr char Hex[] = "0123456789abcdef"; + const size_t Length = std::strlen(Str); + const size_t Start = Output.size(); + Output.resize(Start + Length * 2 + 1); + char* Dst = &Output[Start]; + for (size_t i = 0; i < Length; ++i) + { + const auto Ch = static_cast(Str[i]); + *Dst++ = Hex[Ch >> 4]; + *Dst++ = Hex[Ch & 15]; + } + *Dst = ' '; +} + +void WriteNumber(std::string& Output, Uint32 Value, char Separator = ' ') +{ + char Digits[std::numeric_limits::digits10 + 1]; + const auto Result = std::to_chars(Digits, Digits + sizeof(Digits), Value); + Output.append(Digits, static_cast(Result.ptr - Digits)); + Output += Separator; +} + +// Read directly from the metadata's bounded range in the WGSL source. Only +// decoded names and reflection objects need storage; tokens never allocate. +class ReflectionReader +{ +public: + ReflectionReader(const char* Begin, const char* End) : + m_Pos{Begin}, + m_End{End} + {} + + bool IsAtEnd() + { + SkipWhitespace(); + return m_Pos == m_End; + } + + template + bool ReadNumber(T& Value) + { + SkipWhitespace(); + T Number = 0; + const char* NumberEnd = Parsing::ParseInteger(m_Pos, m_End, Number); + if (NumberEnd == m_Pos || (NumberEnd != m_End && !IsWhitespace(*NumberEnd))) + return false; + m_Pos = NumberEnd; + Value = Number; + return true; + } + + bool ReadString(std::string& Str) + { + SkipWhitespace(); + const char* Begin = m_Pos; + while (m_Pos != m_End && !IsWhitespace(*m_Pos)) + ++m_Pos; + const size_t Length = static_cast(m_Pos - Begin); + if (Length == 1 && *Begin == '-') + { + Str.clear(); + return true; + } + if (Length == 0 || Length % 2 != 0) + return false; + auto Digit = [](char Ch) -> int { + if (Ch >= '0' && Ch <= '9') return Ch - '0'; + if (Ch >= 'a' && Ch <= 'f') return Ch - 'a' + 10; + return -1; + }; + Str.resize(Length / 2); + for (size_t i = 0; i < Str.size(); ++i) + { + const int Hi = Digit(Begin[i * 2]); + const int Lo = Digit(Begin[i * 2 + 1]); + if (Hi < 0 || Lo < 0 || (Hi == 0 && Lo == 0)) + return false; + Str[i] = static_cast((Hi << 4) | Lo); + } + return true; + } + +private: + static bool IsWhitespace(char Ch) + { + // Preserve the classic-locale whitespace accepted by the original format. + return Parsing::IsDelimiter(Ch) || Ch == '\v' || Ch == '\f'; + } + + void SkipWhitespace() + { + while (m_Pos != m_End && IsWhitespace(*m_Pos)) + ++m_Pos; + } + + const char* m_Pos; + const char* const m_End; +}; + +void WriteVariable(std::string& Output, const ShaderCodeVariableDescX& Var) +{ + WriteString(Output, Var.Name); + WriteString(Output, Var.TypeName); + WriteNumber(Output, Var.Class); + WriteNumber(Output, Var.BasicType); + WriteNumber(Output, Var.NumRows); + WriteNumber(Output, Var.NumColumns); + WriteNumber(Output, Var.Offset); + WriteNumber(Output, Var.ArraySize); + WriteNumber(Output, Var.NumMembers, '\n'); + for (Uint32 i = 0; i < Var.NumMembers; ++i) + WriteVariable(Output, Var.GetMember(i)); +} + +bool ReadVariable(ReflectionReader& Reader, ShaderCodeVariableDescX& Var, Uint32 Depth = 0) +{ + // Bound recursion for malformed externally supplied metadata. + if (Depth >= 64) + return false; + std::string Name, TypeName; + Uint8 Class = 0, BasicType = 0; + Uint32 NumMembers = 0; + if (!Reader.ReadString(Name) || !Reader.ReadString(TypeName) || + !Reader.ReadNumber(Class) || !Reader.ReadNumber(BasicType) || + !Reader.ReadNumber(Var.NumRows) || !Reader.ReadNumber(Var.NumColumns) || + !Reader.ReadNumber(Var.Offset) || !Reader.ReadNumber(Var.ArraySize) || + !Reader.ReadNumber(NumMembers)) + return false; + if (Class >= SHADER_CODE_VARIABLE_CLASS_COUNT || BasicType >= SHADER_CODE_BASIC_TYPE_COUNT) + return false; + Var.SetName(std::move(Name)); + Var.SetTypeName(std::move(TypeName)); + Var.Class = static_cast(Class); + Var.BasicType = static_cast(BasicType); + for (Uint32 i = 0; i < NumMembers; ++i) + { + ShaderCodeVariableDescX Member; + if (!ReadVariable(Reader, Member, Depth + 1)) + return false; + Var.AddMember(std::move(Member)); + } + return true; +} + +} // namespace + +void AppendWGSLShaderReflection(std::string& WGSL, const WGSLShaderReflection& Reflection) +{ + if (Reflection.Resources.empty() && Reflection.SpecializationConstants.empty()) + return; + WGSL += '\n'; + WGSL += ReflectionMarker; + WGSL += "1\n"; + // Each record starts with its type: 0 for a resource, 1 for an override. + for (const auto& Entry : Reflection.Resources) + { + WriteNumber(WGSL, 0); + const auto& Resource = Entry.second; + WriteNumber(WGSL, Entry.first.Group); + WriteNumber(WGSL, Entry.first.Binding); + WriteString(WGSL, Resource.Name.c_str()); + WriteNumber(WGSL, Resource.UniformBuffer ? 1 : 0, '\n'); + if (Resource.UniformBuffer) + { + const auto& Buffer = *Resource.UniformBuffer; + WriteNumber(WGSL, Buffer.Size); + WriteNumber(WGSL, Buffer.NumVariables, '\n'); + for (Uint32 i = 0; i < Buffer.NumVariables; ++i) + WriteVariable(WGSL, Buffer.GetVariable(i)); + } + } + for (const auto& Entry : Reflection.SpecializationConstants) + { + WriteNumber(WGSL, 1); + WriteNumber(WGSL, Entry.first); + WriteString(WGSL, Entry.second.c_str()); + WGSL += '\n'; + } + WGSL += "*/\n"; +} + +bool ParseWGSLShaderReflection(const std::string& WGSL, WGSLShaderReflection& Reflection) +{ + const size_t Start = WGSL.find(ReflectionMarker); + if (Start == std::string::npos) + { + Reflection = {}; + return true; + } + const size_t End = WGSL.find("*/", Start); + if (End == std::string::npos || WGSL.find(ReflectionMarker, End) != std::string::npos) + return false; + const size_t DataStart = Start + sizeof(ReflectionMarker) - 1; + ReflectionReader Reader{WGSL.data() + DataStart, WGSL.data() + End}; + Uint32 Version = 0; + if (!Reader.ReadNumber(Version) || Version != 1) + return false; + + WGSLShaderReflection Parsed; + while (!Reader.IsAtEnd()) + { + Uint32 RecordType = 0; + if (!Reader.ReadNumber(RecordType) || RecordType > 1) + return false; + if (RecordType == 1) + { + Uint32 Id = 0; + std::string Name; + if (!Reader.ReadNumber(Id) || Id > 65535 || !Reader.ReadString(Name) || Name.empty() || + !Parsed.SpecializationConstants.emplace(Id, std::move(Name)).second) + return false; + continue; + } + + WGSLShaderReflection::BindingPoint Binding; + WGSLShaderReflection::Resource Resource; + Uint32 HasBuffer = 0; + if (!Reader.ReadNumber(Binding.Group) || !Reader.ReadNumber(Binding.Binding) || + !Reader.ReadString(Resource.Name) || !Reader.ReadNumber(HasBuffer) || HasBuffer > 1) + return false; + if (HasBuffer) + { + Resource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + Uint32 NumVariables = 0; + if (!Reader.ReadNumber(Resource.UniformBuffer->Size) || !Reader.ReadNumber(NumVariables)) + return false; + for (Uint32 i = 0; i < NumVariables; ++i) + { + ShaderCodeVariableDescX Var; + if (!ReadVariable(Reader, Var)) + return false; + Resource.UniformBuffer->AddVariable(std::move(Var)); + } + } + if (!Parsed.Resources.emplace(Binding, std::move(Resource)).second) + return false; + } + Reflection = std::move(Parsed); + return true; +} + +} // namespace Diligent diff --git a/Graphics/ShaderTools/src/WGSLShaderResources.cpp b/Graphics/ShaderTools/src/WGSLShaderResources.cpp index 2ad22f43bb..f49846cd14 100644 --- a/Graphics/ShaderTools/src/WGSLShaderResources.cpp +++ b/Graphics/ShaderTools/src/WGSLShaderResources.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ #include "Align.hpp" #include "StringPool.hpp" #include "WGSLUtils.hpp" +#include "WGSLShaderReflection.hpp" #include "DataBlobImpl.hpp" #include "ShaderToolsCommon.hpp" @@ -82,10 +83,10 @@ SHADER_TYPE TintPipelineStageToShaderType(tint::inspector::PipelineStage Stage) } } -WGSLShaderResourceAttribs::ResourceType TintResourceTypeToWGSLShaderAttribsResourceType(tint::inspector::ResourceBinding::ResourceType TintResType) +WGSLShaderResourceAttribs::ResourceType TintBindingToWGSLShaderAttribsResourceType(const tint::inspector::ResourceBinding& TintBinding) { using TintResourceType = tint::inspector::ResourceBinding::ResourceType; - switch (TintResType) + switch (TintBinding.resource_type) { case TintResourceType::kUniformBuffer: return WGSLShaderResourceAttribs::ResourceType::UniformBuffer; @@ -97,10 +98,9 @@ WGSLShaderResourceAttribs::ResourceType TintResourceTypeToWGSLShaderAttribsResou return WGSLShaderResourceAttribs::ResourceType::ROStorageBuffer; case TintResourceType::kSampler: - return WGSLShaderResourceAttribs::ResourceType::Sampler; - - case TintResourceType::kComparisonSampler: - return WGSLShaderResourceAttribs::ResourceType::ComparisonSampler; + return TintBinding.sampler_type == tint::inspector::ResourceBinding::SamplerType::kComparison ? + WGSLShaderResourceAttribs::ResourceType::ComparisonSampler : + WGSLShaderResourceAttribs::ResourceType::Sampler; case TintResourceType::kSampledTexture: return WGSLShaderResourceAttribs::ResourceType::Texture; @@ -151,6 +151,10 @@ WGSLShaderResourceAttribs::TextureSampleType TintSampleKindToWGSLShaderAttribsSa switch (TintBinding.sampled_kind) { case TintSampledKind::kFloat: + case TintSampledKind::kFilterable: + case TintSampledKind::kUnfilterable: + case TintSampledKind::kUnknownFilterable: + // Filterability is configured through WebGPUResourceAttribs. return WGSLShaderResourceAttribs::TextureSampleType::Float; case TintSampledKind::kSInt: @@ -159,9 +163,6 @@ WGSLShaderResourceAttribs::TextureSampleType TintSampleKindToWGSLShaderAttribsSa case TintSampledKind::kUInt: return WGSLShaderResourceAttribs::TextureSampleType::UInt; - case TintSampledKind::kUnknown: - return WGSLShaderResourceAttribs::TextureSampleType::Unknown; - default: UNEXPECTED("Unexpected sample kind"); return WGSLShaderResourceAttribs::TextureSampleType::Unknown; @@ -221,7 +222,6 @@ RESOURCE_DIMENSION TintBindingToResourceDimension(const tint::inspector::Resourc return RESOURCE_DIM_BUFFER; case TintResourceType::kSampler: - case TintResourceType::kComparisonSampler: return RESOURCE_DIM_UNDEFINED; case TintResourceType::kSampledTexture: @@ -347,7 +347,7 @@ WGSLShaderResourceAttribs::WGSLShaderResourceAttribs(const char* // clang-format off Name {_Name}, ArraySize {static_cast(_ArraySize)}, - Type {TintResourceTypeToWGSLShaderAttribsResourceType(TintBinding.resource_type)}, + Type {TintBindingToWGSLShaderAttribsResourceType(TintBinding)}, ResourceDim {TintBindingToResourceDimension(TintBinding)}, Format {TintTexelFormatToTextureFormat(TintBinding)}, BindGroup {static_cast(TintBinding.bind_group)}, @@ -495,7 +495,7 @@ namespace bool ResourceBindingsCompatibile(const tint::inspector::ResourceBinding& Binding0, const tint::inspector::ResourceBinding& Binding1) { - if (Binding0.resource_type != Binding1.resource_type) + if (TintBindingToWGSLShaderAttribsResourceType(Binding0) != TintBindingToWGSLShaderAttribsResourceType(Binding1)) return false; if (TintBindingToResourceDimension(Binding0) != TintBindingToResourceDimension(Binding1)) @@ -739,19 +739,31 @@ ShaderCodeBufferDescX LoadUBReflection(const tint::Program& Program, const tint: VERIFY(Variable, "Unexpected error"); - const tint::core::type::Struct* WGSLType = Program.TypeOf(Variable->type)->As(); - const uint32_t Size = WGSLType->Size(); + const tint::core::type::Type* WGSLType = Program.TypeOf(Variable->type); ShaderCodeBufferDescX UBDesc; - UBDesc.Size = Size; - for (const tint::core::type::StructMember* Member : WGSLType->Members()) + UBDesc.Size = WGSLType->Size(); + if (const auto* WGSLStruct = WGSLType->As()) { + for (const tint::core::type::StructMember* Member : WGSLStruct->Members()) + { + ShaderCodeVariableDesc VarDesc; + VarDesc.Name = Member->Name().NameView().data(); + VarDesc.Offset = Member->Offset(); + + size_t VariableIdx = UBDesc.AddVariable(VarDesc); + LoadShaderCodeVariableDesc(Program, Member->Type(), Language, UBDesc.GetVariable(VariableIdx)); + } + } + else + { + // WGSL also permits non-struct uniforms. Expose the whole value as one + // variable at offset zero, retaining the resource name restored from metadata. ShaderCodeVariableDesc VarDesc; - VarDesc.Name = Member->Name().NameView().data(); - VarDesc.Offset = Member->Offset(); + VarDesc.Name = UB.variable_name.c_str(); size_t VariableIdx = UBDesc.AddVariable(VarDesc); - LoadShaderCodeVariableDesc(Program, Member->Type(), Language, UBDesc.GetVariable(VariableIdx)); + LoadShaderCodeVariableDesc(Program, WGSLType, Language, UBDesc.GetVariable(VariableIdx)); } return UBDesc; @@ -820,33 +832,26 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, } m_ShaderType = TintPipelineStageToShaderType(EntryPoints[EntryPointIdx].stage); + WGSLShaderReflection SourceReflection; + if (!ParseWGSLShaderReflection(WGSL, SourceReflection)) + { + LOG_ERROR_AND_THROW("Invalid WGSL reflection metadata for shader '", ShaderName, "'"); + } + std::vector ResourceBindings = Inspector.GetResourceBindings(EntryPoint); - if (SourceLanguage != SHADER_SOURCE_LANGUAGE_WGSL) + for (auto& Binding : ResourceBindings) { - // HLSL: - // struct BufferData0 - // { - // float4 data; - // }; - // StructuredBuffer g_Buff0; - // StructuredBuffer g_Buff1; - // StructuredBuffer g_AtomicBuff0; // Used in atomic operations - // StructuredBuffer g_AtomicBuff1; // Used in atomic operations - // WGSL: - // struct g_Buff0 { - // x_data : RTArr, - // } - // @group(0) @binding(0) var g_Buff0_1 : g_Buff0; - // @group(0) @binding(1) var g_Buff1 : g_Buff0; - // @group(0) @binding(2) var g_AtomicBuff0_1 : g_AtomicBuff0_atomic; - // @group(0) @binding(3) var g_AtomicBuff1 : g_AtomicBuff0_atomic; - for (tint::inspector::ResourceBinding& Binding : ResourceBindings) + if (const auto* Resource = SourceReflection.Find(Binding.bind_group, Binding.binding)) { + Binding.variable_name = Resource->Name; + } + else if (SourceLanguage != SHADER_SOURCE_LANGUAGE_WGSL) + { + // Older archives have no original names. Keep the legacy type-name + // fallback for them, but do not apply it to resources with metadata. std::string AltName = GetWGSLResourceAlternativeName(Program, Binding); if (!AltName.empty()) - { Binding.variable_name = std::move(AltName); - } } } @@ -875,7 +880,6 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, break; case TintResourceType::kSampler: - case TintResourceType::kComparisonSampler: ++ResCounters.NumSamplers; break; @@ -919,12 +923,19 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, ResourceNamesPoolSize += strlen(ShaderName) + 1; ResourceNamesPoolSize += strlen(EntryPoint) + 1; + // Keep the source name for application lookup, but use Tint's current ID and + // is_id_specified flag when addressing the override in the generated WGSL. + const auto GetOverrideName = [&SourceReflection](const tint::inspector::Override& Override) -> const std::string& { + const auto It = SourceReflection.SpecializationConstants.find(Override.id.value); + return It != SourceReflection.SpecializationConstants.end() ? It->second : Override.name; + }; + // Count override constants (specialization constants) const auto& Overrides = EntryPoints[EntryPointIdx].overrides; Uint32 NumSpecConstants = static_cast(Overrides.size()); for (const tint::inspector::Override& Override : Overrides) { - ResourceNamesPoolSize += Override.name.length() + 1; + ResourceNamesPoolSize += GetOverrideName(Override).length() + 1; } // Resource names pool is only needed to facilitate string allocation. @@ -948,7 +959,20 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, new (&GetUB(CurrRes.NumUBs++)) WGSLShaderResourceAttribs{Name, Binding, ArraySize}; if (LoadUniformBufferReflection) - UBReflections.emplace_back(LoadUBReflection(Program, Binding, SourceLanguage)); + { + auto* SourceResource = SourceReflection.Find(Binding.bind_group, Binding.binding); + if (SourceResource != nullptr && SourceResource->UniformBuffer) + { + // Report source names, matrix orientation, arrays, and member offsets, + // not the padded structs and renamed members produced by Tint. + UBReflections.emplace_back(std::move(*SourceResource->UniformBuffer)); + } + else + { + // Native WGSL and older archives have no source buffer metadata. + UBReflections.emplace_back(LoadUBReflection(Program, Binding, SourceLanguage)); + } + } } break; @@ -960,7 +984,6 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, break; case TintResourceType::kSampler: - case TintResourceType::kComparisonSampler: { new (&GetSampler(CurrRes.NumSamplers++)) WGSLShaderResourceAttribs{Name, Binding, ArraySize}; } @@ -1010,9 +1033,9 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, Uint32 SpecConstIdx = 0; for (const tint::inspector::Override& Override : Overrides) { - const char* SCName = ResourceNamesPool.CopyString(Override.name); + const char* SCName = ResourceNamesPool.CopyString(GetOverrideName(Override)); SHADER_CODE_BASIC_TYPE SCType = TintOverrideTypeToShaderCodeBasicType(Override.type); - new (&GetSpecConstant(SpecConstIdx++)) WGSLSpecializationConstantAttribs{SCName, Override.id.value, SCType}; + new (&GetSpecConstant(SpecConstIdx++)) WGSLSpecializationConstantAttribs{SCName, Override.id.value, SCType, Override.is_id_specified}; } VERIFY_EXPR(SpecConstIdx == GetNumSpecConstants()); } diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index f528f126f7..d8df689b79 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,20 +24,44 @@ * of the possibility of such damages. */ +#include +#include +#include + #include "WGSLUtils.hpp" +#include "WGSLShaderReflection.hpp" #include "DebugUtilities.hpp" #include "ParsingTools.hpp" #include "ShaderToolsCommon.hpp" +#if defined(USE_SPIRV_TOOLS) && TINT_BUILD_SPV_READER +# define SPIRV_TO_WGSL_SUPPORTED 1 +# include "SPIRVTools.hpp" +# include "SPIRVShaderResources.hpp" +# include "EngineMemory.h" +# include "spirv/unified1/spirv.hpp" +# include "spirv-tools/libspirv.h" +#else +# define SPIRV_TO_WGSL_SUPPORTED 0 +#endif #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4324) // warning C4324: structure was padded due to alignment specifier #endif -#define TINT_BUILD_SPV_READER 1 -#define TINT_BUILD_WGSL_WRITER 1 #include +#include "src/tint/lang/core/ir/module.h" +#include "src/tint/lang/core/ir/builder.h" +#include "src/tint/lang/core/ir/core_builtin_call.h" +#include "src/tint/lang/core/ir/let.h" +#include "src/tint/lang/core/ir/load.h" +#include "src/tint/lang/core/ir/var.h" +#include "src/tint/lang/core/ir/transform/direct_variable_access.h" +#include "src/tint/lang/core/type/storage_texture.h" +#include "src/tint/lang/core/ir/transform/binding_remapper.h" +#include "src/tint/lang/core/ir/transform/single_entry_point.h" #include "src/tint/lang/wgsl/ast/module.h" +#include "src/tint/lang/wgsl/ast/function.h" #include "src/tint/lang/wgsl/ast/identifier_expression.h" #include "src/tint/lang/wgsl/ast/identifier.h" #include "src/tint/lang/wgsl/sem/variable.h" @@ -77,7 +101,8 @@ WGSLEmulatedResourceArrayElement GetWGSLEmulatedArrayElement(const std::string& if (SuffixPos != std::string::npos && SuffixPos + Suffix.length() < Name.length()) { int Index = 0; - if (Parsing::ParseInteger(Name.begin() + SuffixPos + Suffix.length(), Name.end(), Index) == Name.end()) + if (Parsing::IsDigit(Name[SuffixPos + Suffix.length()]) && + Parsing::ParseInteger(Name.begin() + SuffixPos + Suffix.length(), Name.end(), Index) == Name.end()) { // g_Tex2DArr_15 // ^ @@ -89,25 +114,252 @@ WGSLEmulatedResourceArrayElement GetWGSLEmulatedArrayElement(const std::string& return {Name, -1}; } -std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) +#if SPIRV_TO_WGSL_SUPPORTED +// The legacy Tint SPIR-V reader inferred image access from shader operations. +// The IR reader defaults storage images without access decorations to read_write, +// even when they are only loaded or stored. Using these types directly changes +// WGSL resource reflection and the bind-group layouts expected by Diligent. +// +// Restore the legacy mapping per resource: load-only images become sampled textures, +// store-only images become write-only storage textures, and images used both ways +// remain read_write. Queries alone do not infer access: keep the declared access, +// mapping explicitly read-only images to sampled textures. Unknown uses retain +// their original types. Only the temporary Tint IR is changed, not the source SPIR-V. +static bool RestoreTextureAccess(tint::core::ir::Module& IR) { - tint::spirv::reader::Options SPIRVReaderOptions{true, {tint::wgsl::AllowedFeatures::Everything()}}; - tint::Program Program = Read(SPIRV, SPIRVReaderOptions); + namespace ir = tint::core::ir; + namespace type = tint::core::type; + using BuiltinFn = tint::core::BuiltinFn; + + // Resolve texture parameters to global resources, including through nested calls. + // Helpers called with different resources need separate versions so a shared + // parameter does not force all arguments to have the same texture access mode. + // Run this only when needed: full handle specialization can duplicate helpers + // for distinct resources and also specializes other texture/sampler parameters. + bool HasTextureParameters = false; + for (const auto* Function : IR.functions) + { + for (const auto* Param : Function->Params()) + HasTextureParameters |= Param->Type()->UnwrapPtr()->Is(); + } + if (HasTextureParameters) + { + ir::transform::DirectVariableAccessConfig Config; + Config.transform_handle = ir::transform::HandleTransformLevel::kFull; + auto Result = ir::transform::DirectVariableAccess(IR, Config); + if (Result != tint::Success) + { + LOG_ERROR_MESSAGE("Failed to specialize texture parameters:\n", Result.Failure().reason); + return false; + } + } - if (!Program.IsValid()) + ir::Builder Builder{IR}; + auto& Types = IR.Types(); + for (auto* Inst : *IR.root_block) { - LOG_ERROR_MESSAGE("Tint SPIR-V reader failure:\nParser: " + Program.Diagnostics().Str() + "\n"); + auto* Var = Inst->As(); + if (Var == nullptr) + continue; + const auto* Texture = Var->Result()->Type()->UnwrapPtr()->As(); + if (Texture == nullptr) + continue; + + // Follow loads and aliases to find this resource's texture operations. + // Keep the alias values so their types can be updated together with the variable. + std::vector Values{Var->Result()}; + std::unordered_set Visited{Var->Result()}; + std::vector Loads; + + bool HasReads = false; + bool HasWrites = false; + bool HasUnknownUses = false; + for (size_t i = 0; i < Values.size(); ++i) + { + for (const auto& Use : Values[i]->UsagesUnsorted()) + { + if (Use->instruction->Is() || Use->instruction->Is()) + { + auto* Result = Use->instruction->Result(); + if (Visited.insert(Result).second) + Values.push_back(Result); + } + else if (auto* Call = Use->instruction->As()) + { + switch (Call->Func()) + { + case BuiltinFn::kTextureLoad: + HasReads = true; + Loads.push_back(Call); + break; + case BuiltinFn::kTextureStore: + HasWrites = true; + break; + case BuiltinFn::kTextureDimensions: + case BuiltinFn::kTextureNumLayers: + // Metadata queries do not read or write texels. + break; + default: + HasUnknownUses = true; + break; + } + } + else + { + // Retain the declared access for operations we cannot safely narrow. + HasUnknownUses = true; + } + } + } + // Preserve the declared type when narrowing is unsafe or both access modes are needed. + if (HasUnknownUses || (HasReads && HasWrites)) + continue; + + const type::Type* NewTextureType = nullptr; + if (HasWrites) + { + NewTextureType = Types.storage_texture(Texture->Dim(), Texture->TexelFormat(), tint::core::Access::kWrite); + } + else if (HasReads || Texture->Access() == tint::core::Access::kRead) + { + // The legacy reader used sampled bindings for load-only images and for + // query-only images explicitly declared read-only (SPIR-V NonWritable). + NewTextureType = Types.sampled_texture(Texture->Dim(), Texture->Type()); + for (auto* Load : Loads) + Load->AppendArg(Builder.Constant(tint::core::i32{0})); // Sampled texture loads require a mip level. + } + if (NewTextureType == nullptr) + continue; + + // Tint types are shared. Assign replacement types to this resource and its + // aliases instead of mutating a type that may also describe another resource. + for (auto* Value : Values) + { + if (const auto* Ptr = Value->Type()->As()) + Value->SetType(Types.ptr(Ptr->AddressSpace(), NewTextureType, Ptr->Access())); + else + Value->SetType(NewTextureType); + } + } + return true; +} + +static WGSLShaderReflection CaptureSPIRVReflection(const std::vector& SPIRV, + const tint::core::ir::Module& IR, + bool LoadUniformBufferReflection) +{ + WGSLShaderReflection Reflection; + // SPIRVShaderResources uses OpSource to interpret HLSL matrix layouts and buffer + // names. Keep the legacy WGSL fallback for binaries stripped of source metadata. + bool HasSource = false; + for (size_t i = 5; i < SPIRV.size();) + { + const Uint32 WordCount = SPIRV[i] >> 16; + if (WordCount == 0 || WordCount > SPIRV.size() - i) + return Reflection; + if ((SPIRV[i] & 0xffffu) == static_cast(spv::Op::OpSource)) + HasSource = true; + i += WordCount; + } + if (!HasSource) + return Reflection; + + SPIRVShaderResources::CreateInfo ResCI; + ResCI.Name = "SPIR-V to WGSL conversion"; + ResCI.LoadUniformBufferReflection = LoadUniformBufferReflection; + for (const auto* Function : IR.functions) + { + if (!Function->IsEntryPoint()) + continue; + // Conversion normally receives a single shader. Do not infer reflection + // from one entry point when supplied bytecode contains several stages. + if (ResCI.ShaderType != SHADER_TYPE_UNKNOWN) + return Reflection; + + if (Function->IsVertex()) + ResCI.ShaderType = SHADER_TYPE_VERTEX; + else if (Function->IsFragment()) + ResCI.ShaderType = SHADER_TYPE_PIXEL; + else + ResCI.ShaderType = SHADER_TYPE_COMPUTE; + } + if (ResCI.ShaderType == SHADER_TYPE_UNKNOWN) + return Reflection; + + const SPIRVShaderResources Resources{GetRawAllocator(), SPIRV, ResCI}; + for (Uint32 i = 0; i < Resources.GetTotalResources(); ++i) + { + const auto& Res = Resources.GetResource(i); + if (Res.Type == SPIRVShaderResourceAttribs::ResourceType::PushConstant || + Res.Name == nullptr || Res.Name[0] == '\0') + continue; + const WGSLShaderReflection::BindingPoint Binding{ + SPIRV[Res.DescriptorSetDecorationOffset], + SPIRV[Res.BindingDecorationOffset], + }; + auto& Dst = Reflection.Resources[Binding]; + Dst.Name = Res.Name; + if (LoadUniformBufferReflection && i < Resources.GetNumUBs()) + { + const ShaderCodeBufferDesc& Buffer = *Resources.GetUniformBufferDesc(i); + + Dst.UniformBuffer = std::make_unique(); + Dst.UniformBuffer->Size = Buffer.Size; + for (Uint32 v = 0; v < Buffer.NumVariables; ++v) + Dst.UniformBuffer->AddVariable(Buffer.pVariables[v]); + } + } + return Reflection; +} +#endif + +std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV, bool LoadUniformBufferReflection) +{ +#if SPIRV_TO_WGSL_SUPPORTED + // Tint does not support HLSL reflection extensions. Strip them from a copy + // so the original SPIR-V remains available for reflection. + // Match the validation environment used by Tint, including relaxed block layouts. + auto ConversionSPIRV = OptimizeSPIRV(SPIRV, SPV_ENV_VULKAN_1_1, SPIRV_OPTIMIZATION_FLAG_STRIP_REFLECTION); + if (ConversionSPIRV.empty()) + { + LOG_ERROR_MESSAGE("Failed to prepare SPIR-V for Tint conversion"); return {}; } - auto GenerationResult = tint::wgsl::writer::Generate(Program, {}); + auto IR = tint::spirv::reader::ReadIR(ConversionSPIRV); + if (IR != tint::Success) + { + LOG_ERROR_MESSAGE("Tint SPIR-V reader failure:\n", IR.Failure().reason, "\n"); + return {}; + } + + // Capture source reflection from the unmodified SPIR-V, before Tint raises + // buffer layouts or renames resources for WGSL. + WGSLShaderReflection Reflection = CaptureSPIRVReflection(SPIRV, IR.Get(), LoadUniformBufferReflection); + + if (!RestoreTextureAccess(IR.Get())) + return {}; + + tint::wgsl::writer::Options WGSLWriterOptions; + WGSLWriterOptions.allow_non_uniform_derivatives = true; + WGSLWriterOptions.allowed_features = tint::wgsl::AllowedFeatures::Everything(); + + auto GenerationResult = tint::wgsl::writer::WgslFromIR(IR.Get(), WGSLWriterOptions); if (GenerationResult != tint::Success) { - LOG_ERROR_MESSAGE("Tint WGSL writer failure:\nGeneate: " + GenerationResult.Failure().reason.Str() + "\n"); + LOG_ERROR_MESSAGE("Tint WGSL writer failure:\n", GenerationResult.Failure().reason, "\n"); return {}; } - return GenerationResult->wgsl; + std::string WGSL = std::move(GenerationResult->wgsl); + AppendWGSLShaderReflection(WGSL, Reflection); + return WGSL; +#else + (void)SPIRV; + (void)LoadUniformBufferReflection; + LOG_ERROR_MESSAGE("SPIR-V to WGSL conversion is not supported in this build; provide WGSL shaders instead."); + return {}; +#endif } static bool IsAtomic(const tint::core::type::Type* WGSLType) @@ -257,7 +509,7 @@ static WGSLResourceMapping::const_iterator FindResourceAsArrayElement(const WGSL if (BindigIt == ResMapping.end()) return ResMapping.end(); - if (ArrayElem.Index < static_cast(BindigIt->second.ArraySize)) + if (static_cast(ArrayElem.Index) < BindigIt->second.ArraySize) { ArrayIndex = static_cast(ArrayElem.Index); return BindigIt; @@ -269,7 +521,9 @@ static WGSLResourceMapping::const_iterator FindResourceAsArrayElement(const WGSL std::string RemapWGSLResourceBindings(const std::string& WGSL, const WGSLResourceMapping& ResMapping, - const char* EmulatedArrayIndexSuffix) + const char* EmulatedArrayIndexSuffix, + const char* EntryPoint, + std::string* pRemappedEntryPoint) { tint::Source::File srcFile("", WGSL); tint::Program Program = tint::wgsl::reader::Parse(&srcFile, {tint::wgsl::AllowedFeatures::Everything()}); @@ -280,22 +534,55 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, return {}; } - tint::ast::transform::BindingRemapper::BindingPoints BindingPoints; + WGSLShaderReflection Reflection; + if (!ParseWGSLShaderReflection(WGSL, Reflection)) + { + LOG_ERROR_MESSAGE("Invalid WGSL reflection metadata"); + return {}; + } + + std::unordered_map BindingPoints; + WGSLShaderReflection RemappedReflection; tint::inspector::Inspector Inspector{Program}; - for (tint::inspector::EntryPoint& EntryPoint : Inspector.GetEntryPoints()) + const auto EntryPoints = Inspector.GetEntryPoints(); + if (EntryPoint != nullptr && std::none_of(EntryPoints.begin(), EntryPoints.end(), [EntryPoint](const auto& EP) { return EP.name == EntryPoint; })) + { + LOG_ERROR_MESSAGE("WGSL entry point '", EntryPoint, "' was not found"); + return {}; + } + for (const tint::inspector::EntryPoint& EP : EntryPoints) { - for (tint::inspector::ResourceBinding& Binding : Inspector.GetResourceBindings(EntryPoint.name)) + if (EntryPoint != nullptr && EP.name != EntryPoint) + continue; + + // Capture names before Tint's IR and WGSL writers can rename them. IDs + // remain stable, including implicitly assigned IDs and workgroup-size overrides. + for (const auto& Override : EP.overrides) + { + const auto It = Reflection.SpecializationConstants.find(Override.id.value); + RemappedReflection.SpecializationConstants.emplace(Override.id.value, + It != Reflection.SpecializationConstants.end() ? It->second : Override.name); + } + + // Bindings may be shared by resources in separate entry points, but + // resources used by the same entry point need distinct destinations. + std::unordered_map Destinations; + for (const tint::inspector::ResourceBinding& Binding : Inspector.GetResourceBindings(EP.name)) { Uint32 ArrayIndex = 0; - auto DstBindigIt = ResMapping.find(Binding.variable_name); + // Original names take precedence over generated identifiers. Fall back + // to the legacy name rules for native WGSL and older shader archives. + const auto* SourceResource = Reflection.Find(Binding.bind_group, Binding.binding); + std::string ResourceName = SourceResource != nullptr ? SourceResource->Name : Binding.variable_name; + auto DstBindigIt = ResMapping.find(ResourceName); if (EmulatedArrayIndexSuffix != nullptr && DstBindigIt == ResMapping.end()) { - DstBindigIt = FindResourceAsArrayElement(ResMapping, EmulatedArrayIndexSuffix, Binding.variable_name, ArrayIndex); + DstBindigIt = FindResourceAsArrayElement(ResMapping, EmulatedArrayIndexSuffix, ResourceName, ArrayIndex); } - if (DstBindigIt == ResMapping.end()) + if (DstBindigIt == ResMapping.end() && SourceResource == nullptr) { const std::string AltName = GetWGSLResourceAlternativeName(Program, Binding); if (!AltName.empty()) @@ -305,44 +592,146 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, { DstBindigIt = FindResourceAsArrayElement(ResMapping, EmulatedArrayIndexSuffix, AltName, ArrayIndex); } + if (DstBindigIt != ResMapping.end()) + ResourceName = AltName; } } if (DstBindigIt != ResMapping.end()) { const WGSLResourceBindingInfo& DstBindig = DstBindigIt->second; - BindingPoints.emplace(tint::ast::transform::BindingPoint{Binding.bind_group, Binding.binding}, tint::ast::transform::BindingPoint{DstBindig.Group, DstBindig.Index + ArrayIndex}); + if (DstBindig.ArraySize == 0 || ArrayIndex >= DstBindig.ArraySize) + { + LOG_ERROR_MESSAGE("Invalid resource array size for variable '", ResourceName, "'"); + return {}; + } + if (ArrayIndex > (std::numeric_limits::max)() - DstBindig.Index) + { + LOG_ERROR_MESSAGE("Binding index overflows for variable '", ResourceName, "'"); + return {}; + } + + const tint::BindingPoint SrcPoint{Binding.bind_group, Binding.binding}; + const tint::BindingPoint DstPoint{DstBindig.Group, DstBindig.Index + ArrayIndex}; + const auto SrcInsertion = BindingPoints.emplace(SrcPoint, DstPoint); + if (!SrcInsertion.second && !(SrcInsertion.first->second == DstPoint)) + { + LOG_ERROR_MESSAGE("Conflicting remappings for binding ", SrcPoint.binding, " in group ", SrcPoint.group); + return {}; + } + const auto DstInsertion = Destinations.emplace(DstPoint, SrcPoint); + if (!DstInsertion.second && !(DstInsertion.first->second == SrcPoint)) + { + LOG_ERROR_MESSAGE("Resource binding collision for variable '", ResourceName, "' in entry point '", EP.name, "'"); + return {}; + } + + // Native WGSL has no input metadata. Save the name used by the + // application, including an emulated array element's full suffix. + auto NameInsertion = RemappedReflection.Resources.emplace( + WGSLShaderReflection::BindingPoint{DstPoint.group, DstPoint.binding}, WGSLShaderReflection::Resource{}); + if (!NameInsertion.second && NameInsertion.first->second.Name != ResourceName) + { + LOG_ERROR_MESSAGE("Conflicting resource names at remapped binding ", DstPoint.binding, " in group ", DstPoint.group); + return {}; + } + NameInsertion.first->second.Name = std::move(ResourceName); } else { - LOG_ERROR_MESSAGE("Binding for variable '", Binding.variable_name, "' is not found in the remap indices"); + LOG_ERROR_MESSAGE("Binding for variable '", ResourceName, "' is not found in the remap indices"); + return {}; } } } - tint::ast::transform::Manager Manager; - tint::ast::transform::DataMap Inputs; - tint::ast::transform::DataMap Outputs; + auto IR = tint::wgsl::reader::ProgramToLoweredIR(Program); + if (IR != tint::Success) + { + LOG_ERROR_MESSAGE("Tint WGSL to IR conversion failure:\n", IR.Failure().reason, "\n"); + return {}; + } + + if (EntryPoint != nullptr) + { + // Reflection and the pipeline signature describe only the selected entry + // point. Remove the other resources before changing their shared bindings. + auto SelectionResult = tint::core::ir::transform::SingleEntryPoint(IR.Get(), EntryPoint); + if (SelectionResult != tint::Success) + { + LOG_ERROR_MESSAGE("Tint entry point selection failure:\n", SelectionResult.Failure().reason, "\n"); + return {}; + } + } + + auto RemapResult = tint::core::ir::transform::BindingRemapper(IR.Get(), BindingPoints); + if (RemapResult != tint::Success) + { + LOG_ERROR_MESSAGE("Tint binding remapper failure:\n", RemapResult.Failure().reason, "\n"); + return {}; + } - Inputs.Add(BindingPoints, tint::ast::transform::BindingRemapper::AccessControls{}, false); - Manager.Add(); - tint::ast::transform::Output TransformResult = Manager.Run(Program, Inputs, Outputs); + tint::wgsl::writer::Options WGSLWriterOptions; + WGSLWriterOptions.allow_non_uniform_derivatives = true; + WGSLWriterOptions.allowed_features = tint::wgsl::AllowedFeatures::Everything(); + // WgslFromIR performs these same two steps. Keep the generated program so + // we can read the final name: Tint may rename an entry point both in IR and + // while creating the WGSL program, without updating the IR name in the latter case. + auto RemappedProgram = tint::wgsl::writer::ProgramFromIR(IR.Get(), WGSLWriterOptions); + if (RemappedProgram != tint::Success) + { + LOG_ERROR_MESSAGE("Tint WGSL writer failure:\nProgram: ", RemappedProgram.Failure().reason, "\n"); + return {}; + } - auto GenerationResult = tint::wgsl::writer::Generate(TransformResult.program, {}); + std::string RemappedEntryPoint; + if (EntryPoint != nullptr) + { + for (const auto* Function : RemappedProgram->AST().Functions()) + { + if (!Function->IsEntryPoint()) + continue; + if (!RemappedEntryPoint.empty()) + { + LOG_ERROR_MESSAGE("Remapped WGSL contains more than one entry point"); + return {}; + } + RemappedEntryPoint = Function->name->symbol.Name(); + } + if (RemappedEntryPoint.empty()) + { + LOG_ERROR_MESSAGE("Remapped WGSL contains no entry point"); + return {}; + } + } + auto GenerationResult = tint::wgsl::writer::Generate(RemappedProgram.Get(), WGSLWriterOptions); if (GenerationResult != tint::Success) { - LOG_ERROR_MESSAGE("Tint WGSL writer failure:\nGeneate: ", GenerationResult.Failure().reason.Str(), "\n"); + LOG_ERROR_MESSAGE("Tint WGSL writer failure:\nGenerate: ", GenerationResult.Failure().reason, "\n"); return {}; } std::string PatchedWGSL = std::move(GenerationResult->wgsl); + // Tint does not preserve comments. Carry reflection across the rewrite using + // the same binding-point mapping as the shader, including emulated arrays. + for (auto& Entry : Reflection.Resources) + { + auto It = BindingPoints.find(tint::BindingPoint{Entry.first.Group, Entry.first.Binding}); + if (It != BindingPoints.end()) + RemappedReflection.Resources[{It->second.group, It->second.binding}] = std::move(Entry.second); + } + AppendWGSLShaderReflection(PatchedWGSL, RemappedReflection); + // If original WGSL contains shader source language definition, append it to the patched WGSL SHADER_SOURCE_LANGUAGE SrcLang = ParseShaderSourceLanguageDefinition(WGSL); if (SrcLang != SHADER_SOURCE_LANGUAGE_DEFAULT) AppendShaderSourceLanguageDefinition(PatchedWGSL, SrcLang); + if (pRemappedEntryPoint != nullptr) + *pRemappedEntryPoint = std::move(RemappedEntryPoint); + return PatchedWGSL; } diff --git a/Platforms/Apple/interface/MacOSNativeWindow.h b/Platforms/Apple/interface/MacOSNativeWindow.h index 6dde6fb574..fbbddb59af 100644 --- a/Platforms/Apple/interface/MacOSNativeWindow.h +++ b/Platforms/Apple/interface/MacOSNativeWindow.h @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * Copyright 2015-2019 Egor Yusov * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ #pragma once #include "../../../Primitives/interface/CommonDefinitions.h" +#include "../../../Primitives/interface/BasicTypes.h" DILIGENT_BEGIN_NAMESPACE(Diligent) @@ -44,6 +45,9 @@ struct MacOSNativeWindow {} void* GetLayer() const; + + /// Returns the view size in backing-store pixels, or zero if no view is set. + void GetBackingSize(Uint32& Width, Uint32& Height) const; #endif }; diff --git a/Platforms/Apple/src/MacOSNativeWindow.mm b/Platforms/Apple/src/MacOSNativeWindow.mm index 37d0edae1e..688ccbced3 100644 --- a/Platforms/Apple/src/MacOSNativeWindow.mm +++ b/Platforms/Apple/src/MacOSNativeWindow.mm @@ -1,5 +1,5 @@ /* - * Copyright 2024 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,4 +34,20 @@ return pNSView ? (__bridge void*)((__bridge NSView*)pNSView).layer : nullptr; } +void MacOSNativeWindow::GetBackingSize(Uint32& Width, Uint32& Height) const +{ + if (pNSView) + { + NSView* const pView = (__bridge NSView*)pNSView; + const NSRect ViewRectPixels = [pView convertRectToBacking:[pView bounds]]; + Width = static_cast(ViewRectPixels.size.width); + Height = static_cast(ViewRectPixels.size.height); + } + else + { + Width = 0; + Height = 0; + } +} + } // namespace Diligent diff --git a/Tests/DiligentCoreAPITest/CMakeLists.txt b/Tests/DiligentCoreAPITest/CMakeLists.txt index b7036a86dc..afb78105b3 100644 --- a/Tests/DiligentCoreAPITest/CMakeLists.txt +++ b/Tests/DiligentCoreAPITest/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.17) +cmake_minimum_required (VERSION 3.17...3.22) if (PLATFORM_WEB) set(CMAKE_EXECUTABLE_SUFFIX ".html") diff --git a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp index f4274855e5..eff06eb172 100644 --- a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp @@ -2909,4 +2909,227 @@ TEST(ArchiveTest, MergeArchives) } } + +TEST(ArchiveTest, WGSLEntryPointRemapping) +{ + GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); + IRenderDevice* pDevice = pEnv->GetDevice(); + if (!pDevice->GetDeviceInfo().IsWebGPUDevice()) + GTEST_SKIP() << "WGSL entry point remapping requires WebGPU"; + + IArchiverFactory* pArchiverFactory = pEnv->GetArchiverFactory(); + if (pArchiverFactory == nullptr) + GTEST_SKIP() << "Archiver library is not loaded"; + + GPUTestingEnvironment::ScopedReset EnvironmentAutoReset; + + SerializationDeviceCreateInfo SerDeviceCI; + RefCntAutoPtr pSerializationDevice; + pArchiverFactory->CreateSerializationDevice(SerDeviceCI, &pSerializationDevice); + ASSERT_NE(pSerializationDevice, nullptr); + + // An archive must store Tint's generated entry point along with the patched + // WGSL, including when reflection is stripped and unpacking skips remapping. + const SHADER_TYPE ShaderTypes[] = {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL, SHADER_TYPE_COMPUTE}; + const char* ShaderNames[] = {"WGSLEntryPointRemapping - VS", "WGSLEntryPointRemapping - PS", "WGSLEntryPointRemapping - CS"}; + const char* Sources[] = { + "@vertex fn f32() -> @builtin(position) vec4f { return vec4f(0.0, 0.0, 0.0, 1.0); }", + "@fragment fn f32() -> @location(0) vec4f { return vec4f(1.0); }", + "@compute @workgroup_size(1) fn f32() {}", + }; + RefCntAutoPtr Shaders[3]; + for (Uint32 i = 0; i < _countof(Shaders); ++i) + { + ShaderCreateInfo ShaderCI; + ShaderCI.Desc = {ShaderNames[i], ShaderTypes[i]}; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; + ShaderCI.Source = Sources[i]; + ShaderCI.EntryPoint = "f32"; + pSerializationDevice->CreateShader(ShaderCI, ShaderArchiveInfo{ARCHIVE_DEVICE_DATA_FLAG_WEBGPU}, &Shaders[i]); + ASSERT_NE(Shaders[i], nullptr); + } + + for (PSO_ARCHIVE_FLAGS Flags : {PSO_ARCHIVE_FLAG_NONE, PSO_ARCHIVE_FLAG_STRIP_REFLECTION}) + { + SCOPED_TRACE(static_cast(Flags)); + RefCntAutoPtr pArchiver; + pArchiverFactory->CreateArchiver(pSerializationDevice, &pArchiver); + ASSERT_NE(pArchiver, nullptr); + + PipelineStateArchiveInfo ArchiveInfo; + ArchiveInfo.DeviceFlags = ARCHIVE_DEVICE_DATA_FLAG_WEBGPU; + ArchiveInfo.PSOFlags = Flags; + + GraphicsPipelineStateCreateInfo GraphicsCI; + GraphicsCI.PSODesc.Name = "WGSLEntryPointRemapping - Graphics"; + GraphicsCI.GraphicsPipeline.NumRenderTargets = 1; + GraphicsCI.GraphicsPipeline.RTVFormats[0] = pEnv->GetSwapChain()->GetDesc().ColorBufferFormat; + GraphicsCI.GraphicsPipeline.PrimitiveTopology = PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + GraphicsCI.GraphicsPipeline.RasterizerDesc.CullMode = CULL_MODE_NONE; + GraphicsCI.GraphicsPipeline.DepthStencilDesc.DepthEnable = False; + GraphicsCI.pVS = Shaders[0]; + GraphicsCI.pPS = Shaders[1]; + + RefCntAutoPtr pGraphicsPSO; + pSerializationDevice->CreateGraphicsPipelineState(GraphicsCI, ArchiveInfo, &pGraphicsPSO); + ASSERT_NE(pGraphicsPSO, nullptr); + ASSERT_TRUE(pArchiver->AddPipelineState(pGraphicsPSO)); + + ComputePipelineStateCreateInfo ComputeCI; + ComputeCI.PSODesc.Name = "WGSLEntryPointRemapping - Compute"; + ComputeCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; + ComputeCI.pCS = Shaders[2]; + + RefCntAutoPtr pComputePSO; + pSerializationDevice->CreateComputePipelineState(ComputeCI, ArchiveInfo, &pComputePSO); + ASSERT_NE(pComputePSO, nullptr); + ASSERT_TRUE(pArchiver->AddPipelineState(pComputePSO)); + + RefCntAutoPtr pArchive; + pArchiver->SerializeToBlob(ContentVersion, &pArchive); + ASSERT_NE(pArchive, nullptr); + + RefCntAutoPtr pDearchiver; + DearchiverCreateInfo DearchiverCI; + pDevice->GetEngineFactory()->CreateDearchiver(DearchiverCI, &pDearchiver); + ASSERT_NE(pDearchiver, nullptr); + ASSERT_TRUE(pDearchiver->LoadArchive(pArchive, ContentVersion)); + + for (const PipelineStateDesc* pDesc : {&GraphicsCI.PSODesc, &ComputeCI.PSODesc}) + { + SCOPED_TRACE(pDesc->Name); + PipelineStateUnpackInfo UnpackInfo; + UnpackInfo.Name = pDesc->Name; + UnpackInfo.pDevice = pDevice; + UnpackInfo.PipelineType = pDesc->PipelineType; + + RefCntAutoPtr pUnpackedPSO; + pDearchiver->UnpackPipelineState(UnpackInfo, &pUnpackedPSO); + ASSERT_NE(pUnpackedPSO, nullptr); + EXPECT_EQ(pUnpackedPSO->GetStatus(), PIPELINE_STATE_STATUS_READY); + } + } +} + + +TEST(ArchiveTest, WGSLResourceAndSpecializationNames) +{ + GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); + IRenderDevice* pDevice = pEnv->GetDevice(); + if (!pDevice->GetDeviceInfo().IsWebGPUDevice()) + GTEST_SKIP() << "WGSL name preservation requires WebGPU"; + + IArchiverFactory* pArchiverFactory = pEnv->GetArchiverFactory(); + if (pArchiverFactory == nullptr) + GTEST_SKIP() << "Archiver library is not loaded"; + + GPUTestingEnvironment::ScopedReset EnvironmentAutoReset; + IDeviceContext* pContext = pEnv->GetDeviceContext(); + ISwapChain* pSwapChain = pEnv->GetSwapChain(); + RefCntAutoPtr pTestingSwapChain{pSwapChain, IID_TestingSwapChain}; + ASSERT_NE(pTestingSwapChain, nullptr); + + ComputeShaderReference(pSwapChain); + pContext->Flush(); + pContext->InvalidateState(); + + SerializationDeviceCreateInfo SerDeviceCI; + RefCntAutoPtr pSerializationDevice; + pArchiverFactory->CreateSerializationDevice(SerDeviceCI, &pSerializationDevice); + ASSERT_NE(pSerializationDevice, nullptr); + + // All four application-facing names shadow WGSL predeclared types and are + // renamed by Tint. Defaults intentionally produce a different image. + ShaderCreateInfo ShaderCI; + ShaderCI.Desc = {"WGSL archive names - CS", SHADER_TYPE_COMPUTE}; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; + ShaderCI.EntryPoint = "main"; + ShaderCI.Source = R"( +@group(0) @binding(4) var texture_2d: texture_storage_2d; +override vec4f: f32 = -1.0; +override vec3f: f32 = -1.0; +@id(65535) override texture_storage_2d_array: f32 = -1.0; +@compute @workgroup_size(16, 16) fn main(@builtin(global_invocation_id) id: vec3u) { + if (any(id.xy >= textureDimensions(texture_2d))) { return; } + let uv = vec2(id.xy % vec2u(256u)) / 256.0; + textureStore(texture_2d, id.xy, vec4(uv.x * vec4f, uv.y * vec3f, uv.x * texture_storage_2d_array, 1.0)); +} +)"; + RefCntAutoPtr pSerializedCS; + pSerializationDevice->CreateShader(ShaderCI, ShaderArchiveInfo{ARCHIVE_DEVICE_DATA_FLAG_WEBGPU}, &pSerializedCS); + ASSERT_NE(pSerializedCS, nullptr); + + const float One = 1.0f; + const float Zero = 0.0f; + const SpecializationConstant SpecConsts[] = { + {"vec4f", SHADER_TYPE_COMPUTE, sizeof(One), &One}, + {"vec3f", SHADER_TYPE_COMPUTE, sizeof(One), &One}, + {"texture_storage_2d_array", SHADER_TYPE_COMPUTE, sizeof(Zero), &Zero}, + }; + + for (Uint32 Pass = 0; Pass < 3; ++Pass) + { + SCOPED_TRACE(Pass); + ComputePipelineStateCreateInfo PsoCI; + PsoCI.PSODesc.Name = "WGSL archive names - PSO"; + PsoCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; + PsoCI.pCS = pSerializedCS; + PsoCI.NumSpecializationConstants = _countof(SpecConsts); + PsoCI.pSpecializationConstants = SpecConsts; + PipelineStateArchiveInfo ArchiveInfo; + ArchiveInfo.DeviceFlags = ARCHIVE_DEVICE_DATA_FLAG_WEBGPU; + + RefCntAutoPtr pSerializedPSO; + pSerializationDevice->CreateComputePipelineState(PsoCI, ArchiveInfo, &pSerializedPSO); + ASSERT_NE(pSerializedPSO, nullptr); + RefCntAutoPtr pArchiver; + pArchiverFactory->CreateArchiver(pSerializationDevice, &pArchiver); + ASSERT_NE(pArchiver, nullptr); + ASSERT_TRUE(pArchiver->AddPipelineState(pSerializedPSO)); + RefCntAutoPtr pArchive; + pArchiver->SerializeToBlob(ContentVersion, &pArchive); + ASSERT_NE(pArchive, nullptr); + + RefCntAutoPtr pDearchiver; + DearchiverCreateInfo DearchiverCI; + pDevice->GetEngineFactory()->CreateDearchiver(DearchiverCI, &pDearchiver); + ASSERT_NE(pDearchiver, nullptr); + ASSERT_TRUE(pDearchiver->LoadArchive(pArchive, ContentVersion)); + PipelineStateUnpackInfo UnpackInfo; + UnpackInfo.pDevice = pDevice; + UnpackInfo.Name = PsoCI.PSODesc.Name; + UnpackInfo.PipelineType = PIPELINE_TYPE_COMPUTE; + RefCntAutoPtr pPSO; + pDearchiver->UnpackPipelineState(UnpackInfo, &pPSO); + ASSERT_NE(pPSO, nullptr); + ASSERT_EQ(pPSO->GetStatus(), PIPELINE_STATE_STATUS_READY); + + IShaderResourceVariable* pOutput = pPSO->GetStaticVariableByName(SHADER_TYPE_COMPUTE, "texture_2d"); + ASSERT_NE(pOutput, nullptr); + pOutput->Set(pTestingSwapChain->GetCurrentBackBufferUAV()); + RefCntAutoPtr pSRB; + pPSO->CreateShaderResourceBinding(&pSRB, true); + ASSERT_NE(pSRB, nullptr); + pContext->SetPipelineState(pPSO); + pContext->CommitShaderResources(pSRB, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + DispatchComputeAttribs Dispatch; + Dispatch.ThreadGroupCountX = (pSwapChain->GetDesc().Width + 15) / 16; + Dispatch.ThreadGroupCountY = (pSwapChain->GetDesc().Height + 15) / 16; + pContext->DispatchCompute(Dispatch); + pSwapChain->Present(); + + // Re-archive the generated WGSL to verify that another rewrite retains + // the original names instead of replacing them with Tint's identifiers. + if (Pass < 2) + { + RefCntAutoPtr pSerialized{pSerializedPSO, IID_SerializedPipelineState}; + ASSERT_NE(pSerialized, nullptr); + ShaderCreateInfo PatchedCI = pSerialized->GetPatchedShaderCreateInfo(ARCHIVE_DEVICE_DATA_FLAG_WEBGPU, 0); + pSerializedCS.Release(); + pSerializationDevice->CreateShader(PatchedCI, ShaderArchiveInfo{ARCHIVE_DEVICE_DATA_FLAG_WEBGPU}, &pSerializedCS); + ASSERT_NE(pSerializedCS, nullptr); + } + } +} + } // namespace diff --git a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp index 618f4ab571..74bbc2c40b 100644 --- a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp @@ -245,4 +245,76 @@ TEST(ShaderCreationTest, FromBytecode) } } + +void TestWGSLEntryPointRemapping(PSO_CREATE_FLAGS Flags) +{ + GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); + IRenderDevice* pDevice = pEnv->GetDevice(); + if (!pDevice->GetDeviceInfo().IsWebGPUDevice()) + GTEST_SKIP() << "WGSL entry point remapping requires WebGPU"; + + GPUTestingEnvironment::ScopedReset EnvironmentAutoReset; + + // Tint renames f32 when generating WGSL. Pipeline creation must use the + // generated name for every shader stage, including shaders without resources. + const SHADER_TYPE ShaderTypes[] = {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL, SHADER_TYPE_COMPUTE}; + const char* Sources[] = { + "@vertex fn f32() -> @builtin(position) vec4f { return vec4f(0.0, 0.0, 0.0, 1.0); }", + "@fragment fn f32() -> @location(0) vec4f { return vec4f(1.0); }", + "@compute @workgroup_size(1) fn f32() {}", + }; + RefCntAutoPtr Shaders[3]; + for (Uint32 i = 0; i < _countof(Shaders); ++i) + { + ShaderCreateInfo ShaderCI; + ShaderCI.Desc = {"WGSLEntryPointRemapping", ShaderTypes[i]}; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; + ShaderCI.Source = Sources[i]; + ShaderCI.EntryPoint = "f32"; + pDevice->CreateShader(ShaderCI, &Shaders[i]); + ASSERT_NE(Shaders[i], nullptr); + } + + GraphicsPipelineStateCreateInfo GraphicsCI; + GraphicsCI.PSODesc.Name = "WGSLEntryPointRemapping - Graphics"; + GraphicsCI.GraphicsPipeline.NumRenderTargets = 1; + GraphicsCI.GraphicsPipeline.RTVFormats[0] = pEnv->GetSwapChain()->GetDesc().ColorBufferFormat; + GraphicsCI.GraphicsPipeline.PrimitiveTopology = PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + GraphicsCI.GraphicsPipeline.RasterizerDesc.CullMode = CULL_MODE_NONE; + GraphicsCI.GraphicsPipeline.DepthStencilDesc.DepthEnable = False; + GraphicsCI.Flags = Flags; + GraphicsCI.pVS = Shaders[0]; + GraphicsCI.pPS = Shaders[1]; + + RefCntAutoPtr pGraphicsPSO; + pDevice->CreateGraphicsPipelineState(GraphicsCI, &pGraphicsPSO); + ASSERT_NE(pGraphicsPSO, nullptr); + EXPECT_EQ(pGraphicsPSO->GetStatus(true), PIPELINE_STATE_STATUS_READY); + + ComputePipelineStateCreateInfo ComputeCI; + ComputeCI.PSODesc.Name = "WGSLEntryPointRemapping - Compute"; + ComputeCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; + ComputeCI.Flags = Flags; + ComputeCI.pCS = Shaders[2]; + + RefCntAutoPtr pComputePSO; + pDevice->CreateComputePipelineState(ComputeCI, &pComputePSO); + ASSERT_NE(pComputePSO, nullptr); + EXPECT_EQ(pComputePSO->GetStatus(true), PIPELINE_STATE_STATUS_READY); +} + +TEST(ShaderCreationTest, WGSLEntryPointRemapping) +{ + TestWGSLEntryPointRemapping(PSO_CREATE_FLAG_NONE); +} + +TEST(ShaderCreationTest, WGSLEntryPointRemapping_Async) +{ +#if PLATFORM_WEB + GTEST_SKIP() << "The synchronous test harness cannot wait for browser asynchronous pipelines"; +#else + TestWGSLEntryPointRemapping(PSO_CREATE_FLAG_ASYNCHRONOUS); +#endif +} + } // namespace diff --git a/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp b/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp index e17b4ad6bc..37465ecfc1 100644 --- a/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp +++ b/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp @@ -329,6 +329,8 @@ class SpecializationConstants : public ::testing::Test pSwapChain->Present(); } + void TestComputePath(PSO_CREATE_FLAGS Flags = PSO_CREATE_FLAG_NONE, bool ExplicitIds = false, bool MultipleEntryPoints = false); + static FastRandFloat sm_Rnd; }; @@ -341,7 +343,7 @@ FastRandFloat SpecializationConstants::sm_Rnd{0, 0.f, 1.f}; // InlineConstantsTest::ComputeResourceLayout. // --------------------------------------------------------------------------- -TEST_F(SpecializationConstants, ComputePath) +void SpecializationConstants::TestComputePath(PSO_CREATE_FLAGS Flags, bool ExplicitIds, bool MultipleEntryPoints) { GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); IRenderDevice* pDevice = pEnv->GetDevice(); @@ -349,6 +351,9 @@ TEST_F(SpecializationConstants, ComputePath) ISwapChain* pSwapChain = pEnv->GetSwapChain(); const RenderDeviceInfo& DeviceInfo = pDevice->GetDeviceInfo(); + if ((Flags != PSO_CREATE_FLAG_NONE || ExplicitIds || MultipleEntryPoints) && !DeviceInfo.IsWebGPUDevice()) + GTEST_SKIP() << "This variant tests WebGPU override and entry point handling"; + if (DeviceInfo.Features.SpecializationConstants != DEVICE_FEATURE_STATE_ENABLED) GTEST_SKIP() << "Specialization constants are not supported by this device"; if (!DeviceInfo.Features.ComputeShaders) @@ -369,6 +374,21 @@ TEST_F(SpecializationConstants, ComputePath) // --- Spec-const pass: same gradient, channel multipliers via specialization constants --- { + std::string WebGPUSource = ComputeCS_WGSL; + if (ExplicitIds) + { + // Mix explicit IDs (including the maximum ID) with a named override. + WebGPUSource.insert(WebGPUSource.find("override sc_MulR"), "@id(0) "); + WebGPUSource.insert(WebGPUSource.find("override sc_MulB"), "@id(65535) "); + } + if (MultipleEntryPoints) + { + WebGPUSource += R"( +@group(0) @binding(0) var OtherOutput: array; +@compute @workgroup_size(1) fn other() { OtherOutput[0] = 42; } +)"; + } + ShaderCreateInfo ShaderCI; ShaderCI.Desc = {"SpecConst Compute CS", SHADER_TYPE_COMPUTE, true}; ShaderCI.EntryPoint = "main"; @@ -376,7 +396,7 @@ TEST_F(SpecializationConstants, ComputePath) if (DeviceInfo.IsWebGPUDevice()) { ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; - ShaderCI.Source = ComputeCS_WGSL; + ShaderCI.Source = WebGPUSource.c_str(); } else { @@ -405,12 +425,14 @@ TEST_F(SpecializationConstants, ComputePath) PsoCI.PSODesc.Name = "SpecConst Compute Test"; PsoCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; PsoCI.pCS = pCS; + PsoCI.Flags = Flags; PsoCI.NumSpecializationConstants = _countof(SpecConsts); PsoCI.pSpecializationConstants = SpecConsts; RefCntAutoPtr pPSO; pDevice->CreateComputePipelineState(PsoCI, &pPSO); ASSERT_NE(pPSO, nullptr); + ASSERT_EQ(pPSO->GetStatus(true), PIPELINE_STATE_STATUS_READY); pPSO->GetStaticVariableByName(SHADER_TYPE_COMPUTE, "g_tex2DUAV")->Set(pTestingSwapChain->GetCurrentBackBufferUAV()); @@ -431,6 +453,37 @@ TEST_F(SpecializationConstants, ComputePath) } +TEST_F(SpecializationConstants, ComputePath) +{ + TestComputePath(); +} + +TEST_F(SpecializationConstants, ComputePath_ExplicitIds) +{ + TestComputePath(PSO_CREATE_FLAG_NONE, true); +} + +TEST_F(SpecializationConstants, ComputePath_NoRemap) +{ + TestComputePath(PSO_CREATE_FLAG_DONT_REMAP_SHADER_RESOURCES); +} + +TEST_F(SpecializationConstants, ComputePath_NoRemap_ExplicitIds) +{ + TestComputePath(PSO_CREATE_FLAG_DONT_REMAP_SHADER_RESOURCES, true); +} + +TEST_F(SpecializationConstants, ComputePath_Async_ExplicitIds) +{ + TestComputePath(PSO_CREATE_FLAG_ASYNCHRONOUS, true); +} + +TEST_F(SpecializationConstants, ComputePath_MultipleEntryPoints) +{ + TestComputePath(PSO_CREATE_FLAG_NONE, true, true); +} + + // --------------------------------------------------------------------------- // Graphics path: draw two triangles with per-vertex colors from spec constants. // Uses RenderDrawCommandReference for the reference snapshot, just like @@ -901,4 +954,145 @@ TEST_F(SpecializationConstants, RenderStateCacheTest_DistinctEntries_Async) TestDistinctEntries(/*CompileAsync = */ true); } + +void TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAGS Flags, bool UseFloat16 = false) +{ + GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); + IRenderDevice* pDevice = pEnv->GetDevice(); + const auto& Features = pDevice->GetDeviceInfo().Features; + if (!pDevice->GetDeviceInfo().IsWebGPUDevice() || !Features.SpecializationConstants) + GTEST_SKIP() << "This test requires WebGPU specialization constants"; + if (UseFloat16 && !Features.ShaderFloat16) + GTEST_SKIP() << "16-bit shader floats are not supported by this device"; + + GPUTestingEnvironment::ScopedReset EnvironmentAutoReset; + IDeviceContext* pContext = pEnv->GetDeviceContext(); + ISwapChain* pSwapChain = pEnv->GetSwapChain(); + RefCntAutoPtr pTestingSwapChain{pSwapChain, IID_TestingSwapChain}; + ASSERT_NE(pTestingSwapChain, nullptr); + ComputeShaderReference(pSwapChain); + pContext->Flush(); + pContext->InvalidateState(); + + // Detect incomplete dispatch coverage if the workgroup-size override is lost, + // even when a previous test left the expected gradient in the output texture. + ITextureView* pRTV = pSwapChain->GetCurrentBackBufferRTV(); + const float ClearColor[] = {1.0f, 0.0f, 1.0f, 1.0f}; + pContext->SetRenderTargets(1, &pRTV, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->ClearRenderTarget(pRTV, ClearColor, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->SetRenderTargets(0, nullptr, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + + // Check scalar conversion, a required override, a retained default, and an + // override used only by the workgroup size. The large unsigned value detects + // signed conversion; the negative value detects unsigned conversion. + std::string Source = R"( +@id(0) override sc_True: bool = false; +override sc_False: bool = true; +override sc_Int: i32 = 0; +@id(65535) override sc_Uint: u32 = 0; +override sc_Float: f32 = 0.0; +override sc_Required: u32; +override sc_Default: f32 = 0.5; +override sc_LocalX: u32 = 1; +@group(0) @binding(0) var Output: texture_storage_2d; +@compute @workgroup_size(sc_LocalX, 16) fn vec4u(@builtin(global_invocation_id) id: vec3u) { + if (any(id.xy >= textureDimensions(Output))) { return; } + var color = vec4(vec2(id.xy % vec2u(256u)) / 256.0, 0.0, 1.0); + if (!sc_True || sc_False || sc_Int != -1234567 || sc_Uint != 0xf1234567u || + sc_Float != 0.25 || sc_Required != 17u || sc_Default != 0.5 /* HALF_CHECK */) { + color = vec4(1.0, 0.0, 1.0, 1.0); + } + textureStore(Output, id.xy, color); +} +)"; + if (UseFloat16) + { + Source.insert(0, "enable f16;\noverride sc_Half: f16 = 0.0h;\n"); + const std::string Marker = "/* HALF_CHECK */"; + Source.replace(Source.find(Marker), Marker.length(), "|| sc_Half != 1.5h"); + } + + ShaderCreateInfo ShaderCI; + ShaderCI.Desc = {"WebGPU scalar specialization constants", SHADER_TYPE_COMPUTE}; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_WGSL; + ShaderCI.EntryPoint = "vec4u"; // Tint renames this entry point on the remapping path. + ShaderCI.Source = Source.c_str(); + RefCntAutoPtr pCS; + pDevice->CreateShader(ShaderCI, &pCS); + ASSERT_NE(pCS, nullptr); + + const Uint32 TrueValue = 0x10000; // Every non-zero bit pattern must convert to true. + const Uint32 FalseValue = 0; + const Int32 IntValue = -1234567; + const Uint32 UintValue = 0xf1234567u; + const float FloatValue = 0.25f; + const Uint32 RequiredValue = 17; + const Uint32 LocalX = 16; + const Uint16 HalfValue = 0x3e00; // IEEE 754 half-precision 1.5. + const SpecializationConstant SpecConsts[] = { + {"sc_True", SHADER_TYPE_COMPUTE, sizeof(TrueValue), &TrueValue}, + {"sc_False", SHADER_TYPE_COMPUTE, sizeof(FalseValue), &FalseValue}, + {"sc_Int", SHADER_TYPE_COMPUTE, sizeof(IntValue), &IntValue}, + {"sc_Uint", SHADER_TYPE_COMPUTE, sizeof(UintValue), &UintValue}, + {"sc_Float", SHADER_TYPE_COMPUTE, sizeof(FloatValue), &FloatValue}, + {"sc_Required", SHADER_TYPE_COMPUTE, sizeof(RequiredValue), &RequiredValue}, + {"sc_LocalX", SHADER_TYPE_COMPUTE, sizeof(LocalX), &LocalX}, + {"sc_Half", SHADER_TYPE_COMPUTE, sizeof(HalfValue), &HalfValue}, + }; + ComputePipelineStateCreateInfo PsoCI; + PsoCI.PSODesc.Name = "WebGPU scalar specialization constants"; + PsoCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; + PsoCI.pCS = pCS; + PsoCI.Flags = Flags; + PsoCI.NumSpecializationConstants = static_cast(_countof(SpecConsts)) - (UseFloat16 ? 0u : 1u); + PsoCI.pSpecializationConstants = SpecConsts; + RefCntAutoPtr pPSO; + pDevice->CreateComputePipelineState(PsoCI, &pPSO); + ASSERT_NE(pPSO, nullptr); + ASSERT_EQ(pPSO->GetStatus(true), PIPELINE_STATE_STATUS_READY); + + IShaderResourceVariable* pOutput = pPSO->GetStaticVariableByName(SHADER_TYPE_COMPUTE, "Output"); + ASSERT_NE(pOutput, nullptr); + pOutput->Set(pTestingSwapChain->GetCurrentBackBufferUAV()); + RefCntAutoPtr pSRB; + pPSO->CreateShaderResourceBinding(&pSRB, true); + ASSERT_NE(pSRB, nullptr); + pContext->SetPipelineState(pPSO); + pContext->CommitShaderResources(pSRB, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + DispatchComputeAttribs Dispatch; + Dispatch.ThreadGroupCountX = (pSwapChain->GetDesc().Width + 15) / 16; + Dispatch.ThreadGroupCountY = (pSwapChain->GetDesc().Height + 15) / 16; + pContext->DispatchCompute(Dispatch); + pSwapChain->Present(); +} + +TEST_F(SpecializationConstants, WebGPUScalarValues) +{ + TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAG_NONE); +} + +TEST_F(SpecializationConstants, WebGPUScalarValues_NoRemap) +{ + TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAG_DONT_REMAP_SHADER_RESOURCES); +} + +TEST_F(SpecializationConstants, WebGPUScalarValues_Async) +{ +#if PLATFORM_WEB + GTEST_SKIP() << "The synchronous test harness cannot wait for browser asynchronous pipelines"; +#else + TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAG_ASYNCHRONOUS); +#endif +} + +TEST_F(SpecializationConstants, WebGPUFloat16Value) +{ + TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAG_NONE, true); +} + +TEST_F(SpecializationConstants, WebGPUFloat16Value_NoRemap) +{ + TestWebGPUScalarSpecializationConstants(PSO_CREATE_FLAG_DONT_REMAP_SHADER_RESOURCES, true); +} + } // namespace diff --git a/Tests/DiligentCoreAPITest/src/TextureSwizzleTest.cpp b/Tests/DiligentCoreAPITest/src/TextureSwizzleTest.cpp index fb50155d9a..ef57d15441 100644 --- a/Tests/DiligentCoreAPITest/src/TextureSwizzleTest.cpp +++ b/Tests/DiligentCoreAPITest/src/TextureSwizzleTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -319,4 +319,55 @@ TEST_F(TextureSwizzleTest, Zero) float4{0, 0, 0, 0}); } +TEST_F(TextureSwizzleTest, MixedComponents) +{ + constexpr Uint32 RGBA = 0x00FF00FFu; + + // Exercise per-channel identity defaults inside non-identity mappings. + RunTest(RGBA, + TextureComponentMapping{ + TEXTURE_COMPONENT_SWIZZLE_IDENTITY, + TEXTURE_COMPONENT_SWIZZLE_B, + TEXTURE_COMPONENT_SWIZZLE_G, + TEXTURE_COMPONENT_SWIZZLE_ONE}, + float4{1, 1, 0, 1}); + + RunTest(RGBA, + TextureComponentMapping{ + TEXTURE_COMPONENT_SWIZZLE_ZERO, + TEXTURE_COMPONENT_SWIZZLE_IDENTITY, + TEXTURE_COMPONENT_SWIZZLE_IDENTITY, + TEXTURE_COMPONENT_SWIZZLE_R}, + float4{0, 0, 1, 1}); + + RunTest(RGBA, + TextureComponentMapping{ + TEXTURE_COMPONENT_SWIZZLE_B, + TEXTURE_COMPONENT_SWIZZLE_R, + TEXTURE_COMPONENT_SWIZZLE_ONE, + TEXTURE_COMPONENT_SWIZZLE_IDENTITY}, + float4{1, 1, 1, 0}); +} + +TEST_F(TextureSwizzleTest, Identity) +{ + auto* const pEnv = GPUTestingEnvironment::GetInstance(); + + auto pTex = pEnv->CreateTexture("Identity texture swizzle test", TEX_FORMAT_RGBA8_UNORM, BIND_SHADER_RESOURCE, 16, 16); + ASSERT_TRUE(pTex); + EXPECT_NE(pTex->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE), nullptr); + + // Explicit RGBA is also identity and must not require texture component swizzle support. + TextureViewDesc ViewDesc; + ViewDesc.ViewType = TEXTURE_VIEW_SHADER_RESOURCE; + ViewDesc.Swizzle = TextureComponentMapping{ + TEXTURE_COMPONENT_SWIZZLE_R, + TEXTURE_COMPONENT_SWIZZLE_G, + TEXTURE_COMPONENT_SWIZZLE_B, + TEXTURE_COMPONENT_SWIZZLE_A}; + RefCntAutoPtr pIdentityView; + pTex->CreateView(ViewDesc, &pIdentityView); + ASSERT_TRUE(pIdentityView); +} + } // namespace diff --git a/Tests/DiligentCoreAPITest/src/WebGPU/SwapChainTestWebGPU.cpp b/Tests/DiligentCoreAPITest/src/WebGPU/SwapChainTestWebGPU.cpp new file mode 100644 index 0000000000..927a09d3db --- /dev/null +++ b/Tests/DiligentCoreAPITest/src/WebGPU/SwapChainTestWebGPU.cpp @@ -0,0 +1,214 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include + +#include "GPUTestingEnvironment.hpp" +#include "EngineFactoryWebGPU.h" +#include "RenderDeviceWebGPU.h" +#include "SwapChainWebGPU.h" +#include "TextureWebGPU.h" +#include "GraphicsAccessories.hpp" + +#include "gtest/gtest.h" + +#if PLATFORM_WIN32 + +# include "WinHPreface.h" +# include +# include "WinHPostface.h" + +using namespace Diligent; +using namespace Diligent::Testing; + +namespace +{ + +// These tests use a real surface: the testing environment's offscreen swap chain +// does not exercise acquisition, presentation, or surface texture ownership. +class SwapChainWebGPUTest : public testing::TestWithParam +{ +protected: + void SetUp() override + { + auto* pEnv = GPUTestingEnvironment::GetInstance(); + if (pEnv->GetDevice()->GetDeviceInfo().Type != RENDER_DEVICE_TYPE_WEBGPU) + GTEST_SKIP(); + + m_Window = CreateWindowExW(0, L"STATIC", L"WebGPU swap chain test", WS_OVERLAPPEDWINDOW, + 0, 0, 128, 128, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); + ASSERT_NE(m_Window, nullptr); + } + + void TearDown() override + { + if (m_Window) + { + auto* pEnv = GPUTestingEnvironment::GetInstance(); + pEnv->GetDeviceContext()->WaitForIdle(); + pEnv->GetDeviceContext()->InvalidateState(); + m_pSwapChain.Release(); + DestroyWindow(m_Window); + pEnv->Reset(); + } + } + + void CreateSwapChain(bool IsPrimary = true) + { + auto* pEnv = GPUTestingEnvironment::GetInstance(); + RefCntAutoPtr pFactory{pEnv->GetDevice()->GetEngineFactory(), IID_EngineFactoryWebGPU}; + SwapChainDesc Desc; + Desc.Width = 64; + Desc.Height = 32; + Desc.ColorBufferFormat = GetParam(); + Desc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT; + Desc.Usage = SWAP_CHAIN_USAGE_RENDER_TARGET | SWAP_CHAIN_USAGE_SHADER_RESOURCE | SWAP_CHAIN_USAGE_COPY_SOURCE; + Desc.IsPrimary = IsPrimary; + pFactory->CreateSwapChainWebGPU(pEnv->GetDevice(), pEnv->GetDeviceContext(), Desc, Win32NativeWindow{m_Window}, &m_pSwapChain); + ASSERT_NE(m_pSwapChain, nullptr); + } + + void VerifyClear(ITexture* pBackBuffer) + { + auto* pEnv = GPUTestingEnvironment::GetInstance(); + auto* pContext = pEnv->GetDeviceContext(); + TextureDesc Desc = pBackBuffer->GetDesc(); + Desc.Name = "Surface readback"; + Desc.BindFlags = BIND_NONE; + Desc.Usage = USAGE_STAGING; + Desc.CPUAccessFlags = CPU_ACCESS_READ; + RefCntAutoPtr pStaging; + pEnv->GetDevice()->CreateTexture(Desc, nullptr, &pStaging); + ASSERT_NE(pStaging, nullptr); + + CopyTextureAttribs Copy; + Copy.pSrcTexture = pBackBuffer; + Copy.pDstTexture = pStaging; + Copy.SrcTextureTransitionMode = RESOURCE_STATE_TRANSITION_MODE_TRANSITION; + Copy.DstTextureTransitionMode = RESOURCE_STATE_TRANSITION_MODE_TRANSITION; + pContext->CopyTexture(Copy); + pContext->WaitForIdle(); + + MappedTextureSubresource Mapped; + pContext->MapTextureSubresource(pStaging, 0, 0, MAP_READ, MAP_FLAG_DO_NOT_WAIT, nullptr, Mapped); + ASSERT_NE(Mapped.pData, nullptr); + const bool SRGB = IsSRGBFormat(Desc.Format); + const bool BGRA = SRGBFormatToUnorm(Desc.Format) == TEX_FORMAT_BGRA8_UNORM; + const int Expected[] = {SRGB ? 137 : 64, SRGB ? 188 : 128, SRGB ? 225 : 191, 255}; + for (Uint32 Y = 0; Y < Desc.Height; ++Y) + { + const auto* pRow = static_cast(Mapped.pData) + Y * Mapped.Stride; + for (Uint32 X = 0; X < Desc.Width; ++X) + { + for (Uint32 C = 0; C < 4; ++C) + { + const Uint32 Channel = BGRA && C != 1 && C != 3 ? 2 - C : C; + EXPECT_NEAR(pRow[X * 4 + Channel], Expected[C], 1); + } + } + } + pContext->UnmapTextureSubresource(pStaging, 0, 0); + } + + HWND m_Window = nullptr; + RefCntAutoPtr m_pSwapChain; +}; + +TEST_P(SwapChainWebGPUTest, RenderAndPresent) +{ + ASSERT_NO_FATAL_FAILURE(CreateSwapChain()); + auto* pContext = GPUTestingEnvironment::GetInstance()->GetDeviceContext(); + RefCntAutoPtr pSwapChainWebGPU{m_pSwapChain, IID_SwapChainWebGPU}; + const float Color[] = {0.25f, 0.5f, 0.75f, 1.0f}; + for (Uint32 Frame = 0; Frame < 3; ++Frame) + { + ITextureView* pRTV = m_pSwapChain->GetCurrentBackBufferRTV(); + ASSERT_NE(pRTV, nullptr); + EXPECT_EQ(m_pSwapChain->GetCurrentBackBufferRTV(), pRTV); + EXPECT_EQ(pRTV->GetDesc().Format, m_pSwapChain->GetDesc().ColorBufferFormat); + EXPECT_EQ(pRTV->GetTexture()->GetDesc().Format, m_pSwapChain->GetDesc().ColorBufferFormat); + EXPECT_EQ(IsSRGBFormat(pRTV->GetDesc().Format), IsSRGBFormat(GetParam())); + EXPECT_NE(pRTV->GetTexture()->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE), nullptr); + + RefCntAutoPtr pTextureWebGPU{pRTV->GetTexture(), IID_TextureWebGPU}; + WGPUSurfaceTexture SurfaceTexture{}; + wgpuSurfaceGetCurrentTexture(pSwapChainWebGPU->GetWebGPUSurface(), &SurfaceTexture); + EXPECT_EQ(SurfaceTexture.texture, pTextureWebGPU->GetWebGPUTexture()); + if (SurfaceTexture.texture) + wgpuTextureRelease(SurfaceTexture.texture); + EXPECT_NE(wgpuTextureGetUsage(pTextureWebGPU->GetWebGPUTexture()) & WGPUTextureUsage_TextureBinding, 0u); + EXPECT_NE(wgpuTextureGetUsage(pTextureWebGPU->GetWebGPUTexture()) & WGPUTextureUsage_CopySrc, 0u); + + pContext->SetRenderTargets(1, &pRTV, m_pSwapChain->GetDepthBufferDSV(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->ClearRenderTarget(pRTV, Color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + ASSERT_NO_FATAL_FAILURE(VerifyClear(pRTV->GetTexture())); + const Uint64 FrameNumber = pContext->GetFrameNumber(); + m_pSwapChain->Present(Frame & 1); + EXPECT_EQ(pContext->GetFrameNumber(), FrameNumber + 1); + } +} + +TEST_P(SwapChainWebGPUTest, ResizeAndDestroyWithAcquiredTexture) +{ + ASSERT_NO_FATAL_FAILURE(CreateSwapChain(false)); + auto* pContext = GPUTestingEnvironment::GetInstance()->GetDeviceContext(); + const float Color[] = {0.25f, 0.5f, 0.75f, 1.0f}; + ITextureView* pRTV = m_pSwapChain->GetCurrentBackBufferRTV(); + ASSERT_NE(pRTV, nullptr); + pContext->SetRenderTargets(1, &pRTV, m_pSwapChain->GetDepthBufferDSV(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->ClearRenderTarget(pRTV, Color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + + // Resize must submit the pending clear before invalidating the acquired texture. + m_pSwapChain->Resize(96, 48); + pRTV = m_pSwapChain->GetCurrentBackBufferRTV(); + ASSERT_NE(pRTV, nullptr); + EXPECT_EQ(pRTV->GetTexture()->GetDesc().Width, 96u); + EXPECT_EQ(pRTV->GetTexture()->GetDesc().Height, 48u); + ASSERT_NE(m_pSwapChain->GetDepthBufferDSV(), nullptr); + EXPECT_EQ(m_pSwapChain->GetDepthBufferDSV()->GetTexture()->GetDesc().Width, 96u); + EXPECT_EQ(m_pSwapChain->GetDepthBufferDSV()->GetTexture()->GetDesc().Height, 48u); + pContext->SetRenderTargets(1, &pRTV, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->ClearRenderTarget(pRTV, Color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + ASSERT_NO_FATAL_FAILURE(VerifyClear(pRTV->GetTexture())); + const Uint64 FrameNumber = pContext->GetFrameNumber(); + m_pSwapChain->Present(0); + EXPECT_EQ(pContext->GetFrameNumber(), FrameNumber); + + // Present without acquiring another texture must not call native surface Present. + m_pSwapChain->Present(0); + pRTV = m_pSwapChain->GetCurrentBackBufferRTV(); + ASSERT_NE(pRTV, nullptr); + pContext->SetRenderTargets(1, &pRTV, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + pContext->ClearRenderTarget(pRTV, Color, RESOURCE_STATE_TRANSITION_MODE_TRANSITION); + m_pSwapChain.Release(); + pContext->WaitForIdle(); +} + +INSTANTIATE_TEST_SUITE_P(Formats, SwapChainWebGPUTest, testing::Values(TEX_FORMAT_RGBA8_UNORM, TEX_FORMAT_RGBA8_UNORM_SRGB, TEX_FORMAT_BGRA8_UNORM, TEX_FORMAT_BGRA8_UNORM_SRGB)); + +} // namespace + +#endif diff --git a/Tests/DiligentCoreAPITest/src/WebGPU/WebGPUTypeConversionsTest.cpp b/Tests/DiligentCoreAPITest/src/WebGPU/WebGPUTypeConversionsTest.cpp new file mode 100644 index 0000000000..9fc2997eab --- /dev/null +++ b/Tests/DiligentCoreAPITest/src/WebGPU/WebGPUTypeConversionsTest.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2023-2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include + +#include "../../../../Graphics/GraphicsEngineWebGPU/include/WebGPUTypeConversions.hpp" + +#include "gtest/gtest.h" + +using namespace Diligent; + +namespace +{ + +TEST(WebGPUTypeConversionsTest, TextureFormatRoundTrip) +{ + // Include formats on both sides of the former RGBA16Snorm table boundary. + constexpr TEXTURE_FORMAT Formats[] = { + TEX_FORMAT_R8_UNORM, + TEX_FORMAT_R16_UNORM, + TEX_FORMAT_RG16_SNORM, + TEX_FORMAT_RGBA16_SNORM, + TEX_FORMAT_RGBA16_FLOAT, + TEX_FORMAT_RGBA32_FLOAT, + TEX_FORMAT_D16_UNORM, + TEX_FORMAT_D24_UNORM_S8_UINT, + TEX_FORMAT_D32_FLOAT_S8X24_UINT, + TEX_FORMAT_BC1_UNORM, + TEX_FORMAT_BC7_UNORM_SRGB, + TEX_FORMAT_ETC2_RGBA8_UNORM_SRGB, + }; + for (TEXTURE_FORMAT Format : Formats) + { + const WGPUTextureFormat WebGPUFormat = TextureFormatToWGPUFormat(Format); + ASSERT_NE(WebGPUFormat, WGPUTextureFormat_Undefined); + EXPECT_EQ(WGPUFormatToTextureFormat(WebGPUFormat), Format); + } +} + +TEST(WebGPUTypeConversionsTest, UnsupportedTextureFormats) +{ + EXPECT_EQ(WGPUFormatToTextureFormat(WGPUTextureFormat_Undefined), TEX_FORMAT_UNKNOWN); + EXPECT_EQ(WGPUFormatToTextureFormat(WGPUTextureFormat_ASTC4x4Unorm), TEX_FORMAT_UNKNOWN); + EXPECT_EQ(WGPUFormatToTextureFormat(WGPUTextureFormat_ASTC12x12UnormSrgb), TEX_FORMAT_UNKNOWN); + EXPECT_EQ(WGPUFormatToTextureFormat(WGPUTextureFormat_Force32), TEX_FORMAT_UNKNOWN); +} + +} // namespace diff --git a/Tests/DiligentCoreTest/CMakeLists.txt b/Tests/DiligentCoreTest/CMakeLists.txt index 1cde824899..c36740917c 100644 --- a/Tests/DiligentCoreTest/CMakeLists.txt +++ b/Tests/DiligentCoreTest/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.10...3.22) if (PLATFORM_WEB) set(CMAKE_EXECUTABLE_SUFFIX ".html") @@ -17,6 +17,13 @@ if(NOT WEBGPU_SUPPORTED) list(REMOVE_ITEM SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/WGSLUtilsTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/WGSLShaderResourcesTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/WGSLShaderReflectionTest.cpp + ) +endif() + +if(NOT ${DILIGENT_USE_SPIRV_TOOLCHAIN}) + list(REMOVE_ITEM SOURCE + ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/SPIRVUtilsTest.cpp ) endif() @@ -38,7 +45,11 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") endif() add_executable(DiligentCoreTest ${SOURCE} ${SHADERS}) -set_common_target_properties(DiligentCoreTest) +if(WEBGPU_SUPPORTED) + set_common_target_properties(DiligentCoreTest 20) +else() + set_common_target_properties(DiligentCoreTest) +endif() if (PLATFORM_WEB) set(RESOURCE_PATH "${PROJECT_SOURCE_DIR}/assets/") @@ -61,7 +72,7 @@ PRIVATE ) if(WEBGPU_SUPPORTED) - target_link_libraries(DiligentCoreTest PRIVATE libtint) + target_link_libraries(DiligentCoreTest PRIVATE libtint dawn_utils_config) endif() if (PLATFORM_WIN32) diff --git a/Tests/DiligentCoreTest/assets/shaders/WGSL/SourceReflection.psh b/Tests/DiligentCoreTest/assets/shaders/WGSL/SourceReflection.psh new file mode 100644 index 0000000000..13f83890a0 --- /dev/null +++ b/Tests/DiligentCoreTest/assets/shaders/WGSL/SourceReflection.psh @@ -0,0 +1,29 @@ +// These names collide with WGSL keywords and their generated replacements. +// This is an ordinary user struct, not a Tint-generated matrix stride wrapper. +struct strided_arr +{ + float4 Value; +}; + +cbuffer fn +{ + row_major float2x3 RowMatrix; + column_major float4x3 ColumnMatrix; + float ScalarArray[2]; + strided_arr Items[2]; +}; + +cbuffer fn_1 +{ + float4 Extra; +}; + +StructuredBuffer var; +StructuredBuffer var_1; + +float4 main() : SV_Target +{ + return float4(RowMatrix[0], ColumnMatrix[0][0]) + + ScalarArray[0] + ScalarArray[1] + Items[0].Value + Items[1].Value + + Extra + var[0] + var_1[0]; +} diff --git a/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessQueries.psh b/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessQueries.psh new file mode 100644 index 0000000000..35338954f2 --- /dev/null +++ b/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessQueries.psh @@ -0,0 +1,17 @@ +RWTexture2D ReadTexture; +RWTexture2D WriteTexture; +RWTexture2D QueryTexture; + +float4 main() : SV_Target +{ + uint2 ReadSize; + uint2 WriteSize; + uint2 QuerySize; + ReadTexture.GetDimensions(ReadSize.x, ReadSize.y); + WriteTexture.GetDimensions(WriteSize.x, WriteSize.y); + QueryTexture.GetDimensions(QuerySize.x, QuerySize.y); + + // Use every query result, but never read or write QueryTexture's texels. + WriteTexture[uint2(0, 0)] = ReadTexture[uint2(0, 0)] + float4(ReadSize, QuerySize); + return float4(WriteSize, 0.0, 1.0); +} diff --git a/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessThroughFunctions.psh b/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessThroughFunctions.psh new file mode 100644 index 0000000000..50aac5917b --- /dev/null +++ b/Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessThroughFunctions.psh @@ -0,0 +1,21 @@ +RWTexture2D ReadTexture; +RWTexture2D WriteTexture; +RWTexture2D ReadWriteTexture; + +void CopyTexture(RWTexture2D Source, RWTexture2D Dest) +{ + Dest[uint2(0, 0)] = Source[uint2(0, 0)]; +} + +void ForwardCopy(RWTexture2D Source, RWTexture2D Dest) +{ + CopyTexture(Source, Dest); +} + +float4 main() : SV_Target +{ + // The middle texture is written by the first call and read by the second. + ForwardCopy(ReadTexture, ReadWriteTexture); + ForwardCopy(ReadWriteTexture, WriteTexture); + return float4(0.0, 1.0, 2.0, 3.0); +} diff --git a/Tests/DiligentCoreTest/src/ShaderTools/SPIRVUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/SPIRVUtilsTest.cpp new file mode 100644 index 0000000000..3882c89bcd --- /dev/null +++ b/Tests/DiligentCoreTest/src/ShaderTools/SPIRVUtilsTest.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include "SPIRVUtils.hpp" + +#include "gtest/gtest.h" + +using namespace Diligent; + +namespace +{ + +TEST(SPIRVUtils, PatchImageFormatsIgnoresInstructionOperands) +{ + // Use fixed bytecode so the constant's words cannot be changed or removed by a + // shader compiler. Its low word is OpTypeImage (25), and its high word is the + // image type ID (3). Scanning operands as opcodes would overwrite the image's + // format location with the signedness operand of OpTypeInt below. + // clang-format off + const std::vector SPIRV = + { + 0x07230203, 0x00010000, 0, 12, 0, // Header + 0x00020011, 1, // OpCapability Shader + 0x00020011, 11, // OpCapability Int64 + 0x0003000e, 0, 1, // OpMemoryModel Logical GLSL450 + 0x0005000f, 5, 10, 0x6e69616d, 0, // OpEntryPoint GLCompute %10 "main" + 0x00060010, 10, 17, 1, 1, 1, // OpExecutionMode %10 LocalSize 1 1 1 + 0x00030005, 5, 0x00786554, // OpName %5 "Tex" + 0x00040047, 5, 34, 0, // OpDecorate %5 DescriptorSet 0 + 0x00040047, 5, 33, 0, // OpDecorate %5 Binding 0 + 0x00020013, 1, // %1 = OpTypeVoid + 0x00030016, 2, 32, // %2 = OpTypeFloat 32 + 0x00090019, 3, 2, 1, 0, 0, 0, 2, 0, // %3 = OpTypeImage %2 2D 0 0 0 2 Unknown + 0x00040020, 4, 0, 3, // %4 = OpTypePointer UniformConstant %3 + 0x0004003b, 4, 5, 0, // %5 = OpVariable %4 UniformConstant + 0x00040015, 6, 64, 0, // %6 = OpTypeInt 64 0 + 0x0005002b, 6, 7, 25, 3, // %7 = OpConstant %6 0x0000000300000019 + 0x00030021, 8, 1, // %8 = OpTypeFunction %1 + 0x00040015, 9, 32, 0, // %9 = OpTypeInt 32 0 + 0x00050036, 1, 10, 0, 8, // %10 = OpFunction %1 None %8 + 0x000200f8, 11, // %11 = OpLabel + 0x000100fd, // OpReturn + 0x00010038 // OpFunctionEnd + }; + // clang-format on + + constexpr size_t ImageFormatWord = 47; + ASSERT_EQ(SPIRV[ImageFormatWord], 0u); // ImageFormatUnknown + + std::vector ExpectedSPIRV = SPIRV; + ExpectedSPIRV[ImageFormatWord] = 1; // ImageFormatRgba32f + + std::unordered_map ImageFormats; + ImageFormats.emplace("Tex", TEX_FORMAT_RGBA32_FLOAT); + const auto PatchedSPIRV = PatchImageFormats(SPIRV, ImageFormats); + EXPECT_EQ(PatchedSPIRV, ExpectedSPIRV); +} + +} // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp new file mode 100644 index 0000000000..5c3e972233 --- /dev/null +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp @@ -0,0 +1,727 @@ +/* + * Copyright 2026 Diligent Graphics LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * In no event and under no legal theory, whether in tort (including negligence), + * contract, or otherwise, unless required by applicable law (such as deliberate + * and grossly negligent acts) or agreed to in writing, shall any Contributor be + * liable for any damages, including any direct, indirect, special, incidental, + * or consequential damages of any character arising as a result of this License or + * out of the use or inability to use the software (including but not limited to damages + * for loss of goodwill, work stoppage, computer failure or malfunction, or any and + * all other commercial damages or losses), even if such Contributor has been advised + * of the possibility of such damages. + */ + +#include "WGSLShaderReflection.hpp" + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +using namespace Diligent; + +namespace +{ + +constexpr char Marker[] = "/*$DILIGENT_WGSL_REFLECTION="; + +std::string MakeMetadata(const std::string& Payload) +{ + return std::string{Marker} + Payload + "*/"; +} + +std::string Serialize(const WGSLShaderReflection& Reflection) +{ + std::string Text; + AppendWGSLShaderReflection(Text, Reflection); + return Text; +} + +// Independently specified data and wire text detect field-order and encoding +// mistakes that a serializer/parser round trip alone could hide. +WGSLShaderReflection MakeReferenceReflection() +{ + WGSLShaderReflection Reflection; + Reflection.Resources[{2, 0}].Name = "A"; + Reflection.Resources[{1, 7}].Name = "*/\n"; + auto& EmptyBuffer = Reflection.Resources[{1, 3}]; + EmptyBuffer.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + + auto& Resource = Reflection.Resources[{0, 9}]; + Resource.Name = "CB"; + Resource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + Resource.UniformBuffer->Size = 128; + const ShaderCodeVariableDesc Leaves[] = { + {"x", "float", SHADER_CODE_BASIC_TYPE_FLOAT, 0}, + {"y", nullptr, SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_UINT, 1, 4, 16, 2}, + }; + const ShaderCodeVariableDesc Members[] = { + {"s", "S", 2, Leaves, 0}, + {"m", "float2x3", SHADER_CODE_VARIABLE_CLASS_MATRIX_ROWS, SHADER_CODE_BASIC_TYPE_FLOAT, 2, 3, 32}, + }; + Resource.UniformBuffer->AddVariable(ShaderCodeVariableDesc{"v", "V", 2, Members, 16, 3}); + Resource.UniformBuffer->AddVariable(ShaderCodeVariableDesc{"z", "float3x2", SHADER_CODE_VARIABLE_CLASS_MATRIX_COLUMNS, SHADER_CODE_BASIC_TYPE_FLOAT, 3, 2, 96}); + return Reflection; +} + +constexpr char ReferencePayload[] = + "1\n" + "0 0 9 4342 1\n" + "128 2\n" + "76 56 5 0 0 0 16 3 2\n" + "73 53 5 0 0 0 0 0 2\n" + "78 666c6f6174 1 11 1 1 0 0 0\n" + "79 - 2 7 1 4 16 2 0\n" + "6d 666c6f6174327833 3 11 2 3 32 0 0\n" + "7a 666c6f6174337832 4 11 3 2 96 0 0\n" + "0 1 3 - 1\n" + "0 0\n" + "0 1 7 2a2f0a 0\n" + "0 2 0 41 0\n"; + +void ExpectVariableEqual(const ShaderCodeVariableDescX& Actual, const ShaderCodeVariableDescX& Expected) +{ + EXPECT_STREQ(Actual.Name, Expected.Name != nullptr ? Expected.Name : ""); + EXPECT_STREQ(Actual.TypeName, Expected.TypeName != nullptr ? Expected.TypeName : ""); + EXPECT_EQ(Actual.Class, Expected.Class); + EXPECT_EQ(Actual.BasicType, Expected.BasicType); + EXPECT_EQ(Actual.NumRows, Expected.NumRows); + EXPECT_EQ(Actual.NumColumns, Expected.NumColumns); + EXPECT_EQ(Actual.Offset, Expected.Offset); + EXPECT_EQ(Actual.ArraySize, Expected.ArraySize); + ASSERT_EQ(Actual.NumMembers, Expected.NumMembers); + // The owning descriptors contain vectors of derived types. Traverse their + // accessors rather than indexing the inherited base-descriptor pointers. + for (Uint32 i = 0; i < Expected.NumMembers; ++i) + { + SCOPED_TRACE(i); + ExpectVariableEqual(Actual.GetMember(i), Expected.GetMember(i)); + } +} + +void ExpectReflectionEqual(const WGSLShaderReflection& Actual, const WGSLShaderReflection& Expected) +{ + EXPECT_EQ(Actual.SpecializationConstants, Expected.SpecializationConstants); + ASSERT_EQ(Actual.Resources.size(), Expected.Resources.size()); + for (const auto& Entry : Expected.Resources) + { + SCOPED_TRACE(::testing::Message{} << "Group " << Entry.first.Group << ", binding " << Entry.first.Binding); + const auto* Resource = Actual.Find(Entry.first.Group, Entry.first.Binding); + ASSERT_NE(Resource, nullptr); + EXPECT_EQ(Resource->Name, Entry.second.Name); + ASSERT_EQ(bool{Resource->UniformBuffer}, bool{Entry.second.UniformBuffer}); + if (Resource->UniformBuffer) + { + const auto& Buffer = *Resource->UniformBuffer; + const auto& RefBuffer = *Entry.second.UniformBuffer; + EXPECT_EQ(Buffer.Size, RefBuffer.Size); + ASSERT_EQ(Buffer.NumVariables, RefBuffer.NumVariables); + for (Uint32 i = 0; i < RefBuffer.NumVariables; ++i) + { + SCOPED_TRACE(i); + ExpectVariableEqual(Buffer.GetVariable(i), RefBuffer.GetVariable(i)); + } + } + } +} + +void CheckRoundTrip(const WGSLShaderReflection& Expected) +{ + const std::string Text = Serialize(Expected); + WGSLShaderReflection Actual; + ASSERT_TRUE(ParseWGSLShaderReflection(Text, Actual)); + ExpectReflectionEqual(Actual, Expected); + EXPECT_EQ(Serialize(Actual), Text); +} + +void ExpectRejected(const std::string& Text) +{ + auto Expected = MakeReferenceReflection(); + auto Actual = MakeReferenceReflection(); + Expected.SpecializationConstants[7] = "original"; + Actual.SpecializationConstants = Expected.SpecializationConstants; + const auto* Resource = Actual.Find(0, 9); + const auto* Buffer = Resource->UniformBuffer.get(); + EXPECT_FALSE(ParseWGSLShaderReflection(Text, Actual)); + // Every failure must preserve both the contents and ownership of the result. + ASSERT_EQ(Actual.Find(0, 9), Resource); + EXPECT_EQ(Actual.Find(0, 9)->UniformBuffer.get(), Buffer); + ExpectReflectionEqual(Actual, Expected); +} + +using Tokens = std::array; + +Tokens MakeValidTokens() +{ + // Version; record type; resource group/binding/name/flag; buffer size/count; + // variable name/type/class/basic type/rows/columns/offset/array size/member count. + return {{"1", "0", "0", "1", "52", "1", "64", "1", "56", "54", "1", "11", "1", "1", "0", "0", "0"}}; +} + +std::string JoinTokens(const Tokens& Fields) +{ + std::string Payload; + for (const auto& Field : Fields) + Payload += Field + ' '; + return MakeMetadata(Payload); +} + +TEST(WGSLShaderReflection, EmptyReflectionDoesNotChangeSource) +{ + const WGSLShaderReflection Empty; + for (const std::string& Source : {std::string{}, std::string{"@compute fn main() {}"}, std::string{"a\0b", 3}}) + { + std::string Text = Source; + AppendWGSLShaderReflection(Text, Empty); + EXPECT_EQ(Text, Source); + } +} + +TEST(WGSLShaderReflection, MissingOrEmptyMetadataClearsOutput) +{ + const std::string Inputs[] = { + "", + "@compute fn main() {}", + "/*$SHADER_SOURCE_LANGUAGE=1*/", + "/*$DILIGENT_WGSL_REFLECTION", + MakeMetadata("1"), + MakeMetadata("1 \r\n\t\v\f"), + }; + for (const auto& Text : Inputs) + { + SCOPED_TRACE(Text); + auto Reflection = MakeReferenceReflection(); + Reflection.SpecializationConstants[1] = "previous"; + ASSERT_TRUE(ParseWGSLShaderReflection(Text, Reflection)); + EXPECT_TRUE(Reflection.Resources.empty()); + EXPECT_TRUE(Reflection.SpecializationConstants.empty()); + } +} + +TEST(WGSLShaderReflection, ExactSerialization) +{ + const auto Reflection = MakeReferenceReflection(); + const std::string Prefix = "// Existing source with an unrelated */\n@compute fn main() {}"; + std::string Text = Prefix; + AppendWGSLShaderReflection(Text, Reflection); + EXPECT_EQ(Text, Prefix + '\n' + MakeMetadata(ReferencePayload) + '\n'); + + // Same binding index in another group and duplicate names are independent resources. + WGSLShaderReflection RepeatedNames; + RepeatedNames.Resources[{2, 7}].Name = "A"; + RepeatedNames.Resources[{1, 7}].Name = "A"; + RepeatedNames.Resources[{1, 2}].Name = "A"; + EXPECT_EQ(Serialize(RepeatedNames), "\n" + MakeMetadata("1\n0 1 2 41 0\n0 1 7 41 0\n0 2 7 41 0\n") + '\n'); + CheckRoundTrip(RepeatedNames); +} + +TEST(WGSLShaderReflection, ParseKnownMetadataAndOwnStrings) +{ + std::string Text = "// Earlier comment */\n" + MakeMetadata(std::string{ReferencePayload} + "1 23 6f726967696e616c\n") + "\n/*$SHADER_SOURCE_LANGUAGE=1*/"; + auto Expected = MakeReferenceReflection(); + Expected.SpecializationConstants[23] = "original"; + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(Text, Reflection)); + Text.assign(Text.size(), 'x'); + Text.clear(); + Text.shrink_to_fit(); + ExpectReflectionEqual(Reflection, Expected); +} + +TEST(WGSLShaderReflection, SuccessfulParseReplacesPreviousReflection) +{ + auto Reflection = MakeReferenceReflection(); + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata("1 0 10 20 41 0"), Reflection)); + ASSERT_EQ(Reflection.Resources.size(), 1u); + const auto* Resource = Reflection.Find(10, 20); + ASSERT_NE(Resource, nullptr); + EXPECT_EQ(Resource->Name, "A"); + EXPECT_EQ(Resource->UniformBuffer, nullptr); + EXPECT_EQ(Reflection.Find(0, 9), nullptr); +} + +TEST(WGSLShaderReflection, NestedLayoutsRoundTrip) +{ + CheckRoundTrip(MakeReferenceReflection()); +} + +TEST(WGSLShaderReflection, StringsRoundTrip) +{ + std::string AllBytes; + for (unsigned Ch = 1; Ch <= 255; ++Ch) + AllBytes += static_cast(Ch); + const std::string Names[] = {"", "-", "*/ /*\n\r\t\v\f", Marker, "\xc3\xa9\xf0\x9f\x98\x80", AllBytes, std::string(16384, 'x')}; + for (size_t i = 0; i < std::size(Names); ++i) + { + SCOPED_TRACE(i); + WGSLShaderReflection Reflection; + auto& Resource = Reflection.Resources[{0, 0}]; + Resource.Name = Names[i]; + if (!Names[i].empty()) + Reflection.SpecializationConstants[0] = Names[i]; + Resource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + ShaderCodeVariableDescX Var; + Var.SetName(Names[i]); + Var.SetTypeName(Names[i]); + Resource.UniformBuffer->AddVariable(std::move(Var)); + CheckRoundTrip(Reflection); + const std::string Text = Serialize(Reflection); + EXPECT_EQ(Text.find("*/"), Text.size() - 3); + EXPECT_EQ(Text.find(Marker, 2), std::string::npos); + } + + WGSLShaderReflection NullNames; + auto& Buffer = NullNames.Resources[{0, 0}].UniformBuffer; + Buffer.reset(new ShaderCodeBufferDescX{}); + Buffer->AddVariable(ShaderCodeVariableDescX{}); + Buffer->GetVariable(0).Name = nullptr; + Buffer->GetVariable(0).TypeName = nullptr; + EXPECT_EQ(Serialize(NullNames), "\n" + MakeMetadata("1\n0 0 0 - 1\n0 1\n- - 0 0 0 0 0 0 0\n") + '\n'); + CheckRoundTrip(NullNames); +} + +TEST(WGSLShaderReflection, AllHexDigitInputs) +{ + const std::string Digits = "0123456789abcdef"; + for (unsigned Ch = 0; Ch <= 255; ++Ch) + { + for (size_t Position = 0; Position < 2; ++Position) + { + SCOPED_TRACE(::testing::Message{} << "Byte " << Ch << ", nibble " << Position); + auto Fields = MakeValidTokens(); + Fields[4][Position] = static_cast(Ch); + WGSLShaderReflection Reflection; + const bool IsHex = Digits.find(static_cast(Ch)) != std::string::npos; + ASSERT_EQ(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection), IsHex); + if (IsHex) + { + const auto* Resource = Reflection.Find(0, 1); + ASSERT_NE(Resource, nullptr); + const unsigned Value = Position == 0 ? static_cast(Digits.find(static_cast(Ch))) * 16 + 2 : 0x50 + static_cast(Digits.find(static_cast(Ch))); + EXPECT_EQ(Resource->Name, std::string(1, static_cast(Value))); + } + } + } +} + +TEST(WGSLShaderReflection, Uint32Boundaries) +{ + std::vector Values{0, 1, 0x7fffffffu, 0x80000000u, 0xfffffffeu, 0xffffffffu}; + for (Uint64 Power = 10; Power <= 1000000000; Power *= 10) + { + Values.push_back(static_cast(Power - 1)); + Values.push_back(static_cast(Power)); + Values.push_back(static_cast(Power + 1)); + } + for (Uint32 Value : Values) + { + SCOPED_TRACE(Value); + auto Fields = MakeValidTokens(); + for (size_t Field : {2u, 3u, 6u, 14u, 15u}) + Fields[Field] = std::to_string(Value); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection)); + const auto* Resource = Reflection.Find(Value, Value); + ASSERT_NE(Resource, nullptr); + ASSERT_NE(Resource->UniformBuffer, nullptr); + EXPECT_EQ(Resource->UniformBuffer->Size, Value); + const auto& Var = Resource->UniformBuffer->GetVariable(0); + EXPECT_EQ(Var.Offset, Value); + EXPECT_EQ(Var.ArraySize, Value); + // Compare against independent decimal formatting as well as re-parsing. + const std::string Number = std::to_string(Value); + EXPECT_EQ(Serialize(Reflection), "\n" + MakeMetadata("1\n0 " + Number + ' ' + Number + " 52 1\n" + Number + " 1\n56 54 1 11 1 1 " + Number + ' ' + Number + " 0\n") + '\n'); + CheckRoundTrip(Reflection); + } +} + +TEST(WGSLShaderReflection, AllUint8Dimensions) +{ + for (unsigned Value = 0; Value <= 255; ++Value) + { + SCOPED_TRACE(Value); + auto Fields = MakeValidTokens(); + Fields[12] = std::to_string(Value); + Fields[13] = std::to_string(255 - Value); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection)); + const auto& Var = Reflection.Find(0, 1)->UniformBuffer->GetVariable(0); + EXPECT_EQ(Var.NumRows, Value); + EXPECT_EQ(Var.NumColumns, 255 - Value); + CheckRoundTrip(Reflection); + } +} + +TEST(WGSLShaderReflection, AllVariableClassesAndBasicTypes) +{ + for (unsigned Class = 0; Class < SHADER_CODE_VARIABLE_CLASS_COUNT; ++Class) + { + for (unsigned Type = 0; Type < SHADER_CODE_BASIC_TYPE_COUNT; ++Type) + { + SCOPED_TRACE(::testing::Message{} << "Class " << Class << ", type " << Type); + auto Fields = MakeValidTokens(); + Fields[10] = std::to_string(Class); + Fields[11] = std::to_string(Type); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection)); + const auto& Var = Reflection.Find(0, 1)->UniformBuffer->GetVariable(0); + EXPECT_EQ(Var.Class, Class); + EXPECT_EQ(Var.BasicType, Type); + CheckRoundTrip(Reflection); + } + } +} + +TEST(WGSLShaderReflection, WhitespaceAndNumericSpelling) +{ + for (char Separator : {' ', '\t', '\n', '\r', '\v', '\f'}) + { + SCOPED_TRACE(static_cast(Separator)); + std::string Payload = ReferencePayload; + std::replace(Payload.begin(), Payload.end(), ' ', Separator); + std::replace(Payload.begin(), Payload.end(), '\n', Separator); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata(std::string(3, Separator) + Payload + std::string(3, Separator)), Reflection)); + ExpectReflectionEqual(Reflection, MakeReferenceReflection()); + EXPECT_EQ(Serialize(Reflection), "\n" + MakeMetadata(ReferencePayload) + '\n'); + } + auto Fields = MakeValidTokens(); + for (size_t Field : {0u, 1u, 2u, 3u, 5u, 6u, 7u, 10u, 11u, 12u, 13u, 14u, 15u, 16u}) + Fields[Field] = "+000" + Fields[Field]; + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection)); + WGSLShaderReflection Expected; + ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(MakeValidTokens()), Expected)); + ExpectReflectionEqual(Reflection, Expected); +} + +TEST(WGSLShaderReflection, RejectMalformedEnvelope) +{ + const std::string Inputs[] = { + Marker, + std::string{Marker} + "1", + std::string{Marker} + "1*", + MakeMetadata(""), + MakeMetadata(" \t\n"), + MakeMetadata("0"), + MakeMetadata("2"), + MakeMetadata("3"), + MakeMetadata("4294967295"), + MakeMetadata("1x"), + MakeMetadata("1") + MakeMetadata("1"), + MakeMetadata("1") + Marker, + MakeMetadata(std::string{"1 "} + Marker + "1"), + MakeMetadata("1 junk"), + }; + for (const auto& Text : Inputs) + { + SCOPED_TRACE(Text); + ExpectRejected(Text); + } +} + +TEST(WGSLShaderReflection, RejectInvalidNumberInEveryField) +{ + const std::string BadNumbers[] = {"", "+", "-", "-0", "-1", "++1", "+-1", "0x1", "1.0", "1e0", "1x", "4294967296", "18446744073709551616", std::string(128, '9'), std::string{"1\0", 2}}; + for (size_t Field : {0u, 1u, 2u, 3u, 5u, 6u, 7u, 10u, 11u, 12u, 13u, 14u, 15u, 16u}) + { + for (const auto& Bad : BadNumbers) + { + SCOPED_TRACE(::testing::Message{} << "Field " << Field << ", value " << Bad); + auto Fields = MakeValidTokens(); + Fields[Field] = Bad; + ExpectRejected(JoinTokens(Fields)); + } + } +} + +TEST(WGSLShaderReflection, RejectOutOfRangeFields) +{ + const std::pair InvalidValues[] = { + {1, 2}, + {1, 255}, + {5, 2}, + {5, 255}, + {10, SHADER_CODE_VARIABLE_CLASS_COUNT}, + {10, 255}, + {10, 256}, + {11, SHADER_CODE_BASIC_TYPE_COUNT}, + {11, 255}, + {11, 256}, + {12, 256}, + {12, 65535}, + {13, 256}, + {13, 65535}, + }; + for (const auto& Entry : InvalidValues) + { + SCOPED_TRACE(::testing::Message{} << "Field " << Entry.first << ", value " << Entry.second); + auto Fields = MakeValidTokens(); + Fields[Entry.first] = std::to_string(Entry.second); + ExpectRejected(JoinTokens(Fields)); + } +} + +TEST(WGSLShaderReflection, RejectInvalidStringInEveryField) +{ + const std::string BadStrings[] = {"", "0", "123", "zz", "4g", "g4", "AF", "--", "-0", "00", "410042", "41-42", std::string{"41\0ff", 5}}; + for (size_t Field : {4u, 8u, 9u}) + { + for (const auto& Bad : BadStrings) + { + SCOPED_TRACE(::testing::Message{} << "Field " << Field << ", value " << Bad); + auto Fields = MakeValidTokens(); + Fields[Field] = Bad; + ExpectRejected(JoinTokens(Fields)); + } + } +} + +TEST(WGSLShaderReflection, RejectTruncatedInputAtEveryByte) +{ + const std::string Text = MakeMetadata(ReferencePayload); + for (size_t Length = sizeof(Marker) - 1; Length < Text.size(); ++Length) + { + SCOPED_TRACE(Length); + ExpectRejected(Text.substr(0, Length)); + } + + // Keep the closing delimiter, but truncate inside a required resource, + // buffer, variable, or nested member record. This exercises the reader's + // bounds independently of the initial check for a closing comment. + const std::string Payload = "1\n0 0 0 41 1\n64 1\n42 54 5 0 0 0 0 0 1\n43 - 1 11 1 1 0 0 0"; + WGSLShaderReflection Complete; + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata(Payload), Complete)); + for (size_t Length = 3; Length < Payload.size(); ++Length) + { + SCOPED_TRACE(Length); + ExpectRejected(MakeMetadata(Payload.substr(0, Length))); + } +} + +TEST(WGSLShaderReflection, RejectMismatchedCounts) +{ + for (const char* Count : {"0", "2", "4294967295"}) + { + auto Fields = MakeValidTokens(); + Fields[7] = Count; + ExpectRejected(JoinTokens(Fields)); + } + for (const char* Count : {"1", "2", "4294967295"}) + { + auto Fields = MakeValidTokens(); + Fields[16] = Count; + ExpectRejected(JoinTokens(Fields)); + } + ExpectRejected(MakeMetadata("1\n0 0 0 41 1\n64 0\n42 - 1 11 1 1 0 0 0\n")); +} + +TEST(WGSLShaderReflection, RejectDuplicateBindings) +{ + ExpectRejected(MakeMetadata("1\n0 0 0 41 0\n0 0 0 42 0\n")); + ExpectRejected(MakeMetadata("1\n0 0 0 41 1\n0 0\n0 0 0 41 0\n")); + ExpectRejected(MakeMetadata("1\n0 0 0 41 0\n0 1 0 42 0\n0 0 0 43 0\n")); +} + +TEST(WGSLShaderReflection, NestingLimit) +{ + for (unsigned Depth : {1u, 63u, 64u, 65u}) + { + SCOPED_TRACE(Depth); + std::string Payload = "1\n0 0 0 41 1\n16 1\n"; + for (unsigned Level = 0; Level < Depth; ++Level) + Payload += Level + 1 == Depth ? "78 - 1 11 1 1 0 0 0\n" : "73 53 5 0 0 0 0 0 1\n"; + if (Depth > 64) + { + ExpectRejected(MakeMetadata(Payload)); + continue; + } + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata(Payload), Reflection)); + const ShaderCodeVariableDescX* Var = &Reflection.Find(0, 0)->UniformBuffer->GetVariable(0); + for (unsigned Level = 1; Level < Depth; ++Level) + { + ASSERT_EQ(Var->NumMembers, 1u); + Var = &Var->GetMember(0); + } + EXPECT_EQ(Var->NumMembers, 0u); + EXPECT_EQ(Serialize(Reflection), "\n" + MakeMetadata(Payload) + '\n'); + } +} + +std::string MakeRandomName(std::mt19937& Random) +{ + std::string Name(Random() % 65, '\0'); + for (char& Ch : Name) + Ch = static_cast(1 + Random() % 255); + return Name; +} + +ShaderCodeVariableDescX MakeRandomVariable(std::mt19937& Random, unsigned Depth = 0) +{ + ShaderCodeVariableDescX Var; + Var.SetName(MakeRandomName(Random)); + Var.SetTypeName(MakeRandomName(Random)); + Var.Class = static_cast(Random() % SHADER_CODE_VARIABLE_CLASS_COUNT); + Var.BasicType = static_cast(Random() % SHADER_CODE_BASIC_TYPE_COUNT); + Var.NumRows = static_cast(Random()); + Var.NumColumns = static_cast(Random()); + Var.Offset = Random(); + Var.ArraySize = Random(); + if (Depth < 4 && Random() % 3 == 0) + { + Var.Class = SHADER_CODE_VARIABLE_CLASS_STRUCT; + const auto Count = Random() % 4; + for (unsigned i = 0; i < Count; ++i) + Var.AddMember(MakeRandomVariable(Random, Depth + 1)); + } + return Var; +} + +TEST(WGSLShaderReflection, GeneratedLayoutsRoundTrip) +{ + std::mt19937 Random{0x5747534c}; + for (unsigned Case = 0; Case < 100; ++Case) + { + SCOPED_TRACE(Case); + WGSLShaderReflection Reflection; + const unsigned NumResources = Case == 99 ? 128 : Random() % 17; + for (unsigned i = 0; i < NumResources; ++i) + { + auto& Resource = Reflection.Resources[{static_cast(Random() % 4), i}]; + Resource.Name = MakeRandomName(Random); + if (Random() % 2 != 0) + { + Resource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + Resource.UniformBuffer->Size = Random(); + const auto NumVariables = Random() % 17; + for (unsigned j = 0; j < NumVariables; ++j) + Resource.UniformBuffer->AddVariable(MakeRandomVariable(Random)); + } + } + const unsigned NumOverrides = Case == 99 ? 128 : Random() % 17; + for (unsigned i = 0; i < NumOverrides; ++i) + { + std::string Name = MakeRandomName(Random); + if (Name.empty()) + Name = "override"; + Reflection.SpecializationConstants[Random() % 65536] = std::move(Name); + } + CheckRoundTrip(Reflection); + } +} + + +TEST(WGSLShaderReflection, SpecializationConstants) +{ + WGSLShaderReflection Reflection; + Reflection.Resources[{2, 3}].Name = "R"; + Reflection.SpecializationConstants[0] = "A"; + Reflection.SpecializationConstants[65535] = "*/\n"; + const std::string Expected = "\n" + MakeMetadata("1\n0 2 3 52 0\n1 0 41 \n1 65535 2a2f0a \n") + '\n'; + EXPECT_EQ(Serialize(Reflection), Expected); + + WGSLShaderReflection Parsed; + ASSERT_TRUE(ParseWGSLShaderReflection(Expected, Parsed)); + ExpectReflectionEqual(Parsed, Reflection); + CheckRoundTrip(Reflection); + + // Overrides without resources still require a metadata payload. + Reflection.Resources.clear(); + CheckRoundTrip(Reflection); + EXPECT_FALSE(Serialize(Reflection).empty()); + + // Exercise both record types with nested buffer metadata, and verify that + // reading resource-only metadata clears previous specialization names. + auto Mixed = MakeReferenceReflection(); + Mixed.SpecializationConstants = Reflection.SpecializationConstants; + CheckRoundTrip(Mixed); + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata(ReferencePayload), Parsed)); + EXPECT_TRUE(Parsed.SpecializationConstants.empty()); + ExpectReflectionEqual(Parsed, MakeReferenceReflection()); +} + +TEST(WGSLShaderReflection, InterleavedResourceAndOverrideRecords) +{ + // Record order is not part of resource or override identity. The same ID + // can also occur in the resource binding coordinates without a collision. + const std::string Payload = + "1\n" + "1 65535 4d6178\n" + "0 2 1 41 0\n" + "1 0 5a65726f\n" + "0 0 0 42 1\n" + "16 1\n" + "78 - 1 11 1 1 0 0 0\n" + "1 +0007 536576656e\n"; + WGSLShaderReflection Expected; + Expected.Resources[{2, 1}].Name = "A"; + auto& BufferResource = Expected.Resources[{0, 0}]; + BufferResource.Name = "B"; + BufferResource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + BufferResource.UniformBuffer->Size = 16; + BufferResource.UniformBuffer->AddVariable(ShaderCodeVariableDesc{"x", nullptr, SHADER_CODE_BASIC_TYPE_FLOAT, 0}); + Expected.SpecializationConstants = {{0, "Zero"}, {7, "Seven"}, {65535, "Max"}}; + + WGSLShaderReflection Parsed; + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata(Payload), Parsed)); + ExpectReflectionEqual(Parsed, Expected); + EXPECT_EQ(Serialize(Parsed), Serialize(Expected)); + + // An override-only payload replaces both maps, including previous layout + // ownership, instead of merging with the previously parsed reflection. + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata("1 1 9 4e6577"), Parsed)); + WGSLShaderReflection Replacement; + Replacement.SpecializationConstants[9] = "New"; + ExpectReflectionEqual(Parsed, Replacement); + + // A late duplicate must discard the entire newly parsed mixed payload. + ExpectRejected(MakeMetadata(Payload + "1 65535 4f74686572")); +} + +TEST(WGSLShaderReflection, RejectMalformedSpecializationConstants) +{ + for (const char* Payload : { + "1 2", // Unknown record kind. + "1 x", // Invalid record kind. + "1 4294967296", // Record kind overflow. + "1 1", // Missing ID and name. + "1 1 0", // Missing name. + "1 1 -1 41", // Negative ID. + "1 1 65536 41", // Outside the WGSL override ID range. + "1 1 4294967296 41", // ID overflow. + "1 1 1x 41", // Invalid ID. + "1 1 0 -", // Empty name. + "1 1 0 0", // Truncated hex pair. + "1 1 0 00", // Embedded null. + "1 1 0 gg", // Invalid hex. + "1 1 0 41 1 0 42", // Duplicate override IDs. + "1 0 1 2 52 0 0 1 2 53 0", // Duplicate resource bindings. + "1 1 0 41 0", // Truncated resource following an override. + }) + { + SCOPED_TRACE(Payload); + ExpectRejected(MakeMetadata(Payload)); + // Also fail after successfully decoding nested buffers and an override; + // partial mixed results must never replace the caller's prior state. + ExpectRejected(MakeMetadata(std::string{ReferencePayload} + "1 7 76616c6964\n" + (Payload + 2))); + } +} + +} // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index 41a4245999..8775042e5e 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -26,11 +26,14 @@ #include "WGSLShaderResources.hpp" #include "WGSLUtils.hpp" +#include "WGSLShaderReflection.hpp" +#include "SPIRVShaderResources.hpp" #include "GLSLangUtils.hpp" #include "DefaultShaderSourceStreamFactory.h" #include "RefCntAutoPtr.hpp" #include "EngineMemory.h" +#include #include #include "TestingEnvironment.hpp" @@ -42,7 +45,7 @@ using namespace Diligent::Testing; namespace { -std::string HLSLtoWGLS(const char* FilePath) +std::string HLSLtoWGLS(const char* FilePath, bool LoadUniformBufferReflection = false, std::vector* pSPIRV = nullptr) { ShaderCreateInfo ShaderCI; ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; @@ -64,7 +67,10 @@ std::string HLSLtoWGLS(const char* FilePath) if (SPIRV.empty()) return {}; - return ConvertSPIRVtoWGSL(SPIRV); + std::string WGSL = ConvertSPIRVtoWGSL(SPIRV, LoadUniformBufferReflection); + if (pSPIRV != nullptr) + *pSPIRV = std::move(SPIRV); + return WGSL; } void TestWGSLResources(const char* FilePath, @@ -111,6 +117,106 @@ void TestWGSLResources(const char* FilePath, using WGSLResourceType = WGSLShaderResourceAttribs::ResourceType; using WGSLSampleType = WGSLShaderResourceAttribs::TextureSampleType; + +TEST(WGSLShaderResources, TextureAccessThroughFunctions) +{ + // Identical HLSL resource types must retain distinct access modes after conversion. + // The source compiler may inline the helpers before Tint processes the SPIR-V. + TestWGSLResources("TextureAccessThroughFunctions.psh", + { + {"ReadTexture", WGSLResourceType::Texture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_UNKNOWN, WGSLSampleType::Float}, + {"WriteTexture", WGSLResourceType::WOStorageTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_RGBA32_FLOAT, WGSLSampleType::Float}, + {"ReadWriteTexture", WGSLResourceType::RWStorageTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_RGBA32_FLOAT, WGSLSampleType::Float}, + }); +} + +TEST(WGSLShaderResources, TextureAccessQueries) +{ + // Dimension queries must not turn a write-only texture into a read/write texture + // or narrow the access of a texture used only for queries. + TestWGSLResources("TextureAccessQueries.psh", + { + {"ReadTexture", WGSLResourceType::Texture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_UNKNOWN, WGSLSampleType::Float}, + {"WriteTexture", WGSLResourceType::WOStorageTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_RGBA32_FLOAT, WGSLSampleType::Float}, + {"QueryTexture", WGSLResourceType::RWStorageTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_RGBA32_FLOAT, WGSLSampleType::Float}, + }); +} + +TEST(WGSLShaderResources, PreservesSourceReflection) +{ + std::vector SPIRV; + const std::string WGSL = HLSLtoWGLS("SourceReflection.psh", true, &SPIRV); + ASSERT_FALSE(WGSL.empty()); + + SPIRVShaderResources::CreateInfo ResCI; + ResCI.ShaderType = SHADER_TYPE_PIXEL; + ResCI.Name = "Source reflection test"; + ResCI.LoadUniformBufferReflection = true; + const SPIRVShaderResources SourceResources{GetRawAllocator(), SPIRV, ResCI}; + ASSERT_EQ(SourceResources.GetNumUBs(), 2u); + + const WGSLResourceMapping Mapping = { + {"fn", {2, 5}}, + {"fn_1", {3, 7}}, + {"var", {1, 9}}, + {"var_1", {1, 10}}, + }; + auto CheckReflection = [&](const std::string& Text, bool Remapped) { + // Reload the emitted text as WGSL, as GetBytecode and shader archives do. + const WGSLShaderResources Resources{GetRawAllocator(), Text, SHADER_SOURCE_LANGUAGE_WGSL, + "Source reflection test", nullptr, nullptr, nullptr, true, nullptr}; + ASSERT_EQ(Resources.GetTotalResources(), Mapping.size()); + ASSERT_EQ(Resources.GetNumUBs(), SourceResources.GetNumUBs()); + for (Uint32 i = 0; i < Resources.GetTotalResources(); ++i) + { + const auto& Resource = Resources.GetResource(i); + auto It = Mapping.find(Resource.Name); + ASSERT_NE(It, Mapping.end()) << Resource.Name; + if (Remapped) + { + EXPECT_EQ(Resource.BindGroup, It->second.Group); + EXPECT_EQ(Resource.BindIndex, It->second.Index); + } + if (i < Resources.GetNumUBs()) + { + bool Found = false; + for (Uint32 j = 0; j < SourceResources.GetNumUBs(); ++j) + { + if (strcmp(Resource.Name, SourceResources.GetUB(j).Name) == 0) + { + Found = true; + EXPECT_EQ(*Resources.GetUniformBufferDesc(i), *SourceResources.GetUniformBufferDesc(j)) << Resource.Name; + } + } + EXPECT_TRUE(Found) << Resource.Name; + } + } + }; + CheckReflection(WGSL, false); + const std::string Remapped = RemapWGSLResourceBindings(WGSL, Mapping, nullptr); + ASSERT_FALSE(Remapped.empty()); + CheckReflection(Remapped, true); + // A second rewrite must preserve the same names and layouts as the first. + const std::string RemappedAgain = RemapWGSLResourceBindings(Remapped, Mapping, nullptr); + ASSERT_FALSE(RemappedAgain.empty()); + CheckReflection(RemappedAgain, true); +} + +TEST(WGSLShaderResources, ReflectionMetadataRoundTrip) +{ + const std::string WGSL = HLSLtoWGLS("SourceReflection.psh", true); + ASSERT_FALSE(WGSL.empty()); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(WGSL, Reflection)); + ASSERT_EQ(Reflection.Resources.size(), 4u); + + const size_t Start = WGSL.find("/*$DILIGENT_WGSL_REFLECTION="); + ASSERT_NE(Start, std::string::npos); + std::string Metadata; + AppendWGSLShaderReflection(Metadata, Reflection); + EXPECT_EQ(Metadata, "\n" + WGSL.substr(Start)); +} + TEST(WGSLShaderResources, UniformBuffers) { TestWGSLResources("UniformBuffers.psh", @@ -298,10 +404,10 @@ TEST(WGSLShaderResources, SpecializationConstants) EXPECT_EQ(Resources.GetNumSBs(), 1u); const std::vector RefSpecConstants = { - {"sc_float", 0, SHADER_CODE_BASIC_TYPE_FLOAT}, - {"sc_int", 0, SHADER_CODE_BASIC_TYPE_INT}, - {"sc_uint", 0, SHADER_CODE_BASIC_TYPE_UINT}, - {"sc_bool", 0, SHADER_CODE_BASIC_TYPE_BOOL}, + {"sc_float", 0, SHADER_CODE_BASIC_TYPE_FLOAT, false}, + {"sc_int", 0, SHADER_CODE_BASIC_TYPE_INT, false}, + {"sc_uint", 0, SHADER_CODE_BASIC_TYPE_UINT, false}, + {"sc_bool", 0, SHADER_CODE_BASIC_TYPE_BOOL, false}, }; const WGSLShaderResources& ConstResources = Resources; @@ -320,6 +426,411 @@ TEST(WGSLShaderResources, SpecializationConstants) const WGSLSpecializationConstantAttribs* pRef = it->second; EXPECT_EQ(SC.GetType(), pRef->GetType()) << SC.Name; + EXPECT_EQ(SC.IsIdSpecified, pRef->IsIdSpecified) << SC.Name; + } +} + + +TEST(WGSLShaderResources, NativeUniformBufferLayout) +{ + const std::string WGSL = R"( +struct Nested { uv: vec2u, signed_value: i32 } +struct Constants { + scalar: f32, + vector: vec3f, + tail: u32, + transform: mat2x4f, + values: array, + nested: Nested, +} +@group(2) @binding(3) var constants: Constants; +@fragment fn main() -> @location(0) vec4f { + return constants.transform[0] + vec4f(constants.scalar + constants.vector.x + + f32(constants.tail + constants.nested.uv.x) + + f32(constants.values[1].x + constants.nested.signed_value)); +} +)"; + // Independently specified offsets exercise WGSL alignment, nested members, + // arrays, and matrix orientation without source-language metadata. + const ShaderCodeVariableDesc NestedMembers[] = { + {"uv", "vec2", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_UINT, 2, 1, 0}, + {"signed_value", "i32", SHADER_CODE_BASIC_TYPE_INT, 8}, + }; + const ShaderCodeVariableDesc Variables[] = { + {"scalar", "f32", SHADER_CODE_BASIC_TYPE_FLOAT, 0}, + {"vector", "vec3", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_FLOAT, 3, 1, 16}, + {"tail", "u32", SHADER_CODE_BASIC_TYPE_UINT, 28}, + {"transform", "mat2x4", SHADER_CODE_VARIABLE_CLASS_MATRIX_ROWS, SHADER_CODE_BASIC_TYPE_FLOAT, 4, 2, 32}, + {"values", "vec4", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_INT, 4, 1, 64, 2}, + {"nested", "Nested", 2, NestedMembers, 96}, + }; + const ShaderCodeBufferDesc Expected{112, 6, Variables}; + for (bool LoadReflection : {false, true}) + { + SCOPED_TRACE(LoadReflection); + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Native uniform layout", nullptr, nullptr, nullptr, LoadReflection, nullptr}; + ASSERT_EQ(Resources.GetTotalResources(), 1u); + ASSERT_EQ(Resources.GetNumUBs(), 1u); + EXPECT_STREQ(Resources.GetEntryPoint(), "main"); + EXPECT_EQ(Resources.GetShaderType(), SHADER_TYPE_PIXEL); + EXPECT_STREQ(Resources.GetUB(0).Name, "constants"); + EXPECT_EQ(Resources.GetUB(0).BindGroup, 2u); + EXPECT_EQ(Resources.GetUB(0).BindIndex, 3u); + EXPECT_EQ(Resources.GetUB(0).BufferStaticSize, 112u); + if (LoadReflection) + { + const auto* Buffer = Resources.GetUniformBufferDesc(0); + ASSERT_NE(Buffer, nullptr); + EXPECT_EQ(*Buffer, Expected); + } + } +} + +TEST(WGSLShaderResources, NativeNonStructUniforms) +{ + const ShaderCodeVariableDesc ElementMembers[] = { + {"xy", "vec2", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_FLOAT, 2, 1, 0}, + {"z", "u32", SHADER_CODE_BASIC_TYPE_UINT, 8}, + }; + const struct + { + const char* Type; + const char* Expression; + Uint32 Size; + ShaderCodeVariableDesc Variable; + } Cases[] = { + {"f32", "value", 4, {"value", "f32", SHADER_CODE_BASIC_TYPE_FLOAT, 0}}, + {"i32", "f32(value)", 4, {"value", "i32", SHADER_CODE_BASIC_TYPE_INT, 0}}, + {"u32", "f32(value)", 4, {"value", "u32", SHADER_CODE_BASIC_TYPE_UINT, 0}}, + {"f16", "f32(value)", 2, {"value", "f16", SHADER_CODE_BASIC_TYPE_FLOAT16, 0}}, + {"vec3f", "value.x", 12, {"value", "vec3", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_FLOAT, 3, 1, 0}}, + {"vec4h", "f32(value.x)", 8, {"value", "vec4", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_FLOAT16, 4, 1, 0}}, + {"mat2x4f", "value[1].x", 32, {"value", "mat2x4", SHADER_CODE_VARIABLE_CLASS_MATRIX_ROWS, SHADER_CODE_BASIC_TYPE_FLOAT, 4, 2, 0}}, + {"array", "f32(value[2].x)", 48, {"value", "vec4", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_INT, 4, 1, 0, 3}}, + {"array", "value[1].xy.x + f32(value[1].z)", 32, {"value", "Element", 2, ElementMembers, 0, 2}}, + }; + for (const auto& Test : Cases) + { + SCOPED_TRACE(Test.Type); + const std::string Source = std::string{R"( +enable f16; +struct Element { xy: vec2f, z: u32 } +@group(2) @binding(3) var value: )"} + + Test.Type + R"(; +@fragment fn main() -> @location(0) vec4f { return vec4f()" + + Test.Expression + "); }"; + + for (bool HasMetadata : {false, true}) + { + SCOPED_TRACE(HasMetadata); + std::string WGSL = Source; + if (HasMetadata) + { + // Names-only metadata must still load the native layout safely. + WGSLShaderReflection Metadata; + Metadata.Resources[{2, 3}].Name = "OriginalValue"; + AppendWGSLShaderReflection(WGSL, Metadata); + } + + for (bool LoadReflection : {false, true}) + { + SCOPED_TRACE(LoadReflection); + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Non-struct uniform", nullptr, "main", nullptr, LoadReflection, nullptr}; + ASSERT_EQ(Resources.GetNumUBs(), 1u); + EXPECT_EQ(Resources.GetTotalResources(), 1u); + const char* Name = HasMetadata ? "OriginalValue" : "value"; + EXPECT_STREQ(Resources.GetUB(0).Name, Name); + EXPECT_EQ(Resources.GetUB(0).BufferStaticSize, Test.Size); + EXPECT_EQ(Resources.GetUB(0).BindGroup, 2u); + EXPECT_EQ(Resources.GetUB(0).BindIndex, 3u); + if (LoadReflection) + { + const auto* Buffer = Resources.GetUniformBufferDesc(0); + ASSERT_NE(Buffer, nullptr); + ShaderCodeVariableDesc ExpectedVariable = Test.Variable; + ExpectedVariable.Name = Name; + const ShaderCodeBufferDesc Expected{Test.Size, 1, &ExpectedVariable}; + EXPECT_EQ(*Buffer, Expected); + } + } + } + } +} + +TEST(WGSLShaderResources, NativeHalfPrecisionUniforms) +{ + const std::string WGSL = R"( +enable f16; +struct Constants { @align(16) value: vec4h } +@group(0) @binding(0) var constants: Constants; +@fragment fn main() -> @location(0) vec4f { return vec4f(constants.value); } +)"; + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Half precision uniforms", nullptr, "main", nullptr, true, nullptr}; + ASSERT_EQ(Resources.GetNumUBs(), 1u); + const auto* Buffer = Resources.GetUniformBufferDesc(0); + ASSERT_NE(Buffer, nullptr); + EXPECT_EQ(Buffer->Size, 16u); + ASSERT_EQ(Buffer->NumVariables, 1u); + const ShaderCodeVariableDesc Expected{"value", "vec4", SHADER_CODE_VARIABLE_CLASS_VECTOR, + SHADER_CODE_BASIC_TYPE_FLOAT16, 4, 1, 0}; + EXPECT_EQ(Buffer->pVariables[0], Expected); +} + +TEST(WGSLShaderResources, PartialMetadataOverridesNamesAndLayout) +{ + std::string WGSL = R"( +struct Generated { generated_member: vec4f } +struct Native { native_member: vec4f } +@group(1) @binding(4) var generated: Generated; +@group(2) @binding(4) var native: Native; +@id(7) override generated_scale: f32 = 1.0; +@id(8) override native_scale: f32 = 2.0; +@id(9) override unused: f32 = 3.0; +@fragment fn main() -> @location(0) vec4f { + return generated.generated_member * generated_scale + native.native_member * native_scale; +} +)"; + const ShaderCodeVariableDesc OriginalMember{"original_member", "float4", SHADER_CODE_VARIABLE_CLASS_VECTOR, + SHADER_CODE_BASIC_TYPE_FLOAT, 1, 4, 0}; + const ShaderCodeBufferDesc OriginalBuffer{16, 1, &OriginalMember}; + WGSLShaderReflection Metadata; + auto& Resource = Metadata.Resources[{1, 4}]; + Resource.Name = "OriginalConstants"; + Resource.UniformBuffer.reset(new ShaderCodeBufferDescX{}); + Resource.UniformBuffer->Size = 16; + Resource.UniformBuffer->AddVariable(OriginalMember); + Metadata.Resources[{3, 4}].Name = "NotInShader"; + Metadata.SpecializationConstants[7] = "OriginalScale"; + Metadata.SpecializationConstants[9] = "UnusedScale"; + AppendWGSLShaderReflection(WGSL, Metadata); + + // A metadata record applies to the complete binding point. Missing records + // fall back to WGSL reflection; unrelated records do not create resources. + for (bool LoadReflection : {false, true}) + { + SCOPED_TRACE(LoadReflection); + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Partial reflection metadata", nullptr, "main", nullptr, LoadReflection, nullptr}; + ASSERT_EQ(Resources.GetNumUBs(), 2u); + EXPECT_EQ(Resources.GetTotalResources(), 2u); + for (Uint32 i = 0; i < Resources.GetNumUBs(); ++i) + { + const auto& UB = Resources.GetUB(i); + ASSERT_TRUE(UB.BindGroup == 1 || UB.BindGroup == 2); + EXPECT_EQ(UB.BindIndex, 4u); + EXPECT_STREQ(UB.Name, UB.BindGroup == 1 ? "OriginalConstants" : "native"); + if (LoadReflection) + { + const auto* Buffer = Resources.GetUniformBufferDesc(i); + ASSERT_NE(Buffer, nullptr); + if (UB.BindGroup == 1) + EXPECT_EQ(*Buffer, OriginalBuffer); + else + { + ASSERT_EQ(Buffer->NumVariables, 1u); + EXPECT_STREQ(Buffer->pVariables[0].Name, "native_member"); + EXPECT_STREQ(Buffer->pVariables[0].TypeName, "vec4"); + EXPECT_EQ(Buffer->pVariables[0].NumRows, 4u); + EXPECT_EQ(Buffer->pVariables[0].NumColumns, 1u); + } + } + } + ASSERT_EQ(Resources.GetNumSpecConstants(), 2u); + for (Uint32 i = 0; i < Resources.GetNumSpecConstants(); ++i) + { + const auto& SC = Resources.GetSpecConstant(i); + ASSERT_TRUE(SC.OverrideId == 7 || SC.OverrideId == 8); + EXPECT_STREQ(SC.Name, SC.OverrideId == 7 ? "OriginalScale" : "native_scale"); + EXPECT_TRUE(SC.IsIdSpecified); + EXPECT_EQ(SC.GetType(), SHADER_CODE_BASIC_TYPE_FLOAT); + } + } +} + +TEST(WGSLShaderResources, MetadataNamesAreRestoredBeforeArrayMerging) +{ + std::string WGSL = R"( +@group(0) @binding(3) var generated_a: texture_2d; +@group(0) @binding(8) var generated_b: texture_2d; +@group(1) @binding(3) var native_texture: texture_2d; +@fragment fn main() -> @location(0) vec4f { + return textureLoad(generated_a, vec2i(0), 0) + + textureLoad(generated_b, vec2i(0), 0) + textureLoad(native_texture, vec2i(0), 0); +} +)"; + WGSLShaderReflection Metadata; + Metadata.Resources[{0, 3}].Name = "Textures_element_2"; + Metadata.Resources[{0, 8}].Name = "Textures_element_5"; + AppendWGSLShaderReflection(WGSL, Metadata); + for (const char* Suffix : {static_cast(nullptr), "", "_element_"}) + { + SCOPED_TRACE(Suffix != nullptr ? Suffix : "null"); + const bool Merge = Suffix != nullptr && Suffix[0] != '\0'; + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Metadata resource arrays", nullptr, "main", Suffix, false, nullptr}; + ASSERT_EQ(Resources.GetNumTextures(), Merge ? 2u : 3u); + for (Uint32 i = 0; i < Resources.GetNumTextures(); ++i) + { + const auto& Texture = Resources.GetTexture(i); + EXPECT_EQ(Texture.Type, WGSLResourceType::Texture); + if (Texture.BindGroup == 1) + { + EXPECT_STREQ(Texture.Name, "native_texture"); + EXPECT_EQ(Texture.ArraySize, 1u); + } + else if (Merge) + { + EXPECT_STREQ(Texture.Name, "Textures"); + EXPECT_EQ(Texture.ArraySize, 6u); + } + else + { + EXPECT_STREQ(Texture.Name, Texture.BindIndex == 3 ? "Textures_element_2" : "Textures_element_5"); + EXPECT_EQ(Texture.ArraySize, 1u); + } + } + } +} + +TEST(WGSLShaderResources, SelectedEntryPointReflection) +{ + const std::string WGSL = R"( +struct Uniforms { value: vec4f } +@group(0) @binding(0) var vertex_uniforms: Uniforms; +@group(0) @binding(0) var fragment_uniforms: Uniforms; +@group(0) @binding(1) var shared_uniforms: Uniforms; +@group(0) @binding(2) var output: array; +@id(7) override VertexScale: f32 = 1.0; +@id(9) override FragmentScale: f32 = 2.0; +@id(11) override Width: u32 = 4; +override Unused: u32 = 1; +fn read_vertex() -> vec4f { return vertex_uniforms.value * VertexScale; } +fn read_fragment() -> vec4f { return fragment_uniforms.value * FragmentScale; } +@vertex fn vs() -> @builtin(position) vec4f { return read_vertex() + shared_uniforms.value; } +@fragment fn fs() -> @location(0) vec4f { return read_fragment() + shared_uniforms.value; } +@compute @workgroup_size(Width) fn cs() { output[0] = shared_uniforms.value; } +)"; + const struct + { + const char* EntryPoint; + SHADER_TYPE Stage; + const char* ResourceName; + const char* ConstantName; + Uint32 ConstantId; + } Cases[] = { + {"vs", SHADER_TYPE_VERTEX, "vertex_uniforms", "VertexScale", 7}, + {"fs", SHADER_TYPE_PIXEL, "fragment_uniforms", "FragmentScale", 9}, + {"cs", SHADER_TYPE_COMPUTE, "output", "Width", 11}, + }; + for (const auto& Test : Cases) + { + SCOPED_TRACE(Test.EntryPoint); + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Selected entry point", nullptr, Test.EntryPoint, nullptr, false, nullptr}; + EXPECT_STREQ(Resources.GetEntryPoint(), Test.EntryPoint); + EXPECT_EQ(Resources.GetShaderType(), Test.Stage); + ASSERT_EQ(Resources.GetTotalResources(), 2u); + EXPECT_EQ(Resources.GetNumUBs(), Test.Stage == SHADER_TYPE_COMPUTE ? 1u : 2u); + EXPECT_EQ(Resources.GetNumSBs(), Test.Stage == SHADER_TYPE_COMPUTE ? 1u : 0u); + for (Uint32 i = 0; i < Resources.GetTotalResources(); ++i) + { + const auto& Resource = Resources.GetResource(i); + EXPECT_STREQ(Resource.Name, Resource.BindIndex == 1 ? "shared_uniforms" : Test.ResourceName); + } + ASSERT_EQ(Resources.GetNumSpecConstants(), 1u); + EXPECT_STREQ(Resources.GetSpecConstant(0).Name, Test.ConstantName); + EXPECT_EQ(Resources.GetSpecConstant(0).OverrideId, Test.ConstantId); + EXPECT_TRUE(Resources.GetSpecConstant(0).IsIdSpecified); + } +} + +TEST(WGSLShaderResources, NativeStorageAndMultisampledTextures) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var Read: texture_storage_2d_array; +@group(0) @binding(1) var Write: texture_storage_3d; +@group(0) @binding(2) var ReadWrite: texture_storage_2d; +@group(0) @binding(3) var Depth: texture_depth_multisampled_2d; +@group(0) @binding(4) var MS: texture_multisampled_2d; +@group(0) @binding(5) var Output: array; +@group(0) @binding(6) var External: texture_external; +@compute @workgroup_size(1) fn main() { + Output[0] = textureLoad(Read, vec2i(0), 0).x + textureLoad(Depth, vec2i(0), 0) + + f32(textureLoad(MS, vec2i(0), 0).x) + textureLoad(External, vec2i(0)).x; + textureStore(Write, vec3i(0), vec4u(1)); + textureStore(ReadWrite, vec2i(0), textureLoad(ReadWrite, vec2i(0))); +} +)"; + const WGSLShaderResourceAttribs Expected[] = { + {"Read", WGSLResourceType::ROStorageTexture, 1, RESOURCE_DIM_TEX_2D_ARRAY, TEX_FORMAT_RGBA8_UNORM, WGSLSampleType::Float}, + {"Write", WGSLResourceType::WOStorageTexture, 1, RESOURCE_DIM_TEX_3D, TEX_FORMAT_RGBA32_UINT, WGSLSampleType::UInt}, + {"ReadWrite", WGSLResourceType::RWStorageTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_R32_SINT, WGSLSampleType::SInt}, + {"Depth", WGSLResourceType::DepthTextureMS, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_UNKNOWN, WGSLSampleType::Depth}, + {"MS", WGSLResourceType::TextureMS, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_UNKNOWN, WGSLSampleType::SInt}, + {"Output", WGSLResourceType::RWStorageBuffer, 1, RESOURCE_DIM_BUFFER}, + {"External", WGSLResourceType::ExternalTexture, 1, RESOURCE_DIM_TEX_2D, TEX_FORMAT_UNKNOWN, WGSLSampleType::Float}, + }; + const WGSLShaderResources Resources{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Native texture reflection", nullptr, "main", nullptr, false, nullptr}; + ASSERT_EQ(Resources.GetTotalResources(), 7u); + EXPECT_EQ(Resources.GetNumStTextures(), 3u); + EXPECT_EQ(Resources.GetNumTextures(), 2u); + EXPECT_EQ(Resources.GetNumExtTextures(), 1u); + EXPECT_EQ(Resources.GetNumSBs(), 1u); + for (Uint32 i = 0; i < Resources.GetTotalResources(); ++i) + { + const auto& Resource = Resources.GetResource(i); + SCOPED_TRACE(Resource.Name); + ASSERT_LT(Resource.BindIndex, 7u); + const auto& Ref = Expected[Resource.BindIndex]; + EXPECT_STREQ(Resource.Name, Ref.Name); + EXPECT_EQ(Resource.Type, Ref.Type); + EXPECT_EQ(Resource.ResourceDim, Ref.ResourceDim); + EXPECT_EQ(Resource.Format, Ref.Format); + EXPECT_EQ(Resource.SampleType, Ref.SampleType); + EXPECT_EQ(Resource.IsMultisample(), Resource.BindIndex == 3 || Resource.BindIndex == 4); + } +} + +TEST(WGSLShaderResources, InvalidSourceAndReflectionDiagnostics) +{ + const struct + { + const char* Source; + const char* EntryPoint; + const char* Error; + bool HasTintDiagnostics; + } Cases[] = { + {"invalid WGSL", "main", "Failed to parse shader source", true}, + {"fn helper() {}", nullptr, "does not contain any entry points", false}, + {"@compute @workgroup_size(1) fn main() {}", "missing", "Entry point 'missing' is not found", false}, + {"@compute @workgroup_size(1) fn main() {}\n/*$DILIGENT_WGSL_REFLECTION=invalid*/", + "main", "Invalid WGSL reflection metadata", false}, + }; + for (const auto& Test : Cases) + { + SCOPED_TRACE(Test.Error); + RefCntAutoPtr Diagnostics; + TestingEnvironment::ErrorScope Errors{Test.Error}; + EXPECT_THROW((WGSLShaderResources{GetRawAllocator(), Test.Source, SHADER_SOURCE_LANGUAGE_WGSL, + "Invalid reflection input", nullptr, Test.EntryPoint, nullptr, false, &Diagnostics}), + std::runtime_error); + if (Test.HasTintDiagnostics) + { + ASSERT_NE(Diagnostics, nullptr); + const char* Text = static_cast(Diagnostics->GetConstDataPtr()); + const std::string Message{Text}; + EXPECT_FALSE(Message.empty()); + ASSERT_EQ(Diagnostics->GetSize(), Message.size() + 1 + strlen(Test.Source) + 1); + EXPECT_STREQ(Text + Message.size() + 1, Test.Source); + } + else + { + EXPECT_EQ(Diagnostics, nullptr); + } } } diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index 88430770f6..834719deb2 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2024 Diligent Graphics LLC + * Copyright 2024-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,13 @@ */ #include "WGSLUtils.hpp" +#include "WGSLShaderResources.hpp" +#include "WGSLShaderReflection.hpp" +#include "SPIRVShaderResources.hpp" #include "GLSLangUtils.hpp" #include "DefaultShaderSourceStreamFactory.h" #include "RefCntAutoPtr.hpp" +#include "EngineMemory.h" #include "TestingEnvironment.hpp" #include "gtest/gtest.h" @@ -63,6 +67,160 @@ inline std::ostream& operator<<(std::ostream& os, const WGSLEmulatedResourceArra namespace { +TEST(WGSLUtils, ConvertSPIRVtoWGSLPreservesHLSLReflection) +{ + ShaderCreateInfo ShaderCI; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; + ShaderCI.Desc = {"HLSL reflection preservation test", SHADER_TYPE_VERTEX}; + ShaderCI.EntryPoint = "main"; + ShaderCI.Source = R"( +cbuffer Constants +{ + float Offset; + float3 Scale; +}; + +float4 main(float3 Position : ATTRIB0) : SV_Position +{ + return float4(Position * Scale + Offset, 1.0); +} +)"; + + GLSLangUtils::InitializeGlslang(); + auto SPIRV = GLSLangUtils::HLSLtoSPIRV(ShaderCI, GLSLangUtils::SpirvVersion::Vk110, nullptr, nullptr); + GLSLangUtils::FinalizeGlslang(); + ASSERT_FALSE(SPIRV.empty()); + + // This shader only uses SPIR-V 1.0 instructions. Keep its binary version at 1.0 + // to verify that Vulkan 1.1 relaxed block layout is not inferred from the version. + SPIRV[1] = 0x00010000u; + + const auto OriginalSPIRV = SPIRV; + const auto WGSL = ConvertSPIRVtoWGSL(SPIRV); + ASSERT_FALSE(WGSL.empty()); + EXPECT_EQ(SPIRV, OriginalSPIRV); + + tint::Source::File SrcFile{"", WGSL}; + tint::Program Program = tint::wgsl::reader::Parse(&SrcFile, {tint::wgsl::AllowedFeatures::Everything()}); + ASSERT_TRUE(Program.IsValid()) << Program.Diagnostics().Str(); + + SPIRVShaderResources::CreateInfo ResCI; + ResCI.ShaderType = SHADER_TYPE_VERTEX; + ResCI.Name = ShaderCI.Desc.Name; + ResCI.LoadShaderStageInputs = true; + const SPIRVShaderResources Resources{GetRawAllocator(), SPIRV, ResCI}; + ASSERT_EQ(Resources.GetNumShaderStageInputs(), 1u); + EXPECT_STREQ(Resources.GetShaderStageInputAttribs(0).Semantic, "ATTRIB0"); +} + + +std::vector CompileWGSLConversionTestShader() +{ + ShaderCreateInfo ShaderCI; + ShaderCI.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL; + ShaderCI.Desc = {"WGSL conversion test", SHADER_TYPE_PIXEL}; + ShaderCI.EntryPoint = "main"; + ShaderCI.Source = R"( +cbuffer Constants +{ + float4 Color; +}; +float4 main() : SV_Target +{ + return Color; +} +)"; + GLSLangUtils::InitializeGlslang(); + auto SPIRV = GLSLangUtils::HLSLtoSPIRV(ShaderCI, GLSLangUtils::SpirvVersion::Vk110, nullptr, nullptr); + GLSLangUtils::FinalizeGlslang(); + return SPIRV; +} + +TEST(WGSLUtils, ConvertSPIRVtoWGSLWithoutSourceMetadata) +{ + const auto SPIRV = CompileWGSLConversionTestShader(); + ASSERT_GT(SPIRV.size(), 5u); + + // Remove OpSource (opcode 3) and OpSourceContinued (opcode 2), retaining + // names and layout decorations. Supplied bytecode need not identify its + // original language; conversion must then use WGSL-derived reflection. + std::vector Stripped{SPIRV.begin(), SPIRV.begin() + 5}; + bool RemovedSource = false; + for (size_t Offset = 5; Offset < SPIRV.size();) + { + const Uint32 WordCount = SPIRV[Offset] >> 16; + const Uint32 OpCode = SPIRV[Offset] & 0xffffu; + ASSERT_NE(WordCount, 0u); + ASSERT_LE(WordCount, SPIRV.size() - Offset); + if (OpCode == 3 || OpCode == 2) + RemovedSource |= OpCode == 3; + else + Stripped.insert(Stripped.end(), SPIRV.begin() + Offset, SPIRV.begin() + Offset + WordCount); + Offset += WordCount; + } + ASSERT_TRUE(RemovedSource); + const auto OriginalStripped = Stripped; + for (bool LoadReflection : {false, true}) + { + SCOPED_TRACE(LoadReflection); + const std::string WGSL = ConvertSPIRVtoWGSL(Stripped, LoadReflection); + ASSERT_FALSE(WGSL.empty()); + EXPECT_EQ(Stripped, OriginalStripped); + + WGSLShaderReflection Metadata; + ASSERT_TRUE(ParseWGSLShaderReflection(WGSL, Metadata)); + EXPECT_TRUE(Metadata.Resources.empty()); + EXPECT_TRUE(Metadata.SpecializationConstants.empty()); + + // The legacy type-name alias still permits resource remapping after + // source metadata has been stripped from the input SPIR-V. + const auto Remapped = RemapWGSLResourceBindings(WGSL, {{"Constants", {2, 3}}}, nullptr, "main"); + ASSERT_FALSE(Remapped.empty()); + const WGSLShaderResources Resources{GetRawAllocator(), Remapped, SHADER_SOURCE_LANGUAGE_WGSL, + "Stripped source", nullptr, "main", nullptr, LoadReflection, nullptr}; + ASSERT_EQ(Resources.GetNumUBs(), 1u); + EXPECT_STREQ(Resources.GetUB(0).Name, "Constants"); + EXPECT_EQ(Resources.GetUB(0).BindGroup, 2u); + EXPECT_EQ(Resources.GetUB(0).BindIndex, 3u); + if (LoadReflection) + { + const auto* Buffer = Resources.GetUniformBufferDesc(0); + ASSERT_NE(Buffer, nullptr); + EXPECT_EQ(Buffer->Size, 16u); + ASSERT_EQ(Buffer->NumVariables, 1u); + EXPECT_STREQ(Buffer->pVariables[0].Name, "Color"); + EXPECT_EQ(Buffer->pVariables[0].BasicType, SHADER_CODE_BASIC_TYPE_FLOAT); + EXPECT_EQ(Buffer->pVariables[0].Class, SHADER_CODE_VARIABLE_CLASS_VECTOR); + } + } +} + +TEST(WGSLUtils, ConvertSPIRVtoWGSLRejectsMalformedInput) +{ + const auto SPIRV = CompileWGSLConversionTestShader(); + ASSERT_GT(SPIRV.size(), 5u); + for (Uint32 Case = 0; Case < 5; ++Case) + { + SCOPED_TRACE(Case); + auto Invalid = SPIRV; + switch (Case) + { + case 0: Invalid.clear(); break; + case 1: Invalid.resize(4); break; // Truncated module header. + case 2: Invalid[0] = 0; break; // Invalid magic number. + case 3: Invalid[5] &= 0xffffu; break; // Instruction with zero words. + case 4: Invalid[5] |= 0xffff0000u; break; // Instruction extends past the module. + } + const auto Original = Invalid; + // ErrorScope checks expected messages in reverse order. + TestingEnvironment::ErrorScope Errors{ + "Failed to prepare SPIR-V for Tint conversion", + "Spirv optimizer error"}; + EXPECT_TRUE(ConvertSPIRVtoWGSL(Invalid).empty()); + EXPECT_EQ(Invalid, Original); + } +} + TEST(WGSLUtils, GetWGSLEmulatedArrayElementInfo) { EXPECT_EQ(GetWGSLEmulatedArrayElement("", ""), WGSLEmulatedResourceArrayElement{}); @@ -71,12 +229,17 @@ TEST(WGSLUtils, GetWGSLEmulatedArrayElementInfo) EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D", "_"), WGSLEmulatedResourceArrayElement{"Tex2D"}); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_"}); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_X", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_X"}); + EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_-1", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_-1"}); + EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_+1", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_+1"}); + EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_2147483648", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_2147483648"}); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_123x", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_123x"}); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_123_", "_"), WGSLEmulatedResourceArrayElement{"Tex2D_123_"}); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_0", "_"), WGSLEmulatedResourceArrayElement("Tex2D", 0)); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_1", "_"), WGSLEmulatedResourceArrayElement("Tex2D", 1)); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_17", "_"), WGSLEmulatedResourceArrayElement("Tex2D", 17)); + EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_00017", "_"), WGSLEmulatedResourceArrayElement("Tex2D", 17)); + EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_2147483647", "_"), WGSLEmulatedResourceArrayElement("Tex2D", 2147483647)); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_0_5", "_"), WGSLEmulatedResourceArrayElement("Tex2D_0", 5)); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_1_18", "_"), WGSLEmulatedResourceArrayElement("Tex2D_1", 18)); EXPECT_EQ(GetWGSLEmulatedArrayElement("Tex2D_17_3", "_"), WGSLEmulatedResourceArrayElement("Tex2D_17", 3)); @@ -400,4 +563,569 @@ TEST(WGSLUtils, RWStructBufferArrays) }); } + +TEST(WGSLUtils, RemapReturnsGeneratedEntryPoint) +{ + const struct + { + const char* Description; + const char* WGSL; + const char* EntryPoint; + bool Renamed; + } Cases[] = { + { + "Name renamed while writing floating-point IR", + "@fragment fn f32() -> @location(0) vec4f { return vec4f(1.0); }", + "f32", + true, + }, + { + "Name renamed while generating the AST", + "@compute @workgroup_size(1) fn f32() {}", + "f32", + true, + }, + { + "Ordinary entry point name", + "@compute @workgroup_size(1) fn main() {}", + "main", + false, + }, + }; + for (const auto& Case : Cases) + { + SCOPED_TRACE(Case.Description); + std::string Current = Case.WGSL; + std::string RemappedEntryPoint = Case.EntryPoint; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + // The selected name may alias the output, including on subsequent remaps. + Current = RemapWGSLResourceBindings(Current, {}, nullptr, RemappedEntryPoint.c_str(), &RemappedEntryPoint); + ASSERT_FALSE(Current.empty()); + + tint::Source::File File{"", Current}; + auto Program = tint::wgsl::reader::Parse(&File, {tint::wgsl::AllowedFeatures::Everything()}); + ASSERT_TRUE(Program.IsValid()) << Program.Diagnostics().Str(); + tint::inspector::Inspector Inspector{Program}; + const auto EntryPoints = Inspector.GetEntryPoints(); + ASSERT_EQ(EntryPoints.size(), 1u); + EXPECT_EQ(RemappedEntryPoint, EntryPoints[0].name); + if (Case.Renamed) + EXPECT_NE(RemappedEntryPoint, Case.EntryPoint); + else + EXPECT_EQ(RemappedEntryPoint, Case.EntryPoint); + } + } +} + +TEST(WGSLUtils, RemapWholeModuleClearsEntryPointOutput) +{ + const std::string WGSL = R"( +@compute @workgroup_size(1) fn first() {} +@compute @workgroup_size(1) fn second() {} +)"; + std::string RemappedEntryPoint = "previous_entry_point"; + const std::string Remapped = RemapWGSLResourceBindings(WGSL, {}, nullptr, nullptr, &RemappedEntryPoint); + ASSERT_FALSE(Remapped.empty()); + EXPECT_TRUE(RemappedEntryPoint.empty()); + + tint::Source::File File{"", Remapped}; + auto Program = tint::wgsl::reader::Parse(&File, {tint::wgsl::AllowedFeatures::Everything()}); + ASSERT_TRUE(Program.IsValid()) << Program.Diagnostics().Str(); + tint::inspector::Inspector Inspector{Program}; + const auto EntryPoints = Inspector.GetEntryPoints(); + ASSERT_EQ(EntryPoints.size(), 2u); + EXPECT_NE(EntryPoints[0].name, EntryPoints[1].name); + for (const auto& EntryPoint : EntryPoints) + EXPECT_TRUE(EntryPoint.name == "first" || EntryPoint.name == "second"); +} + +TEST(WGSLUtils, RemapPreservesEntryPointOutputOnFailure) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var Output: array; +@compute @workgroup_size(1) fn main() { Output[0] = 1; } +)"; + std::string RemappedEntryPoint = "unchanged"; + { + TestingEnvironment::ErrorScope Errors{"Tint WGSL reader failure"}; + EXPECT_TRUE(RemapWGSLResourceBindings("invalid WGSL", {}, nullptr, "main", &RemappedEntryPoint).empty()); + EXPECT_EQ(RemappedEntryPoint, "unchanged"); + } + { + TestingEnvironment::ErrorScope Errors{"Binding for variable 'Output' is not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, "main", &RemappedEntryPoint).empty()); + EXPECT_EQ(RemappedEntryPoint, "unchanged"); + } +} + +TEST(WGSLUtils, RemapSelectedEntryPoint) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var FirstOutput: array; +@group(0) @binding(0) var SecondOutput: array; +override Shared: u32 = 1; +@id(7) override FirstValue: u32 = 2; +@id(9) override SecondValue: u32 = 3; +@compute @workgroup_size(1) fn first() { FirstOutput[0] = Shared + FirstValue; } +@compute @workgroup_size(1) fn second() { SecondOutput[0] = Shared + SecondValue; } +)"; + for (const char* EntryPoint : {"first", "second"}) + { + SCOPED_TRACE(EntryPoint); + const char* ResourceName = strcmp(EntryPoint, "first") == 0 ? "FirstOutput" : "SecondOutput"; + const WGSLResourceMapping Mapping{{ResourceName, {2, 3}}}; + std::string RemappedEntryPoint; + const std::string Remapped = RemapWGSLResourceBindings(WGSL, Mapping, nullptr, EntryPoint, &RemappedEntryPoint); + ASSERT_FALSE(Remapped.empty()); + tint::Source::File File{"", Remapped}; + auto Program = tint::wgsl::reader::Parse(&File, {tint::wgsl::AllowedFeatures::Everything()}); + ASSERT_TRUE(Program.IsValid()) << Program.Diagnostics().Str(); + tint::inspector::Inspector Inspector{Program}; + const auto EntryPoints = Inspector.GetEntryPoints(); + ASSERT_EQ(EntryPoints.size(), 1u); + EXPECT_EQ(EntryPoints[0].name, EntryPoint); + EXPECT_EQ(RemappedEntryPoint, EntryPoints[0].name); + EXPECT_EQ(EntryPoints[0].overrides.size(), 2u); + const auto Bindings = Inspector.GetResourceBindings(EntryPoint); + ASSERT_EQ(Bindings.size(), 1u); + EXPECT_EQ(Bindings[0].bind_group, 2u); + EXPECT_EQ(Bindings[0].binding, 3u); + EXPECT_EQ(Bindings[0].variable_name, ResourceName); + } + + { + TestingEnvironment::ErrorScope Errors{"Conflicting remappings for binding"}; + std::string RemappedEntryPoint = "unchanged"; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"FirstOutput", {1, 0}}, {"SecondOutput", {2, 0}}}, nullptr, nullptr, &RemappedEntryPoint).empty()); + EXPECT_EQ(RemappedEntryPoint, "unchanged"); + } + { + TestingEnvironment::ErrorScope Errors{"WGSL entry point 'missing' was not found"}; + std::string RemappedEntryPoint = "missing"; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, RemappedEntryPoint.c_str(), &RemappedEntryPoint).empty()); + EXPECT_EQ(RemappedEntryPoint, "missing"); + } +} + +TEST(WGSLUtils, RemapPreservesOverrideIds) +{ + const std::string WGSL = R"( +enable f16; +override UnusedImplicit: u32 = 9; +@id(0) override Explicit: u32 = 2; +override Named: u32 = 1; +override Floating: f32 = 1.5; +override Signed: i32 = -2; +override Half: f16 = 0.5h; +override Boolean: bool = true; +override Required: u32; +override Size: u32 = 4; +@id(65535) override Unused: u32 = 7; +var Shared: array; +@group(0) @binding(0) var Output: array; +@compute @workgroup_size(Size) fn main() { + Shared[0] = Named + Explicit + Required; + Output[0] = Shared[0] + u32(Floating) + u32(Signed) + u32(Half) + select(0u, 1u, Boolean); +} +)"; + const WGSLShaderResources Original{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Override remapping", nullptr, "main", nullptr, false, nullptr}; + ASSERT_EQ(Original.GetNumSpecConstants(), 8u); + for (Uint32 i = 0; i < Original.GetNumSpecConstants(); ++i) + EXPECT_EQ(Original.GetSpecConstant(i).IsIdSpecified, strcmp(Original.GetSpecConstant(i).Name, "Explicit") == 0); + + std::string Current = WGSL; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + Current = RemapWGSLResourceBindings(Current, {{"Output", {1, 2 + Pass}}}, nullptr, "main"); + ASSERT_FALSE(Current.empty()); + const WGSLShaderResources Remapped{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Remapped overrides", nullptr, "main", nullptr, false, nullptr}; + ASSERT_EQ(Remapped.GetNumSpecConstants(), Original.GetNumSpecConstants()); + for (Uint32 i = 0; i < Original.GetNumSpecConstants(); ++i) + { + const auto& Before = Original.GetSpecConstant(i); + bool Found = false; + for (Uint32 j = 0; j < Remapped.GetNumSpecConstants(); ++j) + { + const auto& After = Remapped.GetSpecConstant(j); + if (After.OverrideId == Before.OverrideId) + { + Found = true; + EXPECT_STREQ(After.Name, Before.Name); + EXPECT_EQ(After.GetType(), Before.GetType()); + EXPECT_TRUE(After.IsIdSpecified); + } + } + EXPECT_TRUE(Found) << Before.Name; + } + tint::Source::File File{"", Current}; + auto Program = tint::wgsl::reader::Parse(&File, {tint::wgsl::AllowedFeatures::Everything()}); + ASSERT_TRUE(Program.IsValid()) << Program.Diagnostics().Str(); + tint::inspector::Inspector Inspector{Program}; + const auto EntryPoints = Inspector.GetEntryPoints(); + ASSERT_EQ(EntryPoints.size(), 1u); + for (const auto& Override : EntryPoints[0].overrides) + EXPECT_EQ(Override.is_initialized, Override.name != "Required"); + } +} + +TEST(WGSLUtils, RemapRejectsInvalidResourceMappings) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var A: array; +@group(0) @binding(1) var B: array; +@compute @workgroup_size(1) fn main() { A[0] = B[0]; } +)"; + { + TestingEnvironment::ErrorScope Errors{"Binding for variable 'B' is not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"A", {0, 2}}}, nullptr, "main").empty()); + } + { + TestingEnvironment::ErrorScope Errors{"Resource binding collision"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"A", {0, 2}}, {"B", {0, 2}}}, nullptr, "main").empty()); + } + { + TestingEnvironment::ErrorScope Errors{"Invalid resource array size"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"A", {0, 2, 0}}, {"B", {0, 3}}}, nullptr, "main").empty()); + } + + const std::string ArrayWGSL = R"( +@group(0) @binding(0) var Buffers_1: array; +@compute @workgroup_size(1) fn main() { Buffers_1[0] = 1; } +)"; + { + TestingEnvironment::ErrorScope Errors{"Binding index overflows"}; + EXPECT_TRUE(RemapWGSLResourceBindings(ArrayWGSL, {{"Buffers", {0, 0xffffffffu, 2}}}, "_", "main").empty()); + } + { + TestingEnvironment::ErrorScope Errors{"Binding for variable 'Buffers_1' is not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(ArrayWGSL, {{"Buffers", {0, 0, 1}}}, "_", "main").empty()); + } +} + + +TEST(WGSLUtils, RemapPreservesNativeNames) +{ + // These legal WGSL identifiers shadow predeclared types, so Tint renames them. + const std::string WGSL = R"( +struct Uniforms { value: u32 } +@group(0) @binding(0) var f32: Uniforms; +@group(0) @binding(1) var vec4f: array; +override texture_storage_2d_array: u32 = 1; +@id(65535) override vec4u: u32 = 2; +@compute @workgroup_size(texture_storage_2d_array) fn main() { + vec4f[0] = f32.value + texture_storage_2d_array + vec4u; +} +@group(0) @binding(0) var OtherOutput: array; +override OtherValue: u32 = 3; +@compute @workgroup_size(1) fn other() { OtherOutput[0] = OtherValue; } +)"; + const WGSLShaderResources Original{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Native names", nullptr, "main", nullptr, true, nullptr}; + ASSERT_EQ(Original.GetNumSpecConstants(), 2u); + + std::string Current = WGSL; + for (Uint32 Pass = 0; Pass < 3; ++Pass) + { + SCOPED_TRACE(Pass); + Current = RemapWGSLResourceBindings(Current, {{"f32", {1, 2 + Pass}}, {"vec4f", {2, 3 + Pass}}}, nullptr, "main"); + ASSERT_FALSE(Current.empty()); + const WGSLShaderResources Remapped{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Remapped native names", nullptr, "main", nullptr, true, nullptr}; + ASSERT_EQ(Remapped.GetNumUBs(), 1u); + ASSERT_EQ(Remapped.GetNumSBs(), 1u); + EXPECT_STREQ(Remapped.GetUB(0).Name, "f32"); + EXPECT_EQ(Remapped.GetUB(0).BindGroup, 1u); + EXPECT_EQ(Remapped.GetUB(0).BindIndex, 2 + Pass); + EXPECT_STREQ(Remapped.GetSB(0).Name, "vec4f"); + EXPECT_EQ(Remapped.GetSB(0).BindGroup, 2u); + EXPECT_EQ(Remapped.GetSB(0).BindIndex, 3 + Pass); + ASSERT_EQ(Remapped.GetNumSpecConstants(), Original.GetNumSpecConstants()); + for (Uint32 i = 0; i < Original.GetNumSpecConstants(); ++i) + { + const auto& Before = Original.GetSpecConstant(i); + bool Found = false; + for (Uint32 j = 0; j < Remapped.GetNumSpecConstants(); ++j) + { + const auto& After = Remapped.GetSpecConstant(j); + if (After.OverrideId == Before.OverrideId) + { + Found = true; + EXPECT_STREQ(After.Name, Before.Name); + EXPECT_EQ(After.GetType(), Before.GetType()); + EXPECT_TRUE(After.IsIdSpecified); + } + } + EXPECT_TRUE(Found) << Before.Name; + } + if (::testing::Test::HasFailure()) + return; + } +} + + +TEST(WGSLUtils, RemapPreservesNamesAcrossEntryPoints) +{ + std::string Current = R"( +@group(0) @binding(0) var f32: array; +@group(0) @binding(1) var vec4f: array; +override texture_storage_2d_array: u32 = 1; +@compute @workgroup_size(1) fn first() { f32[0] = texture_storage_2d_array; } +@compute @workgroup_size(1) fn second() { vec4f[0] = texture_storage_2d_array; } +)"; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + Current = RemapWGSLResourceBindings(Current, {{"f32", {1, 2}}, {"vec4f", {2, 3}}}, nullptr); + ASSERT_FALSE(Current.empty()); + for (const char* EntryPoint : {"first", "second"}) + { + SCOPED_TRACE(EntryPoint); + const WGSLShaderResources Resources{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Multiple entry points", nullptr, EntryPoint, nullptr, false, nullptr}; + ASSERT_EQ(Resources.GetNumSBs(), 1u); + EXPECT_STREQ(Resources.GetSB(0).Name, strcmp(EntryPoint, "first") == 0 ? "f32" : "vec4f"); + ASSERT_EQ(Resources.GetNumSpecConstants(), 1u); + EXPECT_STREQ(Resources.GetSpecConstant(0).Name, "texture_storage_2d_array"); + EXPECT_TRUE(Resources.GetSpecConstant(0).IsIdSpecified); + } + } +} + +TEST(WGSLUtils, RemapRejectsConflictingResourceNames) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var FirstOutput: array; +@group(0) @binding(0) var SecondOutput: array; +@compute @workgroup_size(1) fn first() { FirstOutput[0] = 1; } +@compute @workgroup_size(1) fn second() { SecondOutput[0] = 2; } +)"; + // Binding-point metadata cannot represent different names at one location. + // Selecting either entry point is supported; a whole-module rewrite must not + // silently publish the first entry point's resource name for the second one. + TestingEnvironment::ErrorScope Errors{"Conflicting resource names at remapped binding"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"FirstOutput", {1, 0}}, {"SecondOutput", {1, 0}}}, nullptr).empty()); +} + + +TEST(WGSLUtils, RemapFiltersAndPreservesSourceMetadata) +{ + std::string WGSL = R"( +struct Uniforms { value: vec4u } +@group(0) @binding(0) var GeneratedUniform: Uniforms; +@group(0) @binding(1) var GeneratedOutput: array; +@group(0) @binding(2) var OtherOutput: array; +@id(7) override GeneratedValue: u32 = 1; +@id(8) override OtherValue: u32 = 2; +@compute @workgroup_size(1) fn main() { GeneratedOutput[0] = GeneratedUniform.value.x + GeneratedValue; } +@compute @workgroup_size(1) fn other() { OtherOutput[0] = OtherValue; } +)"; + WGSLShaderReflection Reflection; + auto& Uniform = Reflection.Resources[{0, 0}]; + Uniform.Name = "OriginalUniform"; + Uniform.UniformBuffer = std::make_unique(); + Uniform.UniformBuffer->Size = 16; + Uniform.UniformBuffer->AddVariable( + {"OriginalMember", "uint4", SHADER_CODE_VARIABLE_CLASS_VECTOR, SHADER_CODE_BASIC_TYPE_UINT, 1, 4, 0}); + Reflection.Resources[{0, 1}].Name = "OriginalOutput"; + Reflection.Resources[{0, 2}].Name = "OriginalOther"; + Reflection.Resources[{9, 9}].Name = "StaleResource"; + Reflection.SpecializationConstants = {{7, "OriginalValue"}, {8, "OtherValue"}, {9, "StaleOverride"}}; + AppendWGSLShaderReflection(WGSL, Reflection); + AppendShaderSourceLanguageDefinition(WGSL, SHADER_SOURCE_LANGUAGE_HLSL); + + std::string Current = WGSL; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + // Swap occupied bindings on the first pass. Original names must win over + // both generated identifiers and the legacy uniform-buffer type alias. + Current = RemapWGSLResourceBindings(Current, + {{"OriginalUniform", {0, 1 + Pass}}, {"OriginalOutput", {0, Pass}}, {"GeneratedUniform", {3, 4}}, {"GeneratedOutput", {3, 5}}, {"Uniforms", {3, 6}}}, + nullptr, "main"); + ASSERT_FALSE(Current.empty()); + EXPECT_EQ(ParseShaderSourceLanguageDefinition(Current), SHADER_SOURCE_LANGUAGE_HLSL); + + WGSLShaderReflection Remapped; + ASSERT_TRUE(ParseWGSLShaderReflection(Current, Remapped)); + ASSERT_EQ(Remapped.Resources.size(), 2u); + ASSERT_EQ(Remapped.SpecializationConstants.size(), 1u); + EXPECT_EQ(Remapped.SpecializationConstants.at(7), "OriginalValue"); + const auto* RemappedUniform = Remapped.Find(0, 1 + Pass); + ASSERT_NE(RemappedUniform, nullptr); + EXPECT_EQ(RemappedUniform->Name, "OriginalUniform"); + ASSERT_NE(RemappedUniform->UniformBuffer, nullptr); + EXPECT_EQ(*RemappedUniform->UniformBuffer, *Uniform.UniformBuffer); + const auto* RemappedOutput = Remapped.Find(0, Pass); + ASSERT_NE(RemappedOutput, nullptr); + EXPECT_EQ(RemappedOutput->Name, "OriginalOutput"); + + const WGSLShaderResources Resources{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Filtered metadata", nullptr, "main", nullptr, true, nullptr}; + ASSERT_EQ(Resources.GetNumUBs(), 1u); + ASSERT_EQ(Resources.GetNumSBs(), 1u); + EXPECT_STREQ(Resources.GetUB(0).Name, "OriginalUniform"); + EXPECT_STREQ(Resources.GetSB(0).Name, "OriginalOutput"); + const auto* ReflectedUniform = Resources.GetUniformBufferDesc(0); + ASSERT_NE(ReflectedUniform, nullptr); + EXPECT_EQ(*ReflectedUniform, *Uniform.UniformBuffer); + ASSERT_EQ(Resources.GetNumSpecConstants(), 1u); + EXPECT_STREQ(Resources.GetSpecConstant(0).Name, "OriginalValue"); + EXPECT_EQ(Resources.GetSpecConstant(0).OverrideId, 7u); + } + + // Metadata is authoritative: falling back to generated/type names would hide + // a caller using a stale resource layout after loading an archive. + TestingEnvironment::ErrorScope Errors{"Binding for variable 'OriginalUniform' is not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, + {{"GeneratedUniform", {0, 0}}, {"Uniforms", {0, 0}}, {"OriginalOutput", {0, 1}}}, + nullptr, "main") + .empty()); +} + +TEST(WGSLUtils, RemapPreservesEmulatedArrayMetadata) +{ + std::string Current = R"( +@group(0) @binding(0) var GeneratedFirst: array; +@group(0) @binding(1) var GeneratedLast: array; +@compute @workgroup_size(1) fn main() { GeneratedFirst[0] = GeneratedLast[0]; } +)"; + WGSLShaderReflection Reflection; + Reflection.Resources[{0, 0}].Name = "Buffers_elem_1"; + Reflection.Resources[{0, 1}].Name = "Buffers_elem_3"; + AppendWGSLShaderReflection(Current, Reflection); + + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + Current = RemapWGSLResourceBindings(Current, {{"Buffers", {2, 4 + Pass, 4}}}, "_elem_", "main"); + ASSERT_FALSE(Current.empty()); + WGSLShaderReflection Remapped; + ASSERT_TRUE(ParseWGSLShaderReflection(Current, Remapped)); + ASSERT_EQ(Remapped.Resources.size(), 2u); + const auto* First = Remapped.Find(2, 5 + Pass); + const auto* Last = Remapped.Find(2, 7 + Pass); + ASSERT_NE(First, nullptr); + ASSERT_NE(Last, nullptr); + EXPECT_EQ(First->Name, "Buffers_elem_1"); + EXPECT_EQ(Last->Name, "Buffers_elem_3"); + const WGSLShaderResources Resources{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Emulated array metadata", nullptr, "main", "_elem_", false, nullptr}; + ASSERT_EQ(Resources.GetNumSBs(), 1u); + EXPECT_STREQ(Resources.GetSB(0).Name, "Buffers"); + EXPECT_EQ(Resources.GetSB(0).ArraySize, 4u); + } +} + +TEST(WGSLUtils, RemapExactNamePrecedesArrayAlias) +{ + const std::string WGSL = R"( +@group(0) @binding(0) var Buffers_elem_2: array; +@compute @workgroup_size(1) fn main() { Buffers_elem_2[0] = 1; } +)"; + const std::string Remapped = RemapWGSLResourceBindings(WGSL, + {{"Buffers_elem_2", {1, 7}}, {"Buffers", {2, 10, 4}}}, "_elem_", "main"); + ASSERT_FALSE(Remapped.empty()); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(Remapped, Reflection)); + ASSERT_EQ(Reflection.Resources.size(), 1u); + const auto* Resource = Reflection.Find(1, 7); + ASSERT_NE(Resource, nullptr); + EXPECT_EQ(Resource->Name, "Buffers_elem_2"); + + for (const char* Suffix : {static_cast(nullptr), ""}) + { + // Without an array suffix the base name is not an alternative binding. + TestingEnvironment::ErrorScope Errors{"Binding for variable 'Buffers_elem_2' is not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"Buffers", {2, 10, 4}}}, Suffix, "main").empty()); + } +} + +TEST(WGSLUtils, RemapSharedResourceAcrossEntryPoints) +{ + std::string Current = R"( +@group(0) @binding(0) var f32: array; +override vec4u: u32 = 1; +@compute @workgroup_size(1) fn first() { f32[0] = vec4u; } +@compute @workgroup_size(1) fn second() { f32[1] = vec4u; } +)"; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + // Both entry points report the same resource and override. Their shared + // destination must not be rejected as a collision or duplicate metadata. + Current = RemapWGSLResourceBindings(Current, {{"f32", {1, 2 + Pass}}}, nullptr); + ASSERT_FALSE(Current.empty()); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(Current, Reflection)); + ASSERT_EQ(Reflection.Resources.size(), 1u); + ASSERT_EQ(Reflection.SpecializationConstants.size(), 1u); + for (const char* EntryPoint : {"first", "second"}) + { + const WGSLShaderResources Resources{GetRawAllocator(), Current, SHADER_SOURCE_LANGUAGE_WGSL, + "Shared resource", nullptr, EntryPoint, nullptr, false, nullptr}; + ASSERT_EQ(Resources.GetNumSBs(), 1u); + EXPECT_STREQ(Resources.GetSB(0).Name, "f32"); + EXPECT_EQ(Resources.GetSB(0).BindGroup, 1u); + EXPECT_EQ(Resources.GetSB(0).BindIndex, 2 + Pass); + ASSERT_EQ(Resources.GetNumSpecConstants(), 1u); + EXPECT_STREQ(Resources.GetSpecConstant(0).Name, "vec4u"); + } + } +} + +TEST(WGSLUtils, RemapRejectsMalformedMetadata) +{ + // The source is valid WGSL; a bad metadata comment must fail the remap + // instead of silently losing application-facing names. + const std::string WGSL = "@compute @workgroup_size(1) fn main() {}\n" + "/*$DILIGENT_WGSL_REFLECTION=1 0 0 1 zz 0*/"; + std::string EntryPoint = "main"; + TestingEnvironment::ErrorScope Errors{"Invalid WGSL reflection metadata"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, EntryPoint.c_str(), &EntryPoint).empty()); + EXPECT_EQ(EntryPoint, "main"); +} + + +TEST(WGSLUtils, RemapLegacyBufferAliases) +{ + // Older archives have no source metadata: generated variable names may use + // the buffer type name, an extra suffix, or a type with an "_atomic" suffix. + std::string Current = R"( +struct Constants { value: u32 } +struct Buffers_elem_2 { values: array } +struct Counters_atomic { values: array> } +@group(0) @binding(0) var GeneratedUniform: Constants; +@group(0) @binding(1) var Buffers_elem_2_1: Buffers_elem_2; +@group(0) @binding(2) var Counters_1: Counters_atomic; +@compute @workgroup_size(1) fn main() { + Buffers_elem_2_1.values[0] = GeneratedUniform.value; + atomicStore(&Counters_1.values[0], GeneratedUniform.value); +} +)"; + for (Uint32 Pass = 0; Pass < 2; ++Pass) + { + SCOPED_TRACE(Pass); + Current = RemapWGSLResourceBindings(Current, + {{"Constants", {1, Pass}}, {"Buffers", {1, 3 + Pass, 3}}, {"Counters", {2, 5 + Pass}}}, + "_elem_", "main"); + ASSERT_FALSE(Current.empty()); + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(Current, Reflection)); + ASSERT_EQ(Reflection.Resources.size(), 3u); + const auto* Uniform = Reflection.Find(1, Pass); + const auto* Buffer = Reflection.Find(1, 5 + Pass); + const auto* Atomic = Reflection.Find(2, 5 + Pass); + ASSERT_NE(Uniform, nullptr); + ASSERT_NE(Buffer, nullptr); + ASSERT_NE(Atomic, nullptr); + EXPECT_EQ(Uniform->Name, "Constants"); + EXPECT_EQ(Buffer->Name, "Buffers_elem_2"); + EXPECT_EQ(Atomic->Name, "Counters"); + } +} + } // namespace diff --git a/Tests/GPUTestFramework/CMakeLists.txt b/Tests/GPUTestFramework/CMakeLists.txt index 6f7f19baba..b5f2aae3e1 100644 --- a/Tests/GPUTestFramework/CMakeLists.txt +++ b/Tests/GPUTestFramework/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.17) +cmake_minimum_required (VERSION 3.17...3.22) include(../../BuildTools/CMake/BuildUtils.cmake) diff --git a/Tests/GPUTestFramework/include/WebGPU/TestingEnvironmentWebGPU.hpp b/Tests/GPUTestFramework/include/WebGPU/TestingEnvironmentWebGPU.hpp index f71eaea3e8..034ddcb6d4 100644 --- a/Tests/GPUTestFramework/include/WebGPU/TestingEnvironmentWebGPU.hpp +++ b/Tests/GPUTestFramework/include/WebGPU/TestingEnvironmentWebGPU.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2019-2025 Diligent Graphics LLC + * Copyright 2019-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,24 +59,6 @@ class TestingEnvironmentWebGPU final : public GPUTestingEnvironment WGPUDevice m_wgpuDevice = nullptr; }; -#if PLATFORM_WEB - -using WGPUShaderSourceWGSL = WGPUShaderModuleWGSLDescriptor; -using WGPUStringView = const char*; - -constexpr WGPUSType WGPUSType_ShaderSourceWGSL = WGPUSType_ShaderModuleWGSLDescriptor; - -inline WGPUStringView GetWGPUStringView(const char* Str) -{ - return Str; -} - -inline WGPUStringView GetWGPUStringView(const std::string& Str) -{ - return Str.c_str(); -} - -#else template WGPUStringView GetWGPUStringView(const char (&Str)[Size]) @@ -89,7 +71,6 @@ inline WGPUStringView GetWGPUStringView(const std::string& Str) return {Str.c_str(), Str.length()}; } -#endif } // namespace Testing diff --git a/Tests/GPUTestFramework/src/WebGPU/TestingEnvironmentWebGPU.cpp b/Tests/GPUTestFramework/src/WebGPU/TestingEnvironmentWebGPU.cpp index 57cb8f9edf..ffb7025716 100644 --- a/Tests/GPUTestFramework/src/WebGPU/TestingEnvironmentWebGPU.cpp +++ b/Tests/GPUTestFramework/src/WebGPU/TestingEnvironmentWebGPU.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Diligent Graphics LLC + * Copyright 2023-2026 Diligent Graphics LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,6 @@ #if !PLATFORM_WEB # include -#else -# include #endif @@ -102,22 +100,30 @@ void TestingEnvironmentWebGPU::SubmitCommandEncoder(WGPUCommandEncoder wgpuCmdEn if (WaitForIdle) { +#if PLATFORM_WEB + LOG_ERROR_MESSAGE("Waiting for submitted WebGPU work is not supported on the Web. Use non-blocking synchronization methods."); +#else bool IsWorkDone = false; - auto WorkDoneCallback = [](WGPUQueueWorkDoneStatus Status, void* pUserData) { + auto WorkDoneCallback = [](WGPUQueueWorkDoneStatus Status, WGPUStringView Message, void* pUserData, void* pUserData2) { if (bool* pIsWorkDone = static_cast(pUserData)) - *pIsWorkDone = Status == WGPUQueueWorkDoneStatus_Success; + *pIsWorkDone = true; if (Status != WGPUQueueWorkDoneStatus_Success) DEV_ERROR("Failed wgpuQueueOnSubmittedWorkDone: ", Status); }; - wgpuQueueOnSubmittedWorkDone(wgpuCmdQueue, WorkDoneCallback, &IsWorkDone); + WGPUQueueWorkDoneCallbackInfo CallbackInfo{}; + CallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; + CallbackInfo.callback = WorkDoneCallback; + CallbackInfo.userdata1 = &IsWorkDone; + wgpuQueueOnSubmittedWorkDone(wgpuCmdQueue, CallbackInfo); + + RefCntAutoPtr pDeviceWebGPU{m_pDevice, IID_RenderDeviceWebGPU}; while (!IsWorkDone) { -#if !PLATFORM_WEB - wgpuDeviceTick(m_wgpuDevice); -#endif + wgpuInstanceProcessEvents(pDeviceWebGPU->GetWebGPUInstance()); } +#endif } wgpuQueueRelease(wgpuCmdQueue); diff --git a/Tests/GPUTestFramework/src/WebGPU/TestingSwapChainWebGPU.cpp b/Tests/GPUTestFramework/src/WebGPU/TestingSwapChainWebGPU.cpp index eb8155317b..aee4acffeb 100644 --- a/Tests/GPUTestFramework/src/WebGPU/TestingSwapChainWebGPU.cpp +++ b/Tests/GPUTestFramework/src/WebGPU/TestingSwapChainWebGPU.cpp @@ -32,6 +32,7 @@ #include "DeviceContextWebGPU.h" #include "TextureWebGPU.h" #include "Align.hpp" +#include "../../../../Graphics/GraphicsEngineWebGPU/include/WebGPUDefinitions.hpp" #if PLATFORM_WEB # include @@ -192,12 +193,12 @@ void TestingSwapChainWebGPU::TakeSnapshot(ITexture* pCopyFrom) WGPUCommandEncoderDescriptor wgpuCmdEncoderDesc{}; WGPUCommandEncoder wgpuCmdEncoder = wgpuDeviceCreateCommandEncoder(m_wgpuDevice, &wgpuCmdEncoderDesc); - WGPUImageCopyTexture wgpuImageCopySrc{}; + WGPUTexelCopyTextureInfo wgpuImageCopySrc{}; wgpuImageCopySrc.mipLevel = 0; wgpuImageCopySrc.texture = wgpuSrcTexture; wgpuImageCopySrc.origin = {0, 0, 0}; - WGPUImageCopyBuffer wgpuImageCopyDst{}; + WGPUTexelCopyBufferInfo wgpuImageCopyDst{}; wgpuImageCopyDst.layout.offset = 0; wgpuImageCopyDst.layout.bytesPerRow = m_StagingRowPitch; wgpuImageCopyDst.layout.rowsPerImage = m_SwapChainDesc.Height; @@ -217,37 +218,40 @@ void TestingSwapChainWebGPU::TakeSnapshot(ITexture* pCopyFrom) wgpuCommandEncoderRelease(wgpuCmdEncoder); wgpuCommandBufferRelease(wgpuCmdBuffer); - const size_t DataSize = size_t{m_StagingRowPitch} * m_SwapChainDesc.Height; - wgpuBufferMapAsync( - m_wgpuStagingBuffer, WGPUMapMode_Read, 0, DataSize, [](WGPUBufferMapAsyncStatus MapStatus, void* pUserData) { - if (MapStatus == WGPUBufferMapAsyncStatus_Success) - { - const auto pThis = static_cast(pUserData); - - pThis->m_ReferenceDataPitch = pThis->m_SwapChainDesc.Width * 4; - pThis->m_ReferenceData.resize(pThis->m_ReferenceDataPitch * pThis->m_SwapChainDesc.Height); - - const auto* pMappedData = static_cast(wgpuBufferGetConstMappedRange( - pThis->m_wgpuStagingBuffer, 0, size_t{pThis->m_StagingRowPitch} * pThis->m_SwapChainDesc.Height)); - VERIFY_EXPR(pMappedData != nullptr); - - for (Uint32 Row = 0; Row < pThis->m_SwapChainDesc.Height; ++Row) - { - memcpy(pThis->m_ReferenceData.data() + size_t{Row} * pThis->m_ReferenceDataPitch, - pMappedData + size_t{Row} * pThis->m_StagingRowPitch, - pThis->m_ReferenceDataPitch); - } - wgpuBufferUnmap(pThis->m_wgpuStagingBuffer); - } - else + const size_t DataSize = size_t{m_StagingRowPitch} * m_SwapChainDesc.Height; + WGPUBufferMapCallbackInfo CallbackInfo{}; + CallbackInfo.mode = DefaultWGPUCallbackMode; + CallbackInfo.userdata1 = this; + CallbackInfo.callback = [](WGPUMapAsyncStatus MapStatus, WGPUStringView Message, void* pUserData, void* pUserData2) { + if (MapStatus == WGPUMapAsyncStatus_Success) + { + const auto pThis = static_cast(pUserData); + + pThis->m_ReferenceDataPitch = pThis->m_SwapChainDesc.Width * 4; + pThis->m_ReferenceData.resize(pThis->m_ReferenceDataPitch * pThis->m_SwapChainDesc.Height); + + const auto* pMappedData = static_cast(wgpuBufferGetConstMappedRange( + pThis->m_wgpuStagingBuffer, 0, size_t{pThis->m_StagingRowPitch} * pThis->m_SwapChainDesc.Height)); + VERIFY_EXPR(pMappedData != nullptr); + + for (Uint32 Row = 0; Row < pThis->m_SwapChainDesc.Height; ++Row) { - ADD_FAILURE() << "Failing to map staging buffer"; + memcpy(pThis->m_ReferenceData.data() + size_t{Row} * pThis->m_ReferenceDataPitch, + pMappedData + size_t{Row} * pThis->m_StagingRowPitch, + pThis->m_ReferenceDataPitch); } - }, - this); + wgpuBufferUnmap(pThis->m_wgpuStagingBuffer); + } + else + { + ADD_FAILURE() << "Failing to map staging buffer"; + } + }; + wgpuBufferMapAsync(m_wgpuStagingBuffer, WGPUMapMode_Read, 0, DataSize, CallbackInfo); #if !PLATFORM_WEB - wgpuDeviceTick(m_wgpuDevice); + RefCntAutoPtr pDeviceWebGPU{m_pDevice, IID_RenderDeviceWebGPU}; + wgpuInstanceProcessEvents(pDeviceWebGPU->GetWebGPUInstance()); #endif } diff --git a/Tests/IncludeTest/CMakeLists.txt b/Tests/IncludeTest/CMakeLists.txt index 90b6c6e55d..81d5b8eec1 100644 --- a/Tests/IncludeTest/CMakeLists.txt +++ b/Tests/IncludeTest/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.10) +cmake_minimum_required (VERSION 3.10...3.22) project(DiligentCore-IncludeTest) @@ -59,7 +59,11 @@ target_link_libraries(DiligentCore-IncludeTest PRIVATE Diligent-BuildSettings) set_common_target_properties(DiligentCore-IncludeTest) if (WEBGPU_SUPPORTED) - target_link_libraries(DiligentCore-IncludeTest PUBLIC dawn_headers) + if (PLATFORM_WEB) + target_link_libraries(DiligentCore-IncludeTest PUBLIC emdawnwebgpu_c_include) + else() + target_link_libraries(DiligentCore-IncludeTest PUBLIC dawn_headers) + endif() endif() if(MSVC) diff --git a/ThirdParty/CMakeLists.txt b/ThirdParty/CMakeLists.txt index 01a5ef66d7..83655bbafe 100644 --- a/ThirdParty/CMakeLists.txt +++ b/ThirdParty/CMakeLists.txt @@ -65,6 +65,8 @@ if (VULKAN_SUPPORTED OR METAL_SUPPORTED OR WEBGPU_SUPPORTED OR (ARCHIVER_SUPPORT set_target_properties(${SPIRV_TOOLS_EXCLUDE_TARGETS} PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1) set(THIRD_PARTY_LIBS SPIRV-Tools-static SPIRV-Tools-opt) + # Tint's SPIR-V reader links these libraries into threaded WebAssembly builds. + set_targets_emscripten_properties(SPIRV-Tools-static SPIRV-Tools-opt) install(FILES "${spirv-tools_SOURCE_DIR}/LICENSE" DESTINATION "Licenses/ThirdParty/${DILIGENT_CORE_DIR}" RENAME SPIRV-Tools-License.txt) endif() @@ -115,8 +117,11 @@ if (VULKAN_SUPPORTED OR METAL_SUPPORTED OR WEBGPU_SUPPORTED OR (ARCHIVER_SUPPORT set_directory_root_folder("SPIRV-Cross" "DiligentCore/ThirdParty/SPIRV-Cross") list(APPEND THIRD_PARTY_LIBS spirv-cross-core) + # SPIRV-Cross libraries must support the shared memory used by threaded WebAssembly modules. + set_targets_emscripten_properties(spirv-cross-core) if(SPIRV_CROSS_ENABLE_GLSL) list(APPEND THIRD_PARTY_LIBS spirv-cross-glsl) + set_targets_emscripten_properties(spirv-cross-glsl) endif() if (SPIRV_CROSS_ENABLE_MSL) list(APPEND THIRD_PARTY_LIBS spirv-cross-msl) diff --git a/ThirdParty/abseil-cpp/CMakeLists.txt b/ThirdParty/abseil-cpp/CMakeLists.txt index ec10f14967..a4015d4666 100644 --- a/ThirdParty/abseil-cpp/CMakeLists.txt +++ b/ThirdParty/abseil-cpp/CMakeLists.txt @@ -3,7 +3,8 @@ cmake_minimum_required (VERSION 3.11) FetchContent_DeclareShallowGit( abseil-cpp GIT_REPOSITORY https://chromium.googlesource.com/chromium/src/third_party/abseil-cpp - GIT_TAG 07d2ef8bd61ab88f0b81b0d8c7fb2c7e19b1d01e + # Keep this revision aligned with the Abseil dependency of Dawn. + GIT_TAG af5aede14a0b4013be6890148d34e0fd7b1fac69 ) set(CMAKE_CXX_STANDARD 17) @@ -16,5 +17,13 @@ if(DILIGENT_SANITIZER_COMPILE_OPTIONS) add_compile_options(${DILIGENT_SANITIZER_COMPILE_OPTIONS}) endif() +# Abseil is linked into shared libraries such as the archiver. +set(CMAKE_POSITION_INDEPENDENT_CODE ON) FetchContent_MakeAvailable(abseil-cpp) + +if (PLATFORM_WEB) + find_targets_in_directory(ABSEIL_TARGETS ${abseil-cpp_SOURCE_DIR}) + set_targets_emscripten_properties(${ABSEIL_TARGETS}) +endif() + install(FILES "${abseil-cpp_SOURCE_DIR}/LICENSE" DESTINATION "Licenses/ThirdParty/${DILIGENT_CORE_DIR}" RENAME abseil-cpp-License.txt) diff --git a/ThirdParty/dawn/CMakeLists.txt b/ThirdParty/dawn/CMakeLists.txt index 21bc0f24b3..61fdd6f00a 100644 --- a/ThirdParty/dawn/CMakeLists.txt +++ b/ThirdParty/dawn/CMakeLists.txt @@ -1,26 +1,20 @@ -cmake_minimum_required (VERSION 3.11) +cmake_minimum_required (VERSION 3.22) -find_package(Python3 REQUIRED) - -set(JINJA2_INSTALL_CMD ${Python3_EXECUTABLE} -m pip install --upgrade jinja2>=3.0) - -if(${Python3_VERSION} VERSION_GREATER_EQUAL "3.12") - set(JINJA2_INSTALL_CMD ${JINJA2_INSTALL_CMD} --break-system-packages) -endif() - -execute_process(COMMAND ${JINJA2_INSTALL_CMD} - RESULT_VARIABLE PYTHON_PIP_JINJIA_RESULT) -if(NOT PYTHON_PIP_JINJIA_RESULT EQUAL "0") - message(FATAL_ERROR "Command '${JINJA2_INSTALL_CMD}' failed with error code ${PYTHON_PIP_JINJIA_RESULT}") -endif() +set(DAWN_SUBMODULES + third_party/jinja2 + third_party/markupsafe + third_party/webgpu-headers/src +) FetchContent_DeclareShallowGit( dawn - GIT_REPOSITORY https://dawn.googlesource.com/dawn - GIT_TAG db55f595741106be83b823a02ebdc58efbe997f1 + GIT_REPOSITORY https://github.com/DiligentGraphics/dawn.git + # Work around the MSVC x86 queue serial return miscompilation. + GIT_TAG msvc-x86-queue-serial-fix + GIT_SUBMODULES "${DAWN_SUBMODULES}" ) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -29,15 +23,17 @@ if (MSVC) add_compile_options("$<$:/Ob1>") endif() -if (PLATFORM_WEB) - set(TINT_BUILD_AS_OTHER_OS ON CACHE BOOL "" FORCE) +set(TINT_BUILD_AS_OTHER_OS ${PLATFORM_WEB} CACHE BOOL "" FORCE) +# Keep SPIR-V conversion disabled when both source compilers are disabled. +# Otherwise, require the optimizer target used to strip reflection metadata. +if (TARGET SPIRV-Tools-opt AND NOT (DILIGENT_NO_GLSLANG AND DILIGENT_NO_HLSL)) + set(TINT_BUILD_SPV_READER ON CACHE BOOL "" FORCE) else() - set(TINT_BUILD_AS_OTHER_OS OFF CACHE BOOL "" FORCE) + set(TINT_BUILD_SPV_READER OFF CACHE BOOL "" FORCE) endif() -set(TINT_BUILD_SPV_READER ON CACHE BOOL "" FORCE) -set(TINT_BUILD_WGSL_WRITER ON CACHE BOOL "" FORCE) -set(TINT_BUILD_WGSL_READER ON CACHE BOOL "" FORCE) -set(TINT_BUILD_IR ON CACHE BOOL "" FORCE) +set(TINT_BUILD_WGSL_WRITER ON CACHE BOOL "" FORCE) +set(TINT_BUILD_WGSL_READER ON CACHE BOOL "" FORCE) +set(TINT_BUILD_IR_BINARY OFF CACHE BOOL "" FORCE) if (PLATFORM_WEB) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) else() @@ -54,37 +50,48 @@ set(TINT_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(TINT_BUILD_DOCS OFF CACHE BOOL "" FORCE) set(TINT_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) -set(DAWN_USE_X11 OFF CACHE BOOL "" FORCE) -set(DAWN_USE_WAYLAND OFF CACHE BOOL "" FORCE) +set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) +set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(DAWN_BUILD_PROTOBUF OFF CACHE BOOL "" FORCE) +set(DAWN_BUILD_MONOLITHIC_LIBRARY OFF CACHE STRING "" FORCE) +set(DAWN_USE_X11 OFF CACHE BOOL "" FORCE) +set(DAWN_USE_WAYLAND OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_DESKTOP_GL OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) -set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) -# Dawn's CMake defines DAWN_EMSCRIPTEN_TOOLCHAIN through the option command, which -# sets its default value to OFF instead of "" -set(DAWN_EMSCRIPTEN_TOOLCHAIN "" CACHE STRING "" FORCE) if (PLATFORM_WEB) - set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) else() set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) endif() set(DAWN_USE_GLFW OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) -set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) +set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_SPIRV_VALIDATION OFF CACHE BOOL "" FORCE) set(DAWN_SPIRV_TOOLS_DIR "${spirv-tools_SOURCE_DIR}" CACHE STRING "Directory in which to find SPIRV-Tools" FORCE) set(DAWN_SPIRV_HEADERS_DIR "${SPIRV-Headers_SOURCE_DIR}" CACHE STRING "Directory in which to find SPIRV-Headers" FORCE) -set(DAWN_ABSEIL_DIR "${abseil-cpp_SOURCE_DIR}" CACHE STRING "Directory in which to find Abseil" FORCE) set(BUILD_SHARED_LIBS OFF) +# Reuse the Abseil dependency configured by Diligent for all consumers. +FetchContent_GetProperties(abseil-cpp) +set(DAWN_ABSEIL_DIR "${abseil-cpp_SOURCE_DIR}" CACHE STRING "Directory in which to find Abseil" FORCE) + FetchContent_MakeAvailable(dawn) +if (DAWN_ENABLE_D3D12 AND NOT DAWN_USE_BUILT_DXC) + # Dawn guards DXC support with DAWN_USE_BUILT_DXC, even when loading the compiler + # dynamically. Enable this code while reusing the Windows SDK DLLs supplied by + # Diligent, without adding Dawn's DXC source build. + foreach(DAWN_DXC_TARGET IN ITEMS dawn_native dawn_native_objects dawn_platform) + target_compile_definitions(${DAWN_DXC_TARGET} PRIVATE DAWN_USE_BUILT_DXC) + endforeach() +endif() + if (PLATFORM_WEB) find_targets_in_directory(DAWN_TARGETS ${dawn_SOURCE_DIR}) set_targets_emscripten_properties(${DAWN_TARGETS}) @@ -95,4 +102,4 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_compile_options(tint_utils_strconv PRIVATE -Wno-nullability-extension) endif() -install(FILES "${Dawn_SOURCE_DIR}/LICENSE" DESTINATION "Licenses/ThirdParty/${DILIGENT_CORE_DIR}" RENAME dawn-License.txt) +install(FILES "${dawn_SOURCE_DIR}/LICENSE" DESTINATION "Licenses/ThirdParty/${DILIGENT_CORE_DIR}" RENAME dawn-License.txt)