From 55fdd2c7c7c187b492d909b7f1b5900b5f4e86b1 Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 5 Oct 2026 00:16:16 -0700 Subject: [PATCH 01/21] WebGPU: update Dawn dependencies and CMake integration Update Dawn to v20261002.154047, align shared Abseil, and adjust native and Emscripten build configuration. WebGPU backend API migration will follow separately. --- BuildTools/CMake/BuildUtils.cmake | 7 +++ Graphics/Archiver/CMakeLists.txt | 2 +- Graphics/GraphicsEngineWebGPU/CMakeLists.txt | 8 ++- Graphics/GraphicsTools/CMakeLists.txt | 2 +- Graphics/ShaderTools/CMakeLists.txt | 6 +- Tests/DiligentCoreAPITest/CMakeLists.txt | 2 +- Tests/GPUTestFramework/CMakeLists.txt | 2 +- Tests/IncludeTest/CMakeLists.txt | 2 +- ThirdParty/CMakeLists.txt | 2 + ThirdParty/abseil-cpp/CMakeLists.txt | 9 ++- ThirdParty/dawn/CMakeLists.txt | 64 +++++++++----------- 11 files changed, 60 insertions(+), 46 deletions(-) diff --git a/BuildTools/CMake/BuildUtils.cmake b/BuildTools/CMake/BuildUtils.cmake index 4c8c6f7488..ce2c1ea0b7 100644 --- a/BuildTools/CMake/BuildUtils.cmake +++ b/BuildTools/CMake/BuildUtils.cmake @@ -522,6 +522,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 +542,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/GraphicsEngineWebGPU/CMakeLists.txt b/Graphics/GraphicsEngineWebGPU/CMakeLists.txt index 518e0a80fa..c9de9b4c44 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) @@ -134,7 +134,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() @@ -179,7 +181,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/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..8f7227ca9e 100644 --- a/Graphics/ShaderTools/CMakeLists.txt +++ b/Graphics/ShaderTools/CMakeLists.txt @@ -249,7 +249,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/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/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/IncludeTest/CMakeLists.txt b/Tests/IncludeTest/CMakeLists.txt index 90b6c6e55d..b47682a1f2 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) diff --git a/ThirdParty/CMakeLists.txt b/ThirdParty/CMakeLists.txt index 01a5ef66d7..178a8a57a8 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() diff --git a/ThirdParty/abseil-cpp/CMakeLists.txt b/ThirdParty/abseil-cpp/CMakeLists.txt index ec10f14967..c0b15eb603 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) @@ -17,4 +18,10 @@ if(DILIGENT_SANITIZER_COMPILE_OPTIONS) endif() 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..bfd4272980 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 + # Dawn and Emdawnwebgpu snapshot v20261002.154047. + GIT_TAG b1236a9bb4a47f1262bc3acb253bb1c2181e90dd + 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,11 @@ if (MSVC) add_compile_options("$<$:/Ob1>") endif() -if (PLATFORM_WEB) - set(TINT_BUILD_AS_OTHER_OS ON CACHE BOOL "" FORCE) -else() - set(TINT_BUILD_AS_OTHER_OS 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_AS_OTHER_OS ${PLATFORM_WEB} CACHE BOOL "" FORCE) +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_BINARY OFF CACHE BOOL "" FORCE) if (PLATFORM_WEB) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) else() @@ -54,35 +44,37 @@ 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 (PLATFORM_WEB) @@ -95,4 +87,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) From a14c23ccd030e11e5ceb3b56436b0e484df4917a Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 5 Oct 2026 00:44:56 -0700 Subject: [PATCH 02/21] WebGPU: adapt shader conversion and reflection to updated Tint APIs --- .../ShaderTools/src/WGSLShaderResources.cpp | 27 +++++----- Graphics/ShaderTools/src/WGSLUtils.cpp | 49 ++++++++++--------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Graphics/ShaderTools/src/WGSLShaderResources.cpp b/Graphics/ShaderTools/src/WGSLShaderResources.cpp index 2ad22f43bb..cea71c32c2 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. @@ -82,10 +82,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 +97,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 +150,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 +162,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 +221,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 +346,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 +494,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)) @@ -875,7 +874,6 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, break; case TintResourceType::kSampler: - case TintResourceType::kComparisonSampler: ++ResCounters.NumSamplers; break; @@ -960,7 +958,6 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, break; case TintResourceType::kSampler: - case TintResourceType::kComparisonSampler: { new (&GetSampler(CurrRes.NumSamplers++)) WGSLShaderResourceAttribs{Name, Binding, ArraySize}; } diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index f528f126f7..57ee458db3 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. @@ -37,6 +37,7 @@ #define TINT_BUILD_SPV_READER 1 #define TINT_BUILD_WGSL_WRITER 1 #include +#include "src/tint/lang/core/ir/transform/binding_remapper.h" #include "src/tint/lang/wgsl/ast/module.h" #include "src/tint/lang/wgsl/ast/identifier_expression.h" #include "src/tint/lang/wgsl/ast/identifier.h" @@ -91,23 +92,18 @@ WGSLEmulatedResourceArrayElement GetWGSLEmulatedArrayElement(const std::string& std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) { - tint::spirv::reader::Options SPIRVReaderOptions{true, {tint::wgsl::AllowedFeatures::Everything()}}; - tint::Program Program = Read(SPIRV, SPIRVReaderOptions); + tint::wgsl::writer::Options WGSLWriterOptions; + WGSLWriterOptions.allow_non_uniform_derivatives = true; + WGSLWriterOptions.allowed_features = tint::wgsl::AllowedFeatures::Everything(); - if (!Program.IsValid()) - { - LOG_ERROR_MESSAGE("Tint SPIR-V reader failure:\nParser: " + Program.Diagnostics().Str() + "\n"); - return {}; - } - - auto GenerationResult = tint::wgsl::writer::Generate(Program, {}); + auto GenerationResult = tint::SpirvToWgsl(SPIRV, WGSLWriterOptions); if (GenerationResult != tint::Success) { - LOG_ERROR_MESSAGE("Tint WGSL writer failure:\nGeneate: " + GenerationResult.Failure().reason.Str() + "\n"); + LOG_ERROR_MESSAGE("Tint SPIR-V to WGSL conversion failure:\n", GenerationResult.Failure().reason, "\n"); return {}; } - return GenerationResult->wgsl; + return std::move(GenerationResult.Get()); } static bool IsAtomic(const tint::core::type::Type* WGSLType) @@ -280,7 +276,7 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, return {}; } - tint::ast::transform::BindingRemapper::BindingPoints BindingPoints; + std::unordered_map BindingPoints; tint::inspector::Inspector Inspector{Program}; for (tint::inspector::EntryPoint& EntryPoint : Inspector.GetEntryPoints()) @@ -311,7 +307,7 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, 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}); + BindingPoints.emplace(tint::BindingPoint{Binding.bind_group, Binding.binding}, tint::BindingPoint{DstBindig.Group, DstBindig.Index + ArrayIndex}); } else { @@ -320,19 +316,28 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, } } - 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 {}; + } - Inputs.Add(BindingPoints, tint::ast::transform::BindingRemapper::AccessControls{}, false); - Manager.Add(); - tint::ast::transform::Output TransformResult = Manager.Run(Program, Inputs, Outputs); + 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 {}; + } - auto GenerationResult = tint::wgsl::writer::Generate(TransformResult.program, {}); + 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:\nGenerate: ", GenerationResult.Failure().reason, "\n"); return {}; } From 273631a59b8890236953bbb61912edc17ff67166 Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 12 Oct 2026 20:24:22 -0700 Subject: [PATCH 03/21] WebGPU: adapt backend and tests to updated Dawn API Update WebGPU types, callbacks, and instance event processing. Integrate Emdawnwebgpu headers and preserve browser error forwarding without requiring Asyncify. Preserve DXC support for native Dawn. --- BuildTools/CMake/BuildUtils.cmake | 5 +- Graphics/GraphicsEngineWebGPU/CMakeLists.txt | 7 +- ...{WebGPUStubs.hpp => WebGPUDefinitions.hpp} | 27 ++-- Graphics/GraphicsEngineWebGPU/include/pch.h | 45 +----- .../interface/EngineFactoryWebGPU.h | 5 +- .../src/DeviceContextWebGPUImpl.cpp | 34 +++-- .../src/EngineFactoryWebGPU.cpp | 135 ++++++++---------- .../src/PipelineStateWebGPUImpl.cpp | 79 +++------- .../src/RenderDeviceWebGPUImpl.cpp | 84 ++++++++--- .../src/SwapChainWebGPUImpl.cpp | 28 ++-- .../src/TextureWebGPUImpl.cpp | 6 +- .../src/WebGPUResourceBase.cpp | 14 +- .../src/WebGPUTypeConversions.cpp | 4 +- Tests/DiligentCoreTest/CMakeLists.txt | 8 +- .../WebGPU/TestingEnvironmentWebGPU.hpp | 21 +-- .../src/WebGPU/TestingEnvironmentWebGPU.cpp | 24 ++-- .../src/WebGPU/TestingSwapChainWebGPU.cpp | 62 ++++---- ThirdParty/dawn/CMakeLists.txt | 9 ++ 18 files changed, 289 insertions(+), 308 deletions(-) rename Graphics/GraphicsEngineWebGPU/include/{WebGPUStubs.hpp => WebGPUDefinitions.hpp} (55%) diff --git a/BuildTools/CMake/BuildUtils.cmake b/BuildTools/CMake/BuildUtils.cmake index ce2c1ea0b7..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) diff --git a/Graphics/GraphicsEngineWebGPU/CMakeLists.txt b/Graphics/GraphicsEngineWebGPU/CMakeLists.txt index c9de9b4c44..e229ab14e6 100644 --- a/Graphics/GraphicsEngineWebGPU/CMakeLists.txt +++ b/Graphics/GraphicsEngineWebGPU/CMakeLists.txt @@ -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} @@ -168,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.") 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/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..3edfcf88d4 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{}; @@ -274,8 +258,8 @@ 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); } struct CallbackUserData @@ -286,7 +270,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,7 +282,6 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI } }; -#if !PLATFORM_WEB const char* ToggleNames[] = { "disable_timestamp_query_conversion", "use_dxc", @@ -308,22 +291,19 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI wgpuDawnTogglesDesc.chain.sType = WGPUSType_DawnTogglesDescriptor; wgpuDawnTogglesDesc.enabledToggleCount = _countof(ToggleNames); wgpuDawnTogglesDesc.enabledToggles = ToggleNames; -#endif - - WGPURequiredLimits RequiredLimits{nullptr, SupportedLimits.limits}; 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 +314,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) @@ -457,7 +439,7 @@ GraphicsAdapterInfo GetGraphicsAdapterInfo(WGPUAdapter wgpuAdapter, WGPUDevice w AdapterInfo.Features = GetSupportedFeatures(wgpuAdapter, wgpuDevice); - WGPUSupportedLimits wgpuSupportedLimits{}; + WGPULimits wgpuSupportedLimits{}; if (wgpuAdapter) wgpuAdapterGetLimits(wgpuAdapter, &wgpuSupportedLimits); else @@ -494,16 +476,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 +493,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 +509,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 +532,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 +550,7 @@ void EngineFactoryWebGPUImpl::EnumerateAdapters(Version MinVersion, Adapters[AdapterId] = GetGraphicsAdapterInfo(wgpuAdapter.Get()); } } +#endif } void EngineFactoryWebGPUImpl::CreateDearchiver(const DearchiverCreateInfo& CreateInfo, @@ -583,6 +570,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 +597,7 @@ void EngineFactoryWebGPUImpl::CreateDeviceAndContextsWebGPU(const EngineWebGPUCr catch (const std::runtime_error&) { } +#endif } void EngineFactoryWebGPUImpl::CreateSwapChainWebGPU(IRenderDevice* pDevice, diff --git a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp index 112209d205..c703abba78 100644 --- a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp @@ -422,35 +422,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); } }; @@ -579,7 +563,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 +639,6 @@ void PipelineStateWebGPUImpl::InitializeWebGPURenderPipeline(const TShaderStages wgpuRenderPipelineDesc.depthStencil = &wgpuDepthStencilState; } -#if PLATFORM_WEB - WGPUPrimitiveDepthClipControl wgpuDepthClipControl{}; -#endif { const RasterizerStateDesc& RasterizerDesc = GraphicsPipeline.RasterizerDesc; @@ -679,13 +660,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 +681,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 { @@ -758,18 +729,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..a3cdbbf092 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,8 +481,10 @@ 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 Norm16FilterableSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Unorm16TextureFormats); + const bool R16UnormSupported = TextureFormatsTier1Supported || Norm16FilterableSupported; + const bool R16SnormSupported = TextureFormatsTier1Supported; // https://www.w3.org/TR/webgpu/#texture-format-caps @@ -523,16 +569,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}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_UNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGBA16_UNORM}, 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_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RG16_SNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); + SetTexFormatInfo({TEX_FORMAT_RGBA16_SNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); } if (TextureCompressionETC2Supported) diff --git a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp index 90a7d74fca..e19a2e3f20 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. @@ -31,7 +31,7 @@ #include "DeviceContextWebGPUImpl.hpp" #include "TextureViewWebGPU.h" #include "WebGPUTypeConversions.hpp" -#include "WebGPUStubs.hpp" +#include "WebGPUDefinitions.hpp" #ifdef PLATFORM_WIN32 # include @@ -229,25 +229,18 @@ class SwapChainWebGPUImpl::PresentCommand switch (wgpuSurfaceTexture.status) { - case WGPUSurfaceGetCurrentTextureStatus_Success: - case WGPUSurfaceGetCurrentTextureStatus_Outdated: + case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal: + case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: break; case WGPUSurfaceGetCurrentTextureStatus_Timeout: - break; + case WGPUSurfaceGetCurrentTextureStatus_Outdated: + return wgpuSurfaceTexture.status; 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; @@ -407,7 +400,8 @@ void SwapChainWebGPUImpl::Present(Uint32 SyncInterval) } const bool EnableVSync = SyncInterval != 0; - if (SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Outdated || + if (SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal || + SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Outdated || SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Lost || m_VSyncEnabled != EnableVSync) { @@ -453,9 +447,9 @@ void SwapChainWebGPUImpl::CreateSurface() wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceMetalLayer}; wgpuSurfaceNativeDesc.window = m_NativeWindow.MetalLayer; #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{}; diff --git a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp index 3162d2d9c3..8f6fd87090 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. @@ -385,13 +385,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}; diff --git a/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp index 588d660410..0a85caf6fb 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(); @@ -211,8 +211,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..498d8b03ac 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,7 +27,7 @@ #include "pch.h" #include "WebGPUTypeConversions.hpp" -#include "WebGPUStubs.hpp" +#include "WebGPUDefinitions.hpp" #include "DebugUtilities.hpp" #include "GraphicsAccessories.hpp" diff --git a/Tests/DiligentCoreTest/CMakeLists.txt b/Tests/DiligentCoreTest/CMakeLists.txt index 1cde824899..a662bad82b 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") @@ -38,7 +38,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/") 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/ThirdParty/dawn/CMakeLists.txt b/ThirdParty/dawn/CMakeLists.txt index bfd4272980..ff555460cf 100644 --- a/ThirdParty/dawn/CMakeLists.txt +++ b/ThirdParty/dawn/CMakeLists.txt @@ -77,6 +77,15 @@ set(DAWN_ABSEIL_DIR "${abseil-cpp_SOURCE_DIR}" CACHE STRING "Directory in which 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}) From 0ff99205710417bcc038a0d29e4b40c0ae94bb78 Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 12 Oct 2026 21:22:24 -0700 Subject: [PATCH 04/21] WebGPU: migrate SPIR-V conversion to Tint IR Strip HLSL reflection metadata from the conversion copy while preserving the original SPIR-V. Keep conversion optional when compilers are disabled. --- Graphics/ShaderTools/src/WGSLUtils.cpp | 39 +++++++++++++-- .../src/ShaderTools/WGSLUtilsTest.cpp | 50 ++++++++++++++++++- ThirdParty/dawn/CMakeLists.txt | 8 ++- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index 57ee458db3..537afb5ced 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -28,15 +28,21 @@ #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 "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/transform/binding_remapper.h" #include "src/tint/lang/wgsl/ast/module.h" #include "src/tint/lang/wgsl/ast/identifier_expression.h" @@ -92,18 +98,41 @@ WGSLEmulatedResourceArrayElement GetWGSLEmulatedArrayElement(const std::string& std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) { +#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 IR = tint::spirv::reader::ReadIR(ConversionSPIRV); + if (IR != tint::Success) + { + LOG_ERROR_MESSAGE("Tint SPIR-V reader failure:\n", IR.Failure().reason, "\n"); + return {}; + } + tint::wgsl::writer::Options WGSLWriterOptions; WGSLWriterOptions.allow_non_uniform_derivatives = true; WGSLWriterOptions.allowed_features = tint::wgsl::AllowedFeatures::Everything(); - auto GenerationResult = tint::SpirvToWgsl(SPIRV, WGSLWriterOptions); + auto GenerationResult = tint::wgsl::writer::WgslFromIR(IR.Get(), WGSLWriterOptions); if (GenerationResult != tint::Success) { - LOG_ERROR_MESSAGE("Tint SPIR-V to WGSL conversion failure:\n", GenerationResult.Failure().reason, "\n"); + LOG_ERROR_MESSAGE("Tint WGSL writer failure:\n", GenerationResult.Failure().reason, "\n"); return {}; } - return std::move(GenerationResult.Get()); + return std::move(GenerationResult->wgsl); +#else + (void)SPIRV; + 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) diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index 88430770f6..06048668b4 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,11 @@ */ #include "WGSLUtils.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 +65,52 @@ 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"); +} + TEST(WGSLUtils, GetWGSLEmulatedArrayElementInfo) { EXPECT_EQ(GetWGSLEmulatedArrayElement("", ""), WGSLEmulatedResourceArrayElement{}); diff --git a/ThirdParty/dawn/CMakeLists.txt b/ThirdParty/dawn/CMakeLists.txt index ff555460cf..ecdd1805d2 100644 --- a/ThirdParty/dawn/CMakeLists.txt +++ b/ThirdParty/dawn/CMakeLists.txt @@ -24,7 +24,13 @@ if (MSVC) endif() set(TINT_BUILD_AS_OTHER_OS ${PLATFORM_WEB} CACHE BOOL "" FORCE) -set(TINT_BUILD_SPV_READER ON 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_SPV_READER OFF CACHE BOOL "" FORCE) +endif() 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) From 7f9d4c4ac83bc1eeb97315a6a3f75f5e92afe411 Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 12 Oct 2026 21:58:14 -0700 Subject: [PATCH 05/21] WebGPU: restore texture access inference with Tint IR --- Graphics/ShaderTools/src/WGSLUtils.cpp | 143 ++++++++++++++++++ .../shaders/WGSL/TextureAccessQueries.psh | 17 +++ .../WGSL/TextureAccessThroughFunctions.psh | 21 +++ .../ShaderTools/WGSLShaderResourcesTest.cpp | 25 +++ 4 files changed, 206 insertions(+) create mode 100644 Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessQueries.psh create mode 100644 Tests/DiligentCoreTest/assets/shaders/WGSL/TextureAccessThroughFunctions.psh diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index 537afb5ced..5777ed9002 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -24,6 +24,8 @@ * of the possibility of such damages. */ +#include + #include "WGSLUtils.hpp" #include "DebugUtilities.hpp" #include "ParsingTools.hpp" @@ -43,6 +45,13 @@ #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/wgsl/ast/module.h" #include "src/tint/lang/wgsl/ast/identifier_expression.h" @@ -96,6 +105,137 @@ WGSLEmulatedResourceArrayElement GetWGSLEmulatedArrayElement(const std::string& return {Name, -1}; } +#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) +{ + 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; + } + } + + ir::Builder Builder{IR}; + auto& Types = IR.Types(); + for (auto* Inst : *IR.root_block) + { + 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; +} +#endif + std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) { #if SPIRV_TO_WGSL_SUPPORTED @@ -116,6 +256,9 @@ std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) return {}; } + if (!RestoreTextureAccess(IR.Get())) + return {}; + tint::wgsl::writer::Options WGSLWriterOptions; WGSLWriterOptions.allow_non_uniform_derivatives = true; WGSLWriterOptions.allowed_features = tint::wgsl::AllowedFeatures::Everything(); 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/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index 41a4245999..7f1ac70050 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -111,6 +111,31 @@ 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, UniformBuffers) { TestWGSLResources("UniformBuffers.psh", From 5bfd1665bcb5f865793f49af13737b28ecbcde3c Mon Sep 17 00:00:00 2001 From: assiduous Date: Mon, 12 Oct 2026 23:03:05 -0700 Subject: [PATCH 06/21] WebGPU: preserve source reflection through WGSL conversion and remapping --- .../src/ShaderWebGPUImpl.cpp | 2 +- Graphics/ShaderTools/CMakeLists.txt | 4 +- .../ShaderTools/include/ShaderToolsCommon.hpp | 12 +- .../include/WGSLShaderReflection.hpp | 77 +++ Graphics/ShaderTools/include/WGSLUtils.hpp | 6 +- .../ShaderTools/src/WGSLShaderReflection.cpp | 269 ++++++++ .../ShaderTools/src/WGSLShaderResources.cpp | 51 +- Graphics/ShaderTools/src/WGSLUtils.cpp | 111 +++- Tests/DiligentCoreTest/CMakeLists.txt | 1 + .../assets/shaders/WGSL/SourceReflection.psh | 29 + .../ShaderTools/WGSLShaderReflectionTest.cpp | 610 ++++++++++++++++++ .../ShaderTools/WGSLShaderResourcesTest.cpp | 84 ++- 12 files changed, 1221 insertions(+), 35 deletions(-) create mode 100644 Graphics/ShaderTools/include/WGSLShaderReflection.hpp create mode 100644 Graphics/ShaderTools/src/WGSLShaderReflection.cpp create mode 100644 Tests/DiligentCoreTest/assets/shaders/WGSL/SourceReflection.psh create mode 100644 Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp 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/ShaderTools/CMakeLists.txt b/Graphics/ShaderTools/CMakeLists.txt index 8f7227ca9e..051038a9c8 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) 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..21d5a9db70 --- /dev/null +++ b/Graphics/ShaderTools/include/WGSLShaderReflection.hpp @@ -0,0 +1,77 @@ +/* + * 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; + + 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/WGSLUtils.hpp b/Graphics/ShaderTools/include/WGSLUtils.hpp index e4304a2a12..4b0b4e0512 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 { diff --git a/Graphics/ShaderTools/src/WGSLShaderReflection.cpp b/Graphics/ShaderTools/src/WGSLShaderReflection.cpp new file mode 100644 index 0000000000..a0d603a20a --- /dev/null +++ b/Graphics/ShaderTools/src/WGSLShaderReflection.cpp @@ -0,0 +1,269 @@ +/* + * 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()) + return; + WGSL += '\n'; + WGSL += ReflectionMarker; + WGSL += "1\n"; + for (const auto& Entry : Reflection.Resources) + { + 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)); + } + } + WGSL += "*/\n"; +} + +bool ParseWGSLShaderReflection(const std::string& WGSL, WGSLShaderReflection& Reflection) +{ + const size_t Start = WGSL.find(ReflectionMarker); + if (Start == std::string::npos) + { + Reflection.Resources.clear(); + 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()) + { + 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 cea71c32c2..c8c5966a0e 100644 --- a/Graphics/ShaderTools/src/WGSLShaderResources.cpp +++ b/Graphics/ShaderTools/src/WGSLShaderResources.cpp @@ -28,6 +28,7 @@ #include "Align.hpp" #include "StringPool.hpp" #include "WGSLUtils.hpp" +#include "WGSLShaderReflection.hpp" #include "DataBlobImpl.hpp" #include "ShaderToolsCommon.hpp" @@ -819,33 +820,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); - } } } @@ -946,7 +940,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; diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index 5777ed9002..57270d3d41 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -27,12 +27,16 @@ #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 @@ -234,9 +238,77 @@ static bool RestoreTextureAccess(tint::core::ir::Module& IR) } 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) +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 @@ -256,6 +328,10 @@ std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) 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 {}; @@ -270,9 +346,12 @@ std::string ConvertSPIRVtoWGSL(const std::vector& SPIRV) return {}; } - return std::move(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 @@ -448,6 +527,13 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, return {}; } + WGSLShaderReflection Reflection; + if (!ParseWGSLShaderReflection(WGSL, Reflection)) + { + LOG_ERROR_MESSAGE("Invalid WGSL reflection metadata"); + return {}; + } + std::unordered_map BindingPoints; tint::inspector::Inspector Inspector{Program}; @@ -457,13 +543,17 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, { 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); + const 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()) @@ -515,6 +605,17 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, 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. + WGSLShaderReflection RemappedReflection; + for (auto& Entry : Reflection.Resources) + { + auto It = BindingPoints.find(tint::BindingPoint{Entry.first.Group, Entry.first.Binding}); + if (It != BindingPoints.end()) + RemappedReflection.Resources.emplace(WGSLShaderReflection::BindingPoint{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) diff --git a/Tests/DiligentCoreTest/CMakeLists.txt b/Tests/DiligentCoreTest/CMakeLists.txt index a662bad82b..c45cf19ba8 100644 --- a/Tests/DiligentCoreTest/CMakeLists.txt +++ b/Tests/DiligentCoreTest/CMakeLists.txt @@ -17,6 +17,7 @@ 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() 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/src/ShaderTools/WGSLShaderReflectionTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp new file mode 100644 index 0000000000..9977d81726 --- /dev/null +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp @@ -0,0 +1,610 @@ +/* + * 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 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" + "1 3 - 1\n" + "0 0\n" + "1 7 2a2f0a 0\n" + "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) +{ + 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) +{ + const auto Expected = MakeReferenceReflection(); + auto Actual = MakeReferenceReflection(); + 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; resource group/binding/name/flag; buffer size/count; + // variable name/type/class/basic type/rows/columns/offset/array size/member count. + return {{"1", "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(); + ASSERT_TRUE(ParseWGSLShaderReflection(Text, Reflection)); + EXPECT_TRUE(Reflection.Resources.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\n1 2 41 0\n1 7 41 0\n2 7 41 0\n") + '\n'); + CheckRoundTrip(RepeatedNames); +} + +TEST(WGSLShaderReflection, ParseKnownMetadataAndOwnStrings) +{ + std::string Text = "// Earlier comment */\n" + MakeMetadata(ReferencePayload) + "\n/*$SHADER_SOURCE_LANGUAGE=1*/"; + WGSLShaderReflection Reflection; + ASSERT_TRUE(ParseWGSLShaderReflection(Text, Reflection)); + Text.assign(Text.size(), 'x'); + Text.clear(); + Text.shrink_to_fit(); + ExpectReflectionEqual(Reflection, MakeReferenceReflection()); +} + +TEST(WGSLShaderReflection, SuccessfulParseReplacesPreviousReflection) +{ + auto Reflection = MakeReferenceReflection(); + ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata("1 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]; + 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 - 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[3][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 : {1u, 2u, 5u, 13u, 14u}) + 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\n" + 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[11] = std::to_string(Value); + Fields[12] = 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[9] = std::to_string(Class); + Fields[10] = 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, 4u, 5u, 6u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}) + 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("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, 4u, 5u, 6u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}) + { + 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[] = { + {4, 2}, + {4, 255}, + {9, SHADER_CODE_VARIABLE_CLASS_COUNT}, + {9, 255}, + {9, 256}, + {10, SHADER_CODE_BASIC_TYPE_COUNT}, + {10, 255}, + {10, 256}, + {11, 256}, + {11, 65535}, + {12, 256}, + {12, 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 : {3u, 7u, 8u}) + { + 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 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[6] = Count; + ExpectRejected(JoinTokens(Fields)); + } + for (const char* Count : {"1", "2", "4294967295"}) + { + auto Fields = MakeValidTokens(); + Fields[15] = Count; + ExpectRejected(JoinTokens(Fields)); + } + ExpectRejected(MakeMetadata("1\n0 0 41 1\n64 0\n42 - 1 11 1 1 0 0 0\n")); +} + +TEST(WGSLShaderReflection, RejectDuplicateBindings) +{ + ExpectRejected(MakeMetadata("1\n0 0 41 0\n0 0 42 0\n")); + ExpectRejected(MakeMetadata("1\n0 0 41 1\n0 0\n0 0 41 0\n")); + ExpectRejected(MakeMetadata("1\n0 0 41 0\n1 0 42 0\n0 0 43 0\n")); +} + +TEST(WGSLShaderReflection, NestingLimit) +{ + for (unsigned Depth : {1u, 63u, 64u, 65u}) + { + SCOPED_TRACE(Depth); + std::string Payload = "1\n0 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)); + } + } + CheckRoundTrip(Reflection); + } +} + +} // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index 7f1ac70050..9f95e76151 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -26,6 +26,8 @@ #include "WGSLShaderResources.hpp" #include "WGSLUtils.hpp" +#include "WGSLShaderReflection.hpp" +#include "SPIRVShaderResources.hpp" #include "GLSLangUtils.hpp" #include "DefaultShaderSourceStreamFactory.h" #include "RefCntAutoPtr.hpp" @@ -42,7 +44,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 +66,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, @@ -136,6 +141,81 @@ TEST(WGSLShaderResources, TextureAccessQueries) }); } +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", From bcea25c9d7758f0e37ae35b880a8333a734971ba Mon Sep 17 00:00:00 2001 From: assiduous Date: Tue, 13 Oct 2026 20:23:27 -0700 Subject: [PATCH 07/21] WebGPU: fix resource and specialization constant remapping with Tint IR --- .../include/PipelineStateWebGPUImpl.hpp | 5 +- .../src/PipelineStateWebGPUImpl.cpp | 23 ++- .../include/WGSLShaderResources.hpp | 8 +- Graphics/ShaderTools/include/WGSLUtils.hpp | 7 +- .../ShaderTools/src/WGSLShaderResources.cpp | 2 +- Graphics/ShaderTools/src/WGSLUtils.cpp | 68 ++++++++- .../src/SpecializationConstantsTest.cpp | 57 ++++++- .../ShaderTools/WGSLShaderResourcesTest.cpp | 9 +- .../src/ShaderTools/WGSLUtilsTest.cpp | 139 ++++++++++++++++++ 9 files changed, 296 insertions(+), 22 deletions(-) diff --git a/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp index 7a18d19cb0..c635362103 100644 --- a/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp @@ -87,9 +87,10 @@ class PipelineStateWebGPUImpl final : public PipelineStateBase SpecConstEntries; + std::vector SpecConstKeys; ShaderStageInfo(ShaderWebGPUImpl* _pShader) : Type{_pShader->GetDesc().ShaderType}, diff --git a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp index c703abba78..3582047080 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,12 @@ void PipelineStateWebGPUImpl::RemapOrVerifyShaderResources( if (!bVerifyOnly) { - PatchedWGSL = RemapWGSLResourceBindings(pShader->GetWGSL(), ResMapping, pShader->GetEmulatedArrayIndexSuffix()); + PatchedWGSL = RemapWGSLResourceBindings(pShader->GetWGSL(), ResMapping, pShader->GetEmulatedArrayIndexSuffix(), pShader->GetEntryPoint()); + if (PatchedWGSL.empty()) + { + LOG_ERROR_AND_THROW("Failed to remap resources for shader '", pShader->GetDesc().Name, + "' in pipeline '", PipelineName, "'."); + } } } } 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 4b0b4e0512..6b350e4d43 100644 --- a/Graphics/ShaderTools/include/WGSLUtils.hpp +++ b/Graphics/ShaderTools/include/WGSLUtils.hpp @@ -54,9 +54,14 @@ 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. +// Returns an empty string if an active resource cannot be mapped unambiguously. std::string RemapWGSLResourceBindings(const std::string& WGSL, const WGSLResourceMapping& ResMapping, - const char* EmulatedArrayIndexSuffix); + const char* EmulatedArrayIndexSuffix, + const char* EntryPoint = nullptr); /// When WGSL is generated from SPIR-V, the names of resources may be mangled diff --git a/Graphics/ShaderTools/src/WGSLShaderResources.cpp b/Graphics/ShaderTools/src/WGSLShaderResources.cpp index c8c5966a0e..e65197765f 100644 --- a/Graphics/ShaderTools/src/WGSLShaderResources.cpp +++ b/Graphics/ShaderTools/src/WGSLShaderResources.cpp @@ -1016,7 +1016,7 @@ WGSLShaderResources::WGSLShaderResources(IMemoryAllocator& Allocator, { const char* SCName = ResourceNamesPool.CopyString(Override.name); 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 57270d3d41..a5e6f9565e 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -24,6 +24,8 @@ * of the possibility of such damages. */ +#include +#include #include #include "WGSLUtils.hpp" @@ -57,6 +59,7 @@ #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/identifier_expression.h" #include "src/tint/lang/wgsl/ast/identifier.h" @@ -97,7 +100,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 // ^ @@ -504,7 +508,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; @@ -516,7 +520,8 @@ 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) { tint::Source::File srcFile("", WGSL); tint::Program Program = tint::wgsl::reader::Parse(&srcFile, {tint::wgsl::AllowedFeatures::Everything()}); @@ -537,9 +542,21 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, std::unordered_map BindingPoints; 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; })) { - for (tint::inspector::ResourceBinding& Binding : Inspector.GetResourceBindings(EntryPoint.name)) + LOG_ERROR_MESSAGE("WGSL entry point '", EntryPoint, "' was not found"); + return {}; + } + for (const tint::inspector::EntryPoint& EP : EntryPoints) + { + if (EntryPoint != nullptr && EP.name != EntryPoint) + continue; + + // 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; @@ -569,11 +586,36 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, if (DstBindigIt != ResMapping.end()) { const WGSLResourceBindingInfo& DstBindig = DstBindigIt->second; - BindingPoints.emplace(tint::BindingPoint{Binding.bind_group, Binding.binding}, tint::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 {}; + } } 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 {}; } } } @@ -585,6 +627,18 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, 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) { diff --git a/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp b/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp index e17b4ad6bc..8c3502308d 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 diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index 9f95e76151..e345951d82 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -403,10 +403,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; @@ -425,6 +425,7 @@ TEST(WGSLShaderResources, SpecializationConstants) const WGSLSpecializationConstantAttribs* pRef = it->second; EXPECT_EQ(SC.GetType(), pRef->GetType()) << SC.Name; + EXPECT_EQ(SC.IsIdSpecified, pRef->IsIdSpecified) << SC.Name; } } diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index 06048668b4..c255d2ac39 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp @@ -25,6 +25,7 @@ */ #include "WGSLUtils.hpp" +#include "WGSLShaderResources.hpp" #include "SPIRVShaderResources.hpp" #include "GLSLangUtils.hpp" #include "DefaultShaderSourceStreamFactory.h" @@ -119,6 +120,9 @@ 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_"}); @@ -448,4 +452,139 @@ TEST(WGSLUtils, RWStructBufferArrays) }); } + +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}}}; + const std::string Remapped = RemapWGSLResourceBindings(WGSL, Mapping, nullptr, EntryPoint); + 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(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"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"FirstOutput", {1, 0}}, {"SecondOutput", {2, 0}}}, nullptr).empty()); + } + { + TestingEnvironment::ErrorScope Errors{"WGSL entry point 'missing' was not found"}; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, "missing").empty()); + } +} + +TEST(WGSLUtils, RemapPreservesOverrideIds) +{ + const std::string WGSL = R"( +@id(0) override Explicit: u32 = 2; +override Named: u32 = 1; +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]; +} +)"; + const WGSLShaderResources Original{GetRawAllocator(), WGSL, SHADER_SOURCE_LANGUAGE_WGSL, + "Override remapping", nullptr, "main", nullptr, false, nullptr}; + ASSERT_EQ(Original.GetNumSpecConstants(), 4u); + 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()); + } +} + } // namespace From 5d8a287852668ba845ce9610899995866978e45c Mon Sep 17 00:00:00 2001 From: assiduous Date: Tue, 13 Oct 2026 20:50:30 -0700 Subject: [PATCH 08/21] WebGPU: fix mip-generation view usage and timestamp precision --- .../src/EngineFactoryWebGPU.cpp | 14 ++++++++++---- .../GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp | 14 ++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp index 3edfcf88d4..8764fdb5ff 100644 --- a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp @@ -282,15 +282,21 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI } }; - 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; + wgpuDawnTogglesDesc.enabledToggleCount = _countof(EnabledToggles); + wgpuDawnTogglesDesc.enabledToggles = EnabledToggles; + wgpuDawnTogglesDesc.disabledToggleCount = _countof(DisabledToggles); + wgpuDawnTogglesDesc.disabledToggles = DisabledToggles; WGPUDeviceDescriptor DeviceDesc{}; DeviceDesc.requiredLimits = &SupportedLimits; diff --git a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp index 8f6fd87090..5662972aac 100644 --- a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp @@ -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; } From 21f7fef67436ebfd78ff07a0ca2c1ee2eb1bcf8c Mon Sep 17 00:00:00 2001 From: assiduous Date: Tue, 13 Oct 2026 20:58:29 -0700 Subject: [PATCH 09/21] CI: update Diligent GitHub Actions to v28 Update Emscripten SDK to 6.0.11 --- .github/workflows/build-android.yml | 2 +- .github/workflows/build-apple.yml | 8 ++++---- .github/workflows/build-emscripten.yml | 6 +++--- .github/workflows/build-linux.yml | 16 ++++++++-------- .github/workflows/build-windows.yml | 16 ++++++++-------- .github/workflows/codeql.yml | 6 +++--- .github/workflows/msvc_analysis.yml | 6 +++--- 7 files changed, 30 insertions(+), 30 deletions(-) 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 From 92e062e8e1954691a8b2eb281b56dcb5e144e13c Mon Sep 17 00:00:00 2001 From: assiduous Date: Tue, 13 Oct 2026 21:27:43 -0700 Subject: [PATCH 10/21] CMake: fix SPIRV-Cross threading flags for Emscripten builds --- .../include/ShaderResourceVariableBase.hpp | 4 +- .../src/WebGPUTypeConversions.cpp | 3 +- .../src/WebGPU/WebGPUTypeConversionsTest.cpp | 71 +++++++++++++++++++ Tests/IncludeTest/CMakeLists.txt | 6 +- ThirdParty/CMakeLists.txt | 3 + ThirdParty/abseil-cpp/CMakeLists.txt | 2 + 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 Tests/DiligentCoreAPITest/src/WebGPU/WebGPUTypeConversionsTest.cpp 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/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp index 498d8b03ac..3cdb2de612 100644 --- a/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp @@ -31,7 +31,8 @@ #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 { 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/IncludeTest/CMakeLists.txt b/Tests/IncludeTest/CMakeLists.txt index b47682a1f2..81d5b8eec1 100644 --- a/Tests/IncludeTest/CMakeLists.txt +++ b/Tests/IncludeTest/CMakeLists.txt @@ -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 178a8a57a8..83655bbafe 100644 --- a/ThirdParty/CMakeLists.txt +++ b/ThirdParty/CMakeLists.txt @@ -117,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 c0b15eb603..a4015d4666 100644 --- a/ThirdParty/abseil-cpp/CMakeLists.txt +++ b/ThirdParty/abseil-cpp/CMakeLists.txt @@ -17,6 +17,8 @@ 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) From 64d722705e2b388cc43bd5c2e35ded17d0dd073d Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 01:12:40 -0700 Subject: [PATCH 11/21] WebGPU: Fetch Dawn fork with MSVC x86 queue serial fix --- Graphics/ShaderTools/CMakeLists.txt | 2 ++ Tests/DiligentCoreTest/CMakeLists.txt | 2 +- ThirdParty/dawn/CMakeLists.txt | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Graphics/ShaderTools/CMakeLists.txt b/Graphics/ShaderTools/CMakeLists.txt index 051038a9c8..a411e8df6a 100644 --- a/Graphics/ShaderTools/CMakeLists.txt +++ b/Graphics/ShaderTools/CMakeLists.txt @@ -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 ) diff --git a/Tests/DiligentCoreTest/CMakeLists.txt b/Tests/DiligentCoreTest/CMakeLists.txt index c45cf19ba8..d4e5304561 100644 --- a/Tests/DiligentCoreTest/CMakeLists.txt +++ b/Tests/DiligentCoreTest/CMakeLists.txt @@ -66,7 +66,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/ThirdParty/dawn/CMakeLists.txt b/ThirdParty/dawn/CMakeLists.txt index ecdd1805d2..61fdd6f00a 100644 --- a/ThirdParty/dawn/CMakeLists.txt +++ b/ThirdParty/dawn/CMakeLists.txt @@ -8,9 +8,9 @@ set(DAWN_SUBMODULES FetchContent_DeclareShallowGit( dawn - GIT_REPOSITORY https://dawn.googlesource.com/dawn - # Dawn and Emdawnwebgpu snapshot v20261002.154047. - GIT_TAG b1236a9bb4a47f1262bc3acb253bb1c2181e90dd + 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}" ) From f0bd4e5b66e06eeef4c43f5fa7d3602c5283a69b Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 08:26:45 -0700 Subject: [PATCH 12/21] WebGPU: Preserve generated entry-point names in pipelines and archives --- Graphics/Archiver/src/Archiver_WebGPU.cpp | 4 +- .../include/PipelineStateWebGPUImpl.hpp | 6 + .../src/PipelineStateWebGPUImpl.cpp | 9 +- Graphics/ShaderTools/include/WGSLUtils.hpp | 6 +- Graphics/ShaderTools/src/WGSLUtils.cpp | 39 ++++++- Tests/DiligentCoreAPITest/src/ArchiveTest.cpp | 102 +++++++++++++++++ .../src/ShaderCreationTest.cpp | 56 +++++++++ .../src/ShaderTools/WGSLUtilsTest.cpp | 108 +++++++++++++++++- 8 files changed, 318 insertions(+), 12 deletions(-) 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/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp index c635362103..18d5692329 100644 --- a/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp +++ b/Graphics/GraphicsEngineWebGPU/include/PipelineStateWebGPUImpl.hpp @@ -84,6 +84,7 @@ 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/src/PipelineStateWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp index 3582047080..22ff112e08 100644 --- a/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/PipelineStateWebGPUImpl.cpp @@ -342,7 +342,8 @@ void PipelineStateWebGPUImpl::RemapOrVerifyShaderResources( if (!bVerifyOnly) { - PatchedWGSL = RemapWGSLResourceBindings(pShader->GetWGSL(), ResMapping, pShader->GetEmulatedArrayIndexSuffix(), pShader->GetEntryPoint()); + 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, @@ -537,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; @@ -545,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; @@ -737,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; diff --git a/Graphics/ShaderTools/include/WGSLUtils.hpp b/Graphics/ShaderTools/include/WGSLUtils.hpp index 6b350e4d43..918e9378dd 100644 --- a/Graphics/ShaderTools/include/WGSLUtils.hpp +++ b/Graphics/ShaderTools/include/WGSLUtils.hpp @@ -57,11 +57,15 @@ using WGSLResourceMapping = std::unordered_mapAST().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:\nGenerate: ", GenerationResult.Failure().reason, "\n"); @@ -675,6 +707,9 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, if (SrcLang != SHADER_SOURCE_LANGUAGE_DEFAULT) AppendShaderSourceLanguageDefinition(PatchedWGSL, SrcLang); + if (pRemappedEntryPoint != nullptr) + *pRemappedEntryPoint = std::move(RemappedEntryPoint); + return PatchedWGSL; } diff --git a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp index f4274855e5..48e2f13fb7 100644 --- a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp @@ -2909,4 +2909,106 @@ 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); + } + } +} + } // namespace diff --git a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp index 618f4ab571..7a77ec9e45 100644 --- a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp @@ -245,4 +245,60 @@ TEST(ShaderCreationTest, FromBytecode) } } + +TEST(ShaderCreationTest, WGSLEntryPointRemapping) +{ + 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.pVS = Shaders[0]; + GraphicsCI.pPS = Shaders[1]; + + RefCntAutoPtr pGraphicsPSO; + pDevice->CreateGraphicsPipelineState(GraphicsCI, &pGraphicsPSO); + ASSERT_NE(pGraphicsPSO, nullptr); + EXPECT_EQ(pGraphicsPSO->GetStatus(), PIPELINE_STATE_STATUS_READY); + + ComputePipelineStateCreateInfo ComputeCI; + ComputeCI.PSODesc.Name = "WGSLEntryPointRemapping - Compute"; + ComputeCI.PSODesc.PipelineType = PIPELINE_TYPE_COMPUTE; + ComputeCI.pCS = Shaders[2]; + + RefCntAutoPtr pComputePSO; + pDevice->CreateComputePipelineState(ComputeCI, &pComputePSO); + ASSERT_NE(pComputePSO, nullptr); + EXPECT_EQ(pComputePSO->GetStatus(), PIPELINE_STATE_STATUS_READY); +} + } // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index c255d2ac39..afc645938a 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp @@ -453,6 +453,102 @@ 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"( @@ -469,7 +565,8 @@ override Shared: u32 = 1; SCOPED_TRACE(EntryPoint); const char* ResourceName = strcmp(EntryPoint, "first") == 0 ? "FirstOutput" : "SecondOutput"; const WGSLResourceMapping Mapping{{ResourceName, {2, 3}}}; - const std::string Remapped = RemapWGSLResourceBindings(WGSL, Mapping, nullptr, EntryPoint); + 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()}); @@ -478,6 +575,7 @@ override Shared: u32 = 1; 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); @@ -488,11 +586,15 @@ override Shared: u32 = 1; { TestingEnvironment::ErrorScope Errors{"Conflicting remappings for binding"}; - EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {{"FirstOutput", {1, 0}}, {"SecondOutput", {2, 0}}}, nullptr).empty()); + 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"}; - EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, "missing").empty()); + std::string RemappedEntryPoint = "missing"; + EXPECT_TRUE(RemapWGSLResourceBindings(WGSL, {}, nullptr, RemappedEntryPoint.c_str(), &RemappedEntryPoint).empty()); + EXPECT_EQ(RemappedEntryPoint, "missing"); } } From e3a6063150b9ac458b11f9539ce941b5cabbf541 Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 08:52:46 -0700 Subject: [PATCH 13/21] WebGPU: Preserve native WGSL resource and specialization constant names in archives --- .../include/WGSLShaderReflection.hpp | 4 + Graphics/ShaderTools/include/WGSLUtils.hpp | 5 +- .../ShaderTools/src/WGSLShaderReflection.cpp | 26 +++- .../ShaderTools/src/WGSLShaderResources.cpp | 11 +- Graphics/ShaderTools/src/WGSLUtils.cpp | 32 +++- Tests/DiligentCoreAPITest/src/ArchiveTest.cpp | 121 +++++++++++++++ .../ShaderTools/WGSLShaderReflectionTest.cpp | 144 +++++++++++++----- .../src/ShaderTools/WGSLUtilsTest.cpp | 104 +++++++++++++ 8 files changed, 397 insertions(+), 50 deletions(-) diff --git a/Graphics/ShaderTools/include/WGSLShaderReflection.hpp b/Graphics/ShaderTools/include/WGSLShaderReflection.hpp index 21d5a9db70..a1e29a2d22 100644 --- a/Graphics/ShaderTools/include/WGSLShaderReflection.hpp +++ b/Graphics/ShaderTools/include/WGSLShaderReflection.hpp @@ -53,6 +53,10 @@ struct WGSLShaderReflection }; 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}); diff --git a/Graphics/ShaderTools/include/WGSLUtils.hpp b/Graphics/ShaderTools/include/WGSLUtils.hpp index 918e9378dd..b0eed0d6a6 100644 --- a/Graphics/ShaderTools/include/WGSLUtils.hpp +++ b/Graphics/ShaderTools/include/WGSLUtils.hpp @@ -57,10 +57,13 @@ using WGSLResourceMapping = std::unordered_map 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; diff --git a/Graphics/ShaderTools/src/WGSLShaderResources.cpp b/Graphics/ShaderTools/src/WGSLShaderResources.cpp index e65197765f..6e05ea9e68 100644 --- a/Graphics/ShaderTools/src/WGSLShaderResources.cpp +++ b/Graphics/ShaderTools/src/WGSLShaderResources.cpp @@ -911,12 +911,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. @@ -1014,7 +1021,7 @@ 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, Override.is_id_specified}; } diff --git a/Graphics/ShaderTools/src/WGSLUtils.cpp b/Graphics/ShaderTools/src/WGSLUtils.cpp index aed566d479..d8df689b79 100644 --- a/Graphics/ShaderTools/src/WGSLUtils.cpp +++ b/Graphics/ShaderTools/src/WGSLUtils.cpp @@ -542,6 +542,7 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, } std::unordered_map BindingPoints; + WGSLShaderReflection RemappedReflection; tint::inspector::Inspector Inspector{Program}; const auto EntryPoints = Inspector.GetEntryPoints(); @@ -555,6 +556,15 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, 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; @@ -564,9 +574,9 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, // 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); - const std::string& ResourceName = SourceResource != nullptr ? SourceResource->Name : Binding.variable_name; - auto DstBindigIt = ResMapping.find(ResourceName); + 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, ResourceName, ArrayIndex); @@ -582,6 +592,8 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, { DstBindigIt = FindResourceAsArrayElement(ResMapping, EmulatedArrayIndexSuffix, AltName, ArrayIndex); } + if (DstBindigIt != ResMapping.end()) + ResourceName = AltName; } } @@ -613,6 +625,17 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, 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 { @@ -693,12 +716,11 @@ std::string RemapWGSLResourceBindings(const std::string& WGSL, // Tint does not preserve comments. Carry reflection across the rewrite using // the same binding-point mapping as the shader, including emulated arrays. - WGSLShaderReflection RemappedReflection; for (auto& Entry : Reflection.Resources) { auto It = BindingPoints.find(tint::BindingPoint{Entry.first.Group, Entry.first.Binding}); if (It != BindingPoints.end()) - RemappedReflection.Resources.emplace(WGSLShaderReflection::BindingPoint{It->second.group, It->second.binding}, std::move(Entry.second)); + RemappedReflection.Resources[{It->second.group, It->second.binding}] = std::move(Entry.second); } AppendWGSLShaderReflection(PatchedWGSL, RemappedReflection); diff --git a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp index 48e2f13fb7..eff06eb172 100644 --- a/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ArchiveTest.cpp @@ -3011,4 +3011,125 @@ TEST(ArchiveTest, WGSLEntryPointRemapping) } } + +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/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp index 9977d81726..fc2abc2379 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp @@ -82,7 +82,7 @@ WGSLShaderReflection MakeReferenceReflection() constexpr char ReferencePayload[] = "1\n" - "0 9 4342 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" @@ -90,10 +90,10 @@ constexpr char ReferencePayload[] = "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" - "1 3 - 1\n" + "0 1 3 - 1\n" "0 0\n" - "1 7 2a2f0a 0\n" - "2 0 41 0\n"; + "0 1 7 2a2f0a 0\n" + "0 2 0 41 0\n"; void ExpectVariableEqual(const ShaderCodeVariableDescX& Actual, const ShaderCodeVariableDescX& Expected) { @@ -117,6 +117,7 @@ void ExpectVariableEqual(const ShaderCodeVariableDescX& Actual, const ShaderCode 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) { @@ -151,10 +152,12 @@ void CheckRoundTrip(const WGSLShaderReflection& Expected) void ExpectRejected(const std::string& Text) { - const auto Expected = MakeReferenceReflection(); - auto Actual = MakeReferenceReflection(); - const auto* Resource = Actual.Find(0, 9); - const auto* Buffer = Resource->UniformBuffer.get(); + 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); @@ -162,13 +165,13 @@ void ExpectRejected(const std::string& Text) ExpectReflectionEqual(Actual, Expected); } -using Tokens = std::array; +using Tokens = std::array; Tokens MakeValidTokens() { - // Version; resource group/binding/name/flag; buffer size/count; + // 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", "1", "52", "1", "64", "1", "56", "54", "1", "11", "1", "1", "0", "0", "0"}}; + return {{"1", "0", "0", "1", "52", "1", "64", "1", "56", "54", "1", "11", "1", "1", "0", "0", "0"}}; } std::string JoinTokens(const Tokens& Fields) @@ -203,9 +206,11 @@ TEST(WGSLShaderReflection, MissingOrEmptyMetadataClearsOutput) for (const auto& Text : Inputs) { SCOPED_TRACE(Text); - auto Reflection = MakeReferenceReflection(); + auto Reflection = MakeReferenceReflection(); + Reflection.SpecializationConstants[1] = "previous"; ASSERT_TRUE(ParseWGSLShaderReflection(Text, Reflection)); EXPECT_TRUE(Reflection.Resources.empty()); + EXPECT_TRUE(Reflection.SpecializationConstants.empty()); } } @@ -222,7 +227,7 @@ TEST(WGSLShaderReflection, ExactSerialization) 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\n1 2 41 0\n1 7 41 0\n2 7 41 0\n") + '\n'); + 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); } @@ -240,7 +245,7 @@ TEST(WGSLShaderReflection, ParseKnownMetadataAndOwnStrings) TEST(WGSLShaderReflection, SuccessfulParseReplacesPreviousReflection) { auto Reflection = MakeReferenceReflection(); - ASSERT_TRUE(ParseWGSLShaderReflection(MakeMetadata("1 10 20 41 0"), Reflection)); + 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); @@ -283,7 +288,7 @@ TEST(WGSLShaderReflection, StringsRoundTrip) Buffer->AddVariable(ShaderCodeVariableDescX{}); Buffer->GetVariable(0).Name = nullptr; Buffer->GetVariable(0).TypeName = nullptr; - EXPECT_EQ(Serialize(NullNames), "\n" + MakeMetadata("1\n0 0 - 1\n0 1\n- - 0 0 0 0 0 0 0\n") + '\n'); + 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); } @@ -296,7 +301,7 @@ TEST(WGSLShaderReflection, AllHexDigitInputs) { SCOPED_TRACE(::testing::Message{} << "Byte " << Ch << ", nibble " << Position); auto Fields = MakeValidTokens(); - Fields[3][Position] = static_cast(Ch); + 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); @@ -324,7 +329,7 @@ TEST(WGSLShaderReflection, Uint32Boundaries) { SCOPED_TRACE(Value); auto Fields = MakeValidTokens(); - for (size_t Field : {1u, 2u, 5u, 13u, 14u}) + for (size_t Field : {2u, 3u, 6u, 14u, 15u}) Fields[Field] = std::to_string(Value); WGSLShaderReflection Reflection; ASSERT_TRUE(ParseWGSLShaderReflection(JoinTokens(Fields), Reflection)); @@ -337,7 +342,7 @@ TEST(WGSLShaderReflection, Uint32Boundaries) 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\n" + Number + ' ' + Number + " 52 1\n" + Number + " 1\n56 54 1 11 1 1 " + Number + ' ' + Number + " 0\n") + '\n'); + 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); } } @@ -348,8 +353,8 @@ TEST(WGSLShaderReflection, AllUint8Dimensions) { SCOPED_TRACE(Value); auto Fields = MakeValidTokens(); - Fields[11] = std::to_string(Value); - Fields[12] = std::to_string(255 - Value); + 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); @@ -367,8 +372,8 @@ TEST(WGSLShaderReflection, AllVariableClassesAndBasicTypes) { SCOPED_TRACE(::testing::Message{} << "Class " << Class << ", type " << Type); auto Fields = MakeValidTokens(); - Fields[9] = std::to_string(Class); - Fields[10] = std::to_string(Type); + 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); @@ -393,7 +398,7 @@ TEST(WGSLShaderReflection, WhitespaceAndNumericSpelling) EXPECT_EQ(Serialize(Reflection), "\n" + MakeMetadata(ReferencePayload) + '\n'); } auto Fields = MakeValidTokens(); - for (size_t Field : {0u, 1u, 2u, 4u, 5u, 6u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}) + 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)); @@ -412,6 +417,7 @@ TEST(WGSLShaderReflection, RejectMalformedEnvelope) MakeMetadata(" \t\n"), MakeMetadata("0"), MakeMetadata("2"), + MakeMetadata("3"), MakeMetadata("4294967295"), MakeMetadata("1x"), MakeMetadata("1") + MakeMetadata("1"), @@ -429,7 +435,7 @@ TEST(WGSLShaderReflection, RejectMalformedEnvelope) 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, 4u, 5u, 6u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}) + for (size_t Field : {0u, 1u, 2u, 3u, 5u, 6u, 7u, 10u, 11u, 12u, 13u, 14u, 15u, 16u}) { for (const auto& Bad : BadNumbers) { @@ -444,18 +450,20 @@ TEST(WGSLShaderReflection, RejectInvalidNumberInEveryField) TEST(WGSLShaderReflection, RejectOutOfRangeFields) { const std::pair InvalidValues[] = { - {4, 2}, - {4, 255}, - {9, SHADER_CODE_VARIABLE_CLASS_COUNT}, - {9, 255}, - {9, 256}, - {10, SHADER_CODE_BASIC_TYPE_COUNT}, + {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}, - {11, 65535}, {12, 256}, {12, 65535}, + {13, 256}, + {13, 65535}, }; for (const auto& Entry : InvalidValues) { @@ -469,7 +477,7 @@ TEST(WGSLShaderReflection, RejectOutOfRangeFields) 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 : {3u, 7u, 8u}) + for (size_t Field : {4u, 8u, 9u}) { for (const auto& Bad : BadStrings) { @@ -493,7 +501,7 @@ TEST(WGSLShaderReflection, RejectTruncatedInputAtEveryByte) // 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 41 1\n64 1\n42 54 5 0 0 0 0 0 1\n43 - 1 11 1 1 0 0 0"; + 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) @@ -508,23 +516,23 @@ TEST(WGSLShaderReflection, RejectMismatchedCounts) for (const char* Count : {"0", "2", "4294967295"}) { auto Fields = MakeValidTokens(); - Fields[6] = Count; + Fields[7] = Count; ExpectRejected(JoinTokens(Fields)); } for (const char* Count : {"1", "2", "4294967295"}) { auto Fields = MakeValidTokens(); - Fields[15] = Count; + Fields[16] = Count; ExpectRejected(JoinTokens(Fields)); } - ExpectRejected(MakeMetadata("1\n0 0 41 1\n64 0\n42 - 1 11 1 1 0 0 0\n")); + 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 41 0\n0 0 42 0\n")); - ExpectRejected(MakeMetadata("1\n0 0 41 1\n0 0\n0 0 41 0\n")); - ExpectRejected(MakeMetadata("1\n0 0 41 0\n1 0 42 0\n0 0 43 0\n")); + 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) @@ -532,7 +540,7 @@ TEST(WGSLShaderReflection, NestingLimit) for (unsigned Depth : {1u, 63u, 64u, 65u}) { SCOPED_TRACE(Depth); - std::string Payload = "1\n0 0 41 1\n16 1\n"; + 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) @@ -607,4 +615,60 @@ TEST(WGSLShaderReflection, GeneratedLayoutsRoundTrip) } } + +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, 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)); + } +} + } // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index afc645938a..8297bf7a9a 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp @@ -689,4 +689,108 @@ TEST(WGSLUtils, RemapRejectsInvalidResourceMappings) } } + +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()); +} + } // namespace From 6a6d3e9ffaeeb2bbcfd2d04d32c48dc29d0906bf Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 09:13:45 -0700 Subject: [PATCH 14/21] WebGPU: Expand parsing, reflection, and remapping test coverage Cover metadata edge cases, native WGSL layouts, SPIR-V conversion fallbacks, specialization values, and asynchronous entry-point remapping. --- .../src/ShaderCreationTest.cpp | 22 +- .../src/SpecializationConstantsTest.cpp | 141 ++++++++ .../ShaderTools/WGSLShaderReflectionTest.cpp | 57 ++- .../ShaderTools/WGSLShaderResourcesTest.cpp | 333 +++++++++++++++++ .../src/ShaderTools/WGSLUtilsTest.cpp | 339 +++++++++++++++++- 5 files changed, 885 insertions(+), 7 deletions(-) diff --git a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp index 7a77ec9e45..74bbc2c40b 100644 --- a/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp +++ b/Tests/DiligentCoreAPITest/src/ShaderCreationTest.cpp @@ -246,7 +246,7 @@ TEST(ShaderCreationTest, FromBytecode) } -TEST(ShaderCreationTest, WGSLEntryPointRemapping) +void TestWGSLEntryPointRemapping(PSO_CREATE_FLAGS Flags) { GPUTestingEnvironment* pEnv = GPUTestingEnvironment::GetInstance(); IRenderDevice* pDevice = pEnv->GetDevice(); @@ -282,23 +282,39 @@ TEST(ShaderCreationTest, WGSLEntryPointRemapping) 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(), PIPELINE_STATE_STATUS_READY); + 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(), PIPELINE_STATE_STATUS_READY); + 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 8c3502308d..37465ecfc1 100644 --- a/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp +++ b/Tests/DiligentCoreAPITest/src/SpecializationConstantsTest.cpp @@ -954,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/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp index fc2abc2379..5c3e972233 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderReflectionTest.cpp @@ -233,13 +233,15 @@ TEST(WGSLShaderReflection, ExactSerialization) TEST(WGSLShaderReflection, ParseKnownMetadataAndOwnStrings) { - std::string Text = "// Earlier comment */\n" + MakeMetadata(ReferencePayload) + "\n/*$SHADER_SOURCE_LANGUAGE=1*/"; + 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, MakeReferenceReflection()); + ExpectReflectionEqual(Reflection, Expected); } TEST(WGSLShaderReflection, SuccessfulParseReplacesPreviousReflection) @@ -271,6 +273,8 @@ TEST(WGSLShaderReflection, StringsRoundTrip) 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]); @@ -611,6 +615,14 @@ TEST(WGSLShaderReflection, GeneratedLayoutsRoundTrip) 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); } } @@ -645,6 +657,44 @@ TEST(WGSLShaderReflection, SpecializationConstants) 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 : { @@ -668,6 +718,9 @@ TEST(WGSLShaderReflection, RejectMalformedSpecializationConstants) { 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))); } } diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index e345951d82..786cf8488a 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -33,6 +33,7 @@ #include "RefCntAutoPtr.hpp" #include "EngineMemory.h" +#include #include #include "TestingEnvironment.hpp" @@ -429,4 +430,336 @@ TEST(WGSLShaderResources, SpecializationConstants) } } + +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, 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); + } + } +} + } // namespace diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp index 8297bf7a9a..834719deb2 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLUtilsTest.cpp @@ -26,6 +26,7 @@ #include "WGSLUtils.hpp" #include "WGSLShaderResources.hpp" +#include "WGSLShaderReflection.hpp" #include "SPIRVShaderResources.hpp" #include "GLSLangUtils.hpp" #include "DefaultShaderSourceStreamFactory.h" @@ -112,6 +113,114 @@ float4 main(float3 Position : ATTRIB0) : SV_Position 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{}); @@ -129,6 +238,8 @@ TEST(WGSLUtils, GetWGSLEmulatedArrayElementInfo) 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)); @@ -601,8 +712,14 @@ override Shared: u32 = 1; 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; @@ -610,12 +727,12 @@ 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]; + 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(), 4u); + 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); @@ -793,4 +910,222 @@ TEST(WGSLUtils, RemapRejectsConflictingResourceNames) 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 From d1149afb63effe43b8b7760fc1af3bcc10b5f02e Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 09:19:41 -0700 Subject: [PATCH 15/21] WebGPU: Fix reflection crash for non-struct uniforms Reflect scalar, vector, matrix, and array uniforms as a single variable at offset zero. Add regression tests for native WGSL and names-only metadata. --- .../ShaderTools/src/WGSLShaderResources.cpp | 26 +++++-- .../ShaderTools/WGSLShaderResourcesTest.cpp | 72 +++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/Graphics/ShaderTools/src/WGSLShaderResources.cpp b/Graphics/ShaderTools/src/WGSLShaderResources.cpp index 6e05ea9e68..f49846cd14 100644 --- a/Graphics/ShaderTools/src/WGSLShaderResources.cpp +++ b/Graphics/ShaderTools/src/WGSLShaderResources.cpp @@ -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; diff --git a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp index 786cf8488a..8775042e5e 100644 --- a/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp +++ b/Tests/DiligentCoreTest/src/ShaderTools/WGSLShaderResourcesTest.cpp @@ -487,6 +487,78 @@ struct Constants { } } +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"( From 7c158c9491b7062e34a8419c1e7d4822aa880dfe Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 09:41:48 -0700 Subject: [PATCH 16/21] ShaderTools: Fix SPIR-V instruction traversal when patching image formats Advance by instruction word count to avoid interpreting operands as opcodes. Add a regression test for incorrect patching caused by opcode-like operands. --- Graphics/ShaderTools/src/SPIRVUtils.cpp | 23 ++++-- Tests/DiligentCoreTest/CMakeLists.txt | 6 ++ .../src/ShaderTools/SPIRVUtilsTest.cpp | 82 +++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 Tests/DiligentCoreTest/src/ShaderTools/SPIRVUtilsTest.cpp 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/Tests/DiligentCoreTest/CMakeLists.txt b/Tests/DiligentCoreTest/CMakeLists.txt index d4e5304561..c36740917c 100644 --- a/Tests/DiligentCoreTest/CMakeLists.txt +++ b/Tests/DiligentCoreTest/CMakeLists.txt @@ -21,6 +21,12 @@ if(NOT WEBGPU_SUPPORTED) ) endif() +if(NOT ${DILIGENT_USE_SPIRV_TOOLCHAIN}) + list(REMOVE_ITEM SOURCE + ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/SPIRVUtilsTest.cpp + ) +endif() + if(NOT ${DILIGENT_USE_SPIRV_TOOLCHAIN} OR ${DILIGENT_NO_GLSLANG}) list(REMOVE_ITEM SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/ShaderTools/SPIRVShaderResourcesTest.cpp 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 From 45c5ca076b32ec65221cbcd25d9f8f49ebdb5ee4 Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 18:32:13 -0700 Subject: [PATCH 17/21] WebGPU: Add texture component swizzle support --- .../include/WebGPUTypeConversions.hpp | 7 ++- .../src/EngineFactoryWebGPU.cpp | 3 +- .../src/TextureWebGPUImpl.cpp | 14 +++++ .../src/WebGPUTypeConversions.cpp | 25 +++++++++ .../src/TextureSwizzleTest.cpp | 53 ++++++++++++++++++- 5 files changed, 99 insertions(+), 3 deletions(-) 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/src/EngineFactoryWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp index 8764fdb5ff..c84ac05f45 100644 --- a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp @@ -239,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); @@ -386,7 +387,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; diff --git a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp index 5662972aac..1d13983d75 100644 --- a/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/TextureWebGPUImpl.cpp @@ -521,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/WebGPUTypeConversions.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp index 3cdb2de612..051fe5d182 100644 --- a/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/WebGPUTypeConversions.cpp @@ -955,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/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 From 5786d58e6711131f05ec3baf22daae11e227d626 Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 18:48:18 -0700 Subject: [PATCH 18/21] WebGPU: Enable float32 blending when supported --- Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp index c84ac05f45..e7ee94b181 100644 --- a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp @@ -247,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); From 0880a854cdfdfcb8b731cb7e1de08aa11c1e0c99 Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 18:54:47 -0700 Subject: [PATCH 19/21] WebGPU: Expand texture format capabilities and enable tier 2 support --- .../src/EngineFactoryWebGPU.cpp | 3 ++ .../src/GenerateMipsHelperWebGPU.cpp | 21 +++++++- .../src/RenderDeviceWebGPUImpl.cpp | 52 +++++++++++-------- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp index e7ee94b181..8f8ca736e8 100644 --- a/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/EngineFactoryWebGPU.cpp @@ -264,6 +264,9 @@ WebGPUDeviceWrapper CreateDeviceForAdapter(const DeviceFeatures& Features, WGPUI if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_TextureFormatsTier1)) wgpuFeatures.push_back(WGPUFeatureName_TextureFormatsTier1); + + if (wgpuAdapterHasFeature(wgpuAdapter, WGPUFeatureName_TextureFormatsTier2)) + wgpuFeatures.push_back(WGPUFeatureName_TextureFormatsTier2); } struct CallbackUserData 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/RenderDeviceWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp index a3cdbbf092..5d0ea2ab25 100644 --- a/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/RenderDeviceWebGPUImpl.cpp @@ -482,35 +482,41 @@ void RenderDeviceWebGPUImpl::FindSupportedTextureFormats() const bool TextureCompressionBCSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureCompressionBC); const bool TextureCompressionETC2Supported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureCompressionETC2); const bool TextureFormatsTier1Supported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_TextureFormatsTier1); - const bool Norm16FilterableSupported = wgpuDeviceHasFeature(m_wgpuDevice, WGPUFeatureName_Unorm16TextureFormats); - const bool R16UnormSupported = TextureFormatsTier1Supported || Norm16FilterableSupported; - const bool R16SnormSupported = TextureFormatsTier1Supported; + 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); @@ -526,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); @@ -569,16 +575,16 @@ void RenderDeviceWebGPUImpl::FindSupportedTextureFormats() if (R16UnormSupported) { - SetTexFormatInfo({TEX_FORMAT_R16_UNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_UNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGBA16_UNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : 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, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RG16_SNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : FMT_FLAG_MSAA); - SetTexFormatInfo({TEX_FORMAT_RGBA16_SNORM}, BIND_SR, Norm16FilterableSupported ? FMT_FLAG_FILTER | FMT_FLAG_MSAA : 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) From 43d61282916d2052006c00872e513f97bf88543c Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 19:08:40 -0700 Subject: [PATCH 20/21] WebGPU: Fix macOS surface setup and report readback mapping errors --- .../src/SwapChainWebGPUImpl.cpp | 12 +++++++----- .../src/WebGPUResourceBase.cpp | 4 ++++ Platforms/Apple/interface/MacOSNativeWindow.h | 6 +++++- Platforms/Apple/src/MacOSNativeWindow.mm | 18 +++++++++++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp index e19a2e3f20..bc52da1cad 100644 --- a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp @@ -442,10 +442,10 @@ void SwapChainWebGPUImpl::CreateSurface() wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceXCBWindow}; wgpuSurfaceNativeDesc.connection = m_NativeWindow.pXCBConnection; wgpuSurfaceNativeDesc.window = m_NativeWindow.WindowId; -#elif PLATFROM_MACOS +#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 WGPUEmscriptenSurfaceSourceCanvasHTMLSelector wgpuSurfaceNativeDesc{}; wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector}; @@ -523,12 +523,14 @@ void SwapChainWebGPUImpl::ConfigureSurface() 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 CanvasWidth = 0; int32_t 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 diff --git a/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp b/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp index 0a85caf6fb..41af3874ad 100644 --- a/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/WebGPUResourceBase.cpp @@ -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(); 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 From 199d9f3cc18723e7eafc5986d38b81e0995245ed Mon Sep 17 00:00:00 2001 From: assiduous Date: Wed, 14 Oct 2026 20:06:05 -0700 Subject: [PATCH 21/21] WebGPU: Render directly to swap chain surface textures Remove the intermediate back buffer and presentation draw. Acquire surface textures on demand and handle their lifetime, resize, and presentation on native platforms and the web. Add swap chain tests for linear and sRGB formats. --- Graphics/GraphicsEngine/interface/SwapChain.h | 8 +- .../include/SwapChainWebGPUImpl.hpp | 33 +- .../src/SwapChainWebGPUImpl.cpp | 708 +++++++----------- .../src/WebGPU/SwapChainTestWebGPU.cpp | 214 ++++++ 4 files changed, 501 insertions(+), 462 deletions(-) create mode 100644 Tests/DiligentCoreAPITest/src/WebGPU/SwapChainTestWebGPU.cpp 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/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/src/SwapChainWebGPUImpl.cpp b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp index bc52da1cad..a788c22d69 100644 --- a/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp +++ b/Graphics/GraphicsEngineWebGPU/src/SwapChainWebGPUImpl.cpp @@ -29,11 +29,10 @@ #include "SwapChainWebGPUImpl.hpp" #include "RenderDeviceWebGPUImpl.hpp" #include "DeviceContextWebGPUImpl.hpp" -#include "TextureViewWebGPU.h" +#include "TextureWebGPUImpl.hpp" #include "WebGPUTypeConversions.hpp" -#include "WebGPUDefinitions.hpp" -#ifdef PLATFORM_WIN32 +#if PLATFORM_WIN32 # include #endif @@ -44,320 +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_SuccessOptimal: - case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: - break; - - case WGPUSurfaceGetCurrentTextureStatus_Timeout: - case WGPUSurfaceGetCurrentTextureStatus_Outdated: - return wgpuSurfaceTexture.status; - - case WGPUSurfaceGetCurrentTextureStatus_Lost: - LOG_WARNING_MESSAGE("Unable to present: swap chain surface 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) { @@ -368,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() = default; +SwapChainWebGPUImpl::~SwapChainWebGPUImpl() +{ + ReleaseSwapChainResources(); + if (m_wgpuSurface) + wgpuSurfaceUnconfigure(m_wgpuSurface); +} + +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) { @@ -381,32 +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_SuccessSuboptimal || - SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Outdated || - SurfaceStatus == WGPUSurfaceGetCurrentTextureStatus_Lost || - m_VSyncEnabled != EnableVSync) + if (m_VSyncEnabled != EnableVSync) { - m_VSyncEnabled = EnableVSync; - RecreateSwapChain(); + m_VSyncEnabled = EnableVSync; + m_ReconfigureSurface = true; } } @@ -428,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(); @@ -439,9 +167,9 @@ 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; + wgpuSurfaceNativeDesc.window = m_NativeWindow.WindowId; #elif PLATFORM_MACOS WGPUSurfaceSourceMetalLayer wgpuSurfaceNativeDesc{}; wgpuSurfaceNativeDesc.chain = {nullptr, WGPUSType_SurfaceSourceMetalLayer}; @@ -459,168 +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(); + m_SurfaceConfigured = false; + const RenderDeviceWebGPUImpl* pDevice = m_pRenderDevice.RawPtr(); - WGPUSurfaceCapabilities wgpuSurfaceCapabilities{}; - wgpuSurfaceGetCapabilities(m_wgpuSurface.Get(), pRenderDeviceWebGPU->GetWebGPUAdapter(), &wgpuSurfaceCapabilities); - - 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.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/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