From 81a02ce572bef1651f71d64db1bf15f910847942 Mon Sep 17 00:00:00 2001 From: Manh Hung Date: Wed, 15 Jul 2026 00:04:07 +0700 Subject: [PATCH] Checkpoint renderer architecture and bootstrap RTXDI --- .gitmodules | 3 + CMakeLists.txt | 86 +- cmake/CompileHlslShader.cmake | 41 + cmake/VerifyKnownCommandLineOptions.cmake | 58 + include/rtv/Application.h | 10 +- include/rtv/AssetBrowserPanel.h | 6 +- include/rtv/BindlessResources.h | 21 +- include/rtv/CommandSystem.h | 1 + include/rtv/DescriptorWriteDiagnostics.h | 74 + include/rtv/DescriptorWriter.h | 3 +- include/rtv/DiagnosticImageExport.h | 3 +- include/rtv/DiagnosticTools.h | 9 +- include/rtv/EditorDockspace.h | 2 + include/rtv/EditorLayer.h | 1 + include/rtv/EditorPanels.h | 13 +- include/rtv/EditorPreferences.h | 5 +- include/rtv/EditorUiStyle.h | 212 +- include/rtv/FreeListAllocator.h | 24 +- include/rtv/GpuProfiler.h | 20 +- include/rtv/GpuScene.h | 10 +- include/rtv/GpuValidation.h | 41 + include/rtv/HeadlessDiagnostics.h | 31 + include/rtv/KnownCommandLineOptions.h | 461 +++ include/rtv/PathTracerRenderer.h | 95 +- include/rtv/RenderGraph.h | 2 + include/rtv/RenderGraphResource.h | 1 + include/rtv/RendererCoreRegressionTests.h | 9 + include/rtv/RendererDebug.h | 38 + include/rtv/RendererPassContractTypes.h | 65 + include/rtv/RendererPassContracts.h | 3416 +++++++++++++++++++ include/rtv/RendererPassOwners.h | 172 + include/rtv/RendererSettings.h | 93 +- include/rtv/RtxdiRuntime.h | 52 + include/rtv/SceneComponents.h | 26 +- include/rtv/SceneDocument.h | 2 +- include/rtv/SceneStatsPanel.h | 2 +- include/rtv/ShaderCompiler.h | 2 +- include/rtv/StreamingGpuTransferExecutor.h | 53 +- include/rtv/StreamingStagingRing.h | 4 + include/rtv/Swapchain.h | 3 + include/rtv/UploadContext.h | 1 + include/rtv/ViewportPanel.h | 6 + include/rtv/VulkanContext.h | 18 + include/rtv/passes/CompositePass.h | 67 + include/rtv/passes/DebugViewPass.h | 99 + include/rtv/passes/DenoiserPass.h | 94 + include/rtv/passes/DlssPass.h | 184 + include/rtv/passes/GBufferPass.h | 44 + include/rtv/passes/NrdPass.h | 54 + include/rtv/passes/PathTracePass.h | 146 + include/rtv/passes/ProfilerPass.h | 44 + include/rtv/passes/RegirPass.h | 590 ++++ include/rtv/passes/RestirDIPass.h | 630 ++++ include/rtv/passes/RestirGIPass.h | 437 +++ include/rtv/passes/TemporalAAPass.h | 62 + shaders/denoiser.comp | 61 +- shaders/dlss_guide_visualize.comp | 78 + shaders/dlss_rr_guides.comp | 46 +- shaders/moment_update.comp | 6 +- shaders/nrd_confidence_filter.comp | 120 + shaders/nrd_confidence_gradient.comp | 291 ++ shaders/nrd_prepare.comp | 50 +- shaders/nrd_resolve.comp | 43 + shaders/pathtrace.rahit | 16 +- shaders/pathtrace.rchit | 15 +- shaders/pathtrace.rgen | 93 +- shaders/pathtrace_camera_volume.glsl | 13 +- shaders/pathtrace_integrator.glsl | 101 +- shaders/pathtrace_lighting.glsl | 112 +- shaders/pathtrace_ray_queries.glsl | 255 +- shaders/pathtrace_restir.glsl | 59 +- shaders/pathtrace_shadow.rahit | 17 +- shaders/pathtrace_terminal.rahit | 12 +- shaders/pathtrace_terminal.rchit | 12 +- shaders/psr_guides.glsl | 99 + shaders/regir_build.comp | 12 + shaders/regir_spatial_reuse.comp | 8 + shaders/regir_temporal_reuse.comp | 8 + shaders/restir_di_final.comp | 232 +- shaders/restir_di_light_evaluation.glsl | 152 +- shaders/restir_di_spatial.comp | 69 +- shaders/restir_di_temporal.comp | 119 +- shaders/restir_di_types_accessors.glsl | 39 + shaders/restir_di_visibility.glsl | 2 +- shaders/restir_gi_final.comp | 56 +- shaders/restir_gi_final_production.comp | 109 +- shaders/restir_gi_prod_packing.glsl | 12 + shaders/restir_gi_spatial_production.comp | 21 +- shaders/restir_gi_temporal.comp | 25 +- shaders/rt_common.glsl | 13 + shaders/rt_material.glsl | 60 +- shaders/rt_resources.glsl | 4 + shaders/rt_restir_di_bindings.glsl | 10 +- shaders/rtxdi/rtxdi_contract_smoke.hlsl | 21 + shaders/taa.comp | 219 +- shaders/temporal_common.glsl | 5 +- src/main.cpp | 205 +- src/rtv/Application.cpp | 275 +- src/rtv/AssetBrowserPanel.cpp | 175 +- src/rtv/BindlessResources.cpp | 155 +- src/rtv/CommandSystem.cpp | 28 +- src/rtv/DescriptorWriter.cpp | 151 +- src/rtv/DiagnosticImageExport.cpp | 88 +- src/rtv/DiagnosticTools.cpp | 594 +++- src/rtv/EditorDockspace.cpp | 331 +- src/rtv/EditorLayer.cpp | 30 +- src/rtv/EditorPanels.cpp | 80 +- src/rtv/EditorPreferences.cpp | 6 + src/rtv/GpuProfiler.cpp | 51 +- src/rtv/GpuScene.cpp | 127 +- src/rtv/GpuValidation.cpp | 160 + src/rtv/HeadlessDiagnostics.cpp | 1478 +++++++- src/rtv/ImageBarrier.cpp | 4 + src/rtv/InspectorPanel.cpp | 41 +- src/rtv/PathTracerRenderer.cpp | 3257 ++++++++++++------ src/rtv/RayTracingScene.cpp | 95 +- src/rtv/RenderGraph.cpp | 291 +- src/rtv/RenderGraphDump.cpp | 549 ++- src/rtv/RenderSettingsPanel.cpp | 464 ++- src/rtv/RendererCoreRegressionTests.cpp | 143 + src/rtv/RendererDebug.cpp | 131 +- src/rtv/RtxdiRuntime.cpp | 142 + src/rtv/SceneDocument.cpp | 27 +- src/rtv/SceneHierarchyPanel.cpp | 155 +- src/rtv/SceneRenderSettingsSync.cpp | 8 +- src/rtv/SceneStatsPanel.cpp | 143 +- src/rtv/StreamingGpuTransferExecutor.cpp | 478 ++- src/rtv/StreamingStagingRing.cpp | 7 + src/rtv/Swapchain.cpp | 41 +- src/rtv/UiOverlay.cpp | 82 +- src/rtv/UploadContext.cpp | 27 +- src/rtv/ViewportPanel.cpp | 97 +- src/rtv/VulkanContext.cpp | 80 +- src/third_party/imgui/imgui_impl_vulkan.cpp | 16 +- third_party/RTXDI-Library | 1 + 135 files changed, 18079 insertions(+), 2475 deletions(-) create mode 100644 .gitmodules create mode 100644 cmake/CompileHlslShader.cmake create mode 100644 cmake/VerifyKnownCommandLineOptions.cmake create mode 100644 include/rtv/DescriptorWriteDiagnostics.h create mode 100644 include/rtv/KnownCommandLineOptions.h create mode 100644 include/rtv/RendererCoreRegressionTests.h create mode 100644 include/rtv/RendererPassContractTypes.h create mode 100644 include/rtv/RendererPassContracts.h create mode 100644 include/rtv/RendererPassOwners.h create mode 100644 include/rtv/RtxdiRuntime.h create mode 100644 include/rtv/passes/CompositePass.h create mode 100644 include/rtv/passes/DebugViewPass.h create mode 100644 include/rtv/passes/DenoiserPass.h create mode 100644 include/rtv/passes/DlssPass.h create mode 100644 include/rtv/passes/GBufferPass.h create mode 100644 include/rtv/passes/NrdPass.h create mode 100644 include/rtv/passes/PathTracePass.h create mode 100644 include/rtv/passes/ProfilerPass.h create mode 100644 include/rtv/passes/RegirPass.h create mode 100644 include/rtv/passes/RestirDIPass.h create mode 100644 include/rtv/passes/RestirGIPass.h create mode 100644 include/rtv/passes/TemporalAAPass.h create mode 100644 shaders/dlss_guide_visualize.comp create mode 100644 shaders/nrd_confidence_filter.comp create mode 100644 shaders/nrd_confidence_gradient.comp create mode 100644 shaders/psr_guides.glsl create mode 100644 shaders/rtxdi/rtxdi_contract_smoke.hlsl create mode 100644 src/rtv/RendererCoreRegressionTests.cpp create mode 100644 src/rtv/RtxdiRuntime.cpp create mode 160000 third_party/RTXDI-Library diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..df16c8c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/RTXDI-Library"] + path = third_party/RTXDI-Library + url = https://github.com/NVIDIA-RTX/RTXDI-Library diff --git a/CMakeLists.txt b/CMakeLists.txt index dd976eb..34003ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,15 @@ set(NVTX3_INCLUDE_DIR "" CACHE PATH "Path containing nvtx3/nvToolsExt.h") set(NSIGHT_GRAPHICS_SDK_DIR "" CACHE PATH "Path to NVIDIA Nsight Graphics SDK") set(NSIGHT_AFTERMATH_SDK_DIR "" CACHE PATH "Path to NVIDIA Nsight Aftermath SDK") set(NSIGHT_PERF_SDK_DIR "" CACHE PATH "Path to NVIDIA Nsight Perf SDK") +option(RTV_ENABLE_RTXDI "Enable the native NVIDIA RTXDI runtime" ON) +set(RTXDI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/RTXDI-Library" CACHE PATH "Path to the pinned RTXDI-Library checkout") + +if(RTV_ENABLE_RTXDI) + if(NOT EXISTS "${RTXDI_LIBRARY_DIR}/CMakeLists.txt") + message(FATAL_ERROR "RTXDI-Library was not found at ${RTXDI_LIBRARY_DIR}. Initialize third_party/RTXDI-Library at the pinned submodule commit.") + endif() + add_subdirectory("${RTXDI_LIBRARY_DIR}" "${CMAKE_BINARY_DIR}/rtxdi" EXCLUDE_FROM_ALL) +endif() if(NVTX3_INCLUDE_DIR STREQUAL "" AND WIN32) file(GLOB NVTX3_CANDIDATES LIST_DIRECTORIES true @@ -225,6 +234,8 @@ add_executable(rtvulkan src/rtv/RenderSettingsPanel.cpp src/rtv/RenderGraph.cpp src/rtv/RenderGraphDump.cpp + src/rtv/RendererCoreRegressionTests.cpp + src/rtv/RtxdiRuntime.cpp src/rtv/RendererDebug.cpp src/rtv/RtpkgIO.cpp src/rtv/RuntimeSkeleton.cpp @@ -275,8 +286,10 @@ add_executable(rtvulkan if(WIN32) set(RTV_GLSLANG_VALIDATOR "$ENV{VULKAN_SDK}/Bin/glslangValidator.exe") + set(RTV_DXC "$ENV{VULKAN_SDK}/Bin/dxc.exe") else() set(RTV_GLSLANG_VALIDATOR "$ENV{VULKAN_SDK}/Bin/glslangValidator") + set(RTV_DXC "$ENV{VULKAN_SDK}/Bin/dxc") endif() if(NOT EXISTS "${RTV_GLSLANG_VALIDATOR}") message(FATAL_ERROR "glslangValidator was not found at ${RTV_GLSLANG_VALIDATOR}") @@ -302,6 +315,37 @@ file(GLOB RTV_SHADER_INCLUDE_FILES CONFIGURE_DEPENDS ) set(RTV_SHADER_OUTPUTS) +if(RTV_ENABLE_RTXDI) + if(NOT EXISTS "${RTV_DXC}") + message(FATAL_ERROR "DXC was not found at ${RTV_DXC}; native RTXDI SPIR-V compilation requires the Vulkan SDK DXC package.") + endif() + file(GLOB_RECURSE RTV_RTXDI_SHADER_DEPENDENCIES CONFIGURE_DEPENDS + "${RTXDI_LIBRARY_DIR}/Include/Rtxdi/*.h" + "${RTXDI_LIBRARY_DIR}/Include/Rtxdi/*.hlsli") + + set(RTV_RTXDI_CONTRACT_SMOKE_SOURCE "${RTV_SHADER_SOURCE_DIR}/rtxdi/rtxdi_contract_smoke.hlsl") + set(RTV_RTXDI_CONTRACT_SMOKE_OUTPUT "${RTV_SHADER_OUTPUT_DIR}/rtxdi_contract_smoke.comp.spv") + add_custom_command( + OUTPUT "${RTV_RTXDI_CONTRACT_SMOKE_OUTPUT}" "${RTV_RTXDI_CONTRACT_SMOKE_OUTPUT}.options" + COMMAND ${CMAKE_COMMAND} + "-DDXC=${RTV_DXC}" + "-DSOURCE=${RTV_RTXDI_CONTRACT_SMOKE_SOURCE}" + "-DOUTPUT=${RTV_RTXDI_CONTRACT_SMOKE_OUTPUT}" + "-DOUTPUT_DIR=${RTV_SHADER_OUTPUT_DIR}" + "-DINCLUDE_DIR=${RTXDI_LIBRARY_DIR}/Include" + "-DPROFILE=cs_6_6" + "-DENTRY_POINT=main" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CompileHlslShader.cmake" + DEPENDS + "${RTV_RTXDI_CONTRACT_SMOKE_SOURCE}" + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CompileHlslShader.cmake" + ${RTV_RTXDI_SHADER_DEPENDENCIES} + VERBATIM) + list(APPEND RTV_SHADER_OUTPUTS + "${RTV_RTXDI_CONTRACT_SMOKE_OUTPUT}" + "${RTV_RTXDI_CONTRACT_SMOKE_OUTPUT}.options") +endif() + function(rtv_add_shader_variant source suffix) set(extra_defines ${ARGN}) get_filename_component(shader_name "${source}" NAME) @@ -343,6 +387,9 @@ rtv_add_shader_variant("taa.comp" "") rtv_add_shader_variant("gpu_skinning.comp" "") rtv_add_shader_variant("dlss_guides.comp" "") rtv_add_shader_variant("dlss_rr_guides.comp" "") +rtv_add_shader_variant("dlss_guide_visualize.comp" "") +rtv_add_shader_variant("nrd_confidence_gradient.comp" "") +rtv_add_shader_variant("nrd_confidence_filter.comp" "") rtv_add_shader_variant("nrd_prepare.comp" "") rtv_add_shader_variant("nrd_resolve.comp" "") rtv_add_shader_variant("restir_spatial.comp" "") @@ -386,6 +433,7 @@ rtv_add_shader_variant("fullscreen.vert" "") rtv_add_shader_variant("fullscreen.frag" "") rtv_add_shader_variant("pathtrace.rgen" ".di_packed" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("pathtrace.rgen" ".beauty_fast.di_packed" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=0" "RTV_MATERIAL_RAY_CONE_LOD=1" "RTV_GENERIC_SECONDARY_ONE_INFINITE_LIGHT=1" "RTV_GENERIC_SECONDARY_DIRECT_PROB=0.5" "RTV_GENERIC_DEEP_SECONDARY_DIRECT_PROB=0.5") +rtv_add_shader_variant("pathtrace.rgen" ".beauty_fast.no_textures.di_packed" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=0" "RTV_MATERIAL_RAY_CONE_LOD=1" "RTV_GENERIC_SECONDARY_ONE_INFINITE_LIGHT=1" "RTV_GENERIC_SECONDARY_DIRECT_PROB=0.5" "RTV_GENERIC_DEEP_SECONDARY_DIRECT_PROB=0.5" "RTV_MATERIAL_TEXTURES_ENABLED=0") rtv_add_shader_variant("pathtrace.rgen" ".regir_beauty_fast.di_packed" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=1" "RTV_REGIR_FINITE_LIGHT_TRACE_ENABLED=1" "RTV_REGIR_FRAME_COHERENT_FINITE_QUERY=1" "RTV_MATERIAL_RAY_CONE_LOD=1" "RTV_GENERIC_SECONDARY_ONE_INFINITE_LIGHT=1" "RTV_GENERIC_SECONDARY_DIRECT_PROB=0.5" "RTV_GENERIC_DEEP_SECONDARY_DIRECT_PROB=0.5") rtv_add_shader_variant("pathtrace.rgen" ".regir_stochastic_beauty_fast.di_packed" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=1" "RTV_REGIR_FINITE_LIGHT_TRACE_ENABLED=0" "RTV_MATERIAL_RAY_CONE_LOD=1" "RTV_GENERIC_SECONDARY_ONE_INFINITE_LIGHT=1" "RTV_GENERIC_SECONDARY_DIRECT_PROB=0.5" "RTV_GENERIC_DEEP_SECONDARY_DIRECT_PROB=0.5") rtv_add_shader_variant("pathtrace.rgen" ".di_full" "RTV_RESTIR_DI_VALIDATION_FULL=1" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") @@ -395,13 +443,13 @@ rtv_add_shader_variant("pathtrace.rgen" ".motion.di_packed" "RTV_MOTION_BLUR_ENA rtv_add_shader_variant("pathtrace.rgen" ".motion.di_full" "RTV_MOTION_BLUR_ENABLED=1" "RTV_RESTIR_DI_VALIDATION_FULL=1" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("pathtrace.rgen" ".motion.gi_full" "RTV_MOTION_BLUR_ENABLED=1" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=1") rtv_add_shader_variant("pathtrace.rgen" ".motion.di_full.gi_full" "RTV_MOTION_BLUR_ENABLED=1" "RTV_RESTIR_DI_VALIDATION_FULL=1" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=1") -rtv_add_shader_variant("pathtrace.rgen" ".native2b" "RTV_NATIVE2B_PIPELINE=1" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") -rtv_add_shader_variant("pathtrace.rgen" ".native2b.compact_primary_lights" "RTV_NATIVE2B_PIPELINE=1" "RTV_NATIVE2B_COMPACT_PRIMARY_LIGHTS=1" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") +rtv_add_shader_variant("pathtrace.rgen" ".native2b" "RTV_NATIVE2B_PIPELINE=1" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=0" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") +rtv_add_shader_variant("pathtrace.rgen" ".native2b.compact_primary_lights" "RTV_NATIVE2B_PIPELINE=1" "RTV_NATIVE2B_COMPACT_PRIMARY_LIGHTS=1" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=0" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.di_packed" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.di_full" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_RESTIR_DI_VALIDATION_FULL=1" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.gi_full" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=1") rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.di_full.gi_full" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_RESTIR_DI_VALIDATION_FULL=1" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=1") -rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.native2b" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_NATIVE2B_PIPELINE=1" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") +rtv_add_shader_variant("pathtrace.rgen" ".rt_diag.native2b" "RTV_RT_DIAGNOSTIC_COUNTERS=1" "RTV_NATIVE2B_PIPELINE=1" "RTV_BEAUTY_OUTPUT_ONLY=1" "RTV_REGIR_TRACE_ENABLED=0" "RTV_MOTION_BLUR_ENABLED=0" "RTV_RESTIR_DI_VALIDATION_FULL=0" "RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT=0") rtv_add_shader_variant("wavefront_trace.rgen" "") rtv_add_shader_variant("wavefront_trace.rgen" ".ser" "RTV_SER_ENABLED=1") rtv_add_shader_variant("wavefront_shadow_trace.rgen" "") @@ -491,7 +539,8 @@ target_compile_definitions(rtvulkan PRIVATE RTV_ENABLE_ASSIMP_IMPORTER=$ RTV_ASSIMP_IMPORTER_AVAILABLE=$ RTV_ENABLE_OPENUSD_IMPORTER=$ - RTV_OPENUSD_IMPORTER_AVAILABLE=$) + RTV_OPENUSD_IMPORTER_AVAILABLE=$ + RTV_HAS_RTXDI=$) target_link_libraries(rtvulkan PRIVATE volk glfw @@ -503,6 +552,9 @@ target_link_libraries(rtvulkan PRIVATE KTX::ktx TIFF::TIFF ) +if(RTV_ENABLE_RTXDI) + target_link_libraries(rtvulkan PRIVATE Rtxdi) +endif() if(EXISTS "${RENDERDOC_SDK_DIR}/renderdoc_app.h") target_compile_definitions(rtvulkan PRIVATE RTV_HAS_RENDERDOC=1) @@ -830,3 +882,29 @@ endif() target_precompile_headers(rtvulkan PRIVATE "$<$:${CMAKE_CURRENT_SOURCE_DIR}/include/rtv/Pch.h>" ) + +include(CTest) +if(BUILD_TESTING) + add_test(NAME renderer_core_regressions + COMMAND $ --selftest-renderer-core) + add_test(NAME cli_rejects_unknown_argument + COMMAND $ --selftest-renderer-core --definitely-not-a-real-flag) + add_test(NAME cli_rejects_missing_value + COMMAND $ --selftest-renderer-core --render-scale) + add_test(NAME cli_accepts_known_value_argument + COMMAND $ --selftest-renderer-core --inspection-json renderer_core_cli_probe.json) + add_test(NAME cli_accepts_known_two_value_argument + COMMAND $ --selftest-renderer-core --compare-image baseline.png current.png) + add_test(NAME cli_rejects_missing_second_value + COMMAND $ --selftest-renderer-core --compare-image baseline.png) + add_test(NAME cli_known_option_table_matches_parser + COMMAND ${CMAKE_COMMAND} + -DMAIN_SOURCE=${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp + -DOPTIONS_HEADER=${CMAKE_CURRENT_SOURCE_DIR}/include/rtv/KnownCommandLineOptions.h + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyKnownCommandLineOptions.cmake) + set_tests_properties( + cli_rejects_unknown_argument + cli_rejects_missing_value + cli_rejects_missing_second_value + PROPERTIES WILL_FAIL TRUE) +endif() diff --git a/cmake/CompileHlslShader.cmake b/cmake/CompileHlslShader.cmake new file mode 100644 index 0000000..a6d9659 --- /dev/null +++ b/cmake/CompileHlslShader.cmake @@ -0,0 +1,41 @@ +if(NOT DEFINED DXC OR DXC STREQUAL "") + message(FATAL_ERROR "DXC is required") +endif() +if(NOT DEFINED SOURCE OR SOURCE STREQUAL "") + message(FATAL_ERROR "SOURCE is required") +endif() +if(NOT DEFINED OUTPUT OR OUTPUT STREQUAL "") + message(FATAL_ERROR "OUTPUT is required") +endif() +if(NOT DEFINED INCLUDE_DIR OR INCLUDE_DIR STREQUAL "") + message(FATAL_ERROR "INCLUDE_DIR is required") +endif() +if(NOT DEFINED PROFILE OR PROFILE STREQUAL "") + set(PROFILE "cs_6_6") +endif() +if(NOT DEFINED ENTRY_POINT OR ENTRY_POINT STREQUAL "") + set(ENTRY_POINT "main") +endif() + +file(MAKE_DIRECTORY "${OUTPUT_DIR}") + +execute_process( + COMMAND "${DXC}" + -spirv + -fspv-target-env=vulkan1.3 + -fspv-reflect + -HV 2021 + -T "${PROFILE}" + -E "${ENTRY_POINT}" + -D SPIRV=1 + -I "${INCLUDE_DIR}" + -Fo "${OUTPUT}" + "${SOURCE}" + RESULT_VARIABLE compile_result) + +if(NOT compile_result EQUAL 0) + message(FATAL_ERROR "DXC failed for ${SOURCE}") +endif() + +file(WRITE "${OUTPUT}.options" + "PROFILE=${PROFILE}\nENTRY_POINT=${ENTRY_POINT}\nSPIRV=1\nINCLUDE_DIR=${INCLUDE_DIR}\n") diff --git a/cmake/VerifyKnownCommandLineOptions.cmake b/cmake/VerifyKnownCommandLineOptions.cmake new file mode 100644 index 0000000..d235f79 --- /dev/null +++ b/cmake/VerifyKnownCommandLineOptions.cmake @@ -0,0 +1,58 @@ +if(NOT DEFINED MAIN_SOURCE OR NOT DEFINED OPTIONS_HEADER) + message(FATAL_ERROR "MAIN_SOURCE and OPTIONS_HEADER are required") +endif() + +file(READ "${MAIN_SOURCE}" main_source) +string(REGEX MATCHALL "==[ \t\r\n]*\"--[A-Za-z0-9-]+\"" parser_matches "${main_source}") +set(parser_options) +foreach(match IN LISTS parser_matches) + string(REGEX REPLACE ".*\"(--[A-Za-z0-9-]+)\".*" "\\1" option "${match}") + list(APPEND parser_options "${option}") +endforeach() +list(REMOVE_DUPLICATES parser_options) +list(SORT parser_options) + +file(READ "${OPTIONS_HEADER}" options_header) +string(FIND "${options_header}" "kKnownCommandLineOptions{{" known_start) +if(known_start EQUAL -1) + message(FATAL_ERROR "Could not locate kKnownCommandLineOptions in ${OPTIONS_HEADER}") +endif() +string(SUBSTRING "${options_header}" ${known_start} -1 known_tail) +string(FIND "${known_tail}" "}};" known_end) +if(known_end EQUAL -1) + message(FATAL_ERROR "Could not locate the end of kKnownCommandLineOptions in ${OPTIONS_HEADER}") +endif() +string(SUBSTRING "${known_tail}" 0 ${known_end} known_block) +string(REGEX MATCHALL "\"--[A-Za-z0-9-]+\"" known_matches "${known_block}") +set(known_options) +foreach(match IN LISTS known_matches) + string(REGEX REPLACE "\"(--[A-Za-z0-9-]+)\"" "\\1" option "${match}") + list(APPEND known_options "${option}") +endforeach() +list(LENGTH known_options known_count_with_duplicates) +list(REMOVE_DUPLICATES known_options) +list(LENGTH known_options known_count) +if(NOT known_count EQUAL known_count_with_duplicates) + message(FATAL_ERROR "kKnownCommandLineOptions contains duplicate entries") +endif() +list(SORT known_options) + +set(missing_options) +foreach(option IN LISTS parser_options) + if(NOT option IN_LIST known_options) + list(APPEND missing_options "${option}") + endif() +endforeach() +set(extra_options) +foreach(option IN LISTS known_options) + if(NOT option IN_LIST parser_options) + list(APPEND extra_options "${option}") + endif() +endforeach() + +if(missing_options OR extra_options) + message(FATAL_ERROR + "Known CLI option table is out of sync. Missing: [${missing_options}] Extra: [${extra_options}]") +endif() + +message(STATUS "Known CLI option table matches ${known_count} parser options") diff --git a/include/rtv/Application.h b/include/rtv/Application.h index b97e049..de3d281 100644 --- a/include/rtv/Application.h +++ b/include/rtv/Application.h @@ -105,6 +105,8 @@ class Application final : private NonCopyable { bool debugViewOverride = false, bool validationCameraMotion = false, bool validationObjectMotion = false, + bool validationLightReorder = false, + bool validationLightFlicker = false, bool headless = false, ApplicationMode mode = ApplicationMode::Editor, uint32_t headlessWidth = 1280, @@ -459,6 +461,8 @@ class Application final : private NonCopyable { void mainLoop(uint32_t maxFrames); void applyValidationCameraMotion(uint32_t frameIndex); void applyValidationObjectMotion(uint32_t frameIndex); + void applyValidationLightReorder(uint32_t frameIndex); + void applyValidationLightFlicker(uint32_t frameIndex); void updateAutosave(float deltaSeconds); bool writeAutosave(); void writeCrashMarker(bool running); @@ -637,7 +641,7 @@ class Application final : private NonCopyable { void createPathTracer(const RendererSettings* settingsToRestore = nullptr); void applyActiveSceneCamera(); void syncActiveSceneCameraFromController(); - void rebuildGpuSceneAsset(); + void rebuildGpuSceneAsset(const RendererSettings* settingsOverride = nullptr); void processRendererOnlyRequests(const RendererOnlyRequests& requests); void updateCaptureReadyState(uint32_t frameNumber); [[nodiscard]] std::string activeCaptureSceneName() const; @@ -699,6 +703,10 @@ class Application final : private NonCopyable { bool debugViewOverride_ = false; bool validationCameraMotion_ = false; bool validationObjectMotion_ = false; + bool validationLightReorder_ = false; + bool validationLightReorderApplied_ = false; + bool validationLightFlicker_ = false; + bool validationLightFlickerUnavailableLogged_ = false; EntityId validationObjectMotionEntity_{}; Transform validationObjectMotionBaseTransform_{}; bool disableAsyncCompute_ = false; diff --git a/include/rtv/AssetBrowserPanel.h b/include/rtv/AssetBrowserPanel.h index 77adda6..2e50a89 100644 --- a/include/rtv/AssetBrowserPanel.h +++ b/include/rtv/AssetBrowserPanel.h @@ -167,8 +167,10 @@ class AssetBrowserPanel { std::string selectedRecordGuid_; std::vector backStack_; std::vector forwardStack_; - bool gridView_ = false; - bool showDetails_ = true; + int viewMode_ = 0; + bool gridView_ = true; + bool showDetails_ = false; + bool presentationPreferencesLoaded_ = false; bool compatibilityMode_ = true; bool openImportSettings_ = false; int importMode_ = 0; diff --git a/include/rtv/BindlessResources.h b/include/rtv/BindlessResources.h index faa3597..663f442 100644 --- a/include/rtv/BindlessResources.h +++ b/include/rtv/BindlessResources.h @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -12,6 +13,8 @@ namespace rtv { +inline constexpr uint32_t kBindlessTextureHeapVersionCount = 3; + class Image; class Buffer; @@ -21,6 +24,11 @@ struct BindlessCapabilities { bool partiallyBound = false; bool updateAfterBind = false; uint32_t maxSampledImages = 0; + uint32_t maxPerStageSampledImages = 0; + uint32_t maxPerStageSamplers = 0; + uint32_t maxDescriptorSetSampledImages = 0; + uint32_t maxDescriptorSetSamplers = 0; + uint32_t maxUpdateAfterBindDescriptorsInAllPools = 0; }; [[nodiscard]] BindlessCapabilities queryBindlessCapabilities(VkPhysicalDevice physicalDevice); @@ -56,22 +64,29 @@ class BindlessTextureHeap { void destroy(); void updateAll(const std::vector& descriptors); void patch(uint32_t slot, const VkDescriptorImageInfo& descriptor); + void beginFrame(uint32_t frameIndex); [[nodiscard]] VkDescriptorSetLayout layout() const { return layout_; } - [[nodiscard]] VkDescriptorSet descriptorSet() const { return descriptorSet_; } + [[nodiscard]] VkDescriptorSet descriptorSet() const; [[nodiscard]] uint32_t capacity() const { return capacity_; } [[nodiscard]] uint32_t patchCount() const { return patchCount_; } - [[nodiscard]] bool initialized() const { return descriptorSet_ != VK_NULL_HANDLE; } + [[nodiscard]] bool initialized() const { return descriptorSets_.front() != VK_NULL_HANDLE; } [[nodiscard]] BindlessTextureHeapStats stats() const; private: VkDevice device_ = VK_NULL_HANDLE; VkDescriptorPool pool_ = VK_NULL_HANDLE; VkDescriptorSetLayout layout_ = VK_NULL_HANDLE; - VkDescriptorSet descriptorSet_ = VK_NULL_HANDLE; + std::array descriptorSets_{}; + std::array appliedGenerations_{}; + std::vector descriptors_; + std::vector descriptorValid_; + uint64_t descriptorGeneration_ = 0; + uint32_t currentFrameSlot_ = 0; uint32_t capacity_ = 0; uint32_t descriptorCount_ = 0; uint32_t patchCount_ = 0; + bool frameSetSelected_ = false; }; struct TextureHandle { uint32_t index = UINT32_MAX; [[nodiscard]] bool valid() const { return index != UINT32_MAX; } }; diff --git a/include/rtv/CommandSystem.h b/include/rtv/CommandSystem.h index 7fcefe4..1531887 100644 --- a/include/rtv/CommandSystem.h +++ b/include/rtv/CommandSystem.h @@ -76,6 +76,7 @@ class CommandSystem final : private NonCopyable { UiOverlay* uiOverlay_ = nullptr; std::array frames_{}; std::vector imageRenderFinished_; + std::vector imageInFlightFences_; mutable uint64_t asyncTimelineValue_ = 0; mutable uint64_t asyncHistoryCompleteValue_ = 0; uint32_t frameIndex_ = 0; diff --git a/include/rtv/DescriptorWriteDiagnostics.h b/include/rtv/DescriptorWriteDiagnostics.h new file mode 100644 index 0000000..6f08bfe --- /dev/null +++ b/include/rtv/DescriptorWriteDiagnostics.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include +#include +#include + +namespace rtv { + +struct DescriptorWriteOwner { + const char* owner = ""; + const char* pass = ""; + const char* setName = ""; + int32_t setIndex = -1; +}; + +struct DescriptorWriteDiagnosticEntry { + VkDescriptorSet descriptorSet = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; + uint32_t binding = 0; + uint32_t arrayElement = 0; + uint32_t count = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_MAX_ENUM; + const char* kind = ""; + const char* source = ""; + DescriptorWriteOwner owner; +}; + +struct DescriptorWriteDiagnosticRecord { + uint64_t sequence = 0; + uint64_t descriptorSet = 0; + uint64_t descriptorSetLayout = 0; + uint32_t binding = 0; + uint32_t arrayElement = 0; + uint32_t count = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_MAX_ENUM; + std::string kind; + std::string source; + std::string owner; + std::string pass; + std::string setName; + int32_t setIndex = -1; +}; + +struct DescriptorWriteDiagnosticAggregate { + uint64_t descriptorSetLayout = 0; + uint32_t binding = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_MAX_ENUM; + std::string kind; + std::string source; + std::string owner; + std::string pass; + std::string setName; + int32_t setIndex = -1; + uint32_t minCount = 0; + uint32_t maxCount = 0; + uint64_t occurrenceCount = 0; +}; + +struct DescriptorWriteDiagnosticsSnapshot { + uint64_t updateCallCount = 0; + uint64_t writeCount = 0; + uint64_t droppedRecentWriteCount = 0; + uint32_t recentWriteLimit = 0; + std::vector recentWrites; + std::vector aggregates; +}; + +void recordDescriptorWriteUpdate(const std::vector& entries); +void resetDescriptorWriteDiagnostics(); +[[nodiscard]] DescriptorWriteDiagnosticsSnapshot descriptorWriteDiagnosticsSnapshot(); + +} // namespace rtv diff --git a/include/rtv/DescriptorWriter.h b/include/rtv/DescriptorWriter.h index b11ba31..40407f8 100644 --- a/include/rtv/DescriptorWriter.h +++ b/include/rtv/DescriptorWriter.h @@ -1,5 +1,6 @@ #pragma once +#include "rtv/DescriptorWriteDiagnostics.h" #include "rtv/DescriptorSet.h" #include @@ -15,7 +16,7 @@ class DescriptorWriter { DescriptorWriter& writeImage(uint32_t binding, VkDescriptorType type, const VkDescriptorImageInfo& imageInfo); DescriptorWriter& writeImageArray(uint32_t binding, VkDescriptorType type, const std::vector& imageInfos); DescriptorWriter& writeAccelerationStructure(uint32_t binding, VkAccelerationStructureKHR accelerationStructure); - void update(VkDevice device, DescriptorSet set) const; + void update(VkDevice device, DescriptorSet set, DescriptorWriteOwner owner = {}) const; private: struct PendingWrite { diff --git a/include/rtv/DiagnosticImageExport.h b/include/rtv/DiagnosticImageExport.h index 48e88e1..f77174b 100644 --- a/include/rtv/DiagnosticImageExport.h +++ b/include/rtv/DiagnosticImageExport.h @@ -29,7 +29,8 @@ class DiagnosticImageExport final : private NonCopyable { PathTracerRenderer& renderer, RendererDebugView view, const std::filesystem::path& outputPath, - uint32_t warmupFrames = 4); + uint32_t warmupFrames = 4, + uint32_t maxOutputDimension = 0); void writeExportManifest( const std::filesystem::path& dir, diff --git a/include/rtv/DiagnosticTools.h b/include/rtv/DiagnosticTools.h index 9c778da..91dbdee 100644 --- a/include/rtv/DiagnosticTools.h +++ b/include/rtv/DiagnosticTools.h @@ -29,6 +29,12 @@ struct ImageDiffMetrics { double maxAbsMeanChannelBias = 0.0; }; +struct ImageCompareThresholds { + std::optional minPsnr; + std::optional minSsim; + std::optional maxChangedPixelPercentage; +}; + struct SequenceMetricSummary { uint32_t frameCount = 0; double averageMse = 0.0; @@ -81,7 +87,8 @@ struct BaselinePaths { [[nodiscard]] int compareImageCommand( const std::filesystem::path& baselinePath, const std::filesystem::path& currentPath, - const std::optional& diffOutputPath); + const std::optional& diffOutputPath, + const ImageCompareThresholds& thresholds = {}); [[nodiscard]] int compareImageSequenceCommand( const std::filesystem::path& baselineDir, diff --git a/include/rtv/EditorDockspace.h b/include/rtv/EditorDockspace.h index e2be59a..7235fb0 100644 --- a/include/rtv/EditorDockspace.h +++ b/include/rtv/EditorDockspace.h @@ -23,6 +23,8 @@ class EditorDockspace { void loadLayout(); void executeCommand(EditorCommandId id, EditorRuntimeState& state, EditorPanelVisibility& visibility, EditorRequests& requests); void drawMainMenu(EditorRuntimeState& state, EditorPanelVisibility& visibility, EditorRequests& requests); + void drawTopToolbar(EditorRuntimeState& state, EditorPanelVisibility& visibility, EditorRequests& requests); + void drawBottomStatusBar(EditorRuntimeState& state); void drawDockTabIconOverlays(EditorPanelVisibility& visibility, EditorRequests& requests); void drawDockPanelChromeOverlays(); void drawHelpWindows(); diff --git a/include/rtv/EditorLayer.h b/include/rtv/EditorLayer.h index 39350a1..79d7e06 100644 --- a/include/rtv/EditorLayer.h +++ b/include/rtv/EditorLayer.h @@ -220,6 +220,7 @@ class EditorLayer { bool createDefaultScene_ = true; bool createDefaultContentFolders_ = true; int appliedThemePreset_ = -1; + float appliedUiScale_ = -1.0f; int appliedWorkspacePreset_ = -1; }; diff --git a/include/rtv/EditorPanels.h b/include/rtv/EditorPanels.h index 28ccb24..aa1538f 100644 --- a/include/rtv/EditorPanels.h +++ b/include/rtv/EditorPanels.h @@ -1,6 +1,7 @@ #pragma once #include "rtv/EditorSelection.h" +#include "rtv/EditorCommands.h" #include "rtv/EditorPreferences.h" #include "rtv/FrameWorkScheduler.h" #include "rtv/GpuUploadTicket.h" @@ -42,13 +43,13 @@ struct EditorPanelVisibility { bool inspector = true; bool assetBrowser = true; bool renderWorldSettings = false; - bool timeline = true; + bool timeline = false; bool log = true; - bool console = false; + bool console = true; bool materialEditor = false; bool renderSettings = true; bool debugProfiler = false; - bool sceneStats = false; + bool sceneStats = true; bool gpuDiagnostics = false; bool jobCenter = false; }; @@ -66,6 +67,9 @@ struct EditorViewportState { bool hovered = false; bool mouseCaptureActive = false; bool leftClicked = false; + int activeTool = 0; + bool localTransform = false; + bool snapEnabled = false; }; enum class EditorRenderJobKind : uint32_t { @@ -601,6 +605,7 @@ struct EditorNativeFileMigrationJobResult { struct EditorRequests { std::optional settings; std::optional resetAccumulation; + std::optional viewportCommand; bool newScene = false; std::optional openScene; std::optional saveScene; @@ -724,7 +729,7 @@ struct EditorRequests { std::optional removeFavorite; }; -[[nodiscard]] const std::array& editorDebugViews(); +[[nodiscard]] const std::array& editorDebugViews(); [[nodiscard]] int editorDebugViewIndex(RendererDebugView view); void editorDebugViewCombo(const char* label, RendererSettings& settings, bool& changed); void requestSettings(EditorRequests& requests, const RendererSettings& settings); diff --git a/include/rtv/EditorPreferences.h b/include/rtv/EditorPreferences.h index 304f656..09800a5 100644 --- a/include/rtv/EditorPreferences.h +++ b/include/rtv/EditorPreferences.h @@ -52,7 +52,10 @@ struct EditorPreferences { float uiScale = 1.0f; int themePreset = 0; int workspacePreset = 0; - int layoutVersion = 2; + int layoutVersion = 4; + int contentBrowserMode = 0; + bool contentBrowserGridView = true; + bool contentBrowserShowDetails = false; bool cookEmitNativeTextureTargetSets = false; int cookNativeTextureTargetSetProfile = 0; std::string cookNativeTextureTargetSetName = "editor-custom-target-set"; diff --git a/include/rtv/EditorUiStyle.h b/include/rtv/EditorUiStyle.h index 41d2a0e..1b94304 100644 --- a/include/rtv/EditorUiStyle.h +++ b/include/rtv/EditorUiStyle.h @@ -15,37 +15,47 @@ namespace rtv { namespace EditorUiMetric { -inline constexpr float panelPaddingX = 5.0f; -inline constexpr float panelPaddingY = 4.0f; -inline constexpr float rowPaddingX = 5.0f; -inline constexpr float rowPaddingY = 2.0f; -inline constexpr float compactButtonRounding = 1.0f; +inline constexpr float panelPaddingX = 9.0f; +inline constexpr float panelPaddingY = 8.0f; +inline constexpr float rowPaddingX = 6.0f; +inline constexpr float rowPaddingY = 4.0f; +inline constexpr float compactButtonRounding = 4.0f; +inline constexpr float mainToolbarHeight = 42.0f; +inline constexpr float statusBarHeight = 24.0f; +inline constexpr float toolbarButtonHeight = 30.0f; +inline constexpr float toolbarIconButtonSize = 30.0f; +inline constexpr float toolbarGroupGap = 10.0f; +inline constexpr float statusPillHeight = 26.0f; +inline constexpr float statusPillPaddingX = 10.0f; +inline constexpr float panelHeaderHeight = 28.0f; +inline constexpr float propertyRowHeight = 26.0f; inline constexpr float sidebarWidth = 204.0f; inline constexpr float detailsWidth = 284.0f; -inline constexpr float cardRounding = 4.0f; +inline constexpr float cardRounding = 5.0f; inline constexpr float cardPadding = 8.0f; inline constexpr float projectCardWidth = 214.0f; inline constexpr float projectCardHeight = 128.0f; inline constexpr float projectTemplateCardWidth = 205.0f; inline constexpr float projectCardPreviewHeight = 58.0f; -inline constexpr float contentGridCellWidth = 98.0f; -inline constexpr float contentGridThumbWidth = 78.0f; -inline constexpr float contentGridThumbHeight = 46.0f; +inline constexpr float contentGridCellWidth = 132.0f; +inline constexpr float contentGridCellHeight = 136.0f; +inline constexpr float contentGridThumbWidth = 112.0f; +inline constexpr float contentGridThumbHeight = 82.0f; inline constexpr float assetPreviewMaxWidth = 244.0f; inline constexpr float assetPreviewHeight = 104.0f; -inline constexpr float contentTreePanelRatio = 0.32f; +inline constexpr float contentTreePanelRatio = 0.22f; inline constexpr float contentDetailsPanelRatio = 0.33f; -inline constexpr float contentTreeMinWidth = 220.0f; -inline constexpr float contentTreeMaxWidth = 420.0f; +inline constexpr float contentTreeMinWidth = 190.0f; +inline constexpr float contentTreeMaxWidth = 280.0f; inline constexpr float contentDetailsMinWidth = 260.0f; inline constexpr float contentDetailsMaxWidth = 420.0f; inline constexpr float contentListMinWidth = 220.0f; inline constexpr float progressColumnWidth = 128.0f; -inline constexpr float hierarchyRowHeight = 30.0f; +inline constexpr float hierarchyRowHeight = 26.0f; inline constexpr float hierarchyIndentSpacing = 18.0f; inline constexpr float hierarchyIconSize = 16.0f; inline constexpr float hierarchyRowRightFadeWidth = 58.0f; -inline constexpr float contentRowHeight = 18.0f; +inline constexpr float contentRowHeight = 24.0f; inline constexpr float timelineTrackRowHeight = 24.0f; inline constexpr float timelineFrameWidth = 96.0f; inline constexpr float timelineRangeFrameWidth = 82.0f; @@ -71,16 +81,17 @@ inline constexpr float timelineKeyEditorFrameWidth = 74.0f; inline constexpr float timelineKeyEditorActionWidth = 78.0f; inline constexpr float timelineKeyDeleteButtonWidth = 22.0f; inline constexpr float timelineKeyDeleteButtonHeight = 20.0f; -inline constexpr float inspectorRowHeight = 24.0f; +inline constexpr float inspectorRowHeight = 26.0f; inline constexpr float inspectorLabelWidth = 132.0f; -inline constexpr float inspectorComponentHeaderHeight = 38.0f; +inline constexpr float inspectorComponentHeaderHeight = 34.0f; inline constexpr float inspectorComponentHeaderIconSize = 18.0f; inline constexpr float inspectorComponentActionSize = 22.0f; inline constexpr float viewportOverlayPaddingX = 5.0f; inline constexpr float viewportOverlayPaddingY = 2.0f; inline constexpr float viewportOverlayRounding = 2.0f; -inline constexpr float dockRightPanelRatio = 0.300f; -inline constexpr float dockBottomPanelRatio = 0.405f; +inline constexpr float dockLeftPanelRatio = 0.170f; +inline constexpr float dockRightPanelRatio = 0.250f; +inline constexpr float dockBottomPanelRatio = 0.280f; inline constexpr float dockRightInspectorRatio = 0.485f; inline constexpr float dockSplitterThickness = 5.0f; inline constexpr float dockTabRounding = 2.0f; @@ -107,9 +118,9 @@ inline constexpr float sceneTabCloseIconPaddingY = 2.0f; } // namespace EditorUiMetric namespace EditorDockWindowTitle { -inline constexpr const char* Scene = " Scene ###Scene"; +inline constexpr const char* Scene = " Viewport ###Scene"; inline constexpr const char* Hierarchy = " Hierarchy ###Hierarchy"; -inline constexpr const char* RenderSettings = " Render World Settings ###Render Settings"; +inline constexpr const char* RenderSettings = " Render Settings ###Render Settings"; inline constexpr const char* Inspector = " Inspector ###Inspector"; inline constexpr const char* MaterialEditor = " Material Editor ###Material Editor"; inline constexpr const char* Content = " Content ###Content"; @@ -200,6 +211,19 @@ inline ImFont*& editorTablerIconFontStorage() { return font; } +inline ImFont*& editorHeadingFontStorage() { + static ImFont* font = nullptr; + return font; +} + +inline void editorSetHeadingFont(ImFont* font) { + editorHeadingFontStorage() = font; +} + +inline ImFont* editorHeadingFont() { + return editorHeadingFontStorage(); +} + inline void editorSetTablerIconFont(ImFont* font) { editorTablerIconFontStorage() = font; } @@ -229,31 +253,31 @@ inline ImVec4 editorRowBandColor(bool alternate = false) { } inline ImVec4 editorWindowBgColor() { - return ImVec4(0.105f, 0.110f, 0.118f, 1.0f); + return ImVec4(0.063f, 0.078f, 0.094f, 1.0f); } inline ImVec4 editorChildBgColor() { - return ImVec4(0.128f, 0.132f, 0.140f, 1.0f); + return ImVec4(0.082f, 0.102f, 0.125f, 1.0f); } inline ImVec4 editorPopupBgColor() { - return ImVec4(0.105f, 0.110f, 0.118f, 1.0f); + return ImVec4(0.078f, 0.094f, 0.114f, 1.0f); } inline ImVec4 editorBorderColor() { - return ImVec4(0.215f, 0.225f, 0.245f, 0.92f); + return ImVec4(0.165f, 0.196f, 0.231f, 0.95f); } inline ImVec4 editorFrameBgColor() { - return ImVec4(0.150f, 0.155f, 0.165f, 1.0f); + return ImVec4(0.102f, 0.125f, 0.153f, 1.0f); } inline ImVec4 editorFrameBgHoveredColor() { - return ImVec4(0.185f, 0.195f, 0.215f, 1.0f); + return ImVec4(0.132f, 0.165f, 0.204f, 1.0f); } inline ImVec4 editorFrameBgActiveColor() { - return ImVec4(0.205f, 0.225f, 0.260f, 1.0f); + return ImVec4(0.145f, 0.190f, 0.245f, 1.0f); } inline ImVec4 editorTitleBgColor(bool active) { @@ -261,7 +285,31 @@ inline ImVec4 editorTitleBgColor(bool active) { } inline ImVec4 editorMenuBarBgColor() { - return ImVec4(0.070f, 0.074f, 0.082f, 1.0f); + return ImVec4(0.045f, 0.055f, 0.067f, 1.0f); +} + +inline ImVec4 editorToolbarBgColor() { + return ImVec4(0.061f, 0.075f, 0.091f, 1.0f); +} + +inline ImVec4 editorToolbarBorderColor() { + return ImVec4(0.151f, 0.184f, 0.220f, 1.0f); +} + +inline ImVec4 editorPanelHeaderBgColor() { + return ImVec4(0.095f, 0.108f, 0.126f, 1.0f); +} + +inline ImVec4 editorCardBgColor() { + return ImVec4(0.075f, 0.090f, 0.110f, 1.0f); +} + +inline ImVec4 editorAccentColor() { + return ImVec4(0.184f, 0.494f, 0.859f, 1.0f); +} + +inline ImVec4 editorSuccessColor() { + return ImVec4(0.212f, 0.773f, 0.420f, 1.0f); } inline ImVec4 editorTabColor(bool active, bool hovered = false) { @@ -961,7 +1009,11 @@ inline void editorDrawIconGlyph(EditorGlyphIcon icon, ImVec2 min, ImVec2 max, Im } inline ImVec2 editorIconButtonSize() { - return ImVec2(24.0f, 22.0f); + return ImVec2(28.0f, 26.0f); +} + +inline ImVec2 editorToolbarIconButtonSize() { + return ImVec2(EditorUiMetric::toolbarIconButtonSize, EditorUiMetric::toolbarButtonHeight); } inline float editorIconTextButtonWidth(const char* label) { @@ -1003,9 +1055,109 @@ inline bool editorIconTextButton(const char* id, EditorGlyphIcon icon, const cha return pressed; } +inline bool editorToolbarIconButton(const char* id, EditorGlyphIcon icon, bool active = false) { + return editorIconButton(id, icon, active, editorToolbarIconButtonSize()); +} + +inline ImVec2 editorToolbarTextButtonSize(const char* label) { + const ImGuiStyle& style = ImGui::GetStyle(); + const ImVec2 textSize = ImGui::CalcTextSize(label); + return ImVec2(EditorUiMetric::iconTextButtonTextOffsetX + textSize.x + style.FramePadding.x + 4.0f, EditorUiMetric::toolbarButtonHeight); +} + +inline bool editorToolbarTextButton(const char* id, EditorGlyphIcon icon, const char* label, bool active = false) { + const ImVec2 textSize = ImGui::CalcTextSize(label); + const ImVec2 size = editorToolbarTextButtonSize(label); + const bool pressed = ImGui::InvisibleButton(id, size); + const bool hovered = ImGui::IsItemHovered(); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImU32 bg = active + ? ImGui::GetColorU32(ImVec4(0.105f, 0.255f, 0.430f, 1.0f)) + : ImGui::GetColorU32(hovered ? editorButtonColor(false, true) : ImVec4(0.090f, 0.103f, 0.120f, 1.0f)); + dl->AddRectFilled(min, max, bg, EditorUiMetric::compactButtonRounding); + dl->AddRect(min, max, ImGui::GetColorU32(active ? editorAccentColor() : editorToolbarBorderColor()), EditorUiMetric::compactButtonRounding); + const ImU32 color = ImGui::GetColorU32(active ? ImVec4(0.88f, 0.94f, 1.0f, 1.0f) : editorIconTint(false)); + editorDrawIconGlyph(icon, ImVec2(min.x + 7.0f, min.y + 5.0f), ImVec2(min.x + 23.0f, max.y - 5.0f), color); + dl->AddText(ImVec2(min.x + EditorUiMetric::iconTextButtonTextOffsetX, min.y + (size.y - textSize.y) * 0.5f), color, label); + return pressed; +} + +inline ImVec2 editorStatusPillSize(const char* label) { + const ImVec2 textSize = ImGui::CalcTextSize(label); + return ImVec2(textSize.x + EditorUiMetric::statusPillPaddingX * 2.0f + 18.0f, EditorUiMetric::statusPillHeight); +} + +inline void editorStatusPill(const char* label, bool active = false, EditorGlyphIcon icon = EditorGlyphIcon::Stats) { + const ImVec2 textSize = ImGui::CalcTextSize(label); + const ImVec2 size = editorStatusPillSize(label); + ImGui::InvisibleButton(label, size); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImU32 bg = ImGui::GetColorU32(active ? ImVec4(0.070f, 0.170f, 0.110f, 1.0f) : ImVec4(0.078f, 0.088f, 0.104f, 1.0f)); + dl->AddRectFilled(min, max, bg, EditorUiMetric::compactButtonRounding); + dl->AddRect(min, max, ImGui::GetColorU32(active ? ImVec4(0.190f, 0.420f, 0.250f, 1.0f) : editorToolbarBorderColor()), EditorUiMetric::compactButtonRounding); + const ImU32 iconColor = ImGui::GetColorU32(active ? editorSuccessColor() : editorIconTint(false)); + editorDrawIconGlyph(icon, ImVec2(min.x + 8.0f, min.y + 5.0f), ImVec2(min.x + 22.0f, max.y - 5.0f), iconColor); + dl->AddText(ImVec2(min.x + 28.0f, min.y + (size.y - textSize.y) * 0.5f), ImGui::GetColorU32(ImVec4(0.82f, 0.86f, 0.91f, 1.0f)), label); +} + +inline bool editorPanelSectionHeader(const char* label, EditorGlyphIcon icon, bool open = true) { + if (editorHeadingFont() != nullptr) { + ImGui::PushFont(editorHeadingFont()); + } + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f)); + ImGui::PushStyleColor(ImGuiCol_Header, editorPanelHeaderBgColor()); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, editorHeaderColor(false, true)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, editorHeaderColor(true)); + const std::string id = editorGlyphLabel(label); + const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAvailWidth | (open ? ImGuiTreeNodeFlags_DefaultOpen : 0); + const bool result = ImGui::CollapsingHeader(id.c_str(), flags); + if (ImGui::IsItemVisible()) { + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + const float iconSize = 15.0f; + editorDrawIconGlyph(icon, ImVec2(min.x + 8.0f, min.y + (max.y - min.y - iconSize) * 0.5f), ImVec2(min.x + 8.0f + iconSize, min.y + (max.y - min.y + iconSize) * 0.5f), ImGui::GetColorU32(editorIconTint(result))); + } + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); + if (editorHeadingFont() != nullptr) { + ImGui::PopFont(); + } + return result; +} + +inline bool editorBeginPropertyTable(const char* id, float labelRatio = 0.42f) { + if (!ImGui::BeginTable(id, 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoSavedSettings)) { + return false; + } + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, labelRatio); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 1.0f - labelRatio); + return true; +} + +inline void editorPropertyTableRow(const char* label, const char* tooltip = nullptr) { + ImGui::TableNextRow(ImGuiTableRowFlags_None, EditorUiMetric::propertyRowHeight); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", label); + if (tooltip != nullptr && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("%s", tooltip); + } + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-FLT_MIN); +} + +inline ImVec2 editorIconTextReadoutSize(const char* label) { + const ImVec2 textSize = ImGui::CalcTextSize(label); + return ImVec2(18.0f + EditorUiMetric::iconTextReadoutTextGap + textSize.x, editorIconButtonSize().y); +} + inline void editorIconTextReadout(EditorGlyphIcon icon, const char* label, ImU32 color) { const ImVec2 textSize = ImGui::CalcTextSize(label); - const ImVec2 size(18.0f + EditorUiMetric::iconTextReadoutTextGap + textSize.x, editorIconButtonSize().y); + const ImVec2 size = editorIconTextReadoutSize(label); ImGui::InvisibleButton("##iconTextReadout", size); const ImVec2 min = ImGui::GetItemRectMin(); ImDrawList* dl = ImGui::GetWindowDrawList(); diff --git a/include/rtv/FreeListAllocator.h b/include/rtv/FreeListAllocator.h index 0359b8d..5fc95fd 100644 --- a/include/rtv/FreeListAllocator.h +++ b/include/rtv/FreeListAllocator.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include namespace rtv { @@ -9,7 +11,7 @@ template class FreeListAllocator { public: explicit FreeListAllocator(IndexType capacity) - : capacity_(capacity) { + : capacity_(capacity), allocated_(static_cast(capacity), false) { freeIndices_.reserve(static_cast(capacity)); for (IndexType i = 0; i < capacity; ++i) { freeIndices_.push_back(capacity - 1 - i); @@ -20,22 +22,20 @@ class FreeListAllocator { if (!freeIndices_.empty()) { const IndexType index = freeIndices_.back(); freeIndices_.pop_back(); + allocated_[static_cast(index)] = true; ++allocatedCount_; return index; } - if (nextIndex_ < capacity_) { - const IndexType index = nextIndex_++; - ++allocatedCount_; - return index; - } - return UINT32_MAX; + return std::numeric_limits::max(); } void free(IndexType index) { - if (index < capacity_) { - freeIndices_.push_back(index); - --allocatedCount_; + if (index >= capacity_ || !allocated_[static_cast(index)]) { + return; } + allocated_[static_cast(index)] = false; + freeIndices_.push_back(index); + --allocatedCount_; } void clear() { @@ -44,7 +44,7 @@ class FreeListAllocator { for (IndexType i = 0; i < capacity_; ++i) { freeIndices_.push_back(capacity_ - 1 - i); } - nextIndex_ = 0; + std::fill(allocated_.begin(), allocated_.end(), false); allocatedCount_ = 0; } @@ -58,9 +58,9 @@ class FreeListAllocator { private: IndexType capacity_ = 0; - IndexType nextIndex_ = 0; IndexType allocatedCount_ = 0; std::vector freeIndices_; + std::vector allocated_; }; } // namespace rtv diff --git a/include/rtv/GpuProfiler.h b/include/rtv/GpuProfiler.h index ef85bc1..ab6dc79 100644 --- a/include/rtv/GpuProfiler.h +++ b/include/rtv/GpuProfiler.h @@ -44,6 +44,10 @@ struct GpuFrameTimings { float skipDenoiserCopyMs = 0.0f; float taaMs = 0.0f; float taaHistoryCopyMs = 0.0f; + float dlssGuidesMs = 0.0f; + float dlssMs = 0.0f; + float dlssRayReconstructionGuidesMs = 0.0f; + float dlssRayReconstructionMs = 0.0f; float autoExposureMs = 0.0f; float autoExposureHistogramClearMs = 0.0f; float autoExposureHistogramMs = 0.0f; @@ -96,6 +100,10 @@ struct GpuFrameTimings { skipDenoiserCopyMs + taaMs + taaHistoryCopyMs + + dlssGuidesMs + + dlssMs + + dlssRayReconstructionGuidesMs + + dlssRayReconstructionMs + autoExposureMs + toneMapMs + selectionOutlineMs + @@ -229,7 +237,15 @@ class GpuProfiler final : private NonCopyable { AdaptiveSamplingDiagnosticsEnd = 102, AdaptiveSamplingFillStart = 103, AdaptiveSamplingFillEnd = 104, - Count = 105, + DlssGuidesStart = 105, + DlssGuidesEnd = 106, + DlssStart = 107, + DlssEnd = 108, + DlssRayReconstructionGuidesStart = 109, + DlssRayReconstructionGuidesEnd = 110, + DlssRayReconstructionStart = 111, + DlssRayReconstructionEnd = 112, + Count = 113, }; GpuProfiler() = default; @@ -249,7 +265,7 @@ class GpuProfiler final : private NonCopyable { void beginPipelineStats(VkCommandBuffer commandBuffer) const; void endPipelineStats(VkCommandBuffer commandBuffer) const; void markSubmitted() { submitted_ = true; } - void markStatsSubmitted() { statsSubmitted_ = true; } + void markStatsSubmitted() { statsSubmitted_ = statsQueryPool_ != VK_NULL_HANDLE; } void setGpuMarkersEnabled(bool enabled); [[nodiscard]] bool gpuMarkersEnabled() const { return gpuMarkersEnabled_; } diff --git a/include/rtv/GpuScene.h b/include/rtv/GpuScene.h index a132157..8405603 100644 --- a/include/rtv/GpuScene.h +++ b/include/rtv/GpuScene.h @@ -284,6 +284,12 @@ class GpuScene { [[nodiscard]] const std::vector& materialTextureDescriptors() const { return materialTextureTable_.descriptors(); } [[nodiscard]] std::vector materialCombinedDescriptors() const; [[nodiscard]] VkDescriptorImageInfo materialCombinedDescriptor(uint32_t slot) const; + [[nodiscard]] bool streamedMaterialTextureDescriptor( + const SceneAsset& scene, + TextureAssetHandle texture, + const Image& image, + uint32_t& slotOut, + VkDescriptorImageInfo& descriptorOut) const; [[nodiscard]] VkSampler materialSampler() const { return materialSampler_; } [[nodiscard]] const BindlessTextureTable& materialTextureTable() const { return materialTextureTable_; } [[nodiscard]] VkImageView materialTextureImageView(uint32_t index) const { return materialTextureTable_.imageView(index); } @@ -302,6 +308,7 @@ class GpuScene { [[nodiscard]] const std::vector& lightRecordsCpu() const { return lightRecordCpu_; } [[nodiscard]] const OpacityMicromapCpuData& opacityMicromapData() const { return opacityMicromapData_; } [[nodiscard]] bool hasTransmissiveMaterials() const { return hasTransmissiveMaterials_; } + [[nodiscard]] bool hasMaterialTextures() const { return hasMaterialTextures_; } struct MemoryBreakdown { VkDeviceSize totalBufferBytes = 0; @@ -378,7 +385,7 @@ class GpuScene { void createCornellBox(BufferUploader& uploader); void createImportedScene(BufferUploader& uploader, const SceneAsset& importedScene, const AssetManager& assets); - void createImportedSceneFromCache(BufferUploader& uploader, const CachedScene& cached, const std::vector& activeSceneLights); + void createImportedSceneFromCache(BufferUploader& uploader, const CachedScene& cached, const SceneAsset& activeScene); void createImportedSceneGeometryFromCache(BufferUploader& uploader, const CachedScene& cached, const SceneAsset& activeScene); void createDefaultMaterialTexture(BufferUploader& uploader); void createImportedMaterialTextures(BufferUploader& uploader, const SceneAsset& importedScene, const AssetManager& assets); @@ -466,6 +473,7 @@ class GpuScene { std::vector emissiveLightRecords_; std::vector instanceRecordCpu_; bool hasTransmissiveMaterials_ = false; + bool hasMaterialTextures_ = false; }; } // namespace rtv diff --git a/include/rtv/GpuValidation.h b/include/rtv/GpuValidation.h index f0d75f1..2c4a1e9 100644 --- a/include/rtv/GpuValidation.h +++ b/include/rtv/GpuValidation.h @@ -49,6 +49,47 @@ struct ResourceStateEvent { VkAccessFlags2 afterAccess = VK_ACCESS_2_NONE; }; +struct ManualBarrierEscapeEvent { + uint64_t sequence = 0; + std::string source; + std::string label; + std::string resourceKind; + uint64_t resourceHandle = 0; + VkPipelineStageFlags2 srcStage = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 srcAccess = VK_ACCESS_2_NONE; + VkPipelineStageFlags2 dstStage = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 dstAccess = VK_ACCESS_2_NONE; + VkImageLayout oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout newLayout = VK_IMAGE_LAYOUT_UNDEFINED; +}; + +struct ManualBarrierEscapeAggregate { + std::string source; + std::string label; + std::string resourceKind; + VkPipelineStageFlags2 srcStage = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 srcAccess = VK_ACCESS_2_NONE; + VkPipelineStageFlags2 dstStage = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 dstAccess = VK_ACCESS_2_NONE; + VkImageLayout oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout newLayout = VK_IMAGE_LAYOUT_UNDEFINED; + uint64_t count = 0; +}; + +struct ManualBarrierEscapeDiagnosticsSnapshot { + bool enabled = false; + uint64_t dependencyCallCount = 0; + uint64_t barrierCount = 0; + uint64_t droppedRecentEventCount = 0; + std::vector recentEvents; + std::vector aggregates; +}; + +void setManualBarrierEscapeDiagnosticsEnabled(bool enabled); +void resetManualBarrierEscapeDiagnostics(); +void recordManualBarrierEscape(std::string source, std::string label, const VkDependencyInfo& dependency); +[[nodiscard]] ManualBarrierEscapeDiagnosticsSnapshot manualBarrierEscapeDiagnosticsSnapshot(); + class RendererValidationLog { public: void recordBarrier(std::string label, VkPipelineStageFlags2 srcStage, VkAccessFlags2 srcAccess, VkPipelineStageFlags2 dstStage, VkAccessFlags2 dstAccess); diff --git a/include/rtv/HeadlessDiagnostics.h b/include/rtv/HeadlessDiagnostics.h index 9e90a80..3654604 100644 --- a/include/rtv/HeadlessDiagnostics.h +++ b/include/rtv/HeadlessDiagnostics.h @@ -80,6 +80,8 @@ struct ProfileReport { uint32_t warmupFrames = 0; uint32_t profiledFrames = 0; bool rayTracingDiagnosticCountersEnabled = false; + std::string lastAccumulationResetReason = "Startup"; + nlohmann::json temporalSystemDiagnostics = nlohmann::json::object(); struct MinMaxAvg { float min = 0.0f; @@ -126,6 +128,10 @@ struct ProfileReport { float skipDenoiserCopy = 0.0f; float taa = 0.0f; float taaHistoryCopy = 0.0f; + float dlssGuides = 0.0f; + float dlss = 0.0f; + float dlssRayReconstructionGuides = 0.0f; + float dlssRayReconstruction = 0.0f; float autoExposureHistogramClear = 0.0f; float autoExposureHistogram = 0.0f; float autoExposureReduce = 0.0f; @@ -256,6 +262,9 @@ struct ProfileReport { uint64_t rayInvocations = 0; uint64_t triangleHits = 0; uint64_t aabbHits = 0; + bool supported = false; + bool valid = false; + std::string unavailableReason; } pipelineStatistics{}; struct RayTracingDiagnosticCounterReport { @@ -322,6 +331,7 @@ struct ProfileReport { std::vector restirDiCounters; std::vector restirGiCounters; bool restirDiHistoryValid = false; + bool restirGiHistoryValid = false; struct RayTracingGeometryReport { uint32_t opaquePrimitiveCount = 0; @@ -852,6 +862,12 @@ struct ProfileReport { bool nrdRequestable = false; bool nrdAvailable = false; std::string nrdUnavailableReason; + bool nrdDirectRuntimeResourcesReady = false; + bool nrdHistoryConfidenceInputsAllocated = false; + bool nrdHistoryConfidenceAvailable = false; + bool nrdValidationOutputAllocated = false; + bool nrdValidationOutputEnabled = false; + std::string nrdGuideContractReason; std::string nrdBackendPolicy = "disabled"; std::string nrdBackendPolicyReason; bool nrdBackendsMutuallyExclusive = true; @@ -864,10 +880,23 @@ struct ProfileReport { std::string dlssRayReconstructionUnavailableReason; bool requestedDlssRayReconstruction = false; bool effectiveDlssRayReconstruction = false; + bool dlssRayReconstructionGuidePassReady = false; + bool dlssRayReconstructionGuideImagesAllocated = false; + bool dlssRayReconstructionPsrGuideBufferAllocated = false; + bool dlssRayReconstructionPsrHistorySignaturesAllocated = false; + bool dlssRayReconstructionUsesPsrGuides = false; + uint32_t dlssRayReconstructionGuideImageCount = 0; + std::string dlssRayReconstructionGuideMode; bool dlssFrameGenerationAvailable = false; std::string dlssFrameGenerationUnavailableReason; bool requestedDlssFrameGeneration = false; bool effectiveDlssFrameGeneration = false; + bool dlssAutoExposureEnabled = false; + bool dlssExposureBufferAvailable = false; + bool dlssExposureBufferPassedToSdk = false; + float dlssManualExposure = 1.0f; + float dlssPreExposure = 1.0f; + float dlssExposureScale = 1.0f; float dlssSharpeningStrength = 0.0f; std::string requestedTemporalUpscaler = "taa-tsr"; std::string effectiveTemporalUpscaler = "taa-tsr"; @@ -939,6 +968,7 @@ struct ProfileReport { uint64_t topologyRebuildLatestGeneration = 0; uint64_t topologyRebuildNextTimelineValue = 0; + bool validationEnabled = false; uint32_t validationErrorCount = 0; std::vector warnings; nlohmann::json textureDiagnostics = nlohmann::json::object(); @@ -956,6 +986,7 @@ struct ValidationSceneResult { std::string name; std::string status; float gpuMsTotal = 0.0f; + bool validationEnabled = false; uint32_t validationErrors = 0; uint32_t framesRendered = 0; bool wavefrontValidationEnabled = false; diff --git a/include/rtv/KnownCommandLineOptions.h b/include/rtv/KnownCommandLineOptions.h new file mode 100644 index 0000000..e620680 --- /dev/null +++ b/include/rtv/KnownCommandLineOptions.h @@ -0,0 +1,461 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rtv { + +// Generated from the option comparisons in src/main.cpp. +inline constexpr std::array kKnownCommandLineOptions{{ + "--adaptive-sampling", + "--adaptive-sampling-budget", + "--adaptive-sampling-mode", + "--apply-cancel-after-frame", + "--apply-frame-budget-ms", + "--apply-operation-cost-ms", + "--apply-operation-count", + "--atrous-iterations", + "--backend-policy", + "--baseline-dir", + "--blended-decal-shadow-mode", + "--bokeh-rotation", + "--bounces", + "--camera", + "--capture-frame", + "--capture-ready-after-frames", + "--capture-ready-file", + "--capture-ready-log", + "--capture-renderdoc", + "--caustics", + "--check-baseline", + "--check-budget", + "--check-streaming-budget", + "--compare-image", + "--compare-image-sequence", + "--compare-max-changed-pixels", + "--compare-min-psnr", + "--compare-min-ssim", + "--compare-profile", + "--controller-dry-run", + "--controller-force-overwrite", + "--controller-mutation-dry-run", + "--controller-mutation-json", + "--controller-output", + "--cook-animation-controller", + "--cook-manifest", + "--cook-output", + "--cook-project", + "--crash-dump-package", + "--debug-view", + "--denoiser", + "--denoiser-atrous-iterations", + "--denoiser-backend", + "--denoiser-mode", + "--descriptor-lifetime-stress", + "--descriptor-lifetime-stress-cycles", + "--descriptor-lifetime-stress-frames", + "--disable-async-compute", + "--disable-pass", + "--disable-resource-aliasing", + "--dlss", + "--dlss-fg", + "--dlss-frame-generation", + "--dlss-ray-reconstruction", + "--dlss-rr", + "--dlss-sharpening", + "--dlss-sharpness", + "--dof-aperture-radius", + "--dof-blade-count", + "--dof-blades", + "--dof-bokeh-rotation", + "--dof-focus-distance", + "--dry-run", + "--dump-bindings", + "--dump-frame-timeline", + "--dump-memory", + "--dump-rendergraph", + "--dump-rendergraph-dot", + "--dump-resource-lifetimes", + "--dump-shader-report", + "--dump-streaming", + "--emit-basisu-ktx2-fixture", + "--emit-native-fixture", + "--environment-direct-samples", + "--environment-samples", + "--export-animation-controller", + "--final-bounce-fast-path", + "--final-bounce-visibility", + "--fixed-seed", + "--focus-distance", + "--force-opaque-camera-rays", + "--frame-generation", + "--frame-index", + "--frames", + "--gltf", + "--gpu-crash-dump-dir", + "--gpu-crash-dumps", + "--gpu-markers", + "--gpu-scene-streaming-instances", + "--hardware-backface-culling", + "--hdr", + "--headless", + "--headless-height", + "--headless-width", + "--homogeneous-volume", + "--homogeneous-volume-absorption", + "--homogeneous-volume-anisotropy", + "--homogeneous-volume-scattering", + "--incremental-gpu-scene-cancel-after-frame", + "--incremental-gpu-scene-descriptor-patches", + "--incremental-gpu-scene-instances", + "--incremental-gpu-scene-ms", + "--incremental-gpu-scene-operations", + "--incremental-gpu-scene-reset-masks", + "--incremental-gpu-scene-tlas-patches", + "--inspect-animation-controller", + "--inspection-json", + "--inspect-native-asset", + "--inspect-native-store", + "--inspect-package", + "--inspect-runtime-skeleton", + "--lighting-reuse-mode", + "--limit-spp", + "--load-native-runtime-assets", + "--make-debug-package", + "--max-bounces", + "--migrate-native-asset", + "--migrate-package", + "--migration-report", + "--mixed-sided-split", + "--mnee-caustics", + "--motion-blur", + "--motion-blur-shutter-close", + "--motion-blur-shutter-open", + "--mutate-animation-controller", + "--native2b-direct-reuse", + "--native2b-terminal-direct-rate", + "--native2b-terminal-direct-sample-probability", + "--native-asset-kind", + "--native-fixture-guid", + "--native-fixture-material-texture-guid", + "--native-fixture-texture-format", + "--native-fixture-texture-role", + "--native-gpu-cache-assets", + "--native-gpu-cache-cpu-budget-mb", + "--native-gpu-cache-evict-pinned", + "--native-gpu-cache-evict-selected", + "--native-gpu-cache-gpu-budget-mb", + "--native-output", + "--native-package-animation-entity", + "--native-package-animation-entity-uuid", + "--native-package-controller-guid", + "--native-package-controller-path", + "--native-package-scene", + "--native-package-texture-target-set-json", + "--native-store-package", + "--native-store-query", + "--native-store-release", + "--native-store-retain", + "--native-store-root", + "--native-store-unmount-package", + "--native-texture-format-support-json", + "--nrd", + "--nvperf", + "--nvperf-capture-after-frames", + "--nvperf-csv", + "--nvperf-html", + "--nvperf-output", + "--nvperf-raw-counter-images", + "--omm", + "--omm-blend", + "--omm-subdivision", + "--opacity-micromap-blend", + "--opacity-micromaps", + "--opacity-micromap-subdivision", + "--out", + "--package-input", + "--package-root", + "--path-reservoir-layout", + "--pathtrace-kernel", + "--plan-rtpkg-patch", + "--print-backend-policy", + "--profile", + "--profile-json", + "--ray-reconstruction", + "--reflex", + "--regir", + "--regir-candidates-per-reservoir", + "--regir-canonical-mix", + "--regir-environment", + "--regir-finite-query-frame-period", + "--regir-grid-dimensions", + "--regir-grid-mode", + "--regir-infinite-lights", + "--regir-query-mode", + "--regir-reservoirs-per-cell", + "--regir-spatial-reuse", + "--regir-spatial-rounds", + "--regir-temporal-history", + "--regir-temporal-max-m", + "--regir-temporal-reuse", + "--regir-visibility-reuse", + "--renderdoc-dll", + "--renderer-only", + "--renderer-only-linger-ms", + "--render-preset", + "--render-resolution-scale", + "--render-scale", + "--reservoir-layout", + "--resolution", + "--restir", + "--restir-counters", + "--restir-di", + "--restir-di-clamp-luminance", + "--restir-di-final-visibility", + "--restir-di-include-environment", + "--restir-di-include-sun", + "--restir-di-layout", + "--restir-di-max-age", + "--restir-di-max-m", + "--restir-di-spatial", + "--restir-di-spatial-radius", + "--restir-di-spatial-rounds", + "--restir-di-stabilization", + "--restir-di-temporal", + "--restir-di-visibility-ray-budget", + "--restir-di-visibility-rays", + "--restir-gi", + "--restir-gi-active-tile-mask", + "--restir-gi-depth-threshold-scale", + "--restir-gi-final-stabilization", + "--restir-gi-half-resolution", + "--restir-gi-layout", + "--restir-gi-max-age", + "--restir-gi-min-final-blend-strength", + "--restir-gi-mode", + "--restir-gi-spatial-compatibility-threshold", + "--restir-gi-spatial-radius", + "--restir-gi-spatial-rounds", + "--restir-gi-stabilization", + "--restir-gi-temporal-max-age", + "--restir-gi-visibility-ray-budget", + "--restir-gi-visibility-rays", + "--restir-history-copy-mode", + "--rt-diagnostic-counters", + "--rtlevel", + "--rt-motion-blur", + "--rtpkg-compression-profile", + "--run-validation-suite", + "--samples-per-pixel", + "--save-debug-views", + "--save-frame-sequence", + "--save-present-frame", + "--save-present-frame-on-hotkey", + "--scene", + "--secondary-direct-lighting", + "--selftest-renderer-core", + "--selftest-streaming-transfer", + "--sequence-frame-count", + "--sequence-start-frame", + "--sequence-step", + "--sequence-views", + "--ser", + "--shader-execution-reordering", + "--shader-hot-reload-report", + "--simulate-gpu-scene-streaming", + "--simulate-gpu-upload-ticket", + "--simulate-incremental-gpu-scene-update", + "--simulate-main-thread-apply", + "--simulate-native-gpu-cache", + "--simulate-native-texture-format-policy", + "--simulate-rtpkg-compression", + "--simulate-rtpkg-streaming-io", + "--simulate-streaming-gpu-work", + "--simulate-streaming-io", + "--simulate-streaming-io-batch", + "--simulate-streaming-scheduler", + "--simulate-topology-rebuild", + "--single-queue-fallback", + "--spp", + "--spp-limit", + "--stage-import", + "--stage-import-emissive-scale", + "--stage-import-json", + "--stage-import-no-blas-cache", + "--stage-import-workspace", + "--streaming", + "--streaming-async-compute", + "--streaming-async-compute-frame-budget-us", + "--streaming-async-compute-min-headroom-us", + "--streaming-budget-mb", + "--streaming-budget-preset", + "--streaming-catalog-json", + "--streaming-catalog-root", + "--streaming-cpu-memory-mb", + "--streaming-directstorage", + "--streaming-disable-eviction", + "--streaming-force-cpu-decompress", + "--streaming-gpu-memory-mb", + "--streaming-gpu-work-blas-builds", + "--streaming-gpu-work-complete-lag-frames", + "--streaming-gpu-work-descriptor-updates", + "--streaming-gpu-work-ms", + "--streaming-gpu-work-staging-mb", + "--streaming-gpu-work-submissions", + "--streaming-gpu-work-tickets", + "--streaming-gpu-work-tlas-patches", + "--streaming-gpu-work-upload-mb", + "--streaming-io-backend", + "--streaming-io-batch-chunk-kb", + "--streaming-io-batch-requests", + "--streaming-io-json", + "--streaming-preset", + "--streaming-scheduler-cancel-after-frame", + "--streaming-scheduler-cpu-ms", + "--streaming-scheduler-io-mb", + "--streaming-scheduler-max-tasks", + "--streaming-scheduler-memory-mb", + "--streaming-scheduler-task-count", + "--streaming-scheduler-upload-mb", + "--streaming-upload-mb-per-frame", + "--streaming-validation-scene", + "--streamline-nvperf", + "--streamline-reflex", + "--taa-motion-feedback", + "--taa-reactive-feedback", + "--temporal-upscaler", + "--topology-frame-budget-ms", + "--topology-newer-edit-frame", + "--topology-stage-cost-ms", + "--update-baseline", + "--upload-cancel-after-submit", + "--upload-cancel-before-submit", + "--upload-chunk-bytes", + "--upload-frame-byte-limit", + "--upload-total-bytes", + "--upscaler", + "--validate-async-compute-parity", + "--validate-gpu-labels", + "--validate-native-catalog", + "--validate-rtpkg", + "--validation-camera-motion", + "--validation-light-flicker", + "--validation-light-reorder", + "--validation-object-motion", + "--validation-output", + "--volume", + "--volume-absorption", + "--volume-anisotropy", + "--volume-scattering", + "--warmup-frames", + "--wavefront-compact", + "--wavefront-final-output", + "--wavefront-generate", + "--wavefront-primary-generate", + "--wavefront-queue-compact", + "--wavefront-queues", + "--wavefront-ray-sort", + "--wavefront-renderer", + "--wavefront-shade", + "--wavefront-shade-compute", + "--wavefront-shadow", + "--wavefront-shadow-trace", + "--wavefront-sort", + "--wavefront-trace", + "--wavefront-trace-wrapper", + "--wavefront-validation", + "--write-rtpkg", +}}; + +inline constexpr std::array kFlagOnlyCommandLineOptions{{ + "--backend-policy", + "--capture-ready-log", + "--check-baseline", + "--controller-dry-run", + "--controller-force-overwrite", + "--controller-mutation-dry-run", + "--disable-async-compute", + "--disable-resource-aliasing", + "--dry-run", + "--headless", + "--inspect-native-store", + "--native-gpu-cache-evict-pinned", + "--native-gpu-cache-evict-selected", + "--nvperf", + "--print-backend-policy", + "--profile", + "--renderer-only", + "--run-validation-suite", + "--selftest-renderer-core", + "--selftest-streaming-transfer", + "--shader-hot-reload-report", + "--simulate-gpu-scene-streaming", + "--simulate-gpu-upload-ticket", + "--simulate-incremental-gpu-scene-update", + "--simulate-main-thread-apply", + "--simulate-native-gpu-cache", + "--simulate-native-texture-format-policy", + "--simulate-streaming-gpu-work", + "--simulate-streaming-scheduler", + "--simulate-topology-rebuild", + "--single-queue-fallback", + "--stage-import-no-blas-cache", + "--streaming-disable-eviction", + "--update-baseline", + "--upload-cancel-after-submit", + "--upload-cancel-before-submit", + "--validate-async-compute-parity", + "--validate-gpu-labels", + "--validation-camera-motion", + "--validation-light-flicker", + "--validation-light-reorder", + "--validation-object-motion", + "--wavefront-validation", +}}; + +inline constexpr std::array kTwoValueCommandLineOptions{{ + "--compare-image", + "--compare-image-sequence", + "--compare-profile", + "--plan-rtpkg-patch", +}}; + +inline bool isKnownCommandLineOption(std::string_view option) { + return std::find(kKnownCommandLineOptions.begin(), kKnownCommandLineOptions.end(), option) != + kKnownCommandLineOptions.end(); +} + +inline uint32_t commandLineOptionArity(std::string_view option) { + if (std::find(kFlagOnlyCommandLineOptions.begin(), kFlagOnlyCommandLineOptions.end(), option) != + kFlagOnlyCommandLineOptions.end()) { + return 0u; + } + return std::find(kTwoValueCommandLineOptions.begin(), kTwoValueCommandLineOptions.end(), option) != + kTwoValueCommandLineOptions.end() ? 2u : 1u; +} + +inline void validateCommandLineArguments(int argc, char* const* argv) { + for (int index = 1; index < argc; ++index) { + const std::string_view option(argv[index]); + if (!isKnownCommandLineOption(option)) { + throw std::runtime_error("Unknown command-line argument: " + std::string(option)); + } + const uint32_t arity = commandLineOptionArity(option); + if (index + static_cast(arity) >= argc) { + throw std::runtime_error("Missing value for command-line argument: " + std::string(option)); + } + for (uint32_t valueIndex = 1u; valueIndex <= arity; ++valueIndex) { + const std::string_view value(argv[index + static_cast(valueIndex)]); + if (value.starts_with("--")) { + throw std::runtime_error("Missing value for command-line argument: " + std::string(option)); + } + } + index += static_cast(arity); + } +} + +} // namespace rtv diff --git a/include/rtv/PathTracerRenderer.h b/include/rtv/PathTracerRenderer.h index e18c3c8..ca74aa0 100644 --- a/include/rtv/PathTracerRenderer.h +++ b/include/rtv/PathTracerRenderer.h @@ -266,6 +266,10 @@ class PathTracerRenderer { ~PathTracerRenderer(); void releaseExclusiveRuntimeForRendererReplacement(); + [[nodiscard]] static RendererSettings normalizeSettingsForDevice( + const RendererSettings& settings, + const VulkanContext& context); + void beginFrame(uint32_t frameIndex, VkExtent2D renderExtent, VkExtent2D displayExtent); void setFrameDeltaSeconds(float deltaSeconds) { frameDeltaSeconds_ = deltaSeconds; } void recordPathTrace(VkCommandBuffer commandBuffer, bool deferPostTraceCompute = false); @@ -292,6 +296,7 @@ class PathTracerRenderer { float nearPlane, float farPlane); void resetAccumulation(AccumulationResetReason reason = AccumulationResetReason::Manual); + void resetAccumulationPreserveTemporalHistory(AccumulationResetReason reason); void applyStreamingResetMasks( const std::vector& temporalEntityUuids, const std::vector& restirEntityUuids, @@ -300,6 +305,10 @@ class PathTracerRenderer { void loadEnvironment(const std::filesystem::path& path); [[nodiscard]] bool shadersNeedReload(); bool updateMaterials(const SceneAsset& scene, const AssetManager& assets); + bool patchStreamedMaterialTexture( + const SceneAsset& scene, + TextureAssetHandle texture, + const Image& image); bool updateSceneLights(const SceneAsset& scene, bool rebuildLightBvh = true); bool updateSceneTransforms(const SceneAsset& scene, const AssetManager& assets); bool updateSceneVisibility(const SceneAsset& scene, const AssetManager& assets); @@ -354,6 +363,12 @@ class PathTracerRenderer { bool nrdRequestable = false; bool nrdAvailable = false; std::string nrdUnavailableReason; + bool nrdDirectRuntimeResourcesReady = false; + bool nrdHistoryConfidenceInputsAllocated = false; + bool nrdHistoryConfidenceAvailable = false; + bool nrdValidationOutputAllocated = false; + bool nrdValidationOutputEnabled = false; + std::string nrdGuideContractReason; bool dlssSdkConfigured = false; bool dlssRequestable = false; bool dlssAvailable = false; @@ -361,9 +376,22 @@ class PathTracerRenderer { bool dlssRayReconstructionRequestable = false; bool dlssRayReconstructionAvailable = false; std::string dlssRayReconstructionUnavailableReason; + bool dlssRayReconstructionGuidePassReady = false; + bool dlssRayReconstructionGuideImagesAllocated = false; + bool dlssRayReconstructionPsrGuideBufferAllocated = false; + bool dlssRayReconstructionPsrHistorySignaturesAllocated = false; + bool dlssRayReconstructionUsesPsrGuides = false; + uint32_t dlssRayReconstructionGuideImageCount = 0; + std::string dlssRayReconstructionGuideMode; bool dlssFrameGenerationRequestable = false; bool dlssFrameGenerationAvailable = false; std::string dlssFrameGenerationUnavailableReason; + bool dlssAutoExposureEnabled = false; + bool dlssExposureBufferAvailable = false; + bool dlssExposureBufferPassedToSdk = false; + float dlssManualExposure = 1.0f; + float dlssPreExposure = 1.0f; + float dlssExposureScale = 1.0f; bool streamlineSdkConfigured = false; bool streamlineRuntimeConfigured = false; bool streamlineInitialized = false; @@ -466,6 +494,7 @@ class PathTracerRenderer { return reinterpret_cast(bytes + slot * sizeof(uint32_t) * 64u); } [[nodiscard]] bool restirDiHistoryValid() const { return restirDiHistoryValid_; } + [[nodiscard]] bool restirGiHistoryValid() const { return restirGiHistoryValid_; } [[nodiscard]] RestirHistoryCopyMode effectiveRestirHistoryCopyMode() const; [[nodiscard]] const char* restirHistoryCopyFallbackReason() const; [[nodiscard]] bool effectiveRestirGiActiveTileMaskEnabled() const; @@ -713,6 +742,13 @@ class PathTracerRenderer { float whitePoint = 4.0f; }; + struct DlssGuideVisualizeParams { + uint32_t mode = 0; + float scale = 1.0f; + float bias = 0.0f; + float reserved0 = 0.0f; + }; + struct HistogramParams { uint32_t width = 0; uint32_t height = 0; @@ -756,8 +792,12 @@ class PathTracerRenderer { uint32_t cameraMoving = 0; uint32_t renderWidth = 0; uint32_t renderHeight = 0; - float motionFeedback = 0.90f; - float reactiveFeedback = 0.98f; + float motionFeedback = 0.90f; + float reactiveFeedback = 0.98f; + float inputPixelOffsetX = 0.0f; + float inputPixelOffsetY = 0.0f; + float clampingFactor = 1.3f; + float maxRadiance = 200.0f; }; struct RestirSpatialParams { @@ -801,7 +841,9 @@ class PathTracerRenderer { uint32_t materialVisibilityFlags = 0; uint32_t counterEnabled = 0; uint32_t rawOutputIsCurrentSample = 0; - uint32_t padding2 = 0; + float shadowDistanceBias = 0.002f; + uint32_t lightVersion = 0; + uint32_t environmentVersion = 0; }; struct FogParams { @@ -942,6 +984,13 @@ class PathTracerRenderer { glm::vec4 restirGiFallbackReactive{}; }; + struct PsrGuideGpu { + glm::uvec4 geometry{}; + glm::uvec4 material{}; + glm::vec4 distances{}; + }; + static_assert(sizeof(PsrGuideGpu) == 48); + struct alignas(16) WavefrontQueueHeaderGpu { // counters: x=count, y=capacity, z=read offset, w=write offset. glm::uvec4 counters{}; @@ -1163,8 +1212,10 @@ class PathTracerRenderer { struct alignas(16) ReGIRReservoirGpu { glm::uvec4 metadata{}; glm::vec4 samplePositionWeight{}; + glm::vec4 proposalPdfM{}; + glm::uvec4 lightIdentity{}; }; - static_assert(sizeof(ReGIRReservoirGpu) == 32); + static_assert(sizeof(ReGIRReservoirGpu) == 64); struct alignas(16) ReGIREnvironmentReservoirGpu { glm::uvec4 metadata{}; @@ -1254,6 +1305,8 @@ class PathTracerRenderer { void recordDenoiser(VkCommandBuffer commandBuffer); void recordDenoiserPass(VkCommandBuffer commandBuffer); [[nodiscard]] bool recordNrdDenoiser(VkCommandBuffer commandBuffer); + void recordNrdConfidenceGradientPass(VkCommandBuffer commandBuffer); + void recordNrdConfidenceFilterPass(VkCommandBuffer commandBuffer); void recordNrdPreparePass(VkCommandBuffer commandBuffer); [[nodiscard]] bool recordNrdDispatches(VkCommandBuffer commandBuffer); void recordNrdResolvePass(VkCommandBuffer commandBuffer); @@ -1283,6 +1336,8 @@ class PathTracerRenderer { void recordDlssRayReconstruction(VkCommandBuffer commandBuffer); void recordDlssRayReconstructionGuidesPass(VkCommandBuffer commandBuffer); void recordDlssRayReconstructionPass(VkCommandBuffer commandBuffer); + void recordDlssGuideVisualization(VkCommandBuffer commandBuffer); + void recordDlssGuideVisualizationPass(VkCommandBuffer commandBuffer); void recordAutoExposure(VkCommandBuffer commandBuffer); void recordAutoExposureHistogramPass(VkCommandBuffer commandBuffer); void recordAutoExposureReducePass(VkCommandBuffer commandBuffer); @@ -1333,6 +1388,12 @@ class PathTracerRenderer { [[nodiscard]] bool shouldUseRegirSpatialReuse() const; [[nodiscard]] bool shouldUseRegirTemporalReuse() const; [[nodiscard]] bool isNonDenoiserDebugView() const; + [[nodiscard]] bool isDlssDebugView() const; + [[nodiscard]] bool isDlssRayReconstructionDebugView() const; + [[nodiscard]] bool isDlssGuideDebugView() const; + [[nodiscard]] const Image& dlssGuideVisualizationSource() const; + [[nodiscard]] uint32_t dlssGuideVisualizationMode() const; + [[nodiscard]] float dlssGuideVisualizationScale() const; [[nodiscard]] bool shouldBypassTemporalUpscalerForDebugView() const; [[nodiscard]] bool shouldRunTaa() const; [[nodiscard]] bool dlssRequested() const; @@ -1420,6 +1481,7 @@ class PathTracerRenderer { void dispatchInitialGpuSkinningForAccelerationStructures(); void writeStbnDescriptors(DescriptorWriter& writer) const; [[nodiscard]] float stbnScalarSample(int32_t x, int32_t y, uint32_t frameIndex) const; + [[nodiscard]] uint32_t sampleFrameIndex() const; void fallbackBlitPostDenoiseToTemporalOutput(VkCommandBuffer commandBuffer); void skipDenoiserPass(VkCommandBuffer commandBuffer); void skipDenoiserCopyPass(VkCommandBuffer commandBuffer); @@ -1457,6 +1519,7 @@ class PathTracerRenderer { const VulkanContext& context_; ResourceAllocator& allocator_; BufferUploader& uploader_; + RendererSettings settings_{}; GpuScene scene_; GpuSkinningResourcePlan gpuSkinningResourcePlan_{}; @@ -1483,7 +1546,6 @@ class PathTracerRenderer { std::string memoryPressureName_ = "normal"; AccumulationResetReason lastResetReason_ = AccumulationResetReason::Startup; CameraUniform camera_{}; - RendererSettings settings_{}; std::optional dumpRenderGraphPath_; std::optional dumpRenderGraphDotPath_; DenoiserParams denoiserParams_{}; @@ -1497,6 +1559,7 @@ class PathTracerRenderer { bool rayTracingDiagnosticCountersEnabled_ = false; bool rayTracingDiagnosticCountersCleared_ = false; glm::mat4 previousViewProj_{1.0f}; + glm::mat4 previousNonJitteredViewProj_{1.0f}; glm::mat4 nrdViewToClip_{1.0f}; glm::mat4 nrdViewToClipPrev_{1.0f}; glm::mat4 nrdWorldToView_{1.0f}; @@ -1587,6 +1650,9 @@ class PathTracerRenderer { Buffer entityIdBuffer_; Buffer entityIdReadbackBuffer_; Buffer pathDataBuffer_; + Buffer psrGuideBuffer_; + Buffer psrGuideSignatureBuffer_; + Buffer previousPsrGuideSignatureBuffer_; Buffer adaptiveSamplingDensityBuffer_; Buffer adaptiveSamplingSampleCountBuffer_; Buffer adaptiveSamplingStatsBuffer_; @@ -1673,6 +1739,9 @@ class PathTracerRenderer { Buffer restirDiTemporalReservoirBuffer_; Buffer restirDiSpatialReservoirBuffer_; Buffer restirDiFinalReservoirBuffer_; + Buffer restirDiTemporalSourcePixelBuffer_; + Buffer restirDiSpatialSourcePixelBuffer_; + Buffer restirDiFinalSourcePixelBuffer_; Buffer previousRestirDiReservoirBuffer_; Buffer previousRestirDiReceiverBuffer_; Buffer restirDiCountersBuffer_; @@ -1700,6 +1769,9 @@ class PathTracerRenderer { std::unique_ptr gpuSkinningShader_; std::unique_ptr dlssGuidesShader_; std::unique_ptr dlssRayReconstructionGuidesShader_; + std::unique_ptr dlssGuideVisualizeShader_; + std::unique_ptr nrdConfidenceGradientShader_; + std::unique_ptr nrdConfidenceFilterShader_; std::unique_ptr nrdPrepareShader_; std::unique_ptr nrdResolveShader_; std::unique_ptr restirSpatialShader_; @@ -1735,6 +1807,7 @@ class PathTracerRenderer { std::unique_ptr fullscreenFragmentShader_; std::unique_ptr raygenShader_; std::unique_ptr raygenBeautyFastShader_; + std::unique_ptr raygenBeautyFastNoTexturesShader_; std::unique_ptr raygenRegirBeautyFastShader_; std::unique_ptr raygenRegirStochasticBeautyFastShader_; std::unique_ptr raygenNative2BShader_; @@ -1788,6 +1861,9 @@ class PathTracerRenderer { std::unique_ptr gpuSkinningPipeline_; std::unique_ptr dlssGuidesPipeline_; std::unique_ptr dlssRayReconstructionGuidesPipeline_; + std::unique_ptr dlssGuideVisualizePipeline_; + std::unique_ptr nrdConfidenceGradientPipeline_; + std::unique_ptr nrdConfidenceFilterPipeline_; std::unique_ptr nrdPreparePipeline_; std::unique_ptr nrdResolvePipeline_; std::unique_ptr restirSpatialPipeline_; @@ -1877,6 +1953,9 @@ class PathTracerRenderer { VkDescriptorSetLayout gpuSkinningSetLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout dlssGuidesSetLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout dlssRayReconstructionGuidesSetLayout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout dlssGuideVisualizeSetLayout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout nrdConfidenceGradientSetLayout_ = VK_NULL_HANDLE; + VkDescriptorSetLayout nrdConfidenceFilterSetLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout nrdPrepareSetLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout nrdResolveSetLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout restirSpatialSetLayout_ = VK_NULL_HANDLE; @@ -1918,6 +1997,12 @@ class PathTracerRenderer { std::vector> extraDefines; }; std::vector shaderReloadDependencies_; + struct ShaderReloadWatchFile { + std::filesystem::path path; + std::filesystem::file_time_type writeTime{}; + bool exists = false; + }; + std::vector shaderReloadWatchFiles_; std::filesystem::path shaderOutputDirectory_; std::chrono::steady_clock::time_point lastShaderReloadCheck_{}; bool shaderReloadCheckPrimed_ = false; diff --git a/include/rtv/RenderGraph.h b/include/rtv/RenderGraph.h index 8f7c264..f44ac72 100644 --- a/include/rtv/RenderGraph.h +++ b/include/rtv/RenderGraph.h @@ -124,6 +124,8 @@ class RenderGraph { std::vector passes_; std::vector compiledPassOrder_; std::vector compiledBarriers_; + std::vector> compiledBarrierBatches_; + std::vector compiledFinalBarrierBatch_; std::vector resourceLifetimes_; std::unique_ptr transientPool_; VkQueue asyncComputeQueue_ = VK_NULL_HANDLE; diff --git a/include/rtv/RenderGraphResource.h b/include/rtv/RenderGraphResource.h index a02e1d9..84df3a2 100644 --- a/include/rtv/RenderGraphResource.h +++ b/include/rtv/RenderGraphResource.h @@ -23,6 +23,7 @@ enum class ResourceState : uint8_t { ShaderRead, ShaderStorage, UniformBuffer, + RayTracingRead, RayTracing, ComputeShaderRead, ComputeShaderStorage, diff --git a/include/rtv/RendererCoreRegressionTests.h b/include/rtv/RendererCoreRegressionTests.h new file mode 100644 index 0000000..ef33391 --- /dev/null +++ b/include/rtv/RendererCoreRegressionTests.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace rtv { + +int runRendererCoreRegressionTests(std::ostream& output); + +} // namespace rtv diff --git a/include/rtv/RendererDebug.h b/include/rtv/RendererDebug.h index 902cfe8..d880f80 100644 --- a/include/rtv/RendererDebug.h +++ b/include/rtv/RendererDebug.h @@ -165,6 +165,42 @@ enum class RendererDebugView : uint32_t { RegirEnvironmentDirection = 149, RegirEnvironmentWeight = 150, RegirEnvironmentGeneration = 151, + RestirGiTarget = 152, + RestirGiSourcePdf = 153, + RestirGiWeightSum = 154, + RestirGiM = 155, + RestirGiConfidence = 156, + RestirGiVisibility = 157, + RestirDiLightMapStatus = 158, + NrdValidation = 159, + NrdDiffuseConfidence = 160, + NrdSpecularConfidence = 161, + NrdRawConfidenceGradient = 162, + NrdFilteredConfidenceGradient = 163, + NrdConfidenceHistory = 164, + PsrActiveMask = 165, + PsrDepth = 166, + PsrMotion = 167, + PsrNormalRoughness = 168, + PsrHitDistance = 169, + PsrAlbedoF0 = 170, + PsrRayDirection = 171, + DlssDepth = 172, + DlssMotionVectors = 173, + DlssInputColor = 174, + DlssOutputColor = 175, + DlssRrDiffuseAlbedo = 176, + DlssRrSpecularAlbedo = 177, + DlssRrNormals = 178, + DlssRrRoughness = 179, + DlssRrDiffuseHitDistance = 180, + DlssRrSpecularHitDistance = 181, + DlssRrReflectedAlbedo = 182, + DlssRrDisocclusionMask = 183, + DlssRrDiffuseRayDirection = 184, + DlssRrSpecularRayDirection = 185, + DlssRrDiffuseRayDirectionHitDistance = 186, + DlssRrSpecularRayDirectionHitDistance = 187, }; enum class RestirMode : uint32_t { @@ -302,10 +338,12 @@ inline constexpr uint32_t rendererDebugFlagRayTracingCounters = 1u << 0u; [[nodiscard]] const char* toneMapperName(ToneMapper toneMapper); [[nodiscard]] const char* restirModeName(RestirMode mode); [[nodiscard]] const char* restirDiModeName(RestirDiMode mode); +[[nodiscard]] bool tryParseRestirDiMode(std::string_view value, RestirDiMode& out); [[nodiscard]] RestirDiMode parseRestirDiMode(std::string_view value); [[nodiscard]] const char* restirDiReservoirLayoutName(RestirDiReservoirLayout layout); [[nodiscard]] RestirDiReservoirLayout parseRestirDiReservoirLayout(std::string_view value); [[nodiscard]] const char* restirGiModeName(RestirGiMode mode); +[[nodiscard]] bool tryParseRestirGiMode(std::string_view value, RestirGiMode& out); [[nodiscard]] RestirGiMode parseRestirGiMode(std::string_view value); [[nodiscard]] const char* restirGiReservoirLayoutName(RestirGiReservoirLayout layout); [[nodiscard]] RestirGiReservoirLayout parseRestirGiReservoirLayout(std::string_view value); diff --git a/include/rtv/RendererPassContractTypes.h b/include/rtv/RendererPassContractTypes.h new file mode 100644 index 0000000..fb6d3f1 --- /dev/null +++ b/include/rtv/RendererPassContractTypes.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +namespace rtv { + +inline nlohmann::json rendererContractArray(std::initializer_list values) { + nlohmann::json result = nlohmann::json::array(); + for (const char* value : values) { + result.push_back(value); + } + return result; +} + +enum class RendererPassContractId { + GBuffer, + PathTrace, + RestirDI, + RestirGI, + Regir, + TemporalAA, + Denoiser, + Nrd, + Dlss, + Composite, + DebugView, + Profiler, +}; + +enum class RendererPassContractRole { + Geometry, + LightingReuse, + Temporal, + Denoising, + Upscaling, + Output, + Diagnostics, +}; + +struct RendererPassContract { + RendererPassContractId id = RendererPassContractId::GBuffer; + RendererPassContractRole role = RendererPassContractRole::Geometry; + const char* name = ""; + bool activeByCurrentSettings = false; + bool requiredForDiagnosticProfile = true; + const char* currentOwnerFile = ""; + const char* featureFlagsRequired = ""; + nlohmann::json inputs = nlohmann::json::array(); + nlohmann::json outputs = nlohmann::json::array(); + nlohmann::json historyResources = nlohmann::json::array(); + nlohmann::json descriptorLayouts = nlohmann::json::array(); + nlohmann::json pushConstants = nlohmann::json::array(); + nlohmann::json pipelineShaderDependencies = nlohmann::json::array(); + nlohmann::json rendergraphReads = nlohmann::json::array(); + nlohmann::json rendergraphWrites = nlohmann::json::array(); + nlohmann::json requiredBarriers = nlohmann::json::array(); + const char* cameraHistoryResetBehavior = ""; + nlohmann::json debugOutputs = nlohmann::json::array(); + nlohmann::json profilingSections = nlohmann::json::array(); + nlohmann::json validationChecks = nlohmann::json::array(); +}; + +} // namespace rtv diff --git a/include/rtv/RendererPassContracts.h b/include/rtv/RendererPassContracts.h new file mode 100644 index 0000000..10df980 --- /dev/null +++ b/include/rtv/RendererPassContracts.h @@ -0,0 +1,3416 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererPassOwners.h" +#include "rtv/RendererSettings.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rtv { + +inline const char* rendererPassContractIdName(RendererPassContractId id) { + switch (id) { + case RendererPassContractId::GBuffer: return "gbuffer"; + case RendererPassContractId::PathTrace: return "path_trace"; + case RendererPassContractId::RestirDI: return "restir_di"; + case RendererPassContractId::RestirGI: return "restir_gi"; + case RendererPassContractId::Regir: return "regir"; + case RendererPassContractId::TemporalAA: return "temporal_aa"; + case RendererPassContractId::Denoiser: return "denoiser"; + case RendererPassContractId::Nrd: return "nrd"; + case RendererPassContractId::Dlss: return "dlss"; + case RendererPassContractId::Composite: return "composite"; + case RendererPassContractId::DebugView: return "debug_view"; + case RendererPassContractId::Profiler: return "profiler"; + } + return "unknown"; +} + +inline const char* rendererPassContractRoleName(RendererPassContractRole role) { + switch (role) { + case RendererPassContractRole::Geometry: return "geometry"; + case RendererPassContractRole::LightingReuse: return "lighting_reuse"; + case RendererPassContractRole::Temporal: return "temporal"; + case RendererPassContractRole::Denoising: return "denoising"; + case RendererPassContractRole::Upscaling: return "upscaling"; + case RendererPassContractRole::Output: return "output"; + case RendererPassContractRole::Diagnostics: return "diagnostics"; + } + return "unknown"; +} + +inline bool rendererStandaloneRestirDiPassRequested(const RendererSettings& settings) { + return passes::RestirDIPass::isActive(settings); +} + +inline nlohmann::json rendererPassContractJson(const RendererPassContract& contract) { + const RendererPassOwnerMetadata* owner = rendererPassOwnerMetadataForId(rendererPassContractIdName(contract.id)); + nlohmann::json result = { + {"id", rendererPassContractIdName(contract.id)}, + {"role", rendererPassContractRoleName(contract.role)}, + {"name", contract.name}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + {"required_for_diagnostic_profile", contract.requiredForDiagnosticProfile}, + {"current_owner_file", contract.currentOwnerFile}, + {"owner_metadata", owner != nullptr ? rendererPassOwnerMetadataJson(*owner) : nlohmann::json(nullptr)}, + {"feature_flags_required", contract.featureFlagsRequired}, + {"inputs", contract.inputs}, + {"outputs", contract.outputs}, + {"history_resources", contract.historyResources}, + {"descriptor_layouts", contract.descriptorLayouts}, + {"push_constants", contract.pushConstants}, + {"pipeline_shader_dependencies", contract.pipelineShaderDependencies}, + {"rendergraph_reads", contract.rendergraphReads}, + {"rendergraph_writes", contract.rendergraphWrites}, + {"required_barriers", contract.requiredBarriers}, + {"camera_history_reset_behavior", contract.cameraHistoryResetBehavior}, + {"debug_outputs", contract.debugOutputs}, + {"profiling_sections", contract.profilingSections}, + {"validation_checks", contract.validationChecks}, + }; + return result; +} + +inline bool rendererReadJsonArtifact( + const std::filesystem::path& path, + nlohmann::json& out, + std::string& error) { + try { + std::ifstream file(path); + if (!file.is_open()) { + error = "not readable"; + return false; + } + file >> out; + return true; + } catch (const std::exception& e) { + error = e.what(); + return false; + } +} + +inline std::vector rendererPassContracts(const RendererSettings& settings) { + std::vector passes; + passes.reserve(12); + passes.push_back(rtv::passes::GBufferPass::contract(settings)); + passes.push_back(rtv::passes::PathTracePass::contract(settings)); + + passes.push_back(rtv::passes::RestirDIPass::contract(settings)); + passes.push_back(rtv::passes::RestirGIPass::contract(settings)); + passes.push_back(rtv::passes::RegirPass::contract(settings)); + + passes.push_back(rtv::passes::TemporalAAPass::contract(settings)); + passes.push_back(rtv::passes::DenoiserPass::contract(settings)); + passes.push_back(rtv::passes::NrdPass::contract(settings)); + + passes.push_back(rtv::passes::DlssPass::contract(settings)); + passes.push_back(rtv::passes::CompositePass::contract(settings)); + passes.push_back(rtv::passes::DebugViewPass::contract(settings)); + passes.push_back(rtv::passes::ProfilerPass::contract(settings)); + + return passes; +} + +inline nlohmann::json rendererSettingsSnapshotJson(const RendererSettings& settings) { + return nlohmann::json{ + {"restir_di_mode", restirDiModeName(settings.restirDiMode)}, + {"restir_gi_mode", restirGiModeName(settings.restirGiMode)}, + {"lighting_reuse_mode", lightingReuseModeName(settings.lightingReuseMode)}, + {"denoiser_backend", denoiserBackendName(settings.denoiserBackend)}, + {"temporal_upscaler", temporalUpscalerName(settings.temporalUpscaler)}, + }; +} + +inline nlohmann::json rendererPassContractsJson(const RendererSettings& settings) { + const std::vector contracts = rendererPassContracts(settings); + nlohmann::json passes = nlohmann::json::array(); + for (const RendererPassContract& contract : contracts) { + passes.push_back(rendererPassContractJson(contract)); + } + + return nlohmann::json{ + {"schema_version", 1}, + {"coordinator", "PathTracerRenderer"}, + {"status", "contract metadata; algorithm extraction remains staged behind the current coordinator"}, + {"settings_snapshot", rendererSettingsSnapshotJson(settings)}, + {"pass_count", contracts.size()}, + {"passes", std::move(passes)}, + {"passed", contracts.size() == 12u}, + }; +} + +inline nlohmann::json rendererPassContractValidationJson(const RendererSettings& settings) { + constexpr size_t kExpectedPassCount = 12; + const std::vector contracts = rendererPassContracts(settings); + nlohmann::json requiredPasses = nlohmann::json::array(); + nlohmann::json activePasses = nlohmann::json::array(); + nlohmann::json missingRequiredFields = nlohmann::json::array(); + + auto requireString = [&](const RendererPassContract& contract, const char* fieldName, const char* value) { + if (value == nullptr || value[0] == '\0') { + missingRequiredFields.push_back({ + {"pass", contract.name}, + {"field", fieldName}, + }); + } + }; + auto requireArray = [&](const RendererPassContract& contract, const char* fieldName, const nlohmann::json& value) { + if (!value.is_array() || value.empty()) { + missingRequiredFields.push_back({ + {"pass", contract.name}, + {"field", fieldName}, + }); + } + }; + + for (const RendererPassContract& contract : contracts) { + if (contract.requiredForDiagnosticProfile) { + requiredPasses.push_back(contract.name); + } + if (contract.activeByCurrentSettings) { + activePasses.push_back(contract.name); + } + requireString(contract, "name", contract.name); + requireString(contract, "current_owner_file", contract.currentOwnerFile); + requireString(contract, "feature_flags_required", contract.featureFlagsRequired); + requireArray(contract, "inputs", contract.inputs); + requireArray(contract, "outputs", contract.outputs); + requireArray(contract, "history_resources", contract.historyResources); + requireArray(contract, "descriptor_layouts", contract.descriptorLayouts); + requireArray(contract, "push_constants", contract.pushConstants); + requireArray(contract, "pipeline_shader_dependencies", contract.pipelineShaderDependencies); + requireArray(contract, "rendergraph_reads", contract.rendergraphReads); + requireArray(contract, "rendergraph_writes", contract.rendergraphWrites); + requireArray(contract, "required_barriers", contract.requiredBarriers); + requireString(contract, "camera_history_reset_behavior", contract.cameraHistoryResetBehavior); + requireArray(contract, "debug_outputs", contract.debugOutputs); + requireArray(contract, "profiling_sections", contract.profilingSections); + requireArray(contract, "validation_checks", contract.validationChecks); + } + + const bool expectedPassCount = contracts.size() == kExpectedPassCount; + const bool passed = expectedPassCount && missingRequiredFields.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"expected_pass_count", kExpectedPassCount}, + {"pass_count", contracts.size()}, + {"required_passes", std::move(requiredPasses)}, + {"active_passes", std::move(activePasses)}, + {"missing_required_fields", std::move(missingRequiredFields)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererPassOwnerRegistryValidationJson(const RendererSettings& settings) { + constexpr size_t kExpectedPassCount = 12; + const std::vector contracts = rendererPassContracts(settings); + constexpr auto registry = rendererPassOwnerRegistry(); + nlohmann::json ownerRows = nlohmann::json::array(); + nlohmann::json contractModuleOwnedRows = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + uint32_t contractModuleOwnedCount = 0; + auto stringPresent = [](const char* value) { + return value != nullptr && value[0] != '\0'; + }; + + for (const RendererPassContract& contract : contracts) { + const char* contractId = rendererPassContractIdName(contract.id); + const RendererPassOwnerMetadata* owner = rendererPassOwnerMetadataForId(contractId); + if (owner == nullptr) { + failures.push_back({ + {"code", "missing_owner_metadata"}, + {"pass", contract.name}, + {"contract_id", contractId}, + }); + continue; + } + const bool rowValid = + stringPresent(owner->contractId) && + stringPresent(owner->passName) && + stringPresent(owner->ownerSymbol) && + stringPresent(owner->metadataHeader) && + stringPresent(owner->plannedImplementationHeader) && + stringPresent(owner->extractionState); + ownerRows.push_back(rendererPassOwnerMetadataJson(*owner)); + if (!rowValid) { + failures.push_back({ + {"code", "incomplete_owner_metadata"}, + {"pass", contract.name}, + {"contract_id", contractId}, + {"owner_metadata", rendererPassOwnerMetadataJson(*owner)}, + }); + } + if (std::string_view(owner->passName) != contract.name) { + failures.push_back({ + {"code", "owner_pass_name_mismatch"}, + {"pass", contract.name}, + {"contract_id", contractId}, + {"owner_pass_name", owner->passName}, + }); + } + const bool contractMetadataOwnedByModule = + std::string_view(contract.currentOwnerFile).find(owner->metadataHeader) != std::string_view::npos; + if (contractMetadataOwnedByModule) { + ++contractModuleOwnedCount; + contractModuleOwnedRows.push_back({ + {"pass", contract.name}, + {"contract_id", contractId}, + {"metadata_header", owner->metadataHeader}, + {"current_owner_file", contract.currentOwnerFile}, + }); + } + } + + if (registry.size() != kExpectedPassCount) { + failures.push_back({ + {"code", "unexpected_owner_registry_count"}, + {"expected", kExpectedPassCount}, + {"actual", registry.size()}, + }); + } + if (contracts.size() != kExpectedPassCount) { + failures.push_back({ + {"code", "unexpected_contract_count"}, + {"expected", kExpectedPassCount}, + {"actual", contracts.size()}, + }); + } + + return nlohmann::json{ + {"schema_version", 1}, + {"expected_pass_count", kExpectedPassCount}, + {"contract_count", contracts.size()}, + {"owner_registry_count", registry.size()}, + {"contract_module_owned_count", contractModuleOwnedCount}, + {"owner_rows", std::move(ownerRows)}, + {"contract_module_owned_rows", std::move(contractModuleOwnedRows)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", failures.empty()}, + }; +} + +inline bool rendererPassProfilingSectionIsAggregate(const std::string& section) { + return section.find(' ') != std::string::npos || + section == "queue_lane_ms" || + section == "all per_pass_gpu_ms fields"; +} + +inline nlohmann::json rendererPassTimingCoverageJson(const RendererSettings& settings, const nlohmann::json& perPassGpuMs) { + const std::vector contracts = rendererPassContracts(settings); + nlohmann::json sections = nlohmann::json::array(); + nlohmann::json unmappedSections = nlohmann::json::array(); + nlohmann::json activeUnmappedSections = nlohmann::json::array(); + + uint32_t coveredSectionCount = 0; + uint32_t aggregateSectionCount = 0; + uint32_t unmappedSectionCount = 0; + uint32_t activeUnmappedSectionCount = 0; + + for (const RendererPassContract& contract : contracts) { + if (!contract.profilingSections.is_array()) { + continue; + } + for (const nlohmann::json& sectionValue : contract.profilingSections) { + if (!sectionValue.is_string()) { + continue; + } + const std::string section = sectionValue.get(); + nlohmann::json sectionReport = { + {"pass", contract.name}, + {"pass_id", rendererPassContractIdName(contract.id)}, + {"role", rendererPassContractRoleName(contract.role)}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + {"section", section}, + }; + if (rendererPassProfilingSectionIsAggregate(section)) { + ++aggregateSectionCount; + sectionReport["status"] = "aggregate_or_external"; + sectionReport["timed_in_per_pass_gpu_ms"] = false; + } else if (perPassGpuMs.is_object() && perPassGpuMs.contains(section) && perPassGpuMs[section].is_number()) { + ++coveredSectionCount; + sectionReport["status"] = "timed"; + sectionReport["timed_in_per_pass_gpu_ms"] = true; + sectionReport["gpu_ms"] = perPassGpuMs[section]; + } else { + ++unmappedSectionCount; + sectionReport["status"] = "unmapped"; + sectionReport["timed_in_per_pass_gpu_ms"] = false; + unmappedSections.push_back({ + {"pass", contract.name}, + {"section", section}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + }); + if (contract.activeByCurrentSettings) { + ++activeUnmappedSectionCount; + activeUnmappedSections.push_back({ + {"pass", contract.name}, + {"section", section}, + }); + } + } + sections.push_back(std::move(sectionReport)); + } + } + + return nlohmann::json{ + {"schema_version", 1}, + {"per_pass_gpu_ms_available", perPassGpuMs.is_object()}, + {"pass_count", contracts.size()}, + {"covered_section_count", coveredSectionCount}, + {"aggregate_or_external_section_count", aggregateSectionCount}, + {"unmapped_section_count", unmappedSectionCount}, + {"active_unmapped_section_count", activeUnmappedSectionCount}, + {"unmapped_sections", std::move(unmappedSections)}, + {"active_unmapped_sections", std::move(activeUnmappedSections)}, + {"sections", std::move(sections)}, + {"passed", perPassGpuMs.is_object() && activeUnmappedSectionCount == 0}, + }; +} + +inline bool rendererDebugOutputIsAggregateOrArtifact(const std::string& output) { + return output.find(".json") != std::string::npos || + output == "debug export manifest" || + output == "debug view selected by RendererDebugView" || + output == "all registered RendererDebugView names"; +} + +inline nlohmann::json rendererDebugOutputCoverageJson( + const RendererSettings& settings, + const std::vector& exportableDebugViews) { + const std::vector contracts = rendererPassContracts(settings); + nlohmann::json outputs = nlohmann::json::array(); + nlohmann::json missingOutputs = nlohmann::json::array(); + nlohmann::json activeMissingOutputs = nlohmann::json::array(); + + uint32_t declaredOutputCount = 0; + uint32_t exportableOutputCount = 0; + uint32_t aggregateOrArtifactCount = 0; + uint32_t missingOutputCount = 0; + uint32_t activeMissingOutputCount = 0; + + for (const RendererPassContract& contract : contracts) { + if (!contract.debugOutputs.is_array()) { + continue; + } + for (const nlohmann::json& outputValue : contract.debugOutputs) { + if (!outputValue.is_string()) { + continue; + } + ++declaredOutputCount; + const std::string output = outputValue.get(); + const bool aggregateOrArtifact = rendererDebugOutputIsAggregateOrArtifact(output); + const bool exportable = !aggregateOrArtifact && + std::find(exportableDebugViews.begin(), exportableDebugViews.end(), output) != exportableDebugViews.end(); + + nlohmann::json outputReport = { + {"pass", contract.name}, + {"pass_id", rendererPassContractIdName(contract.id)}, + {"role", rendererPassContractRoleName(contract.role)}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + {"debug_output", output}, + }; + if (aggregateOrArtifact) { + ++aggregateOrArtifactCount; + outputReport["status"] = "aggregate_or_artifact"; + outputReport["exportable_debug_view"] = false; + } else if (exportable) { + ++exportableOutputCount; + outputReport["status"] = "exportable"; + outputReport["exportable_debug_view"] = true; + } else { + ++missingOutputCount; + outputReport["status"] = "missing_exportable_debug_view"; + outputReport["exportable_debug_view"] = false; + missingOutputs.push_back({ + {"pass", contract.name}, + {"debug_output", output}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + }); + if (contract.activeByCurrentSettings) { + ++activeMissingOutputCount; + activeMissingOutputs.push_back({ + {"pass", contract.name}, + {"debug_output", output}, + }); + } + } + outputs.push_back(std::move(outputReport)); + } + } + + return nlohmann::json{ + {"schema_version", 1}, + {"exportable_debug_view_count", exportableDebugViews.size()}, + {"declared_debug_output_count", declaredOutputCount}, + {"exportable_output_count", exportableOutputCount}, + {"aggregate_or_artifact_count", aggregateOrArtifactCount}, + {"missing_output_count", missingOutputCount}, + {"active_missing_output_count", activeMissingOutputCount}, + {"missing_outputs", std::move(missingOutputs)}, + {"active_missing_outputs", std::move(activeMissingOutputs)}, + {"outputs", std::move(outputs)}, + {"passed", activeMissingOutputCount == 0}, + }; +} + +inline bool rendererStringStartsWith(const std::string& value, const char* prefix) { + const std::string prefixString(prefix); + return value.size() >= prefixString.size() && + value.compare(0, prefixString.size(), prefixString) == 0; +} + +inline bool rendererStringContains(const std::string& value, const char* needle) { + return value.find(needle) != std::string::npos; +} + +inline const char* rendererDebugViewOwnerForName(const std::string& name) { + if (rendererStringStartsWith(name, "restir-di") || rendererStringStartsWith(name, "restir-reservoir")) { + return "RestirDIPass"; + } + if (rendererStringStartsWith(name, "restir-gi")) { + return "RestirGIPass"; + } + if (rendererStringStartsWith(name, "regir")) { + return "RegirPass"; + } + if (rendererStringStartsWith(name, "denoiser") || rendererStringStartsWith(name, "moment")) { + return "DenoiserPass"; + } + if (rendererStringStartsWith(name, "nrd")) { + return "NrdPass"; + } + if (rendererStringStartsWith(name, "dlss")) { + return "DlssPass"; + } + if (rendererStringStartsWith(name, "psr")) { + return "NrdPass"; + } + if (rendererStringStartsWith(name, "temporal") || + rendererStringContains(name, "motion-vector") || + rendererStringContains(name, "reprojection")) { + return "TemporalAAPass"; + } + if (name == "albedo" || + name == "normals" || + name == "depth" || + name == "roughness" || + rendererStringStartsWith(name, "material") || + rendererStringContains(name, "instance-id") || + rendererStringContains(name, "mesh-id")) { + return "GBufferPass"; + } + if (name == "beauty" || + rendererStringContains(name, "lighting") || + rendererStringContains(name, "path-") || + rendererStringContains(name, "bounce") || + rendererStringContains(name, "pdf") || + rendererStringContains(name, "mis-weight") || + rendererStringContains(name, "throughput") || + rendererStringContains(name, "radiance") || + rendererStringContains(name, "environment") || + rendererStringContains(name, "sun")) { + return "PathTracePass"; + } + if (rendererStringStartsWith(name, "atmosphere") || + rendererStringStartsWith(name, "fog") || + rendererStringContains(name, "tone-map") || + rendererStringContains(name, "fullscreen")) { + return "CompositePass"; + } + if (rendererStringStartsWith(name, "wavefront") || + rendererStringStartsWith(name, "adaptive") || + rendererStringContains(name, "validation") || + rendererStringContains(name, "mismatch")) { + return "DebugViewPass"; + } + return "DebugViewPass"; +} + +inline const char* rendererDebugViewValueRangeForName(const std::string& name) { + if (rendererStringContains(name, "normal")) { + return "world-space normal or encoded normal vector, remapped for PNG export"; + } + if (rendererStringContains(name, "motion")) { + return "signed screen-space pixel motion, visualized around zero"; + } + if (rendererStringContains(name, "depth") || rendererStringContains(name, "hit-distance")) { + return "finite nonnegative distance or depth, normalized for diagnostic export"; + } + if (rendererStringContains(name, "confidence") || + rendererStringContains(name, "valid") || + rendererStringContains(name, "mask") || + rendererStringContains(name, "occupancy") || + rendererStringContains(name, "accepted") || + rendererStringContains(name, "visibility")) { + return "[0, 1] confidence, mask, occupancy, or visibility scalar"; + } + if (rendererStringContains(name, "pdf") || + rendererStringContains(name, "weight") || + rendererStringContains(name, "variance") || + rendererStringContains(name, "m") || + rendererStringContains(name, "age") || + rendererStringContains(name, "count")) { + return "nonnegative scalar diagnostic, log/normalized for PNG export when needed"; + } + if (rendererStringContains(name, "id") || + rendererStringContains(name, "class") || + rendererStringContains(name, "type") || + rendererStringContains(name, "cell") || + rendererStringContains(name, "selected-light") || + rendererStringContains(name, "generation")) { + return "categorical integer id or class encoded as a debug color"; + } + if (rendererStringContains(name, "albedo") || + rendererStringContains(name, "color") || + rendererStringContains(name, "lighting") || + rendererStringContains(name, "radiance") || + name == "beauty") { + return "linear HDR color or contribution, tone-mapped or normalized for PNG export"; + } + return "renderer diagnostic scalar/vector normalized for PNG export"; +} + +inline const char* rendererDebugViewMeaningForName(const std::string& name) { + if (rendererStringStartsWith(name, "restir-di")) { + return "ReSTIR DI reservoir, light identity, PDF, visibility, or reuse diagnostic."; + } + if (rendererStringStartsWith(name, "restir-gi")) { + return "ReSTIR GI receiver, sample, reservoir, visibility, path-class, or reuse diagnostic."; + } + if (rendererStringStartsWith(name, "regir")) { + return "ReGIR grid, reservoir, query, environment, or reuse diagnostic."; + } + if (rendererStringStartsWith(name, "denoiser") || rendererStringStartsWith(name, "moment")) { + return "Engine denoiser guide, history, variance, moment, rejection, or confidence diagnostic."; + } + if (rendererStringStartsWith(name, "nrd") || rendererStringStartsWith(name, "psr")) { + return "NRD or primary-surface-replacement guide and confidence diagnostic."; + } + if (rendererStringStartsWith(name, "dlss")) { + return "DLSS or DLSS Ray Reconstruction input/output guide diagnostic."; + } + if (rendererStringStartsWith(name, "temporal") || + rendererStringContains(name, "motion-vector") || + rendererStringContains(name, "reprojection")) { + return "Temporal reprojection, motion, reactive-mask, or history confidence diagnostic."; + } + if (name == "albedo" || name == "normals" || name == "depth" || name == "roughness" || rendererStringStartsWith(name, "material")) { + return "Primary-surface guide data produced by the renderer-application bridge."; + } + if (rendererStringStartsWith(name, "wavefront")) { + return "Wavefront path tracing queue, ray state, material bucket, or lighting diagnostic."; + } + if (rendererStringStartsWith(name, "adaptive")) { + return "Adaptive sampling density, fill, disocclusion, or sample-count diagnostic."; + } + if (name == "beauty" || rendererStringContains(name, "lighting") || rendererStringContains(name, "path-")) { + return "Path-traced color contribution or path-channel diagnostic."; + } + return "Renderer debug output for inspecting feature state, bad pixels, or validation evidence."; +} + +inline nlohmann::json rendererDebugViewRegistryEntryJson(const std::string& name) { + return nlohmann::json{ + {"stable_name", name}, + {"owner_pass", rendererDebugViewOwnerForName(name)}, + {"value_range", rendererDebugViewValueRangeForName(name)}, + {"meaning", rendererDebugViewMeaningForName(name)}, + {"export_artifact_format", "png"}, + {"failure_hints", rendererContractArray({ + "Inspect this view next to beauty, profile.json, and rendergraph.json.", + "Compare against the owning pass timing and validation counters.", + "If values are all black/white/NaN-looking, verify guide resources, reset reason, and pass activation.", + })}, + }; +} + +inline nlohmann::json rendererDebugViewRegistryValidationJson( + const RendererSettings& settings, + const std::vector& exportableDebugViews) { + nlohmann::json registry = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + nlohmann::json duplicateNames = nlohmann::json::array(); + nlohmann::json activeContractViews = nlohmann::json::array(); + nlohmann::json missingActiveRegistryEntries = nlohmann::json::array(); + nlohmann::json missingRequiredMetadata = nlohmann::json::array(); + + std::vector seenNames; + seenNames.reserve(exportableDebugViews.size()); + for (const std::string& name : exportableDebugViews) { + if (name.empty()) { + failures.push_back({ + {"code", "empty_debug_view_name"}, + {"message", "Exportable debug view names must be stable non-empty strings."}, + }); + continue; + } + if (std::find(seenNames.begin(), seenNames.end(), name) != seenNames.end()) { + duplicateNames.push_back(name); + continue; + } + seenNames.push_back(name); + nlohmann::json entry = rendererDebugViewRegistryEntryJson(name); + if (entry.value("owner_pass", std::string{}).empty() || + entry.value("value_range", std::string{}).empty() || + entry.value("meaning", std::string{}).empty() || + !entry.contains("failure_hints") || + !entry["failure_hints"].is_array() || + entry["failure_hints"].empty()) { + missingRequiredMetadata.push_back(name); + } + registry.push_back(std::move(entry)); + } + + auto registryContains = [&](const std::string& name) { + return std::find(seenNames.begin(), seenNames.end(), name) != seenNames.end(); + }; + + const std::vector contracts = rendererPassContracts(settings); + for (const RendererPassContract& contract : contracts) { + if (!contract.activeByCurrentSettings || !contract.debugOutputs.is_array()) { + continue; + } + for (const nlohmann::json& outputValue : contract.debugOutputs) { + if (!outputValue.is_string()) { + continue; + } + const std::string output = outputValue.get(); + if (rendererDebugOutputIsAggregateOrArtifact(output)) { + continue; + } + activeContractViews.push_back({ + {"pass", contract.name}, + {"pass_id", rendererPassContractIdName(contract.id)}, + {"debug_view", output}, + {"registered", registryContains(output)}, + }); + if (!registryContains(output)) { + missingActiveRegistryEntries.push_back({ + {"pass", contract.name}, + {"debug_view", output}, + }); + } + } + } + + if (exportableDebugViews.empty()) { + failures.push_back({ + {"code", "no_exportable_debug_views"}, + {"message", "Debug view registry requires at least one exportable view."}, + }); + } + if (!duplicateNames.empty()) { + failures.push_back({ + {"code", "duplicate_debug_view_names"}, + {"duplicate_count", duplicateNames.size()}, + }); + } + if (!missingRequiredMetadata.empty()) { + failures.push_back({ + {"code", "debug_view_metadata_incomplete"}, + {"missing_count", missingRequiredMetadata.size()}, + }); + } + if (!missingActiveRegistryEntries.empty()) { + failures.push_back({ + {"code", "active_debug_view_missing_registry_entry"}, + {"missing_count", missingActiveRegistryEntries.size()}, + }); + } + if (registry.size() != exportableDebugViews.size()) { + warnings.push_back({ + {"code", "registry_size_differs_from_export_list"}, + {"registry_count", registry.size()}, + {"exportable_count", exportableDebugViews.size()}, + }); + } + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "debug_view_registry"}, + {"exportable_debug_view_count", exportableDebugViews.size()}, + {"registered_debug_view_count", registry.size()}, + {"active_contract_debug_view_count", activeContractViews.size()}, + {"duplicate_names", std::move(duplicateNames)}, + {"missing_required_metadata", std::move(missingRequiredMetadata)}, + {"missing_active_registry_entries", std::move(missingActiveRegistryEntries)}, + {"active_contract_views", std::move(activeContractViews)}, + {"registry", std::move(registry)}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererTemporalContractJson() { + return nlohmann::json{ + {"schema_version", 1}, + {"frame_timeline", { + {"frame_index", "monotonic rendered frame index"}, + {"accumulation_frame_index", "resets to zero when accumulation history is invalidated"}, + {"previous_frame_index", "last frame with valid temporal history"}, + {"camera_cut_flag", "global reset input for TAA, denoiser, ReSTIR, NRD, DLSS, and ReGIR"}, + {"history_valid_flag", "per-pass history validity plus shared reset reason"}, + {"jitter_index", "shared temporal jitter sequence index"}, + {"reset_reason", "startup, resize, camera moved, settings, lighting, environment, denoiser, debug, scene, material, shader"}, + }}, + {"motion_vectors", { + {"space", "screen pixels"}, + {"direction", "current-to-previous reprojection"}, + {"jitter", "consumer must state whether jitter is included; DLSS receives explicit jitter offsets"}, + {"validity", "invalid surface or disocclusion must zero confidence rather than silently reuse history"}, + }}, + {"depth", { + {"primary", "renderer guide depth"}, + {"validity", "finite positive depth for hit surfaces; explicit invalid mask for miss/sky paths"}, + {"reset_on", rendererContractArray({"resolution change", "render scale change", "camera cut", "scene reload"})}, + }}, + {"normal", { + {"space", "world space"}, + {"kind", "shading normal for denoising/reprojection, geometric normal available for visibility/backface policy"}, + {"normalization", "unit length within validation tolerance"}, + }}, + {"shared_reset_rules", rendererContractArray({ + "startup", + "camera cut", + "resolution change", + "render scale change", + "DLSS/NRD/denoiser mode change", + "scene reload", + "material reload", + "light topology or generation change", + "explicit accumulation reset", + })}, + {"debug_overlay", rendererContractArray({ + "history valid", + "motion magnitude", + "disocclusion", + "reset reason", + "TAA rejection", + "ReSTIR temporal rejection", + "NRD/DLSS guide validity", + })}, + }; +} + +inline bool rendererTemporalResetReasonKnown(const std::string& reason) { + return reason == "Startup" || + reason == "Resize" || + reason == "CameraMoved" || + reason == "Manual" || + reason == "RenderSettingsChanged" || + reason == "LightingChanged" || + reason == "EnvironmentChanged" || + reason == "DenoiserChanged" || + reason == "DebugViewChanged" || + reason == "SceneChanged" || + reason == "MaterialChanged" || + reason == "ShaderReloaded"; +} + +inline nlohmann::json rendererTemporalRuntimeValidationJson( + const RendererSettings& settings, + const std::string& lastResetReason, + uint32_t frameCount, + uint32_t profiledFrames, + uint64_t temporalHistoryBytes, + bool restirDiHistoryValid, + bool restirGiHistoryValid, + bool regirTemporalHistoryValid, + const nlohmann::json& temporalSystemDiagnostics) { + const bool restirDiActive = passes::RestirDIPass::isAnyModeActive(settings); + const bool restirGiActive = passes::RestirGIPass::isActive(settings); + const bool regirActive = passes::RegirPass::isActive(settings); + const bool engineDenoiserActive = passes::DenoiserPass::isActive(settings); + const bool nrdActive = passes::NrdPass::isActive(settings); + const bool taaActive = passes::TemporalAAPass::isActive(settings); + const bool dlssActive = passes::DlssPass::isActive(settings); + const bool anyTemporalFeatureActive = + restirDiActive || restirGiActive || regirActive || engineDenoiserActive || taaActive || nrdActive || dlssActive; + + auto findHistorySlot = [&](const char* slotName) -> const nlohmann::json* { + if (!temporalSystemDiagnostics.is_object() || + !temporalSystemDiagnostics.value("available", false) || + !temporalSystemDiagnostics.contains("slots") || + !temporalSystemDiagnostics["slots"].is_array()) { + return nullptr; + } + for (const nlohmann::json& slot : temporalSystemDiagnostics["slots"]) { + if (slot.is_object() && slot.value("name", std::string{}) == slotName) { + return &slot; + } + } + return nullptr; + }; + auto historySlotExists = [&](const char* slotName) { + return findHistorySlot(slotName) != nullptr; + }; + auto historySlotValid = [&](const char* slotName) -> nlohmann::json { + const nlohmann::json* slot = findHistorySlot(slotName); + return slot != nullptr && slot->contains("valid") && (*slot)["valid"].is_boolean() + ? (*slot)["valid"] + : nlohmann::json(nullptr); + }; + auto requiredSlotStatuses = [&](const nlohmann::json& requiredSlots) { + nlohmann::json statuses = nlohmann::json::array(); + if (!requiredSlots.is_array()) { + return statuses; + } + for (const nlohmann::json& requiredSlot : requiredSlots) { + if (!requiredSlot.is_string()) { + continue; + } + const std::string slotName = requiredSlot.get(); + const nlohmann::json* slot = findHistorySlot(slotName.c_str()); + nlohmann::json status = { + {"slot", slotName}, + {"exists", slot != nullptr}, + }; + if (slot != nullptr) { + status["valid"] = slot->value("valid", false); + status["resident"] = slot->value("resident", false); + status["last_written_frame"] = slot->value("last_written_frame", uint64_t{0}); + status["estimated_bytes"] = slot->value("estimated_bytes", uint64_t{0}); + } else { + status["valid"] = false; + status["resident"] = false; + status["last_written_frame"] = nullptr; + status["estimated_bytes"] = 0; + } + statuses.push_back(std::move(status)); + } + return statuses; + }; + auto slotStatusesReady = [](const nlohmann::json& statuses) { + if (!statuses.is_array() || statuses.empty()) { + return nlohmann::json(nullptr); + } + for (const nlohmann::json& status : statuses) { + if (!status.value("exists", false) || + !status.value("valid", false) || + !status.value("resident", false)) { + return nlohmann::json(false); + } + } + return nlohmann::json(true); + }; + + nlohmann::json features = nlohmann::json::array(); + auto feature = [&](const char* name, bool active, nlohmann::json historyValid, const char* owner, nlohmann::json requiredSlots) { + const nlohmann::json slotStatuses = requiredSlotStatuses(requiredSlots); + nlohmann::json historyReady = active ? slotStatusesReady(slotStatuses) : nlohmann::json(nullptr); + if (!active) { + historyValid = nullptr; + } + if (active && historyValid.is_boolean() && !historyReady.is_boolean()) { + historyReady = historyValid; + } + features.push_back({ + {"name", name}, + {"active", active}, + {"history_valid", std::move(historyValid)}, + {"history_ready", std::move(historyReady)}, + {"owner", owner}, + {"required_temporal_system_slots", std::move(requiredSlots)}, + {"temporal_system_slot_status", std::move(slotStatuses)}, + }); + }; + feature("restir_di", restirDiActive, restirDiHistoryValid, "RestirDIPass", rendererContractArray({"restir_reservoir", "previous_world_position"})); + feature("restir_gi", restirGiActive, restirGiHistoryValid, "RestirGIPass", rendererContractArray({"restir_gi_reservoir", "previous_world_position"})); + feature("regir", regirActive, regirTemporalHistoryValid, "RegirPass", rendererContractArray({})); + feature("denoiser", engineDenoiserActive, historySlotValid("denoiser_history"), "DenoiserPass", rendererContractArray({"denoiser_history", "previous_world_position"})); + feature("nrd", nrdActive, nullptr, "NrdPass", rendererContractArray({"denoiser_diffuse_history", "denoiser_specular_history", "previous_world_position"})); + feature("taa_tsr", taaActive, historySlotValid("taa_history"), "TemporalAAPass", rendererContractArray({"taa_history", "previous_world_position"})); + feature("dlss", dlssActive, historySlotValid("previous_world_position"), "DlssPass", rendererContractArray({"previous_world_position"})); + + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + nlohmann::json requiredHistorySlots = nlohmann::json::array(); + nlohmann::json missingRequiredHistorySlots = nlohmann::json::array(); + nlohmann::json unreadyRequiredHistorySlots = nlohmann::json::array(); + auto requireHistorySlot = [&](const char* featureName, bool active, const char* slotName) { + if (!active) { + return; + } + const nlohmann::json* slot = findHistorySlot(slotName); + requiredHistorySlots.push_back({ + {"feature", featureName}, + {"slot", slotName}, + }); + if (slot == nullptr) { + missingRequiredHistorySlots.push_back({ + {"feature", featureName}, + {"slot", slotName}, + }); + return; + } + if (!slot->value("valid", false) || !slot->value("resident", false)) { + unreadyRequiredHistorySlots.push_back({ + {"feature", featureName}, + {"slot", slotName}, + {"valid", slot->value("valid", false)}, + {"resident", slot->value("resident", false)}, + {"last_written_frame", slot->value("last_written_frame", uint64_t{0})}, + {"estimated_bytes", slot->value("estimated_bytes", uint64_t{0})}, + }); + } + }; + requireHistorySlot("restir_di", restirDiActive, "restir_reservoir"); + requireHistorySlot("restir_di", restirDiActive, "previous_world_position"); + requireHistorySlot("restir_gi", restirGiActive, "restir_gi_reservoir"); + requireHistorySlot("restir_gi", restirGiActive, "previous_world_position"); + requireHistorySlot("denoiser", engineDenoiserActive, "denoiser_history"); + requireHistorySlot("denoiser", engineDenoiserActive, "previous_world_position"); + requireHistorySlot("nrd", nrdActive, "denoiser_diffuse_history"); + requireHistorySlot("nrd", nrdActive, "denoiser_specular_history"); + requireHistorySlot("nrd", nrdActive, "previous_world_position"); + requireHistorySlot("taa_tsr", taaActive, "taa_history"); + requireHistorySlot("taa_tsr", taaActive, "previous_world_position"); + requireHistorySlot("dlss", dlssActive, "previous_world_position"); + + const bool resetReasonKnown = rendererTemporalResetReasonKnown(lastResetReason); + if (!resetReasonKnown) { + failures.push_back({ + {"code", "unknown_reset_reason"}, + {"reason", lastResetReason}, + }); + } + if (profiledFrames == 0u && frameCount > 0u) { + failures.push_back({ + {"code", "no_profiled_frames"}, + {"reason", "Temporal runtime validation needs at least one measured frame after warmup."}, + }); + } + if (anyTemporalFeatureActive && temporalHistoryBytes == 0ull) { + failures.push_back({ + {"code", "missing_temporal_history_memory"}, + {"reason", "At least one temporal feature is active, but the renderer reported zero temporal history memory."}, + }); + } + if (anyTemporalFeatureActive && !temporalSystemDiagnostics.value("available", false)) { + failures.push_back({ + {"code", "missing_temporal_system"}, + {"reason", "At least one temporal feature is active, but TemporalSystem diagnostics are unavailable."}, + }); + } + if (!missingRequiredHistorySlots.empty()) { + failures.push_back({ + {"code", "missing_required_temporal_history_slots"}, + {"missing_slot_count", missingRequiredHistorySlots.size()}, + }); + } + if (!unreadyRequiredHistorySlots.empty()) { + warnings.push_back({ + {"code", "unready_required_temporal_history_slots"}, + {"reason", "Required TemporalSystem slots exist but are not valid/resident yet; this can be expected on startup or immediately after a reset."}, + {"slot_count", unreadyRequiredHistorySlots.size()}, + }); + } + + const bool nonCameraResetInvalidatesHistoryAtReset = + lastResetReason != "CameraMoved"; + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"last_accumulation_reset_reason", lastResetReason}, + {"reset_reason_known", resetReasonKnown}, + {"frame_count", frameCount}, + {"profiled_frames", profiledFrames}, + {"temporal_history_bytes", temporalHistoryBytes}, + {"temporal_system_available", temporalSystemDiagnostics.value("available", false)}, + {"temporal_system_slot_count", temporalSystemDiagnostics.value("slot_count", uint64_t{0})}, + {"any_temporal_feature_active", anyTemporalFeatureActive}, + {"non_camera_reset_invalidates_history_at_reset", nonCameraResetInvalidatesHistoryAtReset}, + {"camera_move_preserves_temporal_history_policy", lastResetReason == "CameraMoved"}, + {"required_history_slots", std::move(requiredHistorySlots)}, + {"missing_required_history_slots", std::move(missingRequiredHistorySlots)}, + {"unready_required_history_slots", std::move(unreadyRequiredHistorySlots)}, + {"features", std::move(features)}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererApplicationBridgeContractJson() { + return nlohmann::json{ + {"schema_version", 1}, + {"surface_data", rendererContractArray({ + "world position", + "geometric normal", + "shading normal", + "view vector", + "material id", + "roughness", + "metallic", + "base color", + "emissive", + "object/instance id", + "motion vector", + "validity mask", + })}, + {"previous_surface_data", rendererContractArray({ + "previous world position", + "previous normal", + "previous material id", + "previous object/instance id", + "previous depth", + "previous validity", + })}, + {"light_identity", { + {"analytic_lights", "stable authored light id plus generation"}, + {"emissive_triangles", "stable mesh/primitive/material identity plus generation"}, + {"environment_samples", "environment distribution generation plus source kind"}, + {"deleted_or_unmapped", "reject temporal reuse and mark explicit invalid reason"}, + }}, + {"visibility", { + {"ray_origin_bias", "RendererSettings shadowRayBias/shadowDistanceBias policy"}, + {"backface_policy", "hardware backface culling is explicit and scene-dependent"}, + {"alpha_test_policy", "any-hit visibility honors alpha-tested material state"}, + {"transparent_policy", "transparent/blended materials are visibility-policy inputs, not implicit opaque hits"}, + }}, + {"validation", rendererContractArray({ + "normal length", + "roughness range", + "motion vector range", + "depth validity", + "material id validity", + "light id validity", + })}, + }; +} + +inline nlohmann::json rendererApplicationBridgeRuntimeValidationJson( + const RendererSettings& settings, + const nlohmann::json& temporalSystemDiagnostics, + const nlohmann::json& rayTracingGeometry, + const nlohmann::json& sceneLights, + const nlohmann::json& rayTracingDiagnosticCounters, + const nlohmann::json& accelerationStructureDiagnostics, + const nlohmann::json& restirDiDiagnostics, + const nlohmann::json& nvidiaIntegrations) { + const bool restirDiActive = passes::RestirDIPass::isAnyModeActive(settings); + const bool restirGiActive = passes::RestirGIPass::isActive(settings); + const bool regirActive = passes::RegirPass::isActive(settings); + const bool denoiserActive = settings.denoiserEnabled; + const bool nrdActive = passes::NrdPass::isActive(settings); + const bool taaActive = passes::TemporalAAPass::isActive(settings); + const bool dlssActive = passes::DlssPass::isActive(settings); + const bool anyTemporalBridgeActive = + restirDiActive || restirGiActive || regirActive || denoiserActive || nrdActive || taaActive || dlssActive; + + auto hasField = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field); + }; + auto hasBool = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_boolean(); + }; + auto hasNumber = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_number(); + }; + auto numberValue = [&](const nlohmann::json& object, const char* field) -> uint64_t { + if (!hasNumber(object, field)) { + return 0ull; + } + if (object[field].is_number_unsigned()) { + return object[field].get(); + } + if (object[field].is_number_integer()) { + const int64_t signedValue = object[field].get(); + return signedValue > 0 ? static_cast(signedValue) : 0ull; + } + return static_cast(std::max(0.0, object[field].get())); + }; + auto temporalSlotExists = [&](const char* slotName) { + if (!temporalSystemDiagnostics.is_object() || + !temporalSystemDiagnostics.value("available", false) || + !temporalSystemDiagnostics.contains("slots") || + !temporalSystemDiagnostics["slots"].is_array()) { + return false; + } + for (const nlohmann::json& slot : temporalSystemDiagnostics["slots"]) { + if (slot.is_object() && slot.value("name", std::string{}) == slotName) { + return true; + } + } + return false; + }; + auto stringArrayContains = [](const nlohmann::json& values, const char* required) { + if (!values.is_array()) { + return false; + } + for (const nlohmann::json& value : values) { + if (value.is_string() && value.get() == required) { + return true; + } + } + return false; + }; + + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + auto recordCheck = [&](const char* name, + bool required, + bool passed, + const char* message, + nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"required", required}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (required && !passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } else if (!required && !passed) { + warnings.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + + const uint64_t opaquePrimitiveCount = numberValue(rayTracingGeometry, "opaque_primitive_count"); + const uint64_t alphaTestedPrimitiveCount = numberValue(rayTracingGeometry, "alpha_tested_primitive_count"); + const uint64_t blendedPrimitiveCount = numberValue(rayTracingGeometry, "blended_primitive_count"); + const uint64_t totalPrimitiveClasses = + opaquePrimitiveCount + alphaTestedPrimitiveCount + blendedPrimitiveCount; + const uint64_t geometryTriangleCount = + numberValue(rayTracingGeometry, "opaque_triangle_count") + + numberValue(rayTracingGeometry, "alpha_tested_triangle_count") + + numberValue(rayTracingGeometry, "blended_triangle_count"); + + recordCheck( + "surface_geometry_profile_present", + settings.pathTracingEnabled, + rayTracingGeometry.is_object() && + hasNumber(rayTracingGeometry, "opaque_triangle_count") && + hasNumber(rayTracingGeometry, "alpha_tested_triangle_count") && + hasNumber(rayTracingGeometry, "blended_triangle_count") && + (geometryTriangleCount > 0ull || numberValue(accelerationStructureDiagnostics, "triangle_count") > 0ull), + "Path tracing profiles must expose surface geometry class counts for the renderer-application bridge.", + { + {"geometry_triangle_count", geometryTriangleCount}, + {"acceleration_structure_triangle_count", numberValue(accelerationStructureDiagnostics, "triangle_count")}, + }); + recordCheck( + "surface_material_classes_reported", + settings.pathTracingEnabled, + rayTracingGeometry.is_object() && + hasNumber(rayTracingGeometry, "opaque_primitive_count") && + hasNumber(rayTracingGeometry, "alpha_tested_primitive_count") && + hasNumber(rayTracingGeometry, "blended_primitive_count"), + "Surface material classes must be explicit so visibility and guide generation can distinguish opaque, alpha-tested, and blended surfaces.", + { + {"opaque_primitive_count", opaquePrimitiveCount}, + {"alpha_tested_primitive_count", alphaTestedPrimitiveCount}, + {"blended_primitive_count", blendedPrimitiveCount}, + }); + recordCheck( + "visibility_backface_policy_explicit", + settings.pathTracingEnabled, + hasBool(rayTracingGeometry, "hardware_backface_culling_enabled") || + hasBool(accelerationStructureDiagnostics, "hardware_backface_culling_enabled"), + "The RAB visibility bridge must report whether hardware backface culling is active.", + { + {"ray_tracing_geometry_has_policy", hasBool(rayTracingGeometry, "hardware_backface_culling_enabled")}, + {"acceleration_structure_has_policy", hasBool(accelerationStructureDiagnostics, "hardware_backface_culling_enabled")}, + }); + recordCheck( + "visibility_alpha_policy_observable", + alphaTestedPrimitiveCount > 0ull || blendedPrimitiveCount > 0ull, + rayTracingDiagnosticCounters.is_object() && + hasNumber(rayTracingDiagnosticCounters, "primary_any_hit_alpha_tested") && + hasNumber(rayTracingDiagnosticCounters, "primary_any_hit_blended") && + hasNumber(rayTracingDiagnosticCounters, "terminal_any_hit_alpha_tested") && + hasNumber(rayTracingDiagnosticCounters, "terminal_any_hit_blended"), + "Scenes with alpha-tested or blended geometry should expose any-hit visibility counters for bridge debugging.", + { + {"alpha_tested_primitive_count", alphaTestedPrimitiveCount}, + {"blended_primitive_count", blendedPrimitiveCount}, + {"primary_any_hit_alpha_tested", numberValue(rayTracingDiagnosticCounters, "primary_any_hit_alpha_tested")}, + {"primary_any_hit_blended", numberValue(rayTracingDiagnosticCounters, "primary_any_hit_blended")}, + }); + recordCheck( + "scene_light_records_reported", + restirDiActive || restirGiActive || regirActive, + sceneLights.is_object() && + hasNumber(sceneLights, "record_count") && + hasNumber(sceneLights, "emissive_count") && + hasNumber(sceneLights, "authored_count"), + "Lighting reuse modes must report scene light record counts and source classes.", + { + {"record_count", numberValue(sceneLights, "record_count")}, + {"emissive_count", numberValue(sceneLights, "emissive_count")}, + {"authored_count", numberValue(sceneLights, "authored_count")}, + }); + + const nlohmann::json restirLightHistoryMapping = + restirDiDiagnostics.is_object() && restirDiDiagnostics.contains("light_history_mapping") + ? restirDiDiagnostics["light_history_mapping"] + : nlohmann::json::object(); + recordCheck( + "restir_di_light_history_mapping_contract", + restirDiActive, + restirLightHistoryMapping.is_object() && + restirLightHistoryMapping.value("cached_index_fast_path", false) && + restirLightHistoryMapping.value("stable_identity_remap_on_cached_mismatch", false) && + stringArrayContains(restirLightHistoryMapping.value("stable_identity_fields", nlohmann::json::array()), "identity_hash") && + stringArrayContains(restirLightHistoryMapping.value("stable_identity_fields", nlohmann::json::array()), "identity_generation") && + stringArrayContains(restirLightHistoryMapping.value("stable_identity_fields", nlohmann::json::array()), "light_kind"), + "ReSTIR DI must isolate light identity remapping behind a stable application-bridge contract.", + restirLightHistoryMapping); + const nlohmann::json directLightOwnership = + restirDiDiagnostics.is_object() && restirDiDiagnostics.contains("direct_light_ownership") + ? restirDiDiagnostics["direct_light_ownership"] + : nlohmann::json::object(); + recordCheck( + "restir_di_direct_light_ownership_contract", + restirDiActive, + directLightOwnership.is_object() && + hasField(directLightOwnership, "emissive_and_analytic_lights") && + hasField(directLightOwnership, "sun") && + hasField(directLightOwnership, "environment"), + "Direct light ownership must state which light classes are owned by ReSTIR DI versus classic NEE.", + directLightOwnership); + recordCheck( + "previous_surface_temporal_slot", + anyTemporalBridgeActive, + temporalSlotExists("previous_world_position"), + "Temporal consumers must share an explicit previous-world-position bridge slot.", + { + {"temporal_system_available", temporalSystemDiagnostics.value("available", false)}, + {"slot_count", temporalSystemDiagnostics.value("slot_count", uint64_t{0})}, + }); + const nlohmann::json nrdGuideContract = + nvidiaIntegrations.is_object() && nvidiaIntegrations.contains("nrd_guide_contract") + ? nvidiaIntegrations["nrd_guide_contract"] + : nlohmann::json::object(); + recordCheck( + "nrd_guide_contract", + nrdActive, + nrdGuideContract.is_object() && + nrdGuideContract.value("motion_vectors", false) && + nrdGuideContract.value("normal_roughness", false) && + nrdGuideContract.value("view_z", false), + "NRD mode must expose motion, normal/roughness, and view-Z guide contract evidence.", + nrdGuideContract); + const nlohmann::json dlssGuideContract = + nvidiaIntegrations.is_object() && nvidiaIntegrations.contains("dlss_guide_contract") + ? nvidiaIntegrations["dlss_guide_contract"] + : nlohmann::json::object(); + recordCheck( + "dlss_guide_contract", + passes::DlssPass::isUpscaleActive(settings), + dlssGuideContract.is_object() && + hasField(dlssGuideContract, "depth_convention") && + hasField(dlssGuideContract, "motion_convention") && + hasField(dlssGuideContract, "jitter_convention") && + dlssGuideContract.contains("tagged_resources") && + dlssGuideContract["tagged_resources"].is_array(), + "DLSS upscale mode must expose depth, motion, jitter, and resource tagging guide evidence.", + dlssGuideContract); + const nlohmann::json dlssRrGuideContract = + nvidiaIntegrations.is_object() && nvidiaIntegrations.contains("dlss_ray_reconstruction_guide_contract") + ? nvidiaIntegrations["dlss_ray_reconstruction_guide_contract"] + : nlohmann::json::object(); + const nlohmann::json dlssRrGuideValidator = + dlssRrGuideContract.is_object() && dlssRrGuideContract.contains("guide_consistency_validator") + ? dlssRrGuideContract["guide_consistency_validator"] + : nlohmann::json::object(); + recordCheck( + "dlss_ray_reconstruction_guide_contract", + settings.dlssRayReconstructionEnabled, + dlssRrGuideContract.is_object() && + dlssRrGuideContract.contains("guide_images") && + dlssRrGuideContract["guide_images"].is_array() && + dlssRrGuideValidator.value("previous_world_position", false) && + dlssRrGuideValidator.value("material_id", false) && + dlssRrGuideValidator.value("instance_id", false), + "DLSS Ray Reconstruction must expose guide images plus previous-surface/material/instance consistency evidence.", + dlssRrGuideContract); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "renderer_application_bridge"}, + {"feature_state", { + {"path_tracing", settings.pathTracingEnabled}, + {"restir_di", restirDiActive}, + {"restir_gi", restirGiActive}, + {"regir", regirActive}, + {"denoiser", denoiserActive}, + {"nrd", nrdActive}, + {"taa_tsr", taaActive}, + {"dlss", dlssActive}, + {"dlss_ray_reconstruction", settings.dlssRayReconstructionEnabled}, + }}, + {"surface_class_totals", { + {"opaque_primitive_count", opaquePrimitiveCount}, + {"alpha_tested_primitive_count", alphaTestedPrimitiveCount}, + {"blended_primitive_count", blendedPrimitiveCount}, + {"total_classified_primitive_count", totalPrimitiveClasses}, + }}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererSupportedModeMatrixJson() { + auto row = [](const char* name, + const char* status, + nlohmann::json args, + const char* referencePath, + const char* notes) { + return nlohmann::json{ + {"name", name}, + {"status", status}, + {"canonical_args", std::move(args)}, + {"reference_or_gate", referencePath}, + {"notes", notes}, + }; + }; + + return nlohmann::json{ + {"schema_version", 1}, + {"failure_policy", rendererContractArray({ + "image diff above threshold fails", + "new NaN/Inf or invalid reservoir count fails", + "missing required debug output fails", + "missing timing section fails", + "unsupported feature combinations must fail clearly", + })}, + {"modes", nlohmann::json::array({ + row("path_tracing_only", "supported-diagnostic", + rendererContractArray({"--restir-di", "off", "--restir-gi", "off", "--regir", "off", "--disable-pass", "Denoiser", "--disable-pass", "TAA"}), + "scripts/render_compare_references.ps1", + "Use for ground-truth and renderer-core regressions."), + row("restir_di_only", "supported", + rendererContractArray({"--restir-di", "production", "--restir-gi", "off", "--regir", "off"}), + "scripts/restir_reference_matrix.ps1 -ModeSet di", + "DI temporal/spatial/final plus counters and debug views."), + row("restir_gi", "supported", + rendererContractArray({"--restir-di", "off", "--restir-gi", "production", "--regir", "off"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode restir_gi", + "GI must not depend on DI execution."), + row("restir_di_gi", "supported-default", + rendererContractArray({"--restir-di", "production", "--restir-gi", "production"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode restir_di_gi", + "Default real-time beauty path."), + row("restir_di_regir", "supported", + rendererContractArray({"--restir-di", "production", "--restir-gi", "off", "--regir", "on"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode restir_di_regir", + "ReGIR is a sampling service and must remain optional."), + row("engine_denoiser_taa", "supported-default", + rendererContractArray({"--denoiser", "on", "--denoiser-backend", "engine", "--temporal-upscaler", "taa-tsr"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode engine_denoiser_taa", + "Current safe denoiser/upscaler policy default."), + row("nrd_enabled", "runtime-gated", + rendererContractArray({"--denoiser", "on", "--denoiser-backend", "nrd"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode nrd_enabled", + "Falls back when NRD SDK/runtime is unavailable; profile must report reason."), + row("dlss_enabled", "runtime-gated", + rendererContractArray({"--temporal-upscaler", "dlss", "--dlss", "on"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode dlss_enabled", + "Falls back when DLSS is unavailable; guide tags and evaluation status must be reported."), + row("dlss_ray_reconstruction", "runtime-gated-experimental", + rendererContractArray({"--temporal-upscaler", "dlss", "--dlss", "on", "--dlss-rr", "on"}), + "scripts/renderer_quality_validation_matrix.ps1 -Mode dlss_ray_reconstruction", + "Falls back when DLSS RR is unavailable; guide contracts and unavailable reason must be reported."), + })}, + }; +} + +inline nlohmann::json rendererCurrentModeSupportJson(const RendererSettings& settings) { + const bool restirDiActive = passes::RestirDIPass::isAnyModeActive(settings); + const bool restirGiActive = passes::RestirGIPass::isActive(settings); + const bool regirActive = passes::RegirPass::isActive(settings); + const bool engineDenoiserActive = passes::DenoiserPass::isActive(settings); + const bool nrdActive = passes::NrdPass::isActive(settings); + const bool dlssActive = passes::DlssPass::isActive(settings); + const bool dlssRayReconstructionActive = passes::DlssPass::isRayReconstructionActive(settings); + const bool taaTsrActive = passes::TemporalAAPass::isActive(settings); + + nlohmann::json matchedModes = nlohmann::json::array(); + nlohmann::json unsupportedReasons = nlohmann::json::array(); + nlohmann::json experimentalNotes = nlohmann::json::array(); + + auto match = [&](const char* name, const char* reason) { + matchedModes.push_back({ + {"name", name}, + {"reason", reason}, + }); + }; + auto unsupported = [&](const char* code, const char* reason) { + unsupportedReasons.push_back({ + {"code", code}, + {"reason", reason}, + }); + }; + auto experimental = [&](const char* code, const char* reason) { + experimentalNotes.push_back({ + {"code", code}, + {"reason", reason}, + }); + }; + + if (!settings.pathTracingEnabled) { + unsupported("path_tracing_disabled", "The RTXDI-quality renderer mode matrix assumes path tracing is the active renderer backend."); + } + if (!restirDiActive && !restirGiActive && !regirActive && !settings.denoiserEnabled && !taaTsrActive && !dlssActive) { + match("path_tracing_only", "ReSTIR, ReGIR, denoising, TAA/TSR, and DLSS are inactive."); + } + if (restirDiActive && !restirGiActive && !regirActive) { + match("restir_di_only", "ReSTIR DI is active without ReSTIR GI or ReGIR."); + } + if (!restirDiActive && restirGiActive && !regirActive) { + match("restir_gi", "ReSTIR GI is active without ReSTIR DI or ReGIR."); + } + if (restirDiActive && restirGiActive) { + match("restir_di_gi", "ReSTIR DI and ReSTIR GI are active together."); + } + if (restirDiActive && regirActive) { + match("restir_di_regir", "ReSTIR DI is active with ReGIR light sampling."); + } else if (regirActive) { + experimental("regir_without_restir_di", "The current supported matrix documents ReGIR with ReSTIR DI; this combination should be treated as experimental until a dedicated gate exists."); + } + if (nrdActive) { + match("nrd_enabled", "NRD denoiser backend is requested while denoising is enabled."); + } + if (dlssActive) { + match("dlss_enabled", "DLSS temporal upscaler or DLSS Ray Reconstruction is requested."); + } + if (dlssRayReconstructionActive) { + match("dlss_ray_reconstruction", "DLSS Ray Reconstruction is requested."); + } + if (engineDenoiserActive && taaTsrActive && !nrdActive && !dlssActive) { + match("engine_denoiser_taa", "Engine denoiser and TAA/TSR temporal path are active."); + } + if (settings.temporalUpscaler == TemporalUpscaler::Nis) { + experimental("nis_temporal_upscaler", "NIS is not part of the current RTXDI-quality supported mode matrix."); + } + if (passes::DlssPass::isRayReconstructionActive(settings) && !passes::DlssPass::isUpscaleActive(settings)) { + unsupported("dlss_rr_without_dlss_upscaler", "DLSS Ray Reconstruction requires the DLSS upscaler contract for guide tagging and SDK evaluation."); + } + if (settings.lightingReuseMode == LightingReuseMode::ExperimentalRestirPT || + settings.lightingReuseMode == LightingReuseMode::ValidateRestirPTAgainstLegacy) { + experimental("restir_pt_lighting_reuse", "ReSTIR PT modes are experimental and need a dedicated supported-mode gate before they can be promoted."); + } + + const bool passed = unsupportedReasons.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"settings_snapshot", rendererSettingsSnapshotJson(settings)}, + {"feature_state", { + {"path_tracing", settings.pathTracingEnabled}, + {"restir_di", restirDiActive}, + {"restir_gi", restirGiActive}, + {"regir", regirActive}, + {"denoiser", settings.denoiserEnabled}, + {"nrd", nrdActive}, + {"dlss", dlssActive}, + {"taa_tsr", taaTsrActive}, + {"dlss_ray_reconstruction", settings.dlssRayReconstructionEnabled}, + }}, + {"matched_modes", std::move(matchedModes)}, + {"unsupported_reasons", std::move(unsupportedReasons)}, + {"experimental_notes", std::move(experimentalNotes)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererActivePassRuntimeValidationJson( + const RendererSettings& settings, + const nlohmann::json& activePasses, + const nlohmann::json& perPassGpuMs, + const nlohmann::json& restirDiDiagnostics, + const nlohmann::json& restirGiDiagnostics, + const nlohmann::json& temporalRuntimeValidation, + const nlohmann::json& nvidiaIntegrations) { + const bool restirDiActive = passes::RestirDIPass::isActive(settings); + const bool restirGiActive = passes::RestirGIPass::isActive(settings); + const bool regirActive = passes::RegirPass::isActive(settings); + const bool engineDenoiserActive = passes::DenoiserPass::isActive(settings); + const bool taaActive = passes::TemporalAAPass::isActive(settings); + const bool effectiveDlssRayReconstruction = + nvidiaIntegrations.is_object() && + nvidiaIntegrations.contains("effective_dlss_ray_reconstruction") && + nvidiaIntegrations["effective_dlss_ray_reconstruction"].is_boolean() && + nvidiaIntegrations["effective_dlss_ray_reconstruction"].get(); + const std::string effectiveTemporalUpscaler = + nvidiaIntegrations.is_object() && + nvidiaIntegrations.contains("effective_temporal_upscaler") && + nvidiaIntegrations["effective_temporal_upscaler"].is_string() + ? nvidiaIntegrations["effective_temporal_upscaler"].get() + : std::string{}; + const bool engineDenoiserRunExpected = engineDenoiserActive && !effectiveDlssRayReconstruction; + const bool dlssUpscaleRunExpected = effectiveTemporalUpscaler == "dlss" && !effectiveDlssRayReconstruction; + const bool dlssRayReconstructionRunExpected = effectiveDlssRayReconstruction; + + auto activePass = [&](const char* name) { + return activePasses.is_object() && activePasses.value(name, false); + }; + auto timedPass = [&](const char* name) { + return perPassGpuMs.is_object() && + perPassGpuMs.contains(name) && + perPassGpuMs[name].is_number() && + perPassGpuMs[name].get() > 0.0; + }; + auto diagnosticActive = [](const nlohmann::json& diagnostics, const char* passName) { + return diagnostics.is_object() && + diagnostics.contains("active_passes") && + diagnostics["active_passes"].is_object() && + diagnostics["active_passes"].value(passName, false); + }; + auto temporalFeatureReady = [&](const char* featureName) { + if (!temporalRuntimeValidation.is_object() || + !temporalRuntimeValidation.contains("features") || + !temporalRuntimeValidation["features"].is_array()) { + return false; + } + for (const nlohmann::json& feature : temporalRuntimeValidation["features"]) { + if (!feature.is_object() || feature.value("name", std::string{}) != featureName) { + continue; + } + return feature.value("active", false) && + feature.contains("history_ready") && + feature["history_ready"].is_boolean() && + feature["history_ready"].get(); + } + return false; + }; + + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + auto addCheck = [&](const char* name, + bool required, + bool passed, + const char* message, + nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"required", required}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (required && !passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } else if (!required && !passed) { + warnings.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + auto requireActive = [&](const char* featureName, + bool required, + const char* passKey, + const char* timingKey, + bool diagnosticEvidence = false) { + const bool active = activePass(passKey); + const bool timed = timingKey != nullptr && timingKey[0] != '\0' && timedPass(timingKey); + addCheck( + featureName, + required, + !required || active || timed || diagnosticEvidence, + "Requested feature pass must have active-pass, timing, or subsystem diagnostic evidence.", + { + {"pass_key", passKey}, + {"timing_key", timingKey == nullptr ? "" : timingKey}, + {"active_pass", active}, + {"timed", timed}, + {"diagnostic_evidence", diagnosticEvidence}, + }); + }; + auto requireInactive = [&](const char* featureName, bool shouldBeInactive, nlohmann::json passKeys) { + nlohmann::json activeKeys = nlohmann::json::array(); + if (passKeys.is_array()) { + for (const nlohmann::json& keyValue : passKeys) { + if (keyValue.is_string()) { + const std::string key = keyValue.get(); + if (activePass(key.c_str())) { + activeKeys.push_back(key); + } + } + } + } + addCheck( + featureName, + shouldBeInactive, + !shouldBeInactive || activeKeys.empty(), + "Disabled feature must not leave active renderer pass evidence.", + { + {"active_keys", std::move(activeKeys)}, + {"pass_keys", std::move(passKeys)}, + }); + }; + + requireActive("path_trace_pass_active", settings.pathTracingEnabled, "path_trace", "path_trace"); + requireActive("restir_di_final_pass_active", restirDiActive, "restir_di_final", "restir_di_final", diagnosticActive(restirDiDiagnostics, "final")); + requireActive("restir_di_temporal_pass_active", restirDiActive && settings.restirDiTemporalEnabled, "restir_di_temporal", "restir_di_temporal", diagnosticActive(restirDiDiagnostics, "temporal")); + requireActive("restir_di_spatial_pass_active", restirDiActive && settings.restirDiSpatialEnabled && settings.restirDiSpatialRounds > 0u, "restir_di_spatial", "restir_di_spatial", diagnosticActive(restirDiDiagnostics, "spatial")); + requireActive("restir_gi_temporal_pass_active", restirGiActive, "restir_gi_temporal", "restir_gi_temporal", diagnosticActive(restirGiDiagnostics, "temporal")); + requireActive("restir_gi_final_pass_active", restirGiActive, "restir_gi_final", "restir_gi_final", diagnosticActive(restirGiDiagnostics, "final")); + requireActive("restir_gi_spatial_pass_active", restirGiActive && settings.restirGiSpatialRounds > 0u, "restir_gi_spatial", "restir_gi_spatial", diagnosticActive(restirGiDiagnostics, "spatial")); + requireActive("restir_gi_upsample_pass_active", restirGiActive && settings.restirGiHalfResolution, "restir_gi_upsample", "restir_gi_upsample", diagnosticActive(restirGiDiagnostics, "upsample")); + requireActive("regir_pass_active", regirActive, "regir", "regir_build"); + requireActive("regir_temporal_reuse_pass_active", regirActive && settings.regirTemporalReuse && settings.regirGridMode != RegirGridMode::Hash, "regir_temporal_reuse", "regir_temporal_reuse"); + requireActive("regir_spatial_reuse_pass_active", regirActive && settings.regirSpatialReuse && settings.regirGridMode != RegirGridMode::Hash, "regir_spatial_reuse", "regir_spatial_reuse"); + requireActive("engine_denoiser_pass_active", engineDenoiserRunExpected, "denoiser", "denoiser", temporalFeatureReady("denoiser")); + requireActive("moment_update_pass_active", engineDenoiserRunExpected, "moment_update", "moment_update", temporalFeatureReady("denoiser")); + requireActive("taa_pass_active", taaActive, "taa", "taa", temporalFeatureReady("taa_tsr")); + requireActive("taa_history_copy_pass_active", taaActive, "taa_history_copy", "taa_history_copy", temporalFeatureReady("taa_tsr")); + requireActive("dlss_guides_pass_active", dlssUpscaleRunExpected, "dlss_guides", "dlss_guides"); + requireActive("dlss_pass_active", dlssUpscaleRunExpected, "dlss", "dlss"); + requireActive("dlss_rr_guides_pass_active", dlssRayReconstructionRunExpected, "dlss_rr_guides", "dlss_rr_guides"); + requireActive("dlss_rr_pass_active", dlssRayReconstructionRunExpected, "dlss_rr", "dlss_rr"); + + requireInactive( + "restir_di_disabled_passes_inactive", + !restirDiActive, + rendererContractArray({"restir_di_temporal", "restir_di_spatial", "restir_di_final"})); + requireInactive( + "restir_gi_disabled_passes_inactive", + !restirGiActive, + rendererContractArray({"restir_gi_temporal", "restir_gi_spatial", "restir_gi_final", "restir_gi_upsample"})); + requireInactive( + "regir_disabled_passes_inactive", + !regirActive, + rendererContractArray({"regir", "regir_temporal_reuse", "regir_spatial_reuse", "regir_visibility_reuse", "regir_environment", "regir_sun"})); + requireInactive( + "engine_denoiser_disabled_passes_inactive", + !engineDenoiserActive, + rendererContractArray({"denoiser", "moment_update"})); + requireInactive( + "taa_disabled_passes_inactive", + !taaActive, + rendererContractArray({"taa", "taa_history_copy"})); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"active_passes_available", activePasses.is_object()}, + {"per_pass_gpu_ms_available", perPassGpuMs.is_object()}, + {"feature_state", { + {"path_tracing", settings.pathTracingEnabled}, + {"restir_di", restirDiActive}, + {"restir_gi", restirGiActive}, + {"regir", regirActive}, + {"engine_denoiser", engineDenoiserActive}, + {"engine_denoiser_run_expected", engineDenoiserRunExpected}, + {"taa_tsr", taaActive}, + {"dlss_upscale_run_expected", dlssUpscaleRunExpected}, + {"effective_dlss_ray_reconstruction", effectiveDlssRayReconstruction}, + {"effective_temporal_upscaler", effectiveTemporalUpscaler}, + }}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererRestirReservoirRuntimeValidationJson( + const RendererSettings& settings, + const nlohmann::json& restirDiDiagnostics, + const nlohmann::json& restirGiDiagnostics) { + const bool restirDiRequired = passes::RestirDIPass::isActive(settings); + const bool restirGiRequired = passes::RestirGIPass::isActive(settings); + + auto contractBlock = [](const nlohmann::json& diagnostics) { + return diagnostics.is_object() && diagnostics.contains("reservoir_contract_validation") + ? diagnostics["reservoir_contract_validation"] + : nlohmann::json::object(); + }; + auto numberValue = [](const nlohmann::json& object, const char* field) -> uint64_t { + if (!object.is_object() || !object.contains(field) || !object[field].is_number()) { + return 0ull; + } + if (object[field].is_number_unsigned()) { + return object[field].get(); + } + if (object[field].is_number_integer()) { + const int64_t value = object[field].get(); + return value > 0 ? static_cast(value) : 0ull; + } + return static_cast(std::max(0.0, object[field].get())); + }; + + const nlohmann::json diContract = contractBlock(restirDiDiagnostics); + const nlohmann::json giContract = contractBlock(restirGiDiagnostics); + + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + auto addCheck = [&](const char* name, + bool required, + bool passed, + const char* message, + nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"required", required}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (required && !passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } else if (!required && !passed) { + warnings.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + auto validateContract = [&](const char* name, bool required, const nlohmann::json& contract) { + const bool present = contract.is_object() && !contract.empty(); + const bool active = present && contract.value("active", false); + const bool checked = present && contract.value("checked", false); + const bool passed = present && contract.value("passed", false); + const uint64_t violationCount = numberValue(contract, "violation_count"); + const uint64_t invalidSourcePdf = numberValue(contract, "invalid_source_pdf_count"); + const uint64_t invalidTargetPdf = numberValue(contract, "invalid_target_pdf_count"); + const uint64_t nonFinite = numberValue(contract, "non_finite_count"); + + addCheck( + name, + required, + !required || (present && active && checked && passed && violationCount == 0ull), + "Active ReSTIR reservoir mode must be checked and free of contract, PDF, target, parity, or non-finite violations.", + { + {"present", present}, + {"active", active}, + {"checked", checked}, + {"passed", passed}, + {"violation_count", violationCount}, + {"invalid_source_pdf_count", invalidSourcePdf}, + {"invalid_target_pdf_count", invalidTargetPdf}, + {"non_finite_count", nonFinite}, + {"contract", contract}, + }); + addCheck( + std::string(name).append("_inactive_contract_clean").c_str(), + !required, + required || !present || passed, + "Inactive ReSTIR reservoir contract blocks must not report latent failures.", + { + {"present", present}, + {"active", active}, + {"checked", checked}, + {"passed", passed}, + {"violation_count", violationCount}, + }); + }; + + validateContract("restir_di_reservoir_contract", restirDiRequired, diContract); + validateContract("restir_gi_reservoir_contract", restirGiRequired, giContract); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"feature_state", { + {"standalone_restir_di", restirDiRequired}, + {"restir_gi", restirGiRequired}, + }}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererDenoiserUpscalerRuntimeValidationJson( + const RendererSettings& settings, + const nlohmann::json& nvidiaIntegrations, + const nlohmann::json& temporalRuntimeValidation, + const nlohmann::json& activePassRuntimeValidation) { + const bool engineDenoiserRequested = passes::DenoiserPass::isActive(settings); + const bool nrdRequested = passes::NrdPass::isActive(settings); + const bool taaTsrRequested = passes::TemporalAAPass::isActive(settings); + const bool dlssRequested = passes::DlssPass::isUpscaleActive(settings); + const bool dlssRayReconstructionRequested = passes::DlssPass::isRayReconstructionActive(settings); + const bool safeDefaultRequested = + engineDenoiserRequested && taaTsrRequested && !dlssRayReconstructionRequested; + + auto hasField = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field); + }; + auto hasString = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_string(); + }; + auto hasBool = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_boolean(); + }; + auto hasNumber = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_number(); + }; + auto hasArray = [&](const nlohmann::json& object, const char* field) { + return hasField(object, field) && object[field].is_array(); + }; + auto hasNonEmptyString = [&](const nlohmann::json& object, const char* field) { + return hasString(object, field) && !object[field].get().empty(); + }; + auto stringValue = [&](const nlohmann::json& object, const char* field) -> std::string { + return hasString(object, field) ? object[field].get() : std::string{}; + }; + auto boolValue = [&](const nlohmann::json& object, const char* field) -> bool { + return hasBool(object, field) && object[field].get(); + }; + auto objectValue = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_object() + ? object[field] + : nlohmann::json::object(); + }; + auto arrayContainsString = [](const nlohmann::json& values, const char* expected) { + if (!values.is_array()) { + return false; + } + for (const nlohmann::json& value : values) { + if (value.is_string() && value.get() == expected) { + return true; + } + } + return false; + }; + auto taggedResourceRolePresent = [&](const nlohmann::json& guideContract, const char* role) { + if (!hasArray(guideContract, "tagged_resources")) { + return false; + } + for (const nlohmann::json& resource : guideContract["tagged_resources"]) { + if (resource.is_object() && stringValue(resource, "role") == role) { + return true; + } + } + return false; + }; + auto temporalFeature = [&](const char* featureName) { + if (!temporalRuntimeValidation.is_object() || + !temporalRuntimeValidation.contains("features") || + !temporalRuntimeValidation["features"].is_array()) { + return nlohmann::json::object(); + } + for (const nlohmann::json& feature : temporalRuntimeValidation["features"]) { + if (feature.is_object() && feature.value("name", std::string{}) == featureName) { + return feature; + } + } + return nlohmann::json::object(); + }; + auto temporalFeatureReady = [&](const char* featureName) { + const nlohmann::json feature = temporalFeature(featureName); + return feature.is_object() && + feature.value("active", false) && + feature.contains("history_ready") && + feature["history_ready"].is_boolean() && + feature["history_ready"].get(); + }; + auto activePassCheckPassed = [&](const char* checkName) { + if (!activePassRuntimeValidation.is_object() || + !activePassRuntimeValidation.contains("checks") || + !activePassRuntimeValidation["checks"].is_array()) { + return false; + } + for (const nlohmann::json& check : activePassRuntimeValidation["checks"]) { + if (check.is_object() && check.value("name", std::string{}) == checkName) { + return check.value("passed", false); + } + } + return false; + }; + auto availabilityReported = [&](const char* availableField, const char* reasonField) { + return hasBool(nvidiaIntegrations, availableField) && + (boolValue(nvidiaIntegrations, availableField) || hasNonEmptyString(nvidiaIntegrations, reasonField)); + }; + + const nlohmann::json backendPolicy = objectValue(nvidiaIntegrations, "backend_comparison_policy"); + const nlohmann::json activeMode = objectValue(backendPolicy, "active_mode"); + const nlohmann::json safeDefault = objectValue(backendPolicy, "current_safe_default"); + const nlohmann::json nrdGuideContract = objectValue(nvidiaIntegrations, "nrd_guide_contract"); + const nlohmann::json dlssGuideContract = objectValue(nvidiaIntegrations, "dlss_guide_contract"); + const nlohmann::json dlssExposureContract = objectValue(nvidiaIntegrations, "dlss_exposure_contract"); + const nlohmann::json dlssRrGuideContract = objectValue(nvidiaIntegrations, "dlss_ray_reconstruction_guide_contract"); + const nlohmann::json dlssRrValidator = objectValue(dlssRrGuideContract, "guide_consistency_validator"); + + const std::string requestedDenoiser = stringValue(nvidiaIntegrations, "requested_denoiser_backend"); + const std::string effectiveDenoiser = stringValue(nvidiaIntegrations, "effective_denoiser_backend"); + const std::string requestedUpscaler = stringValue(nvidiaIntegrations, "requested_temporal_upscaler"); + const std::string effectiveUpscaler = stringValue(nvidiaIntegrations, "effective_temporal_upscaler"); + const bool requestedDlssRr = boolValue(nvidiaIntegrations, "requested_dlss_ray_reconstruction"); + const bool effectiveDlssRr = boolValue(nvidiaIntegrations, "effective_dlss_ray_reconstruction"); + const bool engineDenoiserRunExpected = engineDenoiserRequested && !effectiveDlssRr; + + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + auto addCheck = [&](const char* name, + bool passed, + const char* message, + nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (!passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + + addCheck( + "nvidia_backend_diagnostics_present", + nvidiaIntegrations.is_object() && + hasString(nvidiaIntegrations, "requested_denoiser_backend") && + hasString(nvidiaIntegrations, "effective_denoiser_backend") && + hasString(nvidiaIntegrations, "requested_temporal_upscaler") && + hasString(nvidiaIntegrations, "effective_temporal_upscaler") && + hasBool(nvidiaIntegrations, "requested_dlss_ray_reconstruction") && + hasBool(nvidiaIntegrations, "effective_dlss_ray_reconstruction"), + "Profile must expose requested and effective denoiser/upscaler backends.", + { + {"requested_denoiser_backend", requestedDenoiser}, + {"effective_denoiser_backend", effectiveDenoiser}, + {"requested_temporal_upscaler", requestedUpscaler}, + {"effective_temporal_upscaler", effectiveUpscaler}, + {"requested_dlss_ray_reconstruction", requestedDlssRr}, + {"effective_dlss_ray_reconstruction", effectiveDlssRr}, + }); + addCheck( + "backend_comparison_policy_present", + backendPolicy.is_object() && + backendPolicy.value("schema_version", 0) == 1 && + activeMode.is_object() && + safeDefault.is_object() && + hasArray(backendPolicy, "fallback_order") && + !backendPolicy["fallback_order"].empty() && + hasNonEmptyString(backendPolicy, "failure_policy") && + hasNonEmptyString(backendPolicy, "promotion_rule"), + "Profile must expose the Q5D backend comparison policy, fallback order, and failure/promotion rules.", + backendPolicy); + addCheck( + "active_mode_matches_profile_fields", + activeMode.is_object() && + stringValue(activeMode, "requested_denoiser_backend") == requestedDenoiser && + stringValue(activeMode, "effective_denoiser_backend") == effectiveDenoiser && + stringValue(activeMode, "requested_temporal_upscaler") == requestedUpscaler && + stringValue(activeMode, "effective_temporal_upscaler") == effectiveUpscaler && + boolValue(activeMode, "requested_dlss_ray_reconstruction") == requestedDlssRr && + boolValue(activeMode, "effective_dlss_ray_reconstruction") == effectiveDlssRr, + "Backend policy active mode must mirror the top-level backend request/effective fields.", + activeMode); + addCheck( + "requested_backends_match_settings", + (!settings.denoiserEnabled || requestedDenoiser == denoiserBackendName(settings.denoiserBackend)) && + requestedUpscaler == temporalUpscalerName(settings.temporalUpscaler) && + requestedDlssRr == settings.dlssRayReconstructionEnabled, + "Requested denoiser/upscaler backend diagnostics must match RendererSettings.", + { + {"settings_denoiser_enabled", settings.denoiserEnabled}, + {"settings_denoiser_backend", denoiserBackendName(settings.denoiserBackend)}, + {"settings_temporal_upscaler", temporalUpscalerName(settings.temporalUpscaler)}, + {"settings_dlss_ray_reconstruction_enabled", settings.dlssRayReconstructionEnabled}, + {"requested_denoiser_backend", requestedDenoiser}, + {"requested_temporal_upscaler", requestedUpscaler}, + {"requested_dlss_ray_reconstruction", requestedDlssRr}, + }); + addCheck( + "safe_default_effective_when_requested", + !safeDefaultRequested || + (effectiveDenoiser == "engine" && + effectiveUpscaler == "taa-tsr" && + !effectiveDlssRr && + stringValue(safeDefault, "denoiser_backend") == "engine" && + stringValue(safeDefault, "temporal_upscaler") == "taa-tsr" && + !boolValue(safeDefault, "dlss_ray_reconstruction")), + "Engine denoiser plus TAA/TSR must remain the safe default when requested.", + { + {"safe_default_requested", safeDefaultRequested}, + {"current_safe_default", safeDefault}, + {"effective_denoiser_backend", effectiveDenoiser}, + {"effective_temporal_upscaler", effectiveUpscaler}, + {"effective_dlss_ray_reconstruction", effectiveDlssRr}, + }); + addCheck( + "nrd_backends_mutually_exclusive", + hasBool(nvidiaIntegrations, "nrd_backends_mutually_exclusive") && + boolValue(nvidiaIntegrations, "nrd_backends_mutually_exclusive"), + "NRD and engine denoiser backend ownership must be mutually exclusive.", + {{"nrd_backends_mutually_exclusive", boolValue(nvidiaIntegrations, "nrd_backends_mutually_exclusive")}}); + addCheck( + "engine_denoiser_temporal_and_pass_ready", + !engineDenoiserRunExpected || + (temporalFeatureReady("denoiser") && + activePassCheckPassed("engine_denoiser_pass_active") && + activePassCheckPassed("moment_update_pass_active")), + "The engine denoiser path must have temporal history and active-pass evidence when it owns reconstruction.", + { + {"engine_denoiser_requested", engineDenoiserRequested}, + {"engine_denoiser_run_expected", engineDenoiserRunExpected}, + {"effective_dlss_ray_reconstruction", effectiveDlssRr}, + {"denoiser_temporal_feature", temporalFeature("denoiser")}, + {"engine_denoiser_pass_active_check", activePassCheckPassed("engine_denoiser_pass_active")}, + {"moment_update_pass_active_check", activePassCheckPassed("moment_update_pass_active")}, + }); + addCheck( + "taa_tsr_temporal_and_pass_ready", + !taaTsrRequested || + (temporalFeatureReady("taa_tsr") && + activePassCheckPassed("taa_pass_active") && + activePassCheckPassed("taa_history_copy_pass_active")), + "The TAA/TSR path must have temporal history and active-pass evidence when requested.", + { + {"taa_tsr_requested", taaTsrRequested}, + {"taa_tsr_temporal_feature", temporalFeature("taa_tsr")}, + {"taa_pass_active_check", activePassCheckPassed("taa_pass_active")}, + {"taa_history_copy_pass_active_check", activePassCheckPassed("taa_history_copy_pass_active")}, + }); + addCheck( + "nrd_runtime_contract_reported", + !nrdRequested || + (availabilityReported("nrd_available", "nrd_unavailable_reason") && + nrdGuideContract.is_object() && + nrdGuideContract.value("motion_vectors", false) && + nrdGuideContract.value("normal_roughness", false) && + nrdGuideContract.value("view_z", false) && + nrdGuideContract.value("diffuse_radiance_hit_distance", false) && + nrdGuideContract.value("specular_radiance_hit_distance", false) && + (!boolValue(nvidiaIntegrations, "nrd_available") || !effectiveDenoiser.empty())), + "Requested NRD mode must report availability/fallback reason and all guide contracts.", + { + {"nrd_requested", nrdRequested}, + {"nrd_available", boolValue(nvidiaIntegrations, "nrd_available")}, + {"nrd_unavailable_reason", stringValue(nvidiaIntegrations, "nrd_unavailable_reason")}, + {"effective_denoiser_backend", effectiveDenoiser}, + {"nrd_guide_contract", nrdGuideContract}, + }); + addCheck( + "nrd_temporal_ready_when_effective", + effectiveDenoiser != "nrd" || temporalFeatureReady("nrd"), + "The NRD temporal feature must be ready when NRD is the effective denoiser backend.", + { + {"effective_denoiser_backend", effectiveDenoiser}, + {"nrd_temporal_feature", temporalFeature("nrd")}, + }); + addCheck( + "dlss_runtime_contract_reported", + !dlssRequested || + (availabilityReported("dlss_available", "dlss_unavailable_reason") && + dlssGuideContract.is_object() && + hasNonEmptyString(dlssGuideContract, "depth_convention") && + hasNonEmptyString(dlssGuideContract, "motion_convention") && + hasNonEmptyString(dlssGuideContract, "jitter_convention") && + taggedResourceRolePresent(dlssGuideContract, "scaling-input-color") && + taggedResourceRolePresent(dlssGuideContract, "scaling-output-color") && + taggedResourceRolePresent(dlssGuideContract, "depth") && + taggedResourceRolePresent(dlssGuideContract, "motion-vectors")), + "Requested DLSS upscaling must report availability/fallback reason, guide conventions, and tagged resources.", + { + {"dlss_requested", dlssRequested}, + {"dlss_available", boolValue(nvidiaIntegrations, "dlss_available")}, + {"dlss_unavailable_reason", stringValue(nvidiaIntegrations, "dlss_unavailable_reason")}, + {"effective_temporal_upscaler", effectiveUpscaler}, + {"dlss_guide_contract", dlssGuideContract}, + }); + addCheck( + "dlss_exposure_contract_reported", + dlssExposureContract.is_object() && + dlssExposureContract.value("schema_version", 0) == 1 && + hasNumber(dlssExposureContract, "manual_exposure") && + hasNumber(dlssExposureContract, "pre_exposure") && + hasNumber(dlssExposureContract, "exposure_scale") && + hasBool(dlssExposureContract, "auto_exposure_enabled") && + hasBool(dlssExposureContract, "exposure_buffer_available") && + hasBool(dlssExposureContract, "exposure_buffer_passed_to_sdk") && + hasNonEmptyString(dlssExposureContract, "policy"), + "DLSS diagnostics must report manual exposure, pre-exposure, exposure scale, auto-exposure state, and SDK exposure-buffer policy.", + dlssExposureContract); + addCheck( + "dlss_guide_ready_when_effective", + effectiveUpscaler != "dlss" || dlssGuideContract.value("guide_pass_ready", false), + "DLSS guide pass must be ready when DLSS is the effective temporal upscaler.", + { + {"effective_temporal_upscaler", effectiveUpscaler}, + {"guide_pass_ready", dlssGuideContract.value("guide_pass_ready", false)}, + }); + addCheck( + "dlss_ray_reconstruction_runtime_contract_reported", + !dlssRayReconstructionRequested || + (availabilityReported("dlss_ray_reconstruction_available", "dlss_ray_reconstruction_unavailable_reason") && + dlssRrGuideContract.is_object() && + hasArray(dlssRrGuideContract, "guide_images") && + !dlssRrGuideContract["guide_images"].empty() && + dlssRrGuideContract.value("guide_images_allocated", false) && + dlssRrGuideContract.value("psr_guide_buffer_allocated", false) && + dlssRrGuideContract.value("psr_history_signatures_allocated", false) && + dlssRrValidator.value("previous_world_position", false) && + dlssRrValidator.value("material_id", false) && + dlssRrValidator.value("instance_id", false) && + dlssRrValidator.value("psr_history_signature", false) && + arrayContainsString(dlssRrGuideContract.value("guide_images", nlohmann::json::array()), "depth") && + arrayContainsString(dlssRrGuideContract.value("guide_images", nlohmann::json::array()), "motion") && + arrayContainsString(dlssRrGuideContract.value("guide_images", nlohmann::json::array()), "reflected_albedo") && + arrayContainsString(dlssRrGuideContract.value("guide_images", nlohmann::json::array()), "disocclusion_mask")), + "Requested DLSS Ray Reconstruction must report availability/fallback reason, allocated guide images, and consistency validation.", + { + {"dlss_ray_reconstruction_requested", dlssRayReconstructionRequested}, + {"dlss_ray_reconstruction_available", boolValue(nvidiaIntegrations, "dlss_ray_reconstruction_available")}, + {"dlss_ray_reconstruction_unavailable_reason", stringValue(nvidiaIntegrations, "dlss_ray_reconstruction_unavailable_reason")}, + {"effective_dlss_ray_reconstruction", effectiveDlssRr}, + {"dlss_ray_reconstruction_guide_contract", dlssRrGuideContract}, + }); + addCheck( + "dlss_ray_reconstruction_guides_ready_when_effective", + !effectiveDlssRr || dlssRrGuideContract.value("guide_pass_ready", false), + "DLSS Ray Reconstruction guides must be ready when DLSS RR is effective.", + { + {"effective_dlss_ray_reconstruction", effectiveDlssRr}, + {"guide_pass_ready", dlssRrGuideContract.value("guide_pass_ready", false)}, + }); + addCheck( + "dormant_optional_sdk_fallback_reasons_reported", + (nrdRequested || boolValue(nvidiaIntegrations, "nrd_available") || hasNonEmptyString(nvidiaIntegrations, "nrd_unavailable_reason")) && + (dlssRequested || boolValue(nvidiaIntegrations, "dlss_available") || hasNonEmptyString(nvidiaIntegrations, "dlss_unavailable_reason")) && + (dlssRayReconstructionRequested || boolValue(nvidiaIntegrations, "dlss_ray_reconstruction_available") || + hasNonEmptyString(nvidiaIntegrations, "dlss_ray_reconstruction_unavailable_reason")), + "Dormant optional NVIDIA SDK paths must still report availability or fallback reasons.", + { + {"nrd_requested", nrdRequested}, + {"nrd_available", boolValue(nvidiaIntegrations, "nrd_available")}, + {"nrd_unavailable_reason", stringValue(nvidiaIntegrations, "nrd_unavailable_reason")}, + {"dlss_requested", dlssRequested}, + {"dlss_available", boolValue(nvidiaIntegrations, "dlss_available")}, + {"dlss_unavailable_reason", stringValue(nvidiaIntegrations, "dlss_unavailable_reason")}, + {"dlss_ray_reconstruction_requested", dlssRayReconstructionRequested}, + {"dlss_ray_reconstruction_available", boolValue(nvidiaIntegrations, "dlss_ray_reconstruction_available")}, + {"dlss_ray_reconstruction_unavailable_reason", stringValue(nvidiaIntegrations, "dlss_ray_reconstruction_unavailable_reason")}, + }); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "denoiser_upscaler_backend_policy"}, + {"feature_state", { + {"engine_denoiser_requested", engineDenoiserRequested}, + {"engine_denoiser_run_expected", engineDenoiserRunExpected}, + {"nrd_requested", nrdRequested}, + {"taa_tsr_requested", taaTsrRequested}, + {"dlss_requested", dlssRequested}, + {"dlss_ray_reconstruction_requested", dlssRayReconstructionRequested}, + {"safe_default_requested", safeDefaultRequested}, + }}, + {"effective_state", { + {"denoiser_backend", effectiveDenoiser}, + {"temporal_upscaler", effectiveUpscaler}, + {"dlss_ray_reconstruction", effectiveDlssRr}, + }}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererPerformanceBudgetValidationJson( + const RendererSettings& settings, + uint32_t profiledFrames, + const nlohmann::json& resolution, + const nlohmann::json& gpuFrameMs, + const nlohmann::json& perPassGpuMs) { + auto objectField = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_object() + ? object[field] + : nlohmann::json::object(); + }; + auto numberField = [](const nlohmann::json& object, const char* field, double fallback = 0.0) { + return object.is_object() && object.contains(field) && object[field].is_number() + ? object[field].get() + : fallback; + }; + auto hasNumberField = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_number(); + }; + + const nlohmann::json renderExtent = objectField(resolution, "render_extent"); + const double renderWidth = std::max(1.0, numberField(renderExtent, "width", 1280.0)); + const double renderHeight = std::max(1.0, numberField(renderExtent, "height", 720.0)); + const double renderScale = std::max(0.01, numberField(resolution, "render_scale", settings.renderResolutionScale)); + const double pixelScale = std::max(0.25, (renderWidth * renderHeight) / (1280.0 * 720.0)); + const double sppScale = static_cast(std::max(1u, settings.samplesPerPixel)); + + const bool gpuFrameStatsPresent = + gpuFrameMs.is_object() && + hasNumberField(gpuFrameMs, "avg") && + hasNumberField(gpuFrameMs, "p95") && + hasNumberField(gpuFrameMs, "p99"); + bool anyNonZeroPassTiming = false; + if (perPassGpuMs.is_object()) { + for (const auto& item : perPassGpuMs.items()) { + if (item.value().is_number() && item.value().get() > 0.0) { + anyNonZeroPassTiming = true; + break; + } + } + } + const bool frameTimingAvailable = gpuFrameStatsPresent && numberField(gpuFrameMs, "avg") > 0.0; + const bool timingEvaluationAvailable = frameTimingAvailable || anyNonZeroPassTiming; + const bool strictTimingExpected = profiledFrames >= 30u; + + auto sectionBudgetBaseMs = [](const std::string& section) { + if (section.find("path_trace") == 0) { return 18.0; } + if (section.find("restir_di_temporal") == 0) { return 3.5; } + if (section.find("restir_di_spatial") == 0) { return 4.5; } + if (section.find("restir_di_final") == 0) { return 3.5; } + if (section.find("restir_di_history_copy") == 0) { return 1.0; } + if (section.find("restir_gi_temporal") == 0) { return 5.5; } + if (section.find("restir_gi_spatial") == 0) { return 6.5; } + if (section.find("restir_gi_final") == 0) { return 5.0; } + if (section.find("restir_gi_upsample") == 0) { return 2.0; } + if (section.find("restir_gi_clear") == 0) { return 1.0; } + if (section.find("regir_build") == 0) { return 4.0; } + if (section.find("regir_temporal_reuse") == 0) { return 2.0; } + if (section.find("regir_spatial_reuse") == 0) { return 2.5; } + if (section.find("denoiser") == 0) { return 5.0; } + if (section.find("moment_update") == 0) { return 2.0; } + if (section.find("taa") == 0) { return 2.5; } + if (section.find("dlss_rr_guides") == 0) { return 2.5; } + if (section.find("dlss_rr") == 0) { return 4.0; } + if (section.find("dlss_guides") == 0) { return 1.5; } + if (section.find("dlss") == 0) { return 2.5; } + if (section.find("history_copy") == 0) { return 1.0; } + if (section.find("atmosphere") == 0) { return 2.0; } + if (section.find("fog") == 0) { return 1.5; } + if (section.find("tone_map") == 0) { return 1.0; } + if (section.find("fullscreen") == 0) { return 1.0; } + if (section.find("adaptive_sampling") == 0) { return 2.0; } + if (section.find("wavefront") == 0) { return 4.0; } + return 2.0; + }; + + nlohmann::json passBudgets = nlohmann::json::array(); + nlohmann::json budgetViolations = nlohmann::json::array(); + nlohmann::json missingTimingSections = nlohmann::json::array(); + uint32_t budgetedSectionCount = 0; + uint32_t activeBudgetedSectionCount = 0; + uint32_t measuredSectionCount = 0; + uint32_t activeMeasuredSectionCount = 0; + + const std::vector contracts = rendererPassContracts(settings); + for (const RendererPassContract& contract : contracts) { + if (!contract.profilingSections.is_array()) { + continue; + } + for (const nlohmann::json& sectionValue : contract.profilingSections) { + if (!sectionValue.is_string()) { + continue; + } + const std::string section = sectionValue.get(); + if (rendererPassProfilingSectionIsAggregate(section)) { + continue; + } + ++budgetedSectionCount; + if (contract.activeByCurrentSettings) { + ++activeBudgetedSectionCount; + } + const double budgetMs = sectionBudgetBaseMs(section) * pixelScale * sppScale; + const bool timingPresent = + perPassGpuMs.is_object() && + perPassGpuMs.contains(section) && + perPassGpuMs[section].is_number(); + const double observedMs = timingPresent ? perPassGpuMs[section].get() : 0.0; + if (timingPresent) { + ++measuredSectionCount; + if (contract.activeByCurrentSettings) { + ++activeMeasuredSectionCount; + } + } else if (contract.activeByCurrentSettings) { + missingTimingSections.push_back({ + {"pass", contract.name}, + {"section", section}, + }); + } + const bool overBudget = timingEvaluationAvailable && timingPresent && observedMs > budgetMs; + if (overBudget) { + budgetViolations.push_back({ + {"scope", "pass"}, + {"pass", contract.name}, + {"section", section}, + {"observed_ms", observedMs}, + {"budget_ms", budgetMs}, + {"over_by_ms", observedMs - budgetMs}, + }); + } + passBudgets.push_back({ + {"pass", contract.name}, + {"pass_id", rendererPassContractIdName(contract.id)}, + {"role", rendererPassContractRoleName(contract.role)}, + {"active_by_current_settings", contract.activeByCurrentSettings}, + {"section", section}, + {"budget_ms", budgetMs}, + {"observed_ms", timingPresent ? nlohmann::json(observedMs) : nlohmann::json(nullptr)}, + {"timing_present", timingPresent}, + {"over_budget", overBudget}, + }); + } + } + + const double fullFrameBudgetMs = 33.333 * pixelScale * sppScale; + const double modeFrameBudgetMs = fullFrameBudgetMs * + (passes::DlssPass::isUpscaleActive(settings) ? 1.15 : 1.0) * + (passes::NrdPass::isActive(settings) ? 1.10 : 1.0) * + (passes::DlssPass::isRayReconstructionActive(settings) ? 1.20 : 1.0); + const double observedFrameAvgMs = numberField(gpuFrameMs, "avg"); + const double observedFrameP95Ms = numberField(gpuFrameMs, "p95"); + if (timingEvaluationAvailable && frameTimingAvailable && observedFrameAvgMs > modeFrameBudgetMs) { + budgetViolations.push_back({ + {"scope", "full_frame_avg"}, + {"observed_ms", observedFrameAvgMs}, + {"budget_ms", modeFrameBudgetMs}, + {"over_by_ms", observedFrameAvgMs - modeFrameBudgetMs}, + }); + } + if (timingEvaluationAvailable && frameTimingAvailable && observedFrameP95Ms > modeFrameBudgetMs * 1.35) { + budgetViolations.push_back({ + {"scope", "full_frame_p95"}, + {"observed_ms", observedFrameP95Ms}, + {"budget_ms", modeFrameBudgetMs * 1.35}, + {"over_by_ms", observedFrameP95Ms - modeFrameBudgetMs * 1.35}, + }); + } + + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + if (!gpuFrameStatsPresent) { + failures.push_back({ + {"code", "gpu_frame_stats_missing"}, + {"message", "Performance budget validation requires gpu_frame_ms avg/p95/p99 fields."}, + }); + } + if (!perPassGpuMs.is_object()) { + failures.push_back({ + {"code", "per_pass_gpu_ms_missing"}, + {"message", "Performance budget validation requires per_pass_gpu_ms timing fields."}, + }); + } + if (!missingTimingSections.empty()) { + failures.push_back({ + {"code", "active_budgeted_sections_missing_timing"}, + {"missing_count", missingTimingSections.size()}, + }); + } + if (strictTimingExpected && !timingEvaluationAvailable) { + failures.push_back({ + {"code", "strict_profile_has_no_nonzero_gpu_timings"}, + {"message", "Profiles with at least 30 measured frames must have non-zero GPU timing data for budget enforcement."}, + }); + } else if (!timingEvaluationAvailable) { + warnings.push_back({ + {"code", "budget_evaluation_deferred_no_nonzero_gpu_timings"}, + {"message", "GPU timings are present but zero; budget overrun checks are deferred for this short smoke profile."}, + }); + } + if (!budgetViolations.empty()) { + failures.push_back({ + {"code", "performance_budget_exceeded"}, + {"violation_count", budgetViolations.size()}, + }); + } + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "performance_budget"}, + {"resolution", { + {"render_width", renderWidth}, + {"render_height", renderHeight}, + {"render_scale", renderScale}, + {"pixel_scale_720p", pixelScale}, + }}, + {"sample_scale", sppScale}, + {"profiled_frames", profiledFrames}, + {"strict_timing_expected", strictTimingExpected}, + {"timing_evaluation_available", timingEvaluationAvailable}, + {"full_frame_budget_ms", modeFrameBudgetMs}, + {"full_frame_p95_budget_ms", modeFrameBudgetMs * 1.35}, + {"observed_frame_avg_ms", observedFrameAvgMs}, + {"observed_frame_p95_ms", observedFrameP95Ms}, + {"budgeted_section_count", budgetedSectionCount}, + {"active_budgeted_section_count", activeBudgetedSectionCount}, + {"measured_section_count", measuredSectionCount}, + {"active_measured_section_count", activeMeasuredSectionCount}, + {"missing_timing_sections", std::move(missingTimingSections)}, + {"pass_budgets", std::move(passBudgets)}, + {"budget_violations", std::move(budgetViolations)}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererRegirPromotionGateJson( + const RendererSettings& settings, + const passes::RegirPass::PromotionDiagnostics& diagnostics) { + const bool regirRequested = passes::RegirPass::isRequested(settings); + const bool hashGrid = passes::RegirPass::hashGridActive(settings, regirRequested); + const bool activeGrid = passes::RegirPass::activeGridMode(settings, regirRequested); + const bool sparseGrid = hashGrid || activeGrid; + const bool hashSaturated = passes::RegirPass::hashGridSaturated(hashGrid, diagnostics.hashSaturationCount); + const bool hashReuseFallback = passes::RegirPass::hashReuseFallback(settings, hashGrid); + const bool unsupportedAdvanced = passes::RegirPass::unsupportedAdvancedRequested( + settings, + hashGrid, + diagnostics.hashSaturationCount); + const uint32_t queryPeriod = passes::RegirPass::effectiveFiniteQueryFramePeriod(settings, hashGrid); + const uint64_t totalCells = diagnostics.totalCellCount > 0ull + ? diagnostics.totalCellCount + : passes::RegirPass::gridCellCount(settings); + const uint64_t denseBytes = passes::RegirPass::denseReservoirBytes( + settings, + diagnostics.denseReservoirBytes, + 64ull); + const uint64_t activeCells = passes::RegirPass::effectiveActiveCellCount( + sparseGrid, + diagnostics.feedbackAvailable, + diagnostics.activeCellCount, + totalCells, + regirRequested); + const uint64_t effectiveBytes = passes::RegirPass::effectiveReservoirBytes( + sparseGrid, + diagnostics.feedbackAvailable, + diagnostics.effectiveReservoirBytes, + denseBytes, + regirRequested); + const bool quickPlumbingPassed = passes::RegirPass::quickPromotionPlumbingPassed(settings, diagnostics); + + const std::filesystem::path fullPromotionArtifactPath = + std::filesystem::path("out") / "regir_promotion_gate" / "summary.json"; + nlohmann::json fullPromotionArtifact = nlohmann::json::object(); + std::string fullPromotionArtifactError; + const bool fullPromotionArtifactLoaded = + rendererReadJsonArtifact(fullPromotionArtifactPath, fullPromotionArtifact, fullPromotionArtifactError); + const bool fullPromotionArtifactOk = + fullPromotionArtifactLoaded && + fullPromotionArtifact.value("ok", false) && + !fullPromotionArtifact.value("quick", true); + const bool fullPromotionArtifactEligible = + fullPromotionArtifactOk && + fullPromotionArtifact.value("promotion_eligible", false); + auto artifactStepPassed = [&](const char* stepName) { + if (!fullPromotionArtifactLoaded || + !fullPromotionArtifact.contains("steps") || + !fullPromotionArtifact["steps"].is_array()) { + return false; + } + for (const nlohmann::json& step : fullPromotionArtifact["steps"]) { + if (step.is_object() && + step.value("name", std::string{}) == stepName && + step.value("ok", false) && + step.value("exit_code", -1) == 0) { + return true; + } + } + return false; + }; + + const bool finiteLightReferenceMatrixPassed = + diagnostics.finiteLightReferenceMatrixPassed || artifactStepPassed("finite_bias"); + const bool environmentMatrixPassed = + diagnostics.environmentMatrixPassed || artifactStepPassed("environment"); + const bool visibilityReuseValidationPassed = + diagnostics.visibilityReuseValidationPassed || artifactStepPassed("visibility"); + const bool equalTimeQualityPassed = + diagnostics.equalTimeQualityPassed || artifactStepPassed("equal_time"); + const bool manyLightReferencePassed = + diagnostics.manyLightReferencePassed || fullPromotionArtifactEligible; + const bool runtimeFullPromotionEligible = passes::RegirPass::fullPromotionEligible(settings, diagnostics); + const bool fullPromotionEligible = + runtimeFullPromotionEligible || + (quickPlumbingPassed && + finiteLightReferenceMatrixPassed && + environmentMatrixPassed && + visibilityReuseValidationPassed && + equalTimeQualityPassed && + manyLightReferencePassed); + + nlohmann::json matrix = nlohmann::json::array(); + auto addMatrixRow = [&](const char* name, + const char* scope, + bool passed, + bool requiredForFullPromotion, + const char* command) { + matrix.push_back({ + {"name", name}, + {"scope", scope}, + {"passed", passed}, + {"required_for_full_promotion", requiredForFullPromotion}, + {"command", command}, + }); + }; + addMatrixRow( + "quick_plumbing_profile", + "current profile", + quickPlumbingPassed, + false, + ".\\scripts\\regir_promotion_gate.ps1 -Quick"); + addMatrixRow( + "finite_light_bias_matrix", + "finite many-light scenes", + finiteLightReferenceMatrixPassed, + true, + ".\\scripts\\regir_bias_validation.ps1"); + addMatrixRow( + "environment_matrix", + "uniform/bright-texel/rotated/procedural/dynamic environment scenes", + environmentMatrixPassed, + true, + ".\\scripts\\regir_environment_validation.ps1"); + addMatrixRow( + "visibility_reuse_convergence", + "visibility reuse convergence scenes", + visibilityReuseValidationPassed, + true, + ".\\scripts\\regir_visibility_validation.ps1"); + addMatrixRow( + "bistro_equal_time_quality", + "Bistro equal-time quality/performance", + equalTimeQualityPassed, + true, + ".\\scripts\\regir_equal_time_validation.ps1"); + addMatrixRow( + "many_light_reference_cases", + "scene-specific many-light references", + manyLightReferencePassed, + true, + ".\\scripts\\regir_promotion_gate.ps1"); + + nlohmann::json blockers = nlohmann::json::array(); + if (regirRequested && !quickPlumbingPassed) { + blockers.push_back("quick ReGIR plumbing profile has not passed"); + } + if (!finiteLightReferenceMatrixPassed) { + blockers.push_back("finite-light high-SPP bias validation matrix is missing"); + } + if (!environmentMatrixPassed) { + blockers.push_back("environment-source validation matrix is missing"); + } + if (!visibilityReuseValidationPassed) { + blockers.push_back("visibility-reuse convergence validation is missing"); + } + if (!equalTimeQualityPassed) { + blockers.push_back("Bistro equal-time quality/performance gate is missing"); + } + if (!manyLightReferencePassed) { + blockers.push_back("many-light reference scenes are missing"); + } + + nlohmann::json failures = nlohmann::json::array(); + if (fullPromotionEligible && !quickPlumbingPassed) { + failures.push_back({ + {"code", "regir_full_promotion_without_quick_gate"}, + {"message", "Full ReGIR promotion cannot be claimed before the quick plumbing gate passes."}, + }); + } + + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "regir_quality_promotion_gate"}, + {"requested", regirRequested}, + {"effective_grid_mode", regirRequested ? regirGridModeName(passes::RegirPass::effectiveGridMode(settings, true)) : "off"}, + {"finite_query_frame_period", regirRequested ? queryPeriod : 0u}, + {"finite_query_probability", passes::RegirPass::finiteQueryProbability(regirRequested, queryPeriod)}, + {"quick_plumbing_passed", quickPlumbingPassed}, + {"full_promotion_eligible", fullPromotionEligible}, + {"completion_claim_policy", "full_promotion_eligible may be true only after every required full-promotion matrix row passes"}, + {"full_promotion_artifact", { + {"path", fullPromotionArtifactPath.string()}, + {"loaded", fullPromotionArtifactLoaded}, + {"load_error", fullPromotionArtifactLoaded ? nlohmann::json(nullptr) : nlohmann::json(fullPromotionArtifactError)}, + {"ok", fullPromotionArtifactOk}, + {"promotion_eligible", fullPromotionArtifactEligible}, + {"summary", fullPromotionArtifactLoaded ? fullPromotionArtifact : nlohmann::json(nullptr)}, + }}, + {"advanced_fallback", { + {"used", regirRequested && unsupportedAdvanced}, + {"reason", hashSaturated + ? "hash_table_saturated_using_canonical_query_fallback" + : (hashReuseFallback + ? "hash_grid_spatial_temporal_reuse_not_implemented" + : "")}, + {"spatial_reuse_effective", passes::RegirPass::spatialReuseEffective(settings, regirRequested)}, + {"temporal_reuse_effective", passes::RegirPass::temporalReuseEffective(settings, regirRequested)}, + {"visibility_reuse_effective", regirRequested && settings.regirVisibilityReuse}, + }}, + {"grid_evidence", { + {"feedback_available", diagnostics.feedbackAvailable}, + {"total_cell_count", totalCells}, + {"active_cell_count", activeCells}, + {"hash_collision_count", diagnostics.hashCollisionCount}, + {"hash_saturation_count", diagnostics.hashSaturationCount}, + {"hash_cell_capacity", hashGrid ? diagnostics.hashCellCapacity : 0u}, + {"dense_memory_bytes", regirRequested ? denseBytes : 0ull}, + {"effective_memory_bytes", effectiveBytes}, + {"backing_memory_bytes", regirRequested ? diagnostics.backingBytes : 0ull}, + }}, + {"infinite_source_evidence", { + {"environment_effective", diagnostics.environmentEffective}, + {"sun_effective", diagnostics.sunEffective}, + {"environment_bank_size", diagnostics.environmentBankSize}, + {"sun_bank_size", diagnostics.sunBankSize}, + {"valid_environment_reservoirs", diagnostics.validEnvironmentReservoirs}, + {"valid_sun_reservoirs", diagnostics.validSunReservoirs}, + {"environment_bank_bytes", diagnostics.environmentBankBytes}, + }}, + {"temporal_history_valid", diagnostics.temporalHistoryValid}, + {"timing_evidence", { + {"profiled_frames", diagnostics.profiledFrames}, + {"gpu_frame_avg_ms", diagnostics.gpuFrameAvgMs}, + {"regir_total_gpu_ms", passes::RegirPass::totalGpuMs(diagnostics)}, + {"regir_build_ms", diagnostics.regirBuildMs}, + {"regir_spatial_reuse_ms", diagnostics.regirSpatialReuseMs}, + {"regir_temporal_reuse_ms", diagnostics.regirTemporalReuseMs}, + }}, + {"scene_matrix", std::move(matrix)}, + {"completion_blockers", std::move(blockers)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", failures.empty()}, + }; +} + +inline nlohmann::json rendererDiagnosticRuntimeValidationJson( + const RendererSettings& settings, + const nlohmann::json& diagnosticReadiness, + const nlohmann::json& nsightAnalysisPlan, + const nlohmann::json& rayTracingShaderMap, + const nlohmann::json& accelerationStructureDiagnostics, + const nlohmann::json& barrierSyncDiagnostics) { + auto hasString = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_string(); + }; + auto hasNumber = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_number(); + }; + auto hasBool = [](const nlohmann::json& object, const char* field) { + return object.is_object() && object.contains(field) && object[field].is_boolean(); + }; + auto arrayContainsString = [](const nlohmann::json& values, const char* expected) { + if (!values.is_array()) { + return false; + } + for (const nlohmann::json& value : values) { + if (value.is_string() && value.get() == expected) { + return true; + } + } + return false; + }; + auto shaderStagePresent = [&](const char* stageName) { + if (!rayTracingShaderMap.is_object() || + !rayTracingShaderMap.contains("shader_groups") || + !rayTracingShaderMap["shader_groups"].is_array()) { + return false; + } + for (const nlohmann::json& group : rayTracingShaderMap["shader_groups"]) { + if (group.is_object() && group.value("stage", std::string{}) == stageName) { + return hasString(group, "file") && hasString(group, "marker") && hasString(group, "role"); + } + } + return false; + }; + auto nsightStepPresent = [&](const char* scopeName) { + if (!nsightAnalysisPlan.is_object() || + !nsightAnalysisPlan.contains("steps") || + !nsightAnalysisPlan["steps"].is_array()) { + return false; + } + for (const nlohmann::json& step : nsightAnalysisPlan["steps"]) { + if (step.is_object() && + step.value("scope", std::string{}) == scopeName && + step.contains("commands") && + step["commands"].is_array() && + !step["commands"].empty() && + hasString(step, "why")) { + return true; + } + } + return false; + }; + + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json warnings = nlohmann::json::array(); + auto addCheck = [&](const char* name, + bool required, + bool passed, + const char* message, + nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"required", required}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (required && !passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } else if (!required && !passed) { + warnings.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + + uint32_t warningReadinessFailures = 0; + uint32_t errorReadinessFailures = 0; + if (diagnosticReadiness.is_object() && + diagnosticReadiness.contains("checks") && + diagnosticReadiness["checks"].is_array()) { + for (const nlohmann::json& check : diagnosticReadiness["checks"]) { + if (!check.is_object() || check.value("pass", false)) { + continue; + } + const std::string severity = check.value("severity", std::string{}); + if (severity == "warning" || severity == "info") { + ++warningReadinessFailures; + } else { + ++errorReadinessFailures; + } + } + } + + addCheck( + "diagnostic_readiness_schema_present", + true, + diagnosticReadiness.is_object() && + diagnosticReadiness.value("schema", std::string{}) == "RendererDiagnosticReadinessV1" && + diagnosticReadiness.contains("checks") && + diagnosticReadiness["checks"].is_array() && + !diagnosticReadiness["checks"].empty(), + "Profile must include RendererDiagnosticReadinessV1 checks.", + diagnosticReadiness); + addCheck( + "diagnostic_readiness_no_error_failures", + true, + errorReadinessFailures == 0u, + "Diagnostic readiness may include warnings, but error-severity readiness failures must fail the renderer contract gate.", + { + {"status", diagnosticReadiness.value("status", std::string{})}, + {"warning_readiness_failures", warningReadinessFailures}, + {"error_readiness_failures", errorReadinessFailures}, + }); + addCheck( + "nsight_analysis_plan_present", + true, + nsightAnalysisPlan.is_object() && + nsightAnalysisPlan.value("schema", std::string{}) == "NsightAnalysisPlanV1" && + nsightAnalysisPlan.contains("recommended_metric_sets") && + nsightAnalysisPlan["recommended_metric_sets"].is_array() && + !nsightAnalysisPlan["recommended_metric_sets"].empty() && + nsightAnalysisPlan.contains("recommended_capture_flags") && + nsightAnalysisPlan["recommended_capture_flags"].is_array() && + !nsightAnalysisPlan["recommended_capture_flags"].empty() && + nsightStepPresent("whole_frame") && + nsightStepPresent("dominant_marker"), + "Profile must include actionable Nsight capture flags, metric sets, and whole-frame/dominant-marker follow-up steps.", + nsightAnalysisPlan); + addCheck( + "ray_tracing_nsight_step_present", + settings.pathTracingEnabled, + !settings.pathTracingEnabled || nsightStepPresent("ray_tracing"), + "Path tracing profiles must include a ray-tracing-specific Nsight follow-up step.", + nsightAnalysisPlan); + addCheck( + "ray_tracing_shader_map_present", + settings.pathTracingEnabled, + !settings.pathTracingEnabled || + (rayTracingShaderMap.is_object() && + rayTracingShaderMap.value("schema", std::string{}) == "RayTracingShaderMapV1" && + shaderStagePresent("raygen") && + shaderStagePresent("miss") && + shaderStagePresent("closesthit") && + shaderStagePresent("anyhit") && + rayTracingShaderMap.contains("counter_mapping") && + rayTracingShaderMap["counter_mapping"].is_object()), + "Path tracing profiles must map shader groups and diagnostic counter names for Nsight/RenderDoc attribution.", + rayTracingShaderMap); + addCheck( + "acceleration_structure_diagnostics_present", + settings.pathTracingEnabled, + !settings.pathTracingEnabled || + (accelerationStructureDiagnostics.is_object() && + accelerationStructureDiagnostics.value("schema", std::string{}) == "AccelerationStructureDiagnosticsV1" && + hasNumber(accelerationStructureDiagnostics, "triangle_count") && + hasNumber(accelerationStructureDiagnostics, "blas_count") && + hasNumber(accelerationStructureDiagnostics, "as_bytes") && + hasBool(accelerationStructureDiagnostics, "hardware_backface_culling_enabled") && + accelerationStructureDiagnostics.contains("recommendations") && + accelerationStructureDiagnostics["recommendations"].is_array() && + !accelerationStructureDiagnostics["recommendations"].empty()), + "Path tracing profiles must expose AS size/topology/backface policy diagnostics and follow-up recommendations.", + accelerationStructureDiagnostics); + addCheck( + "barrier_sync_diagnostics_present", + true, + barrierSyncDiagnostics.is_object() && + barrierSyncDiagnostics.value("schema", std::string{}) == "BarrierSyncDiagnosticsV1" && + barrierSyncDiagnostics.contains("queue_lane_ms") && + barrierSyncDiagnostics["queue_lane_ms"].is_object() && + barrierSyncDiagnostics.contains("async_compute") && + barrierSyncDiagnostics["async_compute"].is_object() && + barrierSyncDiagnostics.contains("rendergraph_outputs") && + barrierSyncDiagnostics["rendergraph_outputs"].is_array() && + arrayContainsString(barrierSyncDiagnostics["rendergraph_outputs"], "--dump-rendergraph") && + barrierSyncDiagnostics.contains("nsight_followup") && + barrierSyncDiagnostics["nsight_followup"].is_array() && + !barrierSyncDiagnostics["nsight_followup"].empty(), + "Profiles must expose queue lanes, async compute state, RenderGraph dump commands, and Nsight Systems follow-up guidance.", + barrierSyncDiagnostics); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"warning_readiness_failure_count", warningReadinessFailures}, + {"error_readiness_failure_count", errorReadinessFailures}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"warning_count", warnings.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"warnings", std::move(warnings)}, + {"passed", passed}, + }; +} + +inline nlohmann::json rendererReviewChecklistJson() { + return nlohmann::json{ + {"schema_version", 1}, + {"checks", rendererContractArray({ + "Does the pass declare all inputs, outputs, history resources, descriptor bindings, push constants, shader dependencies, and RenderGraph reads/writes?", + "Does the pass obey the shared temporal reset and motion-vector contract?", + "Does it update validation modes, debug views, and profile counters?", + "Does it affect GPU timings or resource lifetime/barrier behavior?", + "Does it change supported feature combinations or fallback policy?", + "Does any reference image or threshold update include a reason?", + })}, + }; +} + +inline nlohmann::json rendererArchitectureDocumentationJson() { + return nlohmann::json{ + {"schema_version", 1}, + {"document", "renderer_architecture_quality_lock"}, + {"purpose", "Machine-readable renderer ownership rules emitted with every profile so review, CI, and debug packages do not depend on implicit PathTracerRenderer knowledge."}, + {"coordinator_rules", rendererContractArray({ + "PathTracerRenderer may coordinate frame execution, shared device objects, and legacy scheduling while extraction is in progress.", + "Feature behavior must be described by pass contracts before scheduling, descriptor, resource, or shader behavior is changed.", + "Coordinator-only state must be surfaced through runtime diagnostics until it is moved behind a pass-owned module.", + "Unsupported feature combinations must fail through the supported mode matrix or explicit runtime validation, not through silent fallback.", + })}, + {"pass_ownership_rules", rendererContractArray({ + "Each major renderer feature owns its inputs, outputs, history resources, descriptor layouts, push constants, shader dependencies, debug views, timings, and validation checks.", + "A pass may write another pass's resource only through a declared shared contract or RenderGraph ownership edge.", + "History resources must name their reset reasons and report validity through temporal diagnostics.", + "Debug views and profiler names are part of the pass API and must remain stable or include a migration note.", + })}, + {"global_data_contracts", { + {"temporal", rendererContractArray({ + "Frame index, accumulation index, previous-frame state, camera cut, history validity, jitter, and reset reason come from the shared temporal contract.", + "Motion vectors use one documented sign/unit convention before TAA, ReSTIR, NRD, DLSS, or debug views consume them.", + "Temporal features must reject history on camera cut, resolution or render-scale change, scene/material/light topology changes, and explicit accumulation resets.", + })}, + {"renderer_application_bridge", rendererContractArray({ + "Surface, material, light identity, environment sampling, visibility, alpha test, and backface behavior are described by the application bridge contract.", + "Lighting algorithms consume bridge facts instead of embedding scattered scene/material assumptions.", + "Bridge validation must cover normal length, roughness range, depth validity, motion bounds, material id validity, and light id validity.", + })}, + {"denoiser_upscaler_guides", rendererContractArray({ + "Depth, normal, roughness, albedo, motion, hit distance, radiance, confidence, exposure, reactive masks, and replacement-surface guides must declare owner, format, range, and coordinate space.", + "NRD, DLSS, DLSS Ray Reconstruction, internal denoiser, TAA, and disabled modes share one backend policy and clear fallback reason.", + })}, + {"rendergraph", rendererContractArray({ + "RenderGraph dumps must include pass/resource ownership, read/write intervals, lifetime validation, alias groups, physical backing diagnostics, and barrier/sync follow-up hints.", + "Resource dependency bugs should be visible from rendergraph.json before inspecting shader side effects.", + })}, + }}, + {"how_to_add_temporal_feature", rendererContractArray({ + "Add the feature's history slot to TemporalSystem or report why the history is stateless.", + "Declare required reset reasons in the pass contract.", + "Expose debug views for history validity, motion/disocclusion, reset reason, and feature-specific rejection.", + "Add runtime validation to renderer_contracts.temporal_runtime_validation or a stricter feature-specific section.", + })}, + {"how_to_add_debug_view", rendererContractArray({ + "Register the view with a stable RendererDebugView enum and rendererDebugViewName string.", + "Add owner-pass metadata, value range, meaning, and failure hint to debug_view_registry_validation.", + "Include the view in the owning pass contract and export filter if it is useful in headless diagnostics.", + "Add or update validation evidence so missing active debug outputs fail clearly.", + })}, + {"how_to_add_renderer_mode", rendererContractArray({ + "Add the mode to supported_mode_matrix with support status, required features, and unsupported combinations.", + "Update active_pass_runtime_validation so requested features match active pass/timing evidence.", + "Update denoiser/upscaler, ReSTIR, ReGIR, temporal, and performance budget validation when the mode touches them.", + "Add reference-image, performance, or diagnostic-package coverage before promoting the mode from experimental to supported.", + })}, + {"reference_image_policy", rendererContractArray({ + "Reference updates must include a reason, fixed scene wrapper, render settings, effective SPP, and whether fixed seed was used only for preview.", + "Threshold changes must include scene/mode-specific rationale and keep denoiser, TAA, auto exposure, and backface-culling policies explicit.", + "Sponza Heavy and Bistro Interior references keep hardware backface culling off unless a new reference note proves no outside-light leakage.", + })}, + {"review_artifacts", rendererContractArray({ + "profile.json renderer_contracts quality gate", + "rendergraph.json resource ownership and lifetime validation", + "present PNG or image comparison output", + "debug view exports for the affected pass", + "pass timing or performance budget diff", + })}, + {"passed", true}, + }; +} + +inline nlohmann::json rendererPlanPhaseQualityLockJson(const nlohmann::json& rendererContracts) { + auto sectionPresent = [&](const char* sectionName) { + return rendererContracts.is_object() && rendererContracts.contains(sectionName); + }; + auto sectionPassed = [&](const char* sectionName) { + return sectionPresent(sectionName) && + rendererContracts[sectionName].is_object() && + rendererContracts[sectionName].value("passed", false); + }; + auto sectionEvidence = [&](std::initializer_list sectionNames) { + nlohmann::json evidence = nlohmann::json::array(); + for (const char* sectionName : sectionNames) { + evidence.push_back({ + {"section", sectionName}, + {"present", sectionPresent(sectionName)}, + {"passed", sectionPassed(sectionName)}, + }); + } + return evidence; + }; + + nlohmann::json phases = nlohmann::json::array(); + nlohmann::json completionBlockers = nlohmann::json::array(); + uint32_t executableEvidencePhaseCount = 0; + uint32_t completionProvenPhaseCount = 0; + auto addPhase = [&](uint32_t number, + const char* name, + const char* status, + bool completionProven, + nlohmann::json evidenceSections, + const char* acceptanceGate, + const char* remainingWork) { + bool hasExecutableEvidence = false; + if (evidenceSections.is_array()) { + for (const nlohmann::json& evidence : evidenceSections) { + if (evidence.is_object() && evidence.value("present", false)) { + hasExecutableEvidence = true; + break; + } + } + } + if (hasExecutableEvidence) { + ++executableEvidencePhaseCount; + } + if (completionProven) { + ++completionProvenPhaseCount; + } else { + completionBlockers.push_back({ + {"phase", number}, + {"name", name}, + {"remaining_work", remainingWork}, + }); + } + phases.push_back({ + {"phase", number}, + {"name", name}, + {"status", status}, + {"has_executable_evidence", hasExecutableEvidence}, + {"completion_proven", completionProven}, + {"evidence_sections", std::move(evidenceSections)}, + {"acceptance_gate", acceptanceGate}, + {"remaining_work", remainingWork}, + }); + }; + + const bool passContractsComplete = sectionPassed("contract_validation"); + const bool passOwnerRegistryPassed = sectionPassed("pass_owner_registry_validation"); + const bool debugCoveragePassed = sectionPassed("debug_output_coverage"); + const bool debugRegistryPassed = sectionPassed("debug_view_registry_validation"); + const bool temporalRuntimePassed = sectionPassed("temporal_runtime_validation"); + const bool bridgeRuntimePassed = sectionPassed("application_bridge_runtime_validation"); + const bool activePassRuntimePassed = sectionPassed("active_pass_runtime_validation"); + const bool reservoirRuntimePassed = sectionPassed("restir_reservoir_runtime_validation"); + const bool restirReferenceMatrixPassed = sectionPassed("restir_reference_matrix_artifact"); + const bool denoiserUpscalerPassed = sectionPassed("denoiser_upscaler_runtime_validation"); + const bool diagnosticRuntimePassed = sectionPassed("diagnostic_runtime_validation"); + const bool performanceBudgetPassed = sectionPassed("performance_budget_validation"); + const bool regirPromotionGatePassed = sectionPassed("regir_promotion_gate"); + const bool regirFullPromotionEligible = + sectionPresent("regir_promotion_gate") && + rendererContracts["regir_promotion_gate"].is_object() && + rendererContracts["regir_promotion_gate"].value("full_promotion_eligible", false); + const bool currentModeSupported = sectionPassed("current_mode_support"); + const bool renderGraphArtifactValidated = + sectionPresent("rendergraph_artifact_validation") && + sectionPassed("rendergraph_artifact_validation") && + rendererContracts["rendergraph_artifact_validation"].is_object() && + rendererContracts["rendergraph_artifact_validation"].value("artifact_requested", false) && + rendererContracts["rendergraph_artifact_validation"].value("validated", false); + const bool architectureDocumentationPassed = sectionPassed("architecture_documentation"); + const uint32_t passContractModuleOwnedCount = + sectionPresent("pass_owner_registry_validation") && + rendererContracts["pass_owner_registry_validation"].is_object() + ? rendererContracts["pass_owner_registry_validation"].value("contract_module_owned_count", 0u) + : 0u; + const char* phase2Status = "partial"; + const bool phase2Complete = + passContractsComplete && + passOwnerRegistryPassed && + activePassRuntimePassed && + passContractModuleOwnedCount >= 12u; + if (phase2Complete) { + phase2Status = "all-pass-module-contract-ownership"; + } else if (passContractsComplete && passOwnerRegistryPassed && activePassRuntimePassed && passContractModuleOwnedCount >= 3u) { + phase2Status = "partial-pass-module-contract-ownership"; + } else if (passContractsComplete && passOwnerRegistryPassed && activePassRuntimePassed) { + phase2Status = "compile-time-pass-modules"; + } else if (passContractsComplete && activePassRuntimePassed) { + phase2Status = "staged-contract-only"; + } + + addPhase( + 0, + "Baseline Audit", + passContractsComplete && debugRegistryPassed ? "executable-baseline-metadata" : "partial", + passContractsComplete && debugRegistryPassed, + sectionEvidence({"pass_contracts", "contract_validation", "debug_view_registry_validation", "supported_mode_matrix"}), + "Profile must enumerate pass contracts, debug views, supported modes, and current settings.", + "Keep extending baseline evidence as real pass classes replace coordinator-owned contracts."); + addPhase( + 1, + "Validation Baseline And Quality Gates", + diagnosticRuntimePassed && currentModeSupported ? "executable-quality-gate" : "partial", + diagnosticRuntimePassed && currentModeSupported, + sectionEvidence({"diagnostic_runtime_validation", "current_mode_support", "performance_budget_validation"}), + "Validation, diagnostics, mode support, and performance budget gates must be machine-readable.", + "Broaden beyond Cornell smoke with reference-scene image/performance matrices."); + addPhase( + 2, + "Renderer Pass Ownership Split", + phase2Status, + phase2Complete, + sectionEvidence({"pass_contracts", "contract_validation", "pass_owner_registry_validation", "active_pass_runtime_validation"}), + "Every major pass declares ownership, owner-tag metadata, and runtime activation evidence.", + "Continue extracting scheduling and GPU-resource code as pass modules grow beyond contract/policy ownership."); + addPhase( + 3, + "Renderer Application Bridge", + bridgeRuntimePassed ? "executable-runtime-contract" : "partial", + bridgeRuntimePassed, + sectionEvidence({"application_bridge_contract", "application_bridge_runtime_validation"}), + "Surface, light, visibility, and guide bridge evidence must pass at runtime.", + "Add a dedicated bridge ABI validation pass for larger scene/material variation."); + addPhase( + 4, + "ReSTIR DI Hardening", + reservoirRuntimePassed && restirReferenceMatrixPassed ? "full-reference-matrix-contract" : + (reservoirRuntimePassed ? "partial-runtime-contract" : "partial"), + reservoirRuntimePassed && bridgeRuntimePassed && debugCoveragePassed && restirReferenceMatrixPassed, + sectionEvidence({"restir_reservoir_runtime_validation", "application_bridge_runtime_validation", "debug_output_coverage", "restir_reference_matrix_artifact"}), + "DI reservoir/debug/runtime contracts must be checked and free of invalid PDF/target/non-finite evidence.", + "Complete standalone DI pass extraction, deterministic remap tables, and DI reference comparison matrix."); + addPhase( + 5, + "ReSTIR GI And Path Tracing Cleanup", + reservoirRuntimePassed && temporalRuntimePassed && restirReferenceMatrixPassed ? "full-reference-matrix-contract" : + (reservoirRuntimePassed && temporalRuntimePassed ? "partial-runtime-contract" : "partial"), + reservoirRuntimePassed && temporalRuntimePassed && activePassRuntimePassed && restirReferenceMatrixPassed, + sectionEvidence({"restir_reservoir_runtime_validation", "temporal_runtime_validation", "active_pass_runtime_validation", "restir_reference_matrix_artifact"}), + "GI/path temporal and reservoir contracts must have runtime evidence.", + "Finish GI receiver/sample ownership split and reference comparison coverage for GI-only/path-tracing variants."); + addPhase( + 6, + "ReGIR Quality Pass", + debugCoveragePassed && currentModeSupported && regirPromotionGatePassed && regirFullPromotionEligible + ? "full-promotion-gate-contract" + : (debugCoveragePassed && currentModeSupported && regirPromotionGatePassed ? "promotion-gate-contract" : "partial"), + debugCoveragePassed && currentModeSupported && regirPromotionGatePassed && regirFullPromotionEligible, + sectionEvidence({"debug_output_coverage", "performance_budget_validation", "current_mode_support", "regir_promotion_gate"}), + "ReGIR debug outputs, supported mode evidence, and performance budget evidence must be present.", + "Run and import the full scene-specific ReGIR quality/performance matrices and many-light reference cases."); + addPhase( + 7, + "Temporal Contract Unification", + temporalRuntimePassed ? "executable-runtime-contract" : "partial", + temporalRuntimePassed, + sectionEvidence({"temporal_contract", "temporal_runtime_validation"}), + "Shared reset reasons, required history slots, and temporal feature readiness must pass.", + "Expand movement/camera-cut/material-light-change validation scenes."); + addPhase( + 8, + "NRD, DLSS, And Denoiser Contract Pass", + denoiserUpscalerPassed ? "executable-runtime-contract" : "partial", + denoiserUpscalerPassed, + sectionEvidence({"denoiser_upscaler_runtime_validation", "application_bridge_runtime_validation", "debug_view_registry_validation"}), + "Backend policy, SDK fallback reasons, guide contracts, and active temporal evidence must pass.", + "Run backend comparison matrices on systems with NRD/DLSS/DLSS RR enabled."); + addPhase( + 9, + "RenderGraph And Resource Lifetime Cleanup", + renderGraphArtifactValidated ? "executable-rendergraph-artifact" : + (diagnosticRuntimePassed ? "external-rendergraph-artifact-required" : "partial"), + renderGraphArtifactValidated, + sectionEvidence({"rendergraph_artifact_validation", "diagnostic_runtime_validation"}), + "Profile-requested rendergraph.json must validate resource ownership, lifetime, and aliasing.", + "Promote rendergraph.json resource_lifetime_validation into CI/package validators across scenes and broader mode/scene coverage."); + addPhase( + 10, + "Performance Optimization", + performanceBudgetPassed ? "executable-budget-contract" : "partial", + performanceBudgetPassed, + sectionEvidence({"performance_budget_validation", "diagnostic_runtime_validation"}), + "Per-pass and full-frame performance budgets must be emitted and enforceable on strict profiles.", + "Replace conservative placeholder budgets with measured scene/mode baselines and Nsight trace-backed thresholds."); + addPhase( + 11, + "Debuggability And Developer UX", + debugRegistryPassed && diagnosticRuntimePassed ? "executable-debug-contract" : "partial", + debugRegistryPassed && diagnosticRuntimePassed, + sectionEvidence({"debug_view_registry_validation", "debug_output_coverage", "diagnostic_runtime_validation"}), + "Debug views, diagnostic readiness, shader maps, and capture guidance must be machine-readable.", + "Keep improving UI overlays and one-command diagnostic bundles with rendered debug images."); + addPhase( + 12, + "Documentation And Long-Term Quality Lock", + architectureDocumentationPassed && sectionPresent("review_checklist") ? "executable-profile-embedded-documentation" : "partial", + architectureDocumentationPassed && sectionPresent("review_checklist") && passContractsComplete && currentModeSupported, + sectionEvidence({"architecture_documentation", "pass_contracts", "supported_mode_matrix", "review_checklist"}), + "Profile must include renderer ownership rules, pass contracts, supported modes, review checklist, and phase-completion evidence without overclaiming completion.", + "Keep external docs synchronized if docs/scripts are intentionally kept ignored or published elsewhere."); + + constexpr uint32_t kExpectedPhaseCount = 13u; + nlohmann::json failures = nlohmann::json::array(); + if (phases.size() != kExpectedPhaseCount) { + failures.push_back({ + {"code", "unexpected_phase_count"}, + {"expected", kExpectedPhaseCount}, + {"actual", phases.size()}, + }); + } + if (executableEvidencePhaseCount == 0u) { + failures.push_back({ + {"code", "no_executable_phase_evidence"}, + {"message", "Quality lock must point at at least one executable evidence section."}, + }); + } + + const bool fullPlanComplete = completionProvenPhaseCount == kExpectedPhaseCount; + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "rtxdi_quality_renderer_plan_phase_lock"}, + {"source_plan", "docs/RTXDI_QUALITY_RENDERER_IMPROVEMENT_PLAN.md"}, + {"phase_count", phases.size()}, + {"expected_phase_count", kExpectedPhaseCount}, + {"executable_evidence_phase_count", executableEvidencePhaseCount}, + {"completion_proven_phase_count", completionProvenPhaseCount}, + {"incomplete_phase_count", kExpectedPhaseCount - completionProvenPhaseCount}, + {"full_plan_complete", fullPlanComplete}, + {"completion_claim_policy", "Do not mark the full plan complete until every phase row has completion_proven=true and current evidence proves every acceptance gate."}, + {"phases", std::move(phases)}, + {"completion_blockers", std::move(completionBlockers)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", failures.empty()}, + }; +} + +inline nlohmann::json rendererQualityGateValidationJson( + const nlohmann::json& rendererContracts, + bool validationEnabled, + uint32_t validationErrorCount) { + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + auto addCheck = [&](const char* name, bool passed, const char* message, nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (!passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + auto contractPassed = [&](const char* sectionName) { + if (!rendererContracts.is_object() || + !rendererContracts.contains(sectionName) || + !rendererContracts[sectionName].is_object()) { + return false; + } + return rendererContracts[sectionName].value("passed", false); + }; + auto evidenceFor = [&](const char* sectionName) { + return rendererContracts.is_object() && rendererContracts.contains(sectionName) + ? rendererContracts[sectionName] + : nlohmann::json(nullptr); + }; + + addCheck( + "renderer_validation_log_clean", + validationEnabled && validationErrorCount == 0u, + validationEnabled + ? "Renderer validation log must not report errors." + : "Vulkan validation must be enabled before the renderer validation log can pass.", + { + {"validation_enabled", validationEnabled}, + {"validation_error_count", validationErrorCount}, + }); + addCheck( + "pass_contracts_complete", + contractPassed("contract_validation"), + "Every major pass contract must declare required ownership fields.", + evidenceFor("contract_validation")); + addCheck( + "pass_owner_registry_valid", + contractPassed("pass_owner_registry_validation"), + "Every major pass contract must map to compile-time owner metadata for staged pass extraction.", + evidenceFor("pass_owner_registry_validation")); + addCheck( + "active_pass_timing_covered", + contractPassed("profile_timing_coverage"), + "Active renderer pass contracts must map to profile timing sections.", + evidenceFor("profile_timing_coverage")); + addCheck( + "active_debug_outputs_covered", + contractPassed("debug_output_coverage"), + "Active renderer pass contracts must map to exportable debug outputs or explicit aggregate artifacts.", + evidenceFor("debug_output_coverage")); + addCheck( + "debug_view_registry_valid", + contractPassed("debug_view_registry_validation"), + "Exportable debug views must have stable names, owner-pass metadata, value ranges, meanings, and failure hints.", + evidenceFor("debug_view_registry_validation")); + addCheck( + "temporal_contract_runtime_valid", + contractPassed("temporal_runtime_validation"), + "Temporal history resources, reset reasons, and required slots must satisfy the shared runtime contract.", + evidenceFor("temporal_runtime_validation")); + addCheck( + "application_bridge_runtime_valid", + contractPassed("application_bridge_runtime_validation"), + "Surface, light, visibility, and guide bridge evidence must satisfy the runtime RAB contract.", + evidenceFor("application_bridge_runtime_validation")); + addCheck( + "denoiser_upscaler_runtime_valid", + contractPassed("denoiser_upscaler_runtime_validation"), + "Denoiser/upscaler backend policy, fallback reasons, guide contracts, and active temporal evidence must satisfy the runtime contract.", + evidenceFor("denoiser_upscaler_runtime_validation")); + addCheck( + "performance_budget_runtime_valid", + contractPassed("performance_budget_validation"), + "Active renderer modes must have per-pass and full-frame budgets, with strict profiles failing on over-budget timings.", + evidenceFor("performance_budget_validation")); + addCheck( + "regir_promotion_gate_valid", + contractPassed("regir_promotion_gate"), + "ReGIR promotion evidence must be machine-readable and must not claim full promotion before the required scene matrices pass.", + evidenceFor("regir_promotion_gate")); + addCheck( + "active_pass_runtime_valid", + contractPassed("active_pass_runtime_validation"), + "Requested renderer modes must match active pass and timing evidence, and disabled features must not leave hidden active passes.", + evidenceFor("active_pass_runtime_validation")); + addCheck( + "restir_reservoir_runtime_valid", + contractPassed("restir_reservoir_runtime_validation"), + "Active ReSTIR DI/GI reservoir contracts must be checked and free of PDF, target, parity, or non-finite violations.", + evidenceFor("restir_reservoir_runtime_validation")); + addCheck( + "rendergraph_artifact_valid_when_requested", + contractPassed("rendergraph_artifact_validation"), + "Requested RenderGraph artifacts must include passing resource ownership and lifetime validation.", + evidenceFor("rendergraph_artifact_validation")); + addCheck( + "diagnostic_runtime_valid", + contractPassed("diagnostic_runtime_validation"), + "Profile diagnostics must include readiness checks, Nsight guidance, shader maps, acceleration-structure attribution, and barrier/sync follow-up data.", + evidenceFor("diagnostic_runtime_validation")); + addCheck( + "current_mode_supported", + contractPassed("current_mode_support"), + "The requested renderer feature combination must be part of the supported mode matrix or fail clearly.", + evidenceFor("current_mode_support")); + addCheck( + "architecture_documentation_present", + contractPassed("architecture_documentation"), + "Profile must include renderer ownership rules, global contracts, mode-addition rules, debug-view rules, and reference-image policy.", + evidenceFor("architecture_documentation")); + addCheck( + "plan_phase_quality_lock_valid", + contractPassed("plan_phase_quality_lock"), + "The profile must include a phase-by-phase quality lock that reports executable evidence and refuses to overclaim full-plan completion.", + evidenceFor("plan_phase_quality_lock")); + + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"gate", "renderer_quality_contracts"}, + {"check_count", checks.size()}, + {"failure_count", failures.size()}, + {"checks", std::move(checks)}, + {"failures", std::move(failures)}, + {"passed", passed}, + }; +} + +} // namespace rtv diff --git a/include/rtv/RendererPassOwners.h b/include/rtv/RendererPassOwners.h new file mode 100644 index 0000000..2b0299f --- /dev/null +++ b/include/rtv/RendererPassOwners.h @@ -0,0 +1,172 @@ +#pragma once + +#include "rtv/passes/CompositePass.h" +#include "rtv/passes/DebugViewPass.h" +#include "rtv/passes/DenoiserPass.h" +#include "rtv/passes/DlssPass.h" +#include "rtv/passes/GBufferPass.h" +#include "rtv/passes/NrdPass.h" +#include "rtv/passes/PathTracePass.h" +#include "rtv/passes/ProfilerPass.h" +#include "rtv/passes/RegirPass.h" +#include "rtv/passes/RestirDIPass.h" +#include "rtv/passes/RestirGIPass.h" +#include "rtv/passes/TemporalAAPass.h" + +#include +#include +#include + +#include + +namespace rtv { + +namespace renderer_pass_owners { + +using GBufferPassOwner = passes::GBufferPass; +using PathTracePassOwner = passes::PathTracePass; +using RestirDIPassOwner = passes::RestirDIPass; +using RestirGIPassOwner = passes::RestirGIPass; +using RegirPassOwner = passes::RegirPass; +using TemporalAAPassOwner = passes::TemporalAAPass; +using DenoiserPassOwner = passes::DenoiserPass; +using NrdPassOwner = passes::NrdPass; +using DlssPassOwner = passes::DlssPass; +using CompositePassOwner = passes::CompositePass; +using DebugViewPassOwner = passes::DebugViewPass; +using ProfilerPassOwner = passes::ProfilerPass; + +} // namespace renderer_pass_owners + +struct RendererPassOwnerMetadata { + const char* contractId = ""; + const char* passName = ""; + const char* ownerSymbol = ""; + const char* metadataHeader = ""; + const char* plannedImplementationHeader = ""; + const char* extractionState = ""; +}; + +inline constexpr std::array rendererPassOwnerRegistry() { + return {{ + { + renderer_pass_owners::GBufferPassOwner::kContractId, + renderer_pass_owners::GBufferPassOwner::kPassName, + "rtv::passes::GBufferPass", + "include/rtv/passes/GBufferPass.h", + "include/rtv/passes/GBufferPass.h", + renderer_pass_owners::GBufferPassOwner::kExtractionState, + }, + { + renderer_pass_owners::PathTracePassOwner::kContractId, + renderer_pass_owners::PathTracePassOwner::kPassName, + "rtv::passes::PathTracePass", + "include/rtv/passes/PathTracePass.h", + "include/rtv/passes/PathTracePass.h", + renderer_pass_owners::PathTracePassOwner::kExtractionState, + }, + { + renderer_pass_owners::RestirDIPassOwner::kContractId, + renderer_pass_owners::RestirDIPassOwner::kPassName, + "rtv::passes::RestirDIPass", + "include/rtv/passes/RestirDIPass.h", + "include/rtv/passes/RestirDIPass.h", + renderer_pass_owners::RestirDIPassOwner::kExtractionState, + }, + { + renderer_pass_owners::RestirGIPassOwner::kContractId, + renderer_pass_owners::RestirGIPassOwner::kPassName, + "rtv::passes::RestirGIPass", + "include/rtv/passes/RestirGIPass.h", + "include/rtv/passes/RestirGIPass.h", + renderer_pass_owners::RestirGIPassOwner::kExtractionState, + }, + { + renderer_pass_owners::RegirPassOwner::kContractId, + renderer_pass_owners::RegirPassOwner::kPassName, + "rtv::passes::RegirPass", + "include/rtv/passes/RegirPass.h", + "include/rtv/passes/RegirPass.h", + renderer_pass_owners::RegirPassOwner::kExtractionState, + }, + { + renderer_pass_owners::TemporalAAPassOwner::kContractId, + renderer_pass_owners::TemporalAAPassOwner::kPassName, + "rtv::passes::TemporalAAPass", + "include/rtv/passes/TemporalAAPass.h", + "include/rtv/passes/TemporalAAPass.h", + renderer_pass_owners::TemporalAAPassOwner::kExtractionState, + }, + { + renderer_pass_owners::DenoiserPassOwner::kContractId, + renderer_pass_owners::DenoiserPassOwner::kPassName, + "rtv::passes::DenoiserPass", + "include/rtv/passes/DenoiserPass.h", + "include/rtv/passes/DenoiserPass.h", + renderer_pass_owners::DenoiserPassOwner::kExtractionState, + }, + { + renderer_pass_owners::NrdPassOwner::kContractId, + renderer_pass_owners::NrdPassOwner::kPassName, + "rtv::passes::NrdPass", + "include/rtv/passes/NrdPass.h", + "include/rtv/passes/NrdPass.h", + renderer_pass_owners::NrdPassOwner::kExtractionState, + }, + { + renderer_pass_owners::DlssPassOwner::kContractId, + renderer_pass_owners::DlssPassOwner::kPassName, + "rtv::passes::DlssPass", + "include/rtv/passes/DlssPass.h", + "include/rtv/passes/DlssPass.h", + renderer_pass_owners::DlssPassOwner::kExtractionState, + }, + { + renderer_pass_owners::CompositePassOwner::kContractId, + renderer_pass_owners::CompositePassOwner::kPassName, + "rtv::passes::CompositePass", + "include/rtv/passes/CompositePass.h", + "include/rtv/passes/CompositePass.h", + renderer_pass_owners::CompositePassOwner::kExtractionState, + }, + { + renderer_pass_owners::DebugViewPassOwner::kContractId, + renderer_pass_owners::DebugViewPassOwner::kPassName, + "rtv::passes::DebugViewPass", + "include/rtv/passes/DebugViewPass.h", + "include/rtv/passes/DebugViewPass.h", + renderer_pass_owners::DebugViewPassOwner::kExtractionState, + }, + { + renderer_pass_owners::ProfilerPassOwner::kContractId, + renderer_pass_owners::ProfilerPassOwner::kPassName, + "rtv::passes::ProfilerPass", + "include/rtv/passes/ProfilerPass.h", + "include/rtv/passes/ProfilerPass.h", + renderer_pass_owners::ProfilerPassOwner::kExtractionState, + }, + }}; +} + +inline const RendererPassOwnerMetadata* rendererPassOwnerMetadataForId(std::string_view contractId) { + static constexpr auto registry = rendererPassOwnerRegistry(); + for (const RendererPassOwnerMetadata& owner : registry) { + if (owner.contractId == contractId) { + return &owner; + } + } + return nullptr; +} + +inline nlohmann::json rendererPassOwnerMetadataJson(const RendererPassOwnerMetadata& owner) { + return nlohmann::json{ + {"contract_id", owner.contractId}, + {"pass_name", owner.passName}, + {"owner_symbol", owner.ownerSymbol}, + {"metadata_header", owner.metadataHeader}, + {"planned_implementation_header", owner.plannedImplementationHeader}, + {"extraction_state", owner.extractionState}, + }; +} + +} // namespace rtv diff --git a/include/rtv/RendererSettings.h b/include/rtv/RendererSettings.h index c61584e..78ccbeb 100644 --- a/include/rtv/RendererSettings.h +++ b/include/rtv/RendererSettings.h @@ -34,8 +34,8 @@ struct RendererSettings { bool streamlineNvPerfEnabled = false; float dlssSharpeningStrength = 0.0f; float taaFeedback = 0.06f; - float taaMotionFeedback = 0.90f; - float taaReactiveFeedback = 0.98f; + float taaMotionFeedback = 0.78f; + float taaReactiveFeedback = 0.55f; float taaSharpeningStrength = 0.05f; bool sunlightEnabled = true; bool directLightingEnabled = true; @@ -88,7 +88,7 @@ struct RendererSettings { RestirDiMode restirDiMode = RestirDiMode::Production; bool restirDiTemporalEnabled = true; bool restirDiSpatialEnabled = true; - bool restirDiFinalVisibilityEnabled = false; + bool restirDiFinalVisibilityEnabled = true; uint32_t restirDiSpatialRounds = 4; float restirDiSpatialRadius = 3.0f; uint32_t restirDiTemporalMaxAge = 32; @@ -146,25 +146,25 @@ struct RendererSettings { bool restirGiFinalStabilizationEnabled = true; float restirGiMinFinalBlendStrength = 0.01f; RestirGiActiveTileMaskMode restirGiActiveTileMaskMode = RestirGiActiveTileMaskMode::Off; - RestirHistoryCopyMode restirHistoryCopyMode = RestirHistoryCopyMode::Copy; + RestirHistoryCopyMode restirHistoryCopyMode = RestirHistoryCopyMode::PingPong; RestirCounterMode restirCounterMode = RestirCounterMode::Auto; - LightingReuseMode lightingReuseMode = LightingReuseMode::LegacyRestirDiGi; + LightingReuseMode lightingReuseMode = LightingReuseMode::LegacyRestirDiGiPlusReGIR; ReservoirLayout pathReservoirLayout = ReservoirLayout::LegacyDI; - glm::uvec3 regirGridDimensions{64u, 64u, 64u}; - uint32_t regirReservoirsPerCell = 16; - uint32_t regirCandidatesPerReservoir = 32; + glm::uvec3 regirGridDimensions{32u, 32u, 32u}; + uint32_t regirReservoirsPerCell = 8; + uint32_t regirCandidatesPerReservoir = 16; float regirGridPadding = 0.10f; - float regirCanonicalMix = 0.25f; + float regirCanonicalMix = 0.70f; RegirQueryMode regirQueryMode = RegirQueryMode::Stochastic; RegirGridMode regirGridMode = RegirGridMode::Dense; uint32_t regirFiniteQueryFramePeriod = 0; - bool regirSpatialReuse = false; + bool regirSpatialReuse = true; uint32_t regirSpatialRounds = 1; - bool regirTemporalReuse = false; - uint32_t regirTemporalHistory = 0; - uint32_t regirTemporalMaxM = 32; - bool regirVisibilityReuse = false; - bool regirEnvironment = false; + bool regirTemporalReuse = true; + uint32_t regirTemporalHistory = 16; + uint32_t regirTemporalMaxM = 64; + bool regirVisibilityReuse = true; + bool regirEnvironment = true; AdaptiveSamplingMode adaptiveSamplingMode = AdaptiveSamplingMode::Disabled; float adaptiveSamplingBudget = 1.0f; float adaptiveWeightVariance = 0.25f; @@ -220,7 +220,7 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.restirDiMode = RestirDiMode::Production; settings.restirDiTemporalEnabled = true; settings.restirDiSpatialEnabled = true; - settings.restirDiFinalVisibilityEnabled = false; + settings.restirDiFinalVisibilityEnabled = true; settings.restirDiSpatialRounds = 4; settings.restirDiSpatialRadius = 3.0f; settings.restirDiTemporalMaxAge = 32; @@ -237,24 +237,24 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.restirGiEnabled = true; settings.restirGiFinalStabilizationEnabled = true; settings.restirGiActiveTileMaskMode = RestirGiActiveTileMaskMode::Off; - settings.restirHistoryCopyMode = RestirHistoryCopyMode::Copy; - settings.lightingReuseMode = LightingReuseMode::LegacyRestirDiGi; + settings.restirHistoryCopyMode = RestirHistoryCopyMode::PingPong; + settings.lightingReuseMode = LightingReuseMode::LegacyRestirDiGiPlusReGIR; settings.pathReservoirLayout = ReservoirLayout::LegacyDI; - settings.regirGridDimensions = glm::uvec3{64u, 64u, 64u}; - settings.regirReservoirsPerCell = 16; - settings.regirCandidatesPerReservoir = 32; + settings.regirGridDimensions = glm::uvec3{32u, 32u, 32u}; + settings.regirReservoirsPerCell = 8; + settings.regirCandidatesPerReservoir = 16; settings.regirGridPadding = 0.10f; - settings.regirCanonicalMix = 0.25f; + settings.regirCanonicalMix = 0.70f; settings.regirQueryMode = RegirQueryMode::Stochastic; settings.regirGridMode = RegirGridMode::Dense; settings.regirFiniteQueryFramePeriod = 0; - settings.regirSpatialReuse = false; + settings.regirSpatialReuse = true; settings.regirSpatialRounds = 1; - settings.regirTemporalReuse = false; - settings.regirTemporalHistory = 0; - settings.regirTemporalMaxM = 32; - settings.regirVisibilityReuse = false; - settings.regirEnvironment = false; + settings.regirTemporalReuse = true; + settings.regirTemporalHistory = 16; + settings.regirTemporalMaxM = 64; + settings.regirVisibilityReuse = true; + settings.regirEnvironment = true; settings.adaptiveSamplingMode = AdaptiveSamplingMode::Disabled; settings.adaptiveSamplingBudget = 1.0f; settings.adaptiveWeightVariance = 0.25f; @@ -288,8 +288,8 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.denoiserMaxHistoryLength = 32; settings.momentValidityThreshold = 0.22f; settings.taaFeedback = 0.08f; - settings.taaMotionFeedback = 0.78f; - settings.taaReactiveFeedback = 0.92f; + settings.taaMotionFeedback = 0.72f; + settings.taaReactiveFeedback = 0.60f; settings.taaSharpeningStrength = 0.08f; settings.materialTextureAnisotropy = 2.0f; settings.specularAaEnabled = false; @@ -306,6 +306,14 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.restirDiSpatialRadius = 2.0f; settings.restirDiTemporalMaxAge = 16; settings.restirDiMaxM = 32; + settings.regirGridDimensions = glm::uvec3{24u, 24u, 24u}; + settings.regirReservoirsPerCell = 4; + settings.regirCandidatesPerReservoir = 8; + settings.regirSpatialReuse = true; + settings.regirSpatialRounds = 1; + settings.regirTemporalReuse = true; + settings.regirTemporalHistory = 8; + settings.regirTemporalMaxM = 32; settings.adaptiveQualityMode = AdaptiveQualityMode::Balanced; break; case RenderPreset::Balanced: @@ -319,8 +327,8 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.denoiserMaxHistoryLength = 48; settings.momentValidityThreshold = 0.20f; settings.taaFeedback = 0.06f; - settings.taaMotionFeedback = 0.90f; - settings.taaReactiveFeedback = 0.98f; + settings.taaMotionFeedback = 0.78f; + settings.taaReactiveFeedback = 0.55f; settings.taaSharpeningStrength = 0.05f; settings.materialTextureAnisotropy = 4.0f; settings.specularAaEnabled = true; @@ -350,8 +358,8 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.denoiserMaxHistoryLength = 64; settings.momentValidityThreshold = 0.18f; settings.taaFeedback = 0.05f; - settings.taaMotionFeedback = 0.80f; - settings.taaReactiveFeedback = 0.94f; + settings.taaMotionFeedback = 0.74f; + settings.taaReactiveFeedback = 0.50f; settings.taaSharpeningStrength = 0.04f; settings.materialTextureAnisotropy = 8.0f; settings.specularAaEnabled = true; @@ -368,6 +376,14 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.restirDiSpatialRadius = 4.0f; settings.restirDiTemporalMaxAge = 48; settings.restirDiMaxM = 96; + settings.regirGridDimensions = glm::uvec3{48u, 48u, 48u}; + settings.regirReservoirsPerCell = 8; + settings.regirCandidatesPerReservoir = 32; + settings.regirSpatialReuse = true; + settings.regirSpatialRounds = 2; + settings.regirTemporalReuse = true; + settings.regirTemporalHistory = 24; + settings.regirTemporalMaxM = 128; settings.adaptiveQualityMode = AdaptiveQualityMode::Off; break; case RenderPreset::Native30: @@ -381,8 +397,8 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.denoiserMaxHistoryLength = 32; settings.momentValidityThreshold = 0.20f; settings.taaFeedback = 0.06f; - settings.taaMotionFeedback = 0.88f; - settings.taaReactiveFeedback = 0.96f; + settings.taaMotionFeedback = 0.76f; + settings.taaReactiveFeedback = 0.55f; settings.taaSharpeningStrength = 0.05f; settings.materialTextureAnisotropy = 1.0f; settings.specularAaEnabled = true; @@ -402,6 +418,11 @@ inline void applyRenderPreset(RendererSettings& settings, RenderPreset preset) { settings.restirDiTemporalMaxAge = 24; settings.restirDiMaxM = 32; settings.restirHistoryCopyMode = RestirHistoryCopyMode::PingPong; + settings.lightingReuseMode = LightingReuseMode::LegacyRestirDiGi; + settings.regirSpatialReuse = false; + settings.regirTemporalReuse = false; + settings.regirTemporalHistory = 0; + settings.regirEnvironment = false; settings.opacityMicromapsEnabled = true; settings.shaderExecutionReorderingEnabled = true; settings.secondaryDirectLightingEnabled = true; diff --git a/include/rtv/RtxdiRuntime.h b/include/rtv/RtxdiRuntime.h new file mode 100644 index 0000000..efff772 --- /dev/null +++ b/include/rtv/RtxdiRuntime.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include +#include + +namespace rtv { + +enum class RtxdiQualityPreset : uint32_t { + Fast, + Medium, + Unbiased, + Ultra, + Reference, +}; + +struct RtxdiRuntimeConfig { + uint32_t renderWidth = 0; + uint32_t renderHeight = 0; + RtxdiQualityPreset qualityPreset = RtxdiQualityPreset::Medium; + bool checkerboard = false; +}; + +struct RtxdiMemoryRequirements { + uint64_t diReservoirBytes = 0; + uint64_t giReservoirBytes = 0; + uint64_t ptReservoirBytes = 0; + uint64_t totalReservoirBytes = 0; +}; + +class RtxdiRuntime { +public: + explicit RtxdiRuntime(const RtxdiRuntimeConfig& config); + + void recreate(const RtxdiRuntimeConfig& config); + void beginFrame(uint32_t frameIndex); + + [[nodiscard]] const RtxdiRuntimeConfig& config() const { return config_; } + [[nodiscard]] RtxdiMemoryRequirements memoryRequirements() const; + + [[nodiscard]] rtxdi::ImportanceSamplingContext& context() { return *context_; } + [[nodiscard]] const rtxdi::ImportanceSamplingContext& context() const { return *context_; } + +private: + void applyQualityPreset(); + + RtxdiRuntimeConfig config_{}; + std::unique_ptr context_; +}; + +} // namespace rtv diff --git a/include/rtv/SceneComponents.h b/include/rtv/SceneComponents.h index a6df668..0b93181 100644 --- a/include/rtv/SceneComponents.h +++ b/include/rtv/SceneComponents.h @@ -285,7 +285,7 @@ struct RenderSettings { RestirDiMode restirDiMode = RestirDiMode::Production; bool restirDiTemporalEnabled = true; bool restirDiSpatialEnabled = true; - bool restirDiFinalVisibilityEnabled = false; + bool restirDiFinalVisibilityEnabled = true; uint32_t restirDiSpatialRounds = 4; float restirDiSpatialRadius = 3.0f; uint32_t restirDiTemporalMaxAge = 32; @@ -339,24 +339,24 @@ struct RenderSettings { uint32_t restirGiVisibilityRayBudget = 1; bool restirGiFinalStabilizationEnabled = true; RestirGiActiveTileMaskMode restirGiActiveTileMaskMode = RestirGiActiveTileMaskMode::Off; - RestirHistoryCopyMode restirHistoryCopyMode = RestirHistoryCopyMode::Copy; - LightingReuseMode lightingReuseMode = LightingReuseMode::LegacyRestirDiGi; + RestirHistoryCopyMode restirHistoryCopyMode = RestirHistoryCopyMode::PingPong; + LightingReuseMode lightingReuseMode = LightingReuseMode::LegacyRestirDiGiPlusReGIR; ReservoirLayout pathReservoirLayout = ReservoirLayout::LegacyDI; - glm::uvec3 regirGridDimensions{64u, 64u, 64u}; - uint32_t regirReservoirsPerCell = 16; - uint32_t regirCandidatesPerReservoir = 32; + glm::uvec3 regirGridDimensions{32u, 32u, 32u}; + uint32_t regirReservoirsPerCell = 8; + uint32_t regirCandidatesPerReservoir = 16; float regirGridPadding = 0.10f; - float regirCanonicalMix = 0.25f; + float regirCanonicalMix = 0.70f; RegirQueryMode regirQueryMode = RegirQueryMode::Stochastic; RegirGridMode regirGridMode = RegirGridMode::Dense; uint32_t regirFiniteQueryFramePeriod = 0; - bool regirSpatialReuse = false; + bool regirSpatialReuse = true; uint32_t regirSpatialRounds = 1; - bool regirTemporalReuse = false; - uint32_t regirTemporalHistory = 0; - uint32_t regirTemporalMaxM = 32; - bool regirVisibilityReuse = false; - bool regirEnvironment = false; + bool regirTemporalReuse = true; + uint32_t regirTemporalHistory = 16; + uint32_t regirTemporalMaxM = 64; + bool regirVisibilityReuse = true; + bool regirEnvironment = true; AdaptiveSamplingMode adaptiveSamplingMode = AdaptiveSamplingMode::Disabled; float adaptiveSamplingBudget = 1.0f; float adaptiveWeightVariance = 0.25f; diff --git a/include/rtv/SceneDocument.h b/include/rtv/SceneDocument.h index 968bb7b..b886b6a 100644 --- a/include/rtv/SceneDocument.h +++ b/include/rtv/SceneDocument.h @@ -17,7 +17,7 @@ namespace rtv { struct RtLevelHeader { - uint32_t formatVersion = 4; + uint32_t formatVersion = 5; std::string sceneGuid; std::string engineVersion = "0.1"; bool projectRelativePaths = true; diff --git a/include/rtv/SceneStatsPanel.h b/include/rtv/SceneStatsPanel.h index 1528e1f..70f585f 100644 --- a/include/rtv/SceneStatsPanel.h +++ b/include/rtv/SceneStatsPanel.h @@ -6,7 +6,7 @@ namespace rtv { class SceneStatsPanel { public: - void draw(const EditorRuntimeState& state); + void draw(const EditorRuntimeState& state, EditorRequests& requests); }; } // namespace rtv diff --git a/include/rtv/ShaderCompiler.h b/include/rtv/ShaderCompiler.h index 3ed162b..4a3cb10 100644 --- a/include/rtv/ShaderCompiler.h +++ b/include/rtv/ShaderCompiler.h @@ -30,9 +30,9 @@ class ShaderCompiler { const std::vector>& extraDefines) const; [[nodiscard]] std::string compileReason(const std::filesystem::path& source, const std::filesystem::path& output) const; [[nodiscard]] static std::vector readSpirv(const std::filesystem::path& path); + [[nodiscard]] std::vector dependenciesFor(const std::filesystem::path& source) const; private: - [[nodiscard]] std::vector dependenciesFor(const std::filesystem::path& source) const; [[nodiscard]] bool needsCompileWithSignature( const std::filesystem::path& source, const std::filesystem::path& output, diff --git a/include/rtv/StreamingGpuTransferExecutor.h b/include/rtv/StreamingGpuTransferExecutor.h index a4744f7..a6d6de3 100644 --- a/include/rtv/StreamingGpuTransferExecutor.h +++ b/include/rtv/StreamingGpuTransferExecutor.h @@ -9,6 +9,7 @@ #include #include +#include #include namespace rtv { @@ -113,27 +114,27 @@ class StreamingGpuTransferExecutor final : private NonCopyable { // Returns the completed timeline value. uint64_t poll(); - // -- Async compute integration (Phase 9) -- - - // Configure a separate compute queue for async-compute streaming work. - // When set and async compute is desired, certain operations (BLAS compaction - // queries, compute-shader decompression, mip generation) can be recorded on - // this queue instead of the transfer queue. - void setComputeQueue(VkQueue queue, uint32_t familyIndex, VkSemaphore timelineSemaphore); - [[nodiscard]] bool hasAsyncComputeQueue() const { return computeQueue_ != VK_NULL_HANDLE; } - - // Submit any pending compute-queue work (e.g. compute-shader decompression - // dispatched via stageComputeShaderDispatch). Waits for `waitTimelineValue` - // on the transfer timeline, signals `signalTimelineValue` on the compute - // timeline when complete. Returns the compute submission's timeline value - // (the signaled value), or 0 if no compute work was pending. - uint64_t submitComputeFrame(uint64_t waitTimelineValue); + enum class ComputeTimelineOwnership { + Borrowed, + Owned, + }; - // Record a compute-shader dispatch for async execution. The pipeline must - // already be bound externally via helper; this records the dispatch command - // into the open compute batch. Returns false if no compute queue is configured. - [[nodiscard]] bool recordComputeDispatch(VkCommandBuffer externalCommandBuffer, - uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ); + // Configure a queue and timeline semaphore used only for dependency markers. + // Borrowed semaphores remain caller-owned and valid until reconfiguration or + // shutdown; Owned semaphores are destroyed by the executor after its final + // marker. The compute timeline must be distinct from the transfer timeline. + [[nodiscard]] bool setComputeQueue( + VkQueue queue, + VkSemaphore timelineSemaphore, + ComputeTimelineOwnership ownership); + [[nodiscard]] bool hasAsyncComputeQueue() const { + return computeQueue_ != VK_NULL_HANDLE && computeTimeline_ != VK_NULL_HANDLE; + } + + // Submit a semaphore-only dependency marker. It waits for + // `waitTimelineValue` on the transfer timeline and signals the next value on + // the compute timeline. No command buffer or compute work is submitted. + uint64_t submitComputeTimelineMarker(uint64_t waitTimelineValue); [[nodiscard]] uint64_t nextTimelineValue() const { return nextTimelineValue_; } [[nodiscard]] Stats stats() const; @@ -154,6 +155,10 @@ class StreamingGpuTransferExecutor final : private NonCopyable { VkQueryPool queryPool = VK_NULL_HANDLE; uint64_t timelineValue = 0; }; + struct TrackedImageLayouts { + VkImage image = VK_NULL_HANDLE; + std::vector mips; + }; [[nodiscard]] VkCommandBuffer beginBatch(); [[nodiscard]] VkCommandBuffer beginGraphicsBatch(); @@ -168,12 +173,13 @@ class StreamingGpuTransferExecutor final : private NonCopyable { VkQueue graphicsQueue_ = VK_NULL_HANDLE; uint32_t graphicsQueueFamily_ = UINT32_MAX; VkQueue computeQueue_ = VK_NULL_HANDLE; - uint32_t computeQueueFamily_ = UINT32_MAX; VkSemaphore computeTimeline_ = VK_NULL_HANDLE; + bool computeTimelineOwned_ = false; uint64_t nextComputeTimelineValue_ = 1; + uint64_t submittedComputeTimeline_ = 0; + uint64_t completedComputeTimeline_ = 0; VkCommandPool commandPool_ = VK_NULL_HANDLE; VkCommandPool graphicsCommandPool_ = VK_NULL_HANDLE; - VkCommandPool computeCommandPool_ = VK_NULL_HANDLE; VkSemaphore timeline_ = VK_NULL_HANDLE; uint64_t nextTimelineValue_ = 1; uint64_t submittedTimeline_ = 0; @@ -188,11 +194,10 @@ class StreamingGpuTransferExecutor final : private NonCopyable { std::vector openGraphicsCompactionQueries_; std::vector pendingCompactionQueries_; std::vector> compactedBlasSizes_; + std::unordered_map imageLayouts_; std::deque inFlight_; std::vector freeCommandBuffers_; std::vector freeGraphicsCommandBuffers_; - std::deque computeInFlight_; - std::vector freeComputeCommandBuffers_; uint32_t totalSubmissions_ = 0; uint32_t totalBufferCopies_ = 0; diff --git a/include/rtv/StreamingStagingRing.h b/include/rtv/StreamingStagingRing.h index a466d29..89aa11c 100644 --- a/include/rtv/StreamingStagingRing.h +++ b/include/rtv/StreamingStagingRing.h @@ -63,6 +63,10 @@ class StreamingStagingRing final : private NonCopyable { // timeline value has signaled. [[nodiscard]] std::optional allocate(uint64_t bytes, uint64_t timelineValue); + // Make a staged CPU write visible to the device before its copy command is + // submitted. VMA turns this into a no-op for host-coherent allocations. + void flush(uint64_t offset, uint64_t bytes) const; + // Reclaim every allocation whose timeline value <= completedTimeline. // Returns the number of bytes reclaimed. uint64_t retire(uint64_t completedTimeline); diff --git a/include/rtv/Swapchain.h b/include/rtv/Swapchain.h index c842038..aeb9e64 100644 --- a/include/rtv/Swapchain.h +++ b/include/rtv/Swapchain.h @@ -24,6 +24,8 @@ class Swapchain final : private NonCopyable { [[nodiscard]] VkSwapchainKHR handle() const { return swapchain_; } [[nodiscard]] VkFormat format() const { return imageFormat_; } + [[nodiscard]] VkImageUsageFlags imageUsage() const { return imageUsage_; } + [[nodiscard]] bool supportsImageUsage(VkImageUsageFlags usage) const { return (imageUsage_ & usage) == usage; } [[nodiscard]] VkExtent2D extent() const { return extent_; } [[nodiscard]] VkImage image(uint32_t index) const { return images_.at(index); } [[nodiscard]] VkImageView imageView(uint32_t index) const { return imageViews_.at(index); } @@ -51,6 +53,7 @@ class Swapchain final : private NonCopyable { bool headless_ = false; VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; VkFormat imageFormat_ = VK_FORMAT_UNDEFINED; + VkImageUsageFlags imageUsage_ = 0; VkExtent2D extent_{}; std::vector images_; std::vector imageViews_; diff --git a/include/rtv/UploadContext.h b/include/rtv/UploadContext.h index f351332..621a966 100644 --- a/include/rtv/UploadContext.h +++ b/include/rtv/UploadContext.h @@ -26,6 +26,7 @@ class UploadContext final : private NonCopyable { VkQueue queue_ = VK_NULL_HANDLE; uint32_t queueFamilyIndex_ = 0; VkCommandPool commandPool_ = VK_NULL_HANDLE; + VkCommandBuffer commandBuffer_ = VK_NULL_HANDLE; VkFence fence_ = VK_NULL_HANDLE; }; diff --git a/include/rtv/ViewportPanel.h b/include/rtv/ViewportPanel.h index 3b4eaa2..f97f501 100644 --- a/include/rtv/ViewportPanel.h +++ b/include/rtv/ViewportPanel.h @@ -44,6 +44,9 @@ class ViewportPanel { [[nodiscard]] bool interactionActive() const { return focused_ || hovered_; } [[nodiscard]] bool hovered() const { return hovered_; } [[nodiscard]] GizmoInteractionState gizmoState() const { return gizmoState_; } + [[nodiscard]] int activeToolMode() const { return transformGizmoMode_; } + [[nodiscard]] bool localTransformMode() const { return localGizmoMode_; } + [[nodiscard]] bool snapEnabled() const { return snap_.enabled; } void executeCommand(EditorCommandId id); void setShowGrid(bool show) { showGrid_ = show; } @@ -73,6 +76,9 @@ class ViewportPanel { void persistViewportPreferences(EditorPreferences& preferences, const std::filesystem::path& path) const; VkExtent2D lastContentExtent_{}; + VkExtent2D stableRenderExtent_{}; + VkExtent2D pendingRenderExtent_{}; + uint32_t pendingRenderExtentFrames_ = 0; int transformGizmoMode_ = 0; bool localGizmoMode_ = false; SnapSettings snap_{}; diff --git a/include/rtv/VulkanContext.h b/include/rtv/VulkanContext.h index 40cd593..20494f2 100644 --- a/include/rtv/VulkanContext.h +++ b/include/rtv/VulkanContext.h @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -140,6 +141,14 @@ class VulkanContext final : private NonCopyable { [[nodiscard]] bool debugUtilsObjectNamesAvailable() const { return debugUtilsExtensionEnabled_ && vkSetDebugUtilsObjectNameEXT != nullptr; } + [[nodiscard]] bool validationEnabled() const noexcept { return validationEnabled_; } + [[nodiscard]] uint64_t validationErrorCount() const noexcept { + return validationErrorCount_.load(std::memory_order_relaxed); + } + [[nodiscard]] uint64_t validationWarningCount() const noexcept { + return validationWarningCount_.load(std::memory_order_relaxed); + } + void resetValidationMessageCounts() const noexcept; private: explicit VulkanContext(bool headless); @@ -149,6 +158,12 @@ class VulkanContext final : private NonCopyable { void pickPhysicalDevice(); void pickPhysicalDeviceHeadless(); void createDevice(); + [[nodiscard]] VkDebugUtilsMessengerCreateInfoEXT debugMessengerCreateInfo(); + static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT severity, + VkDebugUtilsMessageTypeFlagsEXT messageTypes, + const VkDebugUtilsMessengerCallbackDataEXT* callbackData, + void* userData); [[nodiscard]] bool validationRequested() const; [[nodiscard]] bool validationAvailable() const; @@ -189,8 +204,11 @@ class VulkanContext final : private NonCopyable { bool storageBuffer16BitAccess_ = false; bool uniformAndStorageBuffer16BitAccess_ = false; bool debugUtilsExtensionEnabled_ = false; + bool validationEnabled_ = false; float maxSamplerAnisotropy_ = 1.0f; QueueFamilyIndices queueFamilies_{}; + mutable std::atomic validationErrorCount_{0}; + mutable std::atomic validationWarningCount_{0}; }; } // namespace rtv diff --git a/include/rtv/passes/CompositePass.h b/include/rtv/passes/CompositePass.h new file mode 100644 index 0000000..dc54975 --- /dev/null +++ b/include/rtv/passes/CompositePass.h @@ -0,0 +1,67 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +namespace rtv::passes { + +struct CompositePass { + static constexpr const char* kContractId = "composite"; + static constexpr const char* kPassName = "CompositePass"; + static constexpr const char* kRole = "output"; + static constexpr const char* kExtractionState = + "contract-module plus output-source selection policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + enum class HdrPostProcessSource { + AdaptiveFilled, + AdaptiveDebug, + TemporalOutput, + PostDenoise, + }; + + static bool isActive(const RendererSettings&) { + return true; + } + + static HdrPostProcessSource selectHdrPostProcessSource( + const RendererSettings& settings, + bool adaptiveFilledReady, + bool adaptiveDebugReady, + bool temporalOutputReady) { + if (settings.debugView == RendererDebugView::AdaptiveFilledImage && adaptiveFilledReady) { + return HdrPostProcessSource::AdaptiveFilled; + } + if (adaptiveDebugReady) { + return HdrPostProcessSource::AdaptiveDebug; + } + return temporalOutputReady + ? HdrPostProcessSource::TemporalOutput + : HdrPostProcessSource::PostDenoise; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Composite; + contract.role = RendererPassContractRole::Output; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/CompositePass.h (contract + output-source policy), src/rtv/PathTracerRenderer.cpp (GPU resource runtime)"; + contract.featureFlagsRequired = "always"; + contract.inputs = rendererContractArray({"beauty source", "exposure", "debug view output", "selection outline"}); + contract.outputs = rendererContractArray({"presentable image", "saved present frame"}); + contract.historyResources = rendererContractArray({"auto exposure history"}); + contract.descriptorLayouts = rendererContractArray({"tone map descriptor set", "fullscreen descriptor set"}); + contract.pushConstants = rendererContractArray({"ToneMapParams", "RendererDebugParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"tone mapping compute shader", "fullscreen/editor presentation shaders"}); + contract.rendergraphReads = rendererContractArray({"denoised/TAA/DLSS/debug source", "exposure buffer"}); + contract.rendergraphWrites = rendererContractArray({"presentation image", "swapchain image"}); + contract.requiredBarriers = rendererContractArray({"source to tone map", "tone map to present"}); + contract.cameraHistoryResetBehavior = "Does not own temporal history; consumes the selected current-frame output."; + contract.debugOutputs = rendererContractArray({"beauty", "debug view selected by RendererDebugView"}); + contract.profilingSections = rendererContractArray({"tone_map", "fullscreen", "editor_presentation"}); + contract.validationChecks = rendererContractArray({"nonblank present PNG", "image diff output", "swapchain extent validity"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/DebugViewPass.h b/include/rtv/passes/DebugViewPass.h new file mode 100644 index 0000000..819a7b1 --- /dev/null +++ b/include/rtv/passes/DebugViewPass.h @@ -0,0 +1,99 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include + +namespace rtv::passes { + +struct DebugViewPass { + static constexpr const char* kContractId = "debug_view"; + static constexpr const char* kPassName = "DebugViewPass"; + static constexpr const char* kRole = "diagnostics"; + static constexpr const char* kExtractionState = + "contract-module plus wavefront debug-view policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings&) { + return true; + } + + static bool isSelected(const RendererSettings& settings) { + return settings.debugView != RendererDebugView::Beauty; + } + + static constexpr bool isWavefrontView(RendererDebugView view) { + return view == RendererDebugView::WavefrontQueueOccupancy || + view == RendererDebugView::WavefrontPathDepth || + view == RendererDebugView::WavefrontLiveRays || + view == RendererDebugView::WavefrontTerminatedRays || + view == RendererDebugView::WavefrontMaterialBucket || + view == RendererDebugView::WavefrontRestirDi || + view == RendererDebugView::WavefrontDirectLighting || + view == RendererDebugView::WavefrontRestirGi; + } + + static constexpr bool requiresWavefrontShadowTrace(RendererDebugView view) { + return view == RendererDebugView::WavefrontDirectLighting; + } + + static constexpr bool requiresWavefrontRestirReservoir(RendererDebugView view) { + return view == RendererDebugView::WavefrontRestirDi; + } + + static constexpr bool requiresWavefrontRestirGiReservoir(RendererDebugView view) { + return view == RendererDebugView::WavefrontRestirGi; + } + + static constexpr bool isNonDenoiserDebugView(uint32_t view) { + if (view == 0u) { + return false; + } + if (view <= 4u) { + return false; + } + if (view == static_cast(RendererDebugView::TemporalReactiveMask) || + view == static_cast(RendererDebugView::TemporalHistoryWeight)) { + return false; + } + if (view >= static_cast(RendererDebugView::PathDirectDiffuse) && + view <= static_cast(RendererDebugView::DenoiserSpecularHistoryWeight)) { + return false; + } + if (view >= static_cast(RendererDebugView::DenoiserDirectDiffuseVariance) && + view <= static_cast(RendererDebugView::DenoiserSpecularRawVariance)) { + return false; + } + if (view >= static_cast(RendererDebugView::DlssDepth) && + view <= static_cast(RendererDebugView::DlssRrSpecularRayDirectionHitDistance)) { + return false; + } + return true; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::DebugView; + contract.role = RendererPassContractRole::Diagnostics; + contract.name = kPassName; + contract.activeByCurrentSettings = isSelected(settings); + contract.currentOwnerFile = "include/rtv/passes/DebugViewPass.h (contract + debug-view policy), src/rtv/RendererDebug.cpp; src/rtv/DiagnosticImageExport.cpp (runtime)"; + contract.featureFlagsRequired = "debugView != beauty || save-debug-views"; + contract.inputs = rendererContractArray({"registered pass debug outputs", "renderer debug params"}); + contract.outputs = rendererContractArray({"debug view PNG", "debug export manifest"}); + contract.historyResources = rendererContractArray({"none; reads pass-owned resources only"}); + contract.descriptorLayouts = rendererContractArray({"debug visualization descriptor set"}); + contract.pushConstants = rendererContractArray({"RendererDebugParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/pathtrace.rgen", "debug visualization compute paths"}); + contract.rendergraphReads = rendererContractArray({"pass-owned debug resources"}); + contract.rendergraphWrites = rendererContractArray({"debug output image"}); + contract.requiredBarriers = rendererContractArray({"owner pass output to debug read"}); + contract.cameraHistoryResetBehavior = "Debug views must not keep production history alive or mutate pass-owned resources."; + contract.debugOutputs = rendererContractArray({"all registered RendererDebugView names"}); + contract.profilingSections = rendererContractArray({"debug export wall time outside GPU pass timings"}); + contract.validationChecks = rendererContractArray({"missing required debug view list", "export manifest completeness"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/DenoiserPass.h b/include/rtv/passes/DenoiserPass.h new file mode 100644 index 0000000..d8e2c0b --- /dev/null +++ b/include/rtv/passes/DenoiserPass.h @@ -0,0 +1,94 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include +#include + +namespace rtv::passes { + +struct DenoiserPass { + static constexpr const char* kContractId = "denoiser"; + static constexpr const char* kPassName = "DenoiserPass"; + static constexpr const char* kRole = "denoising"; + static constexpr const char* kExtractionState = + "contract-module plus engine-denoiser run/debug policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.denoiserEnabled && settings.denoiserBackend == DenoiserBackend::Engine; + } + + static uint32_t effectiveMaxHistoryLength(const RendererSettings& settings, uint32_t memoryPressureTier) { + if (memoryPressureTier >= 3u) { + return std::min(settings.denoiserMaxHistoryLength, 16u); + } + if (memoryPressureTier >= 2u) { + return std::min(settings.denoiserMaxHistoryLength, 24u); + } + if (memoryPressureTier >= 1u) { + return std::min(settings.denoiserMaxHistoryLength, 32u); + } + return settings.denoiserMaxHistoryLength; + } + + static constexpr bool requestsDebugOutput(uint32_t debugView) { + if (debugView >= 1u && debugView <= 4u) { + return true; + } + if (debugView == static_cast(RendererDebugView::TemporalReactiveMask) || + debugView == static_cast(RendererDebugView::TemporalHistoryWeight)) { + return true; + } + if (debugView >= static_cast(RendererDebugView::PathDirectDiffuse) && + debugView <= static_cast(RendererDebugView::DenoiserSpecularHistoryWeight)) { + return true; + } + if (debugView >= static_cast(RendererDebugView::DenoiserDirectDiffuseVariance) && + debugView <= static_cast(RendererDebugView::DenoiserSpecularRawVariance)) { + return true; + } + return false; + } + + static bool requestsRun( + const RendererSettings& settings, + DenoiserBackend effectiveBackend, + uint32_t enabledParam, + uint32_t debugViewParam) { + if (settings.wavefrontFinalOutputEnabled) { + return false; + } + const bool engineDebugView = debugViewParam != 0u; + if (effectiveBackend != DenoiserBackend::Engine && !engineDebugView) { + return false; + } + return enabledParam != 0u || requestsDebugOutput(debugViewParam); + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Denoiser; + contract.role = RendererPassContractRole::Denoising; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/DenoiserPass.h (contract + run/debug/history-length policy), src/rtv/PathTracerRenderer.cpp (resources/scheduling)"; + contract.featureFlagsRequired = "denoiserEnabled && denoiserBackend == engine"; + contract.inputs = rendererContractArray({"raw radiance", "albedo", "normal", "depth", "hit distance", "moments"}); + contract.outputs = rendererContractArray({"denoised color", "variance/confidence", "moments"}); + contract.historyResources = rendererContractArray({"denoiser history", "moment history"}); + contract.descriptorLayouts = rendererContractArray({"denoiser descriptor set", "moment descriptor set"}); + contract.pushConstants = rendererContractArray({"DenoiserParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/denoiser.comp", "shaders/moment_update.comp"}); + contract.rendergraphReads = rendererContractArray({"path trace output", "guide buffers", "denoiser history"}); + contract.rendergraphWrites = rendererContractArray({"denoised image", "moment buffers", "history images"}); + contract.requiredBarriers = rendererContractArray({"path trace to moment update", "moment update to denoiser", "denoiser to TAA/tonemap"}); + contract.cameraHistoryResetBehavior = "Reset on camera cut, invalid guides, disocclusion spike, denoiser setting changes, scene/material reload, or render extent changes."; + contract.debugOutputs = rendererContractArray({"denoiser-rejection", "denoiser-hit-distance", "denoiser-variance-confidence", "moment-update-validity"}); + contract.profilingSections = rendererContractArray({"moment_update", "denoiser", "history_copy"}); + contract.validationChecks = rendererContractArray({"NaN/Inf guide checks", "history reset count", "variance range", "moment validity"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/DlssPass.h b/include/rtv/passes/DlssPass.h new file mode 100644 index 0000000..e5d7aa2 --- /dev/null +++ b/include/rtv/passes/DlssPass.h @@ -0,0 +1,184 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include + +namespace rtv::passes { + +struct DlssPass { + static constexpr const char* kContractId = "dlss"; + static constexpr const char* kPassName = "DlssPass"; + static constexpr const char* kRole = "upscaling"; + static constexpr const char* kExtractionState = + "contract-module plus request/debug-view/run runtime policy; scheduling and SDK resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.temporalUpscaler == TemporalUpscaler::Dlss || settings.dlssRayReconstructionEnabled; + } + + static bool isUpscaleActive(const RendererSettings& settings) { + return settings.temporalUpscaler == TemporalUpscaler::Dlss; + } + + static bool isRayReconstructionActive(const RendererSettings& settings) { + return settings.dlssRayReconstructionEnabled; + } + + static bool isRequested(const RendererSettings& settings) { + return settings.temporalUpscaler == TemporalUpscaler::Dlss || + settings.dlssRayReconstructionEnabled || + settings.dlssFrameGenerationEnabled; + } + + static constexpr bool isUpscaleDebugView(RendererDebugView view) { + return view == RendererDebugView::DlssDepth || + view == RendererDebugView::DlssMotionVectors || + view == RendererDebugView::DlssInputColor || + view == RendererDebugView::DlssOutputColor; + } + + static constexpr bool isRayReconstructionDebugView(RendererDebugView view) { + return view == RendererDebugView::DlssRrDiffuseAlbedo || + view == RendererDebugView::DlssRrSpecularAlbedo || + view == RendererDebugView::DlssRrNormals || + view == RendererDebugView::DlssRrRoughness || + view == RendererDebugView::DlssRrDiffuseHitDistance || + view == RendererDebugView::DlssRrSpecularHitDistance || + view == RendererDebugView::DlssRrReflectedAlbedo || + view == RendererDebugView::DlssRrDisocclusionMask || + view == RendererDebugView::DlssRrDiffuseRayDirection || + view == RendererDebugView::DlssRrSpecularRayDirection || + view == RendererDebugView::DlssRrDiffuseRayDirectionHitDistance || + view == RendererDebugView::DlssRrSpecularRayDirectionHitDistance; + } + + static constexpr bool isGuideDebugView(RendererDebugView view) { + return isUpscaleDebugView(view) || isRayReconstructionDebugView(view); + } + + static bool shouldBypassTemporalUpscalerForDebugView(const RendererSettings& settings) { + if (settings.debugView == RendererDebugView::Beauty || isUpscaleDebugView(settings.debugView)) { + return false; + } + if (isRayReconstructionDebugView(settings.debugView)) { + return !settings.dlssRayReconstructionEnabled; + } + return true; + } + + static constexpr uint32_t guideVisualizationMode(RendererDebugView view, bool rayReconstructionActive) { + constexpr uint32_t kHdrColor = 0u; + constexpr uint32_t kLinearColor = 1u; + constexpr uint32_t kScalar01 = 2u; + constexpr uint32_t kLinearDepth = 3u; + constexpr uint32_t kHardwareDepth = 4u; + constexpr uint32_t kMotionPixels = 5u; + constexpr uint32_t kDirection = 6u; + constexpr uint32_t kDistance = 7u; + switch (view) { + case RendererDebugView::DlssInputColor: + case RendererDebugView::DlssOutputColor: + return kHdrColor; + case RendererDebugView::DlssDepth: + return rayReconstructionActive ? kLinearDepth : kHardwareDepth; + case RendererDebugView::DlssMotionVectors: + return kMotionPixels; + case RendererDebugView::DlssRrDiffuseAlbedo: + case RendererDebugView::DlssRrSpecularAlbedo: + case RendererDebugView::DlssRrReflectedAlbedo: + return kLinearColor; + case RendererDebugView::DlssRrNormals: + case RendererDebugView::DlssRrDiffuseRayDirection: + case RendererDebugView::DlssRrSpecularRayDirection: + return kDirection; + case RendererDebugView::DlssRrRoughness: + case RendererDebugView::DlssRrDisocclusionMask: + return kScalar01; + case RendererDebugView::DlssRrDiffuseHitDistance: + case RendererDebugView::DlssRrSpecularHitDistance: + case RendererDebugView::DlssRrDiffuseRayDirectionHitDistance: + case RendererDebugView::DlssRrSpecularRayDirectionHitDistance: + return kDistance; + default: + return kHdrColor; + } + } + + static constexpr float guideVisualizationScale(RendererDebugView view, bool rayReconstructionActive) { + switch (view) { + case RendererDebugView::DlssMotionVectors: + return 1.0f / 32.0f; + case RendererDebugView::DlssDepth: + return rayReconstructionActive ? 0.025f : 1.0f; + case RendererDebugView::DlssRrDiffuseHitDistance: + case RendererDebugView::DlssRrSpecularHitDistance: + case RendererDebugView::DlssRrDiffuseRayDirectionHitDistance: + case RendererDebugView::DlssRrSpecularRayDirectionHitDistance: + return 0.05f; + default: + return 1.0f; + } + } + + static bool requestsUpscaleRun( + const RendererSettings& settings, + bool bypassForDebugView, + bool rayReconstructionRunning, + TemporalUpscaler effectiveUpscaler) { + return !settings.wavefrontFinalOutputEnabled && + settings.pathTracingEnabled && + settings.taaEnabled && + !bypassForDebugView && + !rayReconstructionRunning && + effectiveUpscaler == TemporalUpscaler::Dlss; + } + + static bool requestsRayReconstructionRun( + const RendererSettings& settings, + bool bypassForDebugView, + bool runtimeSupported) { + return !settings.wavefrontFinalOutputEnabled && + settings.pathTracingEnabled && + settings.taaEnabled && + !bypassForDebugView && + isRayReconstructionActive(settings) && + runtimeSupported; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Dlss; + contract.role = RendererPassContractRole::Upscaling; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/DlssPass.h (contract + request/debug/guide/run policy), src/rtv/PathTracerRenderer.cpp (SDK resources/scheduling)"; + contract.featureFlagsRequired = "temporalUpscaler == dlss || dlssRayReconstructionEnabled"; + contract.inputs = rendererContractArray({"input color", "depth", "motion vectors", "exposure", "DLSS RR guide buffers"}); + contract.outputs = rendererContractArray({"DLSS output color", "DLSS RR output color", "guide visualization"}); + contract.historyResources = rendererContractArray({"Streamline/NGX feature state", "PSR guide signatures"}); + contract.descriptorLayouts = rendererContractArray({"DLSS guide descriptor set", "external SDK resource tags"}); + contract.pushConstants = rendererContractArray({"DlssGuideParams", "Streamline/NGX constants"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/dlss_rr_guides.comp", "shaders/dlss_guide_visualize.comp", "shaders/psr_guides.glsl"}); + contract.rendergraphReads = rendererContractArray({"denoised/raw input color", "depth", "motion", "normal/roughness", "hit distance"}); + contract.rendergraphWrites = rendererContractArray({"dlss guide images", "dlss output", "psr guide signatures"}); + contract.requiredBarriers = rendererContractArray({"guide generation before SDK evaluate", "SDK output before composite"}); + contract.cameraHistoryResetBehavior = "Reset SDK history on camera cut, DLSS mode change, invalid guide state, render scale change, or resolution change."; + contract.debugOutputs = rendererContractArray({"dlss-depth", "dlss-motion-vectors", "dlss-input-color", "dlss-output-color", "dlss-rr-normals", "dlss-rr-roughness"}); + contract.profilingSections = nlohmann::json::array(); + if (isRayReconstructionActive(settings)) { + contract.profilingSections.push_back("dlss_rr_guides"); + contract.profilingSections.push_back("dlss_rr"); + } else if (isUpscaleActive(settings)) { + contract.profilingSections.push_back("dlss_guides"); + contract.profilingSections.push_back("dlss"); + } else { + contract.profilingSections.push_back("DLSS inactive external SDK timings"); + } + contract.validationChecks = rendererContractArray({"SDK availability", "tag count", "evaluation failure count", "guide image format/extent"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/GBufferPass.h b/include/rtv/passes/GBufferPass.h new file mode 100644 index 0000000..3083cd8 --- /dev/null +++ b/include/rtv/passes/GBufferPass.h @@ -0,0 +1,44 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +namespace rtv::passes { + +struct GBufferPass { + static constexpr const char* kContractId = "gbuffer"; + static constexpr const char* kPassName = "GBufferPass"; + static constexpr const char* kRole = "geometry"; + static constexpr const char* kExtractionState = + "contract-module; scheduling and algorithm work remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.pathTracingEnabled; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::GBuffer; + contract.role = RendererPassContractRole::Geometry; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/GBufferPass.h (contract), src/rtv/PathTracerRenderer.cpp (runtime)"; + contract.featureFlagsRequired = "pathTracingEnabled"; + contract.inputs = rendererContractArray({"camera uniform", "scene TLAS", "instance transforms", "materials", "textures"}); + contract.outputs = rendererContractArray({"path data albedo", "normal/depth guides", "motion vectors", "world position", "roughness"}); + contract.historyResources = rendererContractArray({"previous world position", "previous material/object identity"}); + contract.descriptorLayouts = rendererContractArray({"global scene set", "bindless material texture set", "guide image set"}); + contract.pushConstants = rendererContractArray({"RendererDebugParams", "camera jitter", "render extent"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/pathtrace.rgen", "shaders/nrd_prepare.comp", "shaders/dlss_rr_guides.comp"}); + contract.rendergraphReads = rendererContractArray({"scene buffers", "material buffers", "previous-frame surface state"}); + contract.rendergraphWrites = rendererContractArray({"surface guide buffers", "path data buffers", "motion/depth/normal images"}); + contract.requiredBarriers = rendererContractArray({"ray tracing writes to compute shader reads", "guide image layout GENERAL"}); + contract.cameraHistoryResetBehavior = "Reset guide history on camera cut, scene reload, material reload, resolution change, or render scale change."; + contract.debugOutputs = rendererContractArray({"albedo", "normals", "depth", "roughness", "motion-vectors", "material-alpha", "material-transmission"}); + contract.profilingSections = rendererContractArray({"path_trace"}); + contract.validationChecks = rendererContractArray({"normal length", "roughness range", "depth finite", "motion convention", "material id validity"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/NrdPass.h b/include/rtv/passes/NrdPass.h new file mode 100644 index 0000000..69319a8 --- /dev/null +++ b/include/rtv/passes/NrdPass.h @@ -0,0 +1,54 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +namespace rtv::passes { + +struct NrdPass { + static constexpr const char* kContractId = "nrd"; + static constexpr const char* kPassName = "NrdPass"; + static constexpr const char* kRole = "denoising"; + static constexpr const char* kExtractionState = + "contract-module plus debug-view compatibility policy; scheduling and SDK resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.denoiserEnabled && settings.denoiserBackend == DenoiserBackend::Nrd; + } + + static constexpr bool supportsDebugView(RendererDebugView view) { + return view == RendererDebugView::Beauty || + view == RendererDebugView::NrdValidation || + view == RendererDebugView::NrdDiffuseConfidence || + view == RendererDebugView::NrdSpecularConfidence || + view == RendererDebugView::NrdRawConfidenceGradient || + view == RendererDebugView::NrdFilteredConfidenceGradient || + view == RendererDebugView::NrdConfidenceHistory; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Nrd; + contract.role = RendererPassContractRole::Denoising; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/NrdPass.h (contract + debug-view policy), src/rtv/PathTracerRenderer.cpp (SDK resources/scheduling)"; + contract.featureFlagsRequired = "denoiserEnabled && denoiserBackend == nrd"; + contract.inputs = rendererContractArray({"depth", "normal/roughness", "motion", "hit distance", "diffuse/specular radiance", "confidence"}); + contract.outputs = rendererContractArray({"NRD denoised diffuse", "NRD denoised specular", "NRD validation image"}); + contract.historyResources = rendererContractArray({"NRD runtime internal history", "NRD confidence history"}); + contract.descriptorLayouts = rendererContractArray({"NRD guide descriptor set", "external SDK resource tags"}); + contract.pushConstants = rendererContractArray({"NrdPrepareParams", "NrdResolveParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/nrd_prepare.comp", "shaders/nrd_resolve.comp", "shaders/nrd_confidence_filter.comp"}); + contract.rendergraphReads = rendererContractArray({"guide buffers", "raw radiance", "confidence history"}); + contract.rendergraphWrites = rendererContractArray({"nrd guide images", "nrd output images", "nrd validation output"}); + contract.requiredBarriers = rendererContractArray({"guide preparation before SDK evaluate", "SDK output before composite/TAA"}); + contract.cameraHistoryResetBehavior = "Reset SDK history on camera cut, invalid guides, denoiser backend change, resolution/render-scale change, or DLSS/NRD mode transition."; + contract.debugOutputs = rendererContractArray({"nrd-validation", "nrd-diffuse-confidence", "nrd-specular-confidence", "nrd-confidence-history"}); + contract.profilingSections = rendererContractArray({"nrd_prepare", "nrd_resolve"}); + contract.validationChecks = rendererContractArray({"guide availability", "normal length", "roughness range", "motion convention", "SDK availability"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/PathTracePass.h b/include/rtv/passes/PathTracePass.h new file mode 100644 index 0000000..0a5135a --- /dev/null +++ b/include/rtv/passes/PathTracePass.h @@ -0,0 +1,146 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include +#include +#include + +namespace rtv::passes { + +struct PathTracePass { + static constexpr const char* kContractId = "path_trace"; + static constexpr const char* kPassName = "PathTracePass"; + static constexpr const char* kRole = "geometry"; + static constexpr const char* kExtractionState = + "contract-module plus kernel/beauty-fast-path policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.pathTracingEnabled; + } + + static bool effectiveLimitSamplesPerPixel(const RendererSettings& settings, uint32_t memoryPressureTier) { + return settings.limitSamplesPerPixel || memoryPressureTier > 0u; + } + + static uint32_t wavefrontMaxPathDepth(uint32_t adaptiveEffectiveMaxBounces) { + return std::max(1u, adaptiveEffectiveMaxBounces); + } + + static uint32_t wavefrontQueueCapacityFor( + uint64_t pixelCount, + bool wavefrontShadeEnabled, + uint32_t maxPathDepth) { + if (pixelCount == 0u) { + return 1u; + } + const uint64_t multiplier = wavefrontShadeEnabled + ? static_cast(std::max(1u, maxPathDepth)) + : 1ull; + const uint64_t capacity = pixelCount > std::numeric_limits::max() / multiplier + ? static_cast(std::numeric_limits::max()) + : pixelCount * multiplier; + return capacity > static_cast(std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(capacity); + } + + static bool native2BSettingsEligible( + const RendererSettings& settings, + bool effectiveLimitSamplesPerPixel, + bool regirActive) { + return settings.pathTraceKernelMode == PathTraceKernelMode::Native2B && + settings.pathTracingEnabled && + settings.maxBounces == 2u && + settings.samplesPerPixel == 1u && + effectiveLimitSamplesPerPixel && + settings.restirGiMode == RestirGiMode::Off && + !settings.restirGiEnabled && + !settings.homogeneousVolumeEnabled && + !settings.motionBlurEnabled && + !regirActive && + settings.debugView == RendererDebugView::Beauty && + !settings.wavefrontFinalOutputEnabled; + } + + static const char* native2BSettingsFallbackReason( + const RendererSettings& settings, + bool effectiveLimitSamplesPerPixel, + bool regirActive) { + if (settings.pathTraceKernelMode != PathTraceKernelMode::Native2B) return nullptr; + if (!settings.pathTracingEnabled) return "path_tracing_disabled"; + if (settings.maxBounces != 2u) return "requires_exactly_two_bounces"; + if (settings.samplesPerPixel != 1u || !effectiveLimitSamplesPerPixel) return "requires_effective_one_spp"; + if (settings.restirGiMode != RestirGiMode::Off || settings.restirGiEnabled) return "restir_gi_enabled"; + if (settings.homogeneousVolumeEnabled) return "volume_enabled"; + if (settings.motionBlurEnabled) return "motion_blur_enabled"; + if (regirActive) return "regir_enabled"; + if (settings.debugView != RendererDebugView::Beauty) return "debug_view_active"; + if (settings.wavefrontFinalOutputEnabled) return "wavefront_final_output_enabled"; + return nullptr; + } + + static bool requestsGenericBeautyFastPath( + const RendererSettings& settings, + PathTraceKernelMode effectiveKernelMode, + bool restirDiValidationFull, + bool restirGiInitialFull, + bool diagnosticCountersEnabled, + bool regirActive) { + return effectiveKernelMode == PathTraceKernelMode::Generic && + settings.debugView == RendererDebugView::Beauty && + settings.pathTracingEnabled && + !settings.motionBlurEnabled && + !settings.wavefrontFinalOutputEnabled && + !diagnosticCountersEnabled && + !restirDiValidationFull && + !restirGiInitialFull && + !regirActive; + } + + static bool requestsRegirBeautyFastPath( + const RendererSettings& settings, + PathTraceKernelMode effectiveKernelMode, + bool restirDiValidationFull, + bool restirGiInitialFull, + bool diagnosticCountersEnabled, + bool regirActive) { + return effectiveKernelMode == PathTraceKernelMode::Generic && + settings.finalBounceFastPathEnabled && + settings.debugView == RendererDebugView::Beauty && + settings.pathTracingEnabled && + !settings.motionBlurEnabled && + !settings.wavefrontFinalOutputEnabled && + !diagnosticCountersEnabled && + !restirDiValidationFull && + !restirGiInitialFull && + regirActive; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::PathTrace; + contract.role = RendererPassContractRole::Geometry; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/PathTracePass.h (contract + kernel/fast-path/effective-limit/wavefront-capacity policy), src/rtv/PathTracerRenderer.cpp (resources/scheduling)"; + contract.featureFlagsRequired = "pathTracingEnabled"; + contract.inputs = rendererContractArray({"GBufferPass surface state", "scene lights", "environment", "ReSTIR/ReGIR sampling services"}); + contract.outputs = rendererContractArray({"raw radiance", "direct/indirect channels", "hit distance", "throughput/bounce debug data"}); + contract.historyResources = rendererContractArray({"accumulation buffer", "previous frame radiance when accumulation is valid"}); + contract.descriptorLayouts = rendererContractArray({"ray tracing descriptor set", "bindless textures", "reservoir descriptor sets"}); + contract.pushConstants = rendererContractArray({"RendererDebugParams", "CameraUniform", "raygen extent"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/pathtrace.rgen", "shaders/pathtrace_integrator.glsl", "shaders/pathtrace_lighting.glsl"}); + contract.rendergraphReads = rendererContractArray({"TLAS", "light buffers", "ReSTIR DI final reservoir", "ReGIR reservoirs"}); + contract.rendergraphWrites = rendererContractArray({"radiance image", "path data buffers", "debug counters"}); + contract.requiredBarriers = rendererContractArray({"accumulation read/write", "ray tracing writes to denoiser/tonemap reads"}); + contract.cameraHistoryResetBehavior = "Reset accumulation on camera movement, render setting changes, shader reload, scene/material/light changes, and temporal reset reasons."; + contract.debugOutputs = rendererContractArray({"beauty", "direct-lighting", "indirect-lighting", "path-direct-diffuse", "path-indirect-specular", "bounce-count"}); + contract.profilingSections = rendererContractArray({"path_trace"}); + contract.validationChecks = rendererContractArray({"NaN/Inf counters", "ray count statistics", "image diff against high-spp references"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/ProfilerPass.h b/include/rtv/passes/ProfilerPass.h new file mode 100644 index 0000000..f6237f2 --- /dev/null +++ b/include/rtv/passes/ProfilerPass.h @@ -0,0 +1,44 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +namespace rtv::passes { + +struct ProfilerPass { + static constexpr const char* kContractId = "profiler"; + static constexpr const char* kPassName = "ProfilerPass"; + static constexpr const char* kRole = "diagnostics"; + static constexpr const char* kExtractionState = + "contract-module; scheduling and algorithm work remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings&) { + return true; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Profiler; + contract.role = RendererPassContractRole::Diagnostics; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/ProfilerPass.h (contract), src/rtv/HeadlessDiagnostics.cpp; include/rtv/GpuProfiler.h (runtime)"; + contract.featureFlagsRequired = "profile || diagnostic output"; + contract.inputs = rendererContractArray({"GPU timestamps", "CPU frame timings", "RenderGraph dump", "debug counters"}); + contract.outputs = rendererContractArray({"profile.json", "rendergraph.json", "debug package", "performance history row"}); + contract.historyResources = rendererContractArray({"rolling performance history outside renderer"}); + contract.descriptorLayouts = rendererContractArray({"none"}); + contract.pushConstants = rendererContractArray({"none"}); + contract.pipelineShaderDependencies = rendererContractArray({"none"}); + contract.rendergraphReads = rendererContractArray({"pass timing timestamps", "diagnostic counters"}); + contract.rendergraphWrites = rendererContractArray({"JSON diagnostics", "budget check input"}); + contract.requiredBarriers = rendererContractArray({"timestamp query readback before JSON write"}); + contract.cameraHistoryResetBehavior = "Profiler state is per-run; history is external to renderer frame history."; + contract.debugOutputs = rendererContractArray({"profile.json", "capture_validation.json", "summary.json"}); + contract.profilingSections = rendererContractArray({"all per_pass_gpu_ms fields", "queue_lane_ms"}); + contract.validationChecks = rendererContractArray({"missing timing section", "validation error count", "budget thresholds", "diagnostic readiness"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/RegirPass.h b/include/rtv/passes/RegirPass.h new file mode 100644 index 0000000..80fc737 --- /dev/null +++ b/include/rtv/passes/RegirPass.h @@ -0,0 +1,590 @@ +#pragma once + +#include "rtv/DescriptorWriter.h" +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include + +#include +#include +#include + +namespace rtv::passes { + +struct RegirPass { + static constexpr const char* kContractId = "regir"; + static constexpr const char* kPassName = "RegirPass"; + static constexpr const char* kRole = "lighting_reuse"; + static constexpr const char* kExtractionState = + "contract-module plus mode/grid/resource-sizing/parameter/stage-readiness/query/promotion-gate runtime policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR; + } + + static bool isRequested(const RendererSettings& settings) { + return isActive(settings); + } + + static bool canRun(const RendererSettings& settings) { + return !settings.wavefrontFinalOutputEnabled && isActive(settings); + } + + static bool analyticSunAvailable(const RendererSettings& settings) { + const float sunPower = settings.usePhysicalCamera + ? settings.sunIlluminanceLux + : settings.sunIntensity; + const float luminance = + settings.sunColor.x * 0.2126f + + settings.sunColor.y * 0.7152f + + settings.sunColor.z * 0.0722f; + return settings.sunlightEnabled && + settings.sunDirection.y > 0.0f && + sunPower > 0.0f && + luminance > 0.0f; + } + + static bool requestsEnvironment(const RendererSettings& settings, bool sceneEnvironmentEnabled) { + return settings.regirEnvironment && + (sceneEnvironmentEnabled || analyticSunAvailable(settings)); + } + + static bool requestsActiveGrid(const RendererSettings& settings) { + return settings.regirGridMode == RegirGridMode::Active; + } + + static bool requestsHashGrid(const RendererSettings& settings) { + return settings.regirGridMode == RegirGridMode::Hash; + } + + static bool requestsSpatialReuse(const RendererSettings& settings) { + return settings.regirGridMode != RegirGridMode::Hash && + settings.regirSpatialReuse; + } + + static bool requestsTemporalReuse(const RendererSettings& settings) { + return settings.regirGridMode != RegirGridMode::Hash && + settings.regirTemporalReuse; + } + + static bool spatialReuseEffective(const RendererSettings& settings, bool regirRequested) { + return regirRequested && requestsSpatialReuse(settings); + } + + static bool temporalReuseEffective(const RendererSettings& settings, bool regirRequested) { + return regirRequested && requestsTemporalReuse(settings); + } + + static bool hashGridActive(const RendererSettings& settings, bool regirRequested) { + return regirRequested && settings.regirGridMode == RegirGridMode::Hash; + } + + static bool activeGridMode(const RendererSettings& settings, bool regirRequested) { + return regirRequested && settings.regirGridMode == RegirGridMode::Active; + } + + static bool hashGridSaturated(bool hashGrid, uint32_t hashSaturationCount) { + return hashGrid && hashSaturationCount > 0u; + } + + static bool hashReuseFallback(const RendererSettings& settings, bool hashGrid) { + return hashGrid && (settings.regirSpatialReuse || settings.regirTemporalReuse); + } + + static bool unsupportedAdvancedRequested( + const RendererSettings& settings, + bool hashGrid, + uint32_t hashSaturationCount) { + return hashGridSaturated(hashGrid, hashSaturationCount) || + hashReuseFallback(settings, hashGrid); + } + + static RegirGridMode effectiveGridMode(const RendererSettings& settings, bool regirRequested) { + if (!regirRequested) { + return RegirGridMode::Dense; + } + return settings.regirGridMode; + } + + static uint32_t effectiveFiniteQueryFramePeriod(const RendererSettings& settings, bool hashGrid) { + if (settings.regirQueryMode == RegirQueryMode::Deterministic) { + return 1u; + } + if (settings.regirFiniteQueryFramePeriod > 0u) { + return settings.regirFiniteQueryFramePeriod; + } + return hashGrid ? 256u : 8u; + } + + static double finiteQueryProbability(bool regirRequested, uint32_t framePeriod) { + return !regirRequested || framePeriod == 0u + ? 0.0 + : 1.0 / static_cast(framePeriod); + } + + static uint64_t gridCellCount(const RendererSettings& settings) { + return static_cast(std::max(settings.regirGridDimensions.x, 1u)) * + static_cast(std::max(settings.regirGridDimensions.y, 1u)) * + static_cast(std::max(settings.regirGridDimensions.z, 1u)); + } + + static uint32_t hashCellCapacity(const RendererSettings& settings) { + const uint64_t totalCells = gridCellCount(settings); + const uint64_t target = totalCells <= 64ull ? totalCells : std::max(64ull, totalCells / 4ull); + uint64_t capacity = 1ull; + while (capacity < target && capacity < (1ull << 31u)) { + capacity <<= 1u; + } + return static_cast(std::max(capacity, 1ull)); + } + + static uint32_t storageCellCapacity(const RendererSettings& settings) { + if (settings.regirGridMode == RegirGridMode::Hash) { + return hashCellCapacity(settings); + } + return static_cast(std::min(gridCellCount(settings), std::numeric_limits::max())); + } + + static uint32_t reservoirDispatchCount(uint64_t cellCount, uint32_t reservoirsPerCell) { + const uint64_t total = + cellCount * static_cast(std::max(reservoirsPerCell, 1u)); + return static_cast(std::min(total, std::numeric_limits::max())); + } + + static uint32_t buildReservoirDispatchCount(const RendererSettings& settings) { + return reservoirDispatchCount(storageCellCapacity(settings), settings.regirReservoirsPerCell); + } + + static uint32_t reuseReservoirDispatchCount(const RendererSettings& settings) { + return reservoirDispatchCount(gridCellCount(settings), settings.regirReservoirsPerCell); + } + + static VkDeviceSize reservoirStorageByteSize( + const RendererSettings& settings, + VkDeviceSize reservoirStrideBytes) { + return static_cast(storageCellCapacity(settings)) * + static_cast(std::max(settings.regirReservoirsPerCell, 1u)) * + std::max(reservoirStrideBytes, 1u); + } + + static VkDeviceSize environmentReservoirByteSize( + uint32_t reservoirCount, + VkDeviceSize reservoirStrideBytes) { + return static_cast(reservoirCount) * + std::max(reservoirStrideBytes, 1u); + } + + static VkDeviceSize activeGridFeedbackByteSize(const RendererSettings& settings) { + return static_cast(gridCellCount(settings) + 4ull) * sizeof(uint32_t); + } + + static VkDeviceSize hashGridFeedbackByteSize(uint32_t hashCellCapacity) { + return (static_cast(hashCellCapacity) + 4ull) * sizeof(uint32_t); + } + + static constexpr uint32_t controlFlags(const RendererSettings& settings, bool regirRuns) { + return (regirRuns ? 1u : 0u) | + (static_cast(settings.regirGridMode) << 1u) | + (settings.regirEnvironment ? 8u : 0u) | + (settings.regirVisibilityReuse ? 16u : 0u); + } + + static uint32_t temporalHistoryFrames(const RendererSettings& settings) { + return settings.regirTemporalReuse + ? std::max(settings.regirTemporalHistory, 1u) + : 0u; + } + + struct ParameterInputs { + uint32_t gridDimX = 1; + uint32_t gridDimY = 1; + uint32_t gridDimZ = 1; + uint32_t reservoirsPerCell = 1; + uint32_t sampleFrameIndex = 0; + uint32_t environmentVersion = 0; + uint32_t infiniteLightBankSize = 0; + uint32_t environmentBankSize = 0; + uint32_t sunBankSize = 0; + bool regirRuns = false; + bool temporalHistoryValid = false; + }; + + template + static Params makeParams(const RendererSettings& settings, const ParameterInputs& inputs) { + Params params{}; + using UVec4 = decltype(params.gridDimensionsReservoirs); + using Vec4 = decltype(params.gridPadding); + params.gridDimensionsReservoirs = UVec4( + inputs.gridDimX, + inputs.gridDimY, + inputs.gridDimZ, + std::max(inputs.reservoirsPerCell, 1u)); + params.controls = UVec4( + controlFlags(settings, inputs.regirRuns), + std::max(settings.regirCandidatesPerReservoir, 1u), + inputs.sampleFrameIndex, + temporalHistoryFrames(settings)); + params.gridPadding = Vec4( + std::max(settings.regirGridPadding, 0.0f), + settings.regirTemporalReuse ? 1.0f : 0.0f, + inputs.temporalHistoryValid ? 1.0f : 0.0f, + static_cast(std::max(settings.regirTemporalMaxM, 1u))); + params.queryControls = Vec4( + std::clamp(settings.regirCanonicalMix, 0.0f, 1.0f), + settings.regirQueryMode == RegirQueryMode::Stochastic ? 1.0f : 0.0f, + settings.regirSpatialReuse ? static_cast(std::clamp(settings.regirSpatialRounds, 1u, 8u)) : 1.0f, + settings.regirSpatialReuse ? 1.0f : 0.0f); + params.environmentControls = UVec4( + inputs.environmentVersion, + inputs.infiniteLightBankSize, + inputs.environmentBankSize, + inputs.sunBankSize); + return params; + } + + struct BuildStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool paramsReady = false; + bool reservoirReady = false; + bool reservoirCapacityReady = false; + bool lightRecordsReady = false; + bool lightBvhReady = false; + bool meshParamsReady = false; + }; + + static bool canRunBuild( + const RendererSettings& settings, + const BuildStageResources& resources) { + return canRun(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.paramsReady && + resources.reservoirReady && + resources.reservoirCapacityReady && + resources.lightRecordsReady && + resources.lightBvhReady && + resources.meshParamsReady; + } + + struct EnvironmentStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool reservoirReady = false; + bool reservoirCapacityReady = false; + }; + + static bool canRunEnvironment( + const RendererSettings& settings, + bool regirBuildReady, + bool sceneEnvironmentEnabled, + const EnvironmentStageResources& resources) { + return regirBuildReady && + requestsEnvironment(settings, sceneEnvironmentEnabled) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.reservoirReady && + resources.reservoirCapacityReady; + } + + struct ActiveGridResources { + bool activeCellBufferReady = false; + bool activeCellCapacityReady = false; + bool readbackBufferReady = false; + bool readbackCapacityReady = false; + }; + + static bool canUseActiveGrid( + const RendererSettings& settings, + bool regirBuildReady, + const ActiveGridResources& resources) { + return regirBuildReady && + requestsActiveGrid(settings) && + resources.activeCellBufferReady && + resources.activeCellCapacityReady && + resources.readbackBufferReady && + resources.readbackCapacityReady; + } + + struct HashGridResources { + bool currentCellBufferReady = false; + bool currentCellCapacityReady = false; + bool nextCellBufferReady = false; + bool nextCellCapacityReady = false; + bool readbackBufferReady = false; + }; + + static bool canUseHashGrid( + const RendererSettings& settings, + bool regirBuildReady, + const HashGridResources& resources) { + return regirBuildReady && + requestsHashGrid(settings) && + resources.currentCellBufferReady && + resources.currentCellCapacityReady && + resources.nextCellBufferReady && + resources.nextCellCapacityReady && + resources.readbackBufferReady; + } + + struct SpatialReuseResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool spatialReservoirReady = false; + bool spatialReservoirCapacityReady = false; + }; + + static bool canRunSpatialReuse( + const RendererSettings& settings, + bool regirBuildReady, + const SpatialReuseResources& resources) { + return regirBuildReady && + requestsSpatialReuse(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.spatialReservoirReady && + resources.spatialReservoirCapacityReady; + } + + struct TemporalReuseResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool temporalReservoirReady = false; + bool temporalReservoirCapacityReady = false; + bool previousReservoirReady = false; + bool previousReservoirCapacityReady = false; + }; + + static bool canRunTemporalReuse( + const RendererSettings& settings, + bool regirBuildReady, + const TemporalReuseResources& resources) { + return regirBuildReady && + requestsTemporalReuse(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.temporalReservoirReady && + resources.temporalReservoirCapacityReady && + resources.previousReservoirReady && + resources.previousReservoirCapacityReady; + } + + static uint64_t denseReservoirBytes( + const RendererSettings& settings, + uint64_t reportedDenseReservoirBytes, + uint64_t reservoirStrideBytes) { + if (reportedDenseReservoirBytes > 0ull) { + return reportedDenseReservoirBytes; + } + return gridCellCount(settings) * + static_cast(std::max(settings.regirReservoirsPerCell, 1u)) * + std::max(reservoirStrideBytes, 1ull); + } + + static uint64_t effectiveActiveCellCount( + bool sparseGrid, + bool feedbackAvailable, + uint64_t reportedActiveCellCount, + uint64_t totalCellCount, + bool regirRequested) { + if (!regirRequested) { + return 0ull; + } + if (!sparseGrid) { + return totalCellCount; + } + return feedbackAvailable + ? std::min(reportedActiveCellCount, totalCellCount) + : 0ull; + } + + static uint64_t effectiveReservoirBytes( + bool sparseGrid, + bool feedbackAvailable, + uint64_t reportedEffectiveReservoirBytes, + uint64_t denseReservoirBytes, + bool regirRequested) { + if (!regirRequested) { + return 0ull; + } + if (sparseGrid) { + return feedbackAvailable ? reportedEffectiveReservoirBytes : 0ull; + } + return std::max(reportedEffectiveReservoirBytes, denseReservoirBytes); + } + + struct PromotionDiagnostics { + uint32_t profiledFrames = 0; + double gpuFrameAvgMs = 0.0; + double regirBuildMs = 0.0; + double regirSpatialReuseMs = 0.0; + double regirTemporalReuseMs = 0.0; + bool feedbackAvailable = false; + uint32_t activeCellCount = 0; + uint32_t hashCollisionCount = 0; + uint32_t hashSaturationCount = 0; + uint32_t hashCellCapacity = 0; + uint64_t totalCellCount = 0; + uint64_t denseReservoirBytes = 0; + uint64_t effectiveReservoirBytes = 0; + uint64_t backingBytes = 0; + uint32_t environmentBankSize = 0; + uint32_t sunBankSize = 0; + uint32_t validEnvironmentReservoirs = 0; + uint32_t validSunReservoirs = 0; + uint64_t environmentBankBytes = 0; + bool environmentEffective = false; + bool sunEffective = false; + bool temporalHistoryValid = false; + bool finiteLightReferenceMatrixPassed = false; + bool environmentMatrixPassed = false; + bool visibilityReuseValidationPassed = false; + bool equalTimeQualityPassed = false; + bool manyLightReferencePassed = false; + }; + + static double totalGpuMs(const PromotionDiagnostics& diagnostics) { + return diagnostics.regirBuildMs + + diagnostics.regirSpatialReuseMs + + diagnostics.regirTemporalReuseMs; + } + + static bool quickPromotionPlumbingPassed( + const RendererSettings& settings, + const PromotionDiagnostics& diagnostics) { + if (!isRequested(settings)) { + return true; + } + const bool memoryEvidence = + diagnostics.backingBytes > 0ull || + diagnostics.effectiveReservoirBytes > 0ull || + diagnostics.denseReservoirBytes > 0ull; + return diagnostics.profiledFrames > 0u && + memoryEvidence && + !hashGridSaturated(hashGridActive(settings, true), diagnostics.hashSaturationCount); + } + + static bool fullPromotionEligible( + const RendererSettings& settings, + const PromotionDiagnostics& diagnostics) { + return isRequested(settings) && + quickPromotionPlumbingPassed(settings, diagnostics) && + diagnostics.finiteLightReferenceMatrixPassed && + diagnostics.environmentMatrixPassed && + diagnostics.visibilityReuseValidationPassed && + diagnostics.equalTimeQualityPassed && + diagnostics.manyLightReferencePassed; + } + + struct BuildDescriptorBindings { + VkDescriptorBufferInfo lightRecords{}; + VkDescriptorBufferInfo lightBvhNodes{}; + VkDescriptorBufferInfo meshParams{}; + VkDescriptorBufferInfo params{}; + VkDescriptorBufferInfo reservoirs{}; + VkDescriptorBufferInfo gridCells{}; + }; + + static void writeBuildDescriptors(DescriptorWriter& writer, const BuildDescriptorBindings& bindings) { + writer + .writeBuffer(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.lightRecords) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.lightBvhNodes) + .writeBuffer(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.meshParams) + .writeBuffer(3, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.params) + .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.reservoirs) + .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.gridCells); + } + + struct EnvironmentDescriptorBindings { + VkDescriptorBufferInfo params{}; + VkDescriptorBufferInfo envParams{}; + VkDescriptorImageInfo environmentImage{}; + VkDescriptorImageInfo environmentSampler{}; + VkDescriptorBufferInfo envRows{}; + VkDescriptorBufferInfo envCols{}; + VkDescriptorBufferInfo skyCdfCols{}; + VkDescriptorBufferInfo environmentReservoirs{}; + VkDescriptorBufferInfo cameraUniform{}; + }; + + static void writeEnvironmentDescriptors(DescriptorWriter& writer, const EnvironmentDescriptorBindings& bindings) { + writer + .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.params) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.envParams) + .writeImage(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, bindings.environmentImage) + .writeImage(3, VK_DESCRIPTOR_TYPE_SAMPLER, bindings.environmentSampler) + .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.envRows) + .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.envCols) + .writeBuffer(6, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.skyCdfCols) + .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.environmentReservoirs) + .writeBuffer(8, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.cameraUniform); + } + + struct SpatialReuseDescriptorBindings { + VkDescriptorBufferInfo params{}; + VkDescriptorBufferInfo inputReservoirs{}; + VkDescriptorBufferInfo outputReservoirs{}; + }; + + static void writeSpatialReuseDescriptors(DescriptorWriter& writer, const SpatialReuseDescriptorBindings& bindings) { + writer + .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.params) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.inputReservoirs) + .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.outputReservoirs); + } + + struct TemporalReuseDescriptorBindings { + VkDescriptorBufferInfo params{}; + VkDescriptorBufferInfo currentReservoirs{}; + VkDescriptorBufferInfo previousReservoirs{}; + VkDescriptorBufferInfo outputReservoirs{}; + }; + + static void writeTemporalReuseDescriptors(DescriptorWriter& writer, const TemporalReuseDescriptorBindings& bindings) { + writer + .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, bindings.params) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.currentReservoirs) + .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.previousReservoirs) + .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, bindings.outputReservoirs); + } + + static bool shouldTraceFiniteLightsThisFrame( + const RendererSettings& settings, + uint32_t temporalFrameIndex, + bool hashGridActive) { + if (settings.regirQueryMode == RegirQueryMode::Deterministic) { + return true; + } + const uint32_t framePeriod = settings.regirFiniteQueryFramePeriod > 0u + ? settings.regirFiniteQueryFramePeriod + : (hashGridActive ? 256u : 8u); + return temporalFrameIndex % framePeriod == framePeriod - 1u; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::Regir; + contract.role = RendererPassContractRole::LightingReuse; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/RegirPass.h (contract + mode/grid/capacity/resource-sizing/parameter/stage-readiness/query/promotion-gate/descriptor-binding policy), src/rtv/PathTracerRenderer.cpp (resource descriptor sources/scheduling)"; + contract.featureFlagsRequired = "lightingReuseMode == legacy-regir"; + contract.inputs = rendererContractArray({"scene light records", "environment distribution", "camera/world bounds", "previous ReGIR reservoirs"}); + contract.outputs = rendererContractArray({"grid metadata", "cell reservoirs", "environment reservoirs", "active cell feedback", "ReGIR counters"}); + contract.historyResources = rendererContractArray({"previous ReGIR reservoirs", "active/hash cell feedback", "environment/light generation"}); + contract.descriptorLayouts = rendererContractArray({"ReGIR descriptor set", "scene light descriptor set"}); + contract.pushConstants = rendererContractArray({"RegirParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/regir_build.comp", "shaders/regir_spatial_reuse.comp", "shaders/regir_temporal_reuse.comp"}); + contract.rendergraphReads = rendererContractArray({"scene lights", "previous regir reservoirs", "environment map"}); + contract.rendergraphWrites = rendererContractArray({"regir_reservoirs", "regir_environment_reservoirs", "regir_active_cells", "regir_hash_tables"}); + contract.requiredBarriers = rendererContractArray({"build to temporal/spatial reuse", "reuse to consumer sampling"}); + contract.cameraHistoryResetBehavior = "Invalidate temporal history when light or environment generation changes, grid dimensions change, scene bounds change, or camera reset requests it."; + contract.debugOutputs = rendererContractArray({"regir-grid-occupancy", "regir-selected-light", "regir-reservoir-weight", "regir-infinite-source", "regir-environment-pdf"}); + contract.profilingSections = rendererContractArray({"regir_build", "regir_temporal_reuse", "regir_spatial_reuse"}); + contract.validationChecks = rendererContractArray({"empty-light scene", "many-light stress", "environment-only", "hash/active grid saturation"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/RestirDIPass.h b/include/rtv/passes/RestirDIPass.h new file mode 100644 index 0000000..7de57fc --- /dev/null +++ b/include/rtv/passes/RestirDIPass.h @@ -0,0 +1,630 @@ +#pragma once + +#include "rtv/DescriptorWriter.h" +#include "rtv/GpuScene.h" +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include + +#include +#include +#include +#include + +namespace rtv::passes { + +struct RestirDIPass { + static constexpr const char* kContractId = "restir_di"; + static constexpr const char* kPassName = "RestirDIPass"; + static constexpr const char* kRole = "lighting_reuse"; + static constexpr const char* kExtractionState = + "contract-module plus legacy/estimator/stage-readiness/resource-sizing/parameter/debug/counter/history-slot/history-selection/light-history policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + enum class HistorySlot { + Primary, + Secondary, + }; + + static constexpr HistorySlot currentHistorySlot(uint32_t temporalFrameIndex) { + return (temporalFrameIndex & 1u) == 0u ? HistorySlot::Primary : HistorySlot::Secondary; + } + + static constexpr HistorySlot previousHistorySlot(uint32_t temporalFrameIndex) { + return currentHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? HistorySlot::Secondary + : HistorySlot::Primary; + } + + template + static const Resource& selectCurrentHistoryResource( + bool pingPongHistory, + uint32_t temporalFrameIndex, + const Resource& primary, + const Resource& secondary) { + if (!pingPongHistory) { + return primary; + } + return currentHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? primary + : secondary; + } + + template + static const Resource& selectPreviousHistoryResource( + bool pingPongHistory, + uint32_t temporalFrameIndex, + const Resource& primary, + const Resource& secondary) { + if (!pingPongHistory) { + return secondary; + } + return previousHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? primary + : secondary; + } + + template + static const Resource& selectFinalOutputResource( + bool pingPongHistory, + bool finalReservoirAliased, + uint32_t temporalFrameIndex, + const Resource& finalReservoir, + const Resource& previousReservoir, + const Resource& aliasedFinalReservoir) { + if (pingPongHistory) { + return selectCurrentHistoryResource( + true, + temporalFrameIndex, + finalReservoir, + previousReservoir); + } + return finalReservoirAliased ? aliasedFinalReservoir : finalReservoir; + } + + struct LightHistoryKey { + uint32_t identityLo = 0; + uint32_t identityHi = 0; + uint32_t generation = 0; + uint32_t kind = 0; + + constexpr bool operator<(const LightHistoryKey& rhs) const { + if (identityHi != rhs.identityHi) return identityHi < rhs.identityHi; + if (identityLo != rhs.identityLo) return identityLo < rhs.identityLo; + if (generation != rhs.generation) return generation < rhs.generation; + return kind < rhs.kind; + } + + constexpr bool operator==(const LightHistoryKey& rhs) const { + return identityLo == rhs.identityLo && + identityHi == rhs.identityHi && + generation == rhs.generation && + kind == rhs.kind; + } + }; + + struct StableLightKey { + uint32_t identityLo = 0; + uint32_t identityHi = 0; + uint32_t kind = 0; + + constexpr bool operator<(const StableLightKey& rhs) const { + if (identityHi != rhs.identityHi) return identityHi < rhs.identityHi; + if (identityLo != rhs.identityLo) return identityLo < rhs.identityLo; + return kind < rhs.kind; + } + + constexpr bool operator==(const StableLightKey& rhs) const { + return identityLo == rhs.identityLo && + identityHi == rhs.identityHi && + kind == rhs.kind; + } + }; + + static std::vector lightHistoryKeys(const std::vector& records) { + std::vector keys; + keys.reserve(records.size()); + for (const GpuLightRecord& record : records) { + keys.push_back(LightHistoryKey{ + record.identity.x, + record.identity.y, + record.identity.z, + record.metadata.x, + }); + } + std::sort(keys.begin(), keys.end()); + return keys; + } + + static std::vector stableLightKeys(const std::vector& records) { + std::vector keys; + keys.reserve(records.size()); + for (const GpuLightRecord& record : records) { + keys.push_back(StableLightKey{ + record.identity.x, + record.identity.y, + record.metadata.x, + }); + } + std::sort(keys.begin(), keys.end()); + return keys; + } + + static bool sameStableLightHistorySet( + const std::vector& previousRecords, + const std::vector& currentRecords) { + if (previousRecords.empty() || previousRecords.size() != currentRecords.size()) { + return false; + } + return lightHistoryKeys(previousRecords) == lightHistoryKeys(currentRecords); + } + + static bool sameStableLightIdentitySet( + const std::vector& previousRecords, + const std::vector& currentRecords) { + if (previousRecords.empty() || previousRecords.size() != currentRecords.size()) { + return false; + } + return stableLightKeys(previousRecords) == stableLightKeys(currentRecords); + } + + static bool preservesStableLightHistory( + const std::vector& previousRecords, + const std::vector& currentRecords) { + return sameStableLightHistorySet(previousRecords, currentRecords) || + sameStableLightIdentitySet(previousRecords, currentRecords); + } + + static constexpr bool isStandaloneMode(RestirDiMode mode) { + return mode == RestirDiMode::Production || + mode == RestirDiMode::ReferenceValidation || + mode == RestirDiMode::HybridCompare; + } + + static bool isActive(const RendererSettings& settings) { + return isStandaloneMode(settings.restirDiMode); + } + + static bool isAnyModeActive(const RendererSettings& settings) { + return settings.restirDiMode != RestirDiMode::Off; + } + + static constexpr bool usesDebugView(RendererDebugView view) { + return view == RendererDebugView::RestirDiSelectedLight || + view == RendererDebugView::RestirDiTarget || + view == RendererDebugView::RestirDiSourcePdf || + view == RendererDebugView::RestirDiVisibility || + view == RendererDebugView::RestirDiRejectionReason || + view == RendererDebugView::RestirDiTemporalAcceptance || + view == RendererDebugView::RestirDiSpatialAcceptance || + view == RendererDebugView::RestirDiFinalContribution || + view == RendererDebugView::RestirDiReceiverPosition || + view == RendererDebugView::RestirDiReceiverNormal || + view == RendererDebugView::RestirDiLightVersion || + view == RendererDebugView::RestirDiLightMapStatus || + view == RendererDebugView::RestirDiInitialReservoir || + view == RendererDebugView::RestirDiTemporalReservoir || + view == RendererDebugView::RestirDiSpatialReservoir || + view == RendererDebugView::RestirDiFinalReservoir || + view == RendererDebugView::RestirDiWeightSum || + view == RendererDebugView::RestirDiM || + view == RendererDebugView::RestirDiLightClass || + view == RendererDebugView::RestirDiAge || + view == RendererDebugView::RestirDiConfidence || + view == RendererDebugView::RestirDiReferenceDiff; + } + + static bool requestsCounterCollectionInAutoMode(const RendererSettings& settings) { + return usesDebugView(settings.debugView) || + settings.restirDiMode == RestirDiMode::ReferenceValidation || + settings.restirDiMode == RestirDiMode::HybridCompare; + } + + static bool supportsNewEstimatorMode( + const RendererSettings& settings, + uint32_t effectiveSamplesPerPixel, + bool rayQueryVisibilityUnsupported) { + const bool productionMode = isStandaloneMode(settings.restirDiMode); + const bool diagnosticView = usesDebugView(settings.debugView); + return (productionMode || diagnosticView) && + effectiveSamplesPerPixel == 1u && + !settings.homogeneousVolumeEnabled && + !rayQueryVisibilityUnsupported; + } + + static bool requestsEstimatorWork(const RendererSettings& settings, bool hasUsefulLightCandidate) { + const bool diagnosticOrValidation = usesDebugView(settings.debugView) || + settings.restirDiMode == RestirDiMode::ReferenceValidation || + settings.restirDiMode == RestirDiMode::HybridCompare; + return diagnosticOrValidation || hasUsefulLightCandidate; + } + + static bool canSkipCompactImportedEmissiveDirectSampling( + const RendererSettings& settings, + bool hasImportedEmissive, + bool hasNonCompactEmissive, + size_t lightRecordCount, + float maxImportedPower) { + return settings.compactImportedEmissiveTriangleSampling && + hasImportedEmissive && + !hasNonCompactEmissive && + lightRecordCount <= 16'384u && + maxImportedPower <= 1.0f; + } + + static constexpr uint32_t kCounterUintCount = 64u; + static constexpr VkDeviceSize kReceiverFullByteSize = 96u; + static constexpr VkDeviceSize kReceiverPackedByteSize = 48u; + static constexpr VkDeviceSize kReservoirFullByteSize = 112u; + static constexpr VkDeviceSize kReservoirPackedByteSize = 48u; + + static constexpr VkDeviceSize counterSlotByteSize() { + return sizeof(uint32_t) * kCounterUintCount; + } + + static constexpr VkDeviceSize counterSlotByteOffset(uint32_t temporalFrameIndex, uint32_t framesInFlight) { + return (temporalFrameIndex % framesInFlight) * counterSlotByteSize(); + } + + static constexpr VkDeviceSize counterBufferByteSize(uint32_t framesInFlight) { + const uint32_t slotCount = framesInFlight == 0u ? 1u : framesInFlight; + return counterSlotByteSize() * slotCount; + } + + static constexpr VkDeviceSize receiverStride(RestirDiReservoirLayout layout) { + return layout == RestirDiReservoirLayout::ValidationFull + ? kReceiverFullByteSize + : kReceiverPackedByteSize; + } + + static constexpr VkDeviceSize reservoirStride(RestirDiReservoirLayout layout) { + return layout == RestirDiReservoirLayout::ValidationFull + ? kReservoirFullByteSize + : kReservoirPackedByteSize; + } + + static constexpr VkDeviceSize pixelByteSize(VkDeviceSize pixelCount, VkDeviceSize strideBytes) { + return pixelCount * strideBytes; + } + + static constexpr VkDeviceSize receiverByteSize(VkDeviceSize pixelCount, RestirDiReservoirLayout layout) { + return pixelByteSize(pixelCount, receiverStride(layout)); + } + + static constexpr VkDeviceSize reservoirByteSize(VkDeviceSize pixelCount, RestirDiReservoirLayout layout) { + return pixelByteSize(pixelCount, reservoirStride(layout)); + } + + static constexpr VkDeviceSize sourcePixelByteSize(VkDeviceSize pixelCount) { + return pixelByteSize(pixelCount, sizeof(uint32_t)); + } + + static constexpr bool requiresDedicatedFinalReservoir( + bool finalReservoirAliased, + RestirHistoryCopyMode historyCopyMode) { + return !finalReservoirAliased || historyCopyMode == RestirHistoryCopyMode::PingPong; + } + + static constexpr VkDeviceSize finalReservoirByteSize( + VkDeviceSize pixelCount, + RestirDiReservoirLayout layout, + bool finalReservoirAliased, + RestirHistoryCopyMode historyCopyMode) { + return requiresDedicatedFinalReservoir(finalReservoirAliased, historyCopyMode) + ? reservoirByteSize(pixelCount, layout) + : 0u; + } + + struct ResourceByteSizes { + VkDeviceSize receiverBytes = 0; + VkDeviceSize reservoirBytes = 0; + VkDeviceSize sourcePixelBytes = 0; + VkDeviceSize finalReservoirBytes = 0; + VkDeviceSize counterBytes = 0; + }; + + static constexpr ResourceByteSizes resourceByteSizes( + VkDeviceSize pixelCount, + RestirDiReservoirLayout layout, + bool finalReservoirAliased, + RestirHistoryCopyMode historyCopyMode, + uint32_t framesInFlight) { + return ResourceByteSizes{ + receiverByteSize(pixelCount, layout), + reservoirByteSize(pixelCount, layout), + sourcePixelByteSize(pixelCount), + finalReservoirByteSize(pixelCount, layout, finalReservoirAliased, historyCopyMode), + counterBufferByteSize(framesInFlight), + }; + } + + static constexpr uint32_t kPreviousLightRecordCapacity = 16'384u; + + static constexpr VkDeviceSize previousLightRecordBufferByteSize() { + return static_cast(kPreviousLightRecordCapacity) * sizeof(GpuLightRecord); + } + + static uint32_t clampedLightRecordCount(size_t recordCount) { + return static_cast( + std::min(static_cast(recordCount), kPreviousLightRecordCapacity)); + } + + static VkDeviceSize previousLightRecordCopyBytes( + uint32_t lightRecordCount, + VkDeviceSize sourceBufferBytes, + VkDeviceSize destinationBufferBytes) { + const VkDeviceSize requestedBytes = + static_cast(lightRecordCount) * sizeof(GpuLightRecord); + return std::min(requestedBytes, std::min(sourceBufferBytes, destinationBufferBytes)); + } + + static constexpr bool previousLightHistoryValid(VkDeviceSize copyBytes) { + return copyBytes > 0u; + } + + struct ConfidenceReplayResources { + bool diFinalRuns = false; + bool currentReceiverReady = false; + bool previousReceiverReady = false; + bool finalReservoirReady = false; + bool previousLightRecordsReady = false; + bool previousLightHistoryValid = false; + }; + + static constexpr bool canRunConfidenceReplay(const ConfidenceReplayResources& resources) { + return resources.diFinalRuns && + resources.currentReceiverReady && + resources.previousReceiverReady && + resources.finalReservoirReady && + resources.previousLightRecordsReady && + resources.previousLightHistoryValid; + } + + static constexpr float kNormalCompatibilityThreshold = 0.85f; + static constexpr float kDepthCompatibilityThreshold = 0.05f; + static constexpr float kTemporalLuminanceLimitFactor = 8.0f; + static constexpr float kConfidenceDecay = 0.96f; + static constexpr float kLuminanceClampNeighborAverageFactor = 6.0f; + static constexpr float kLuminanceClampNeighborMaxFactor = 3.0f; + + struct ParameterInputs { + uint32_t width = 0; + uint32_t height = 0; + uint32_t frameIndex = 0; + uint32_t lightVersion = 0; + uint32_t environmentVersion = 0; + bool estimatorRuns = false; + bool temporalStageRuns = false; + bool historyValid = false; + bool materialVisibilityEnabled = false; + bool counterEnabled = false; + bool rawOutputIsCurrentSample = false; + }; + + static constexpr uint32_t encodedMode(const RendererSettings& settings) { + return settings.restirMode == RestirMode::HybridCompare || + settings.restirDiMode == RestirDiMode::HybridCompare + ? static_cast(RestirDiMode::HybridCompare) + : static_cast(settings.restirDiMode); + } + + template + static Params makeParams(const RendererSettings& settings, const ParameterInputs& inputs) { + Params params{}; + if (!inputs.estimatorRuns) { + return params; + } + + params.width = inputs.width; + params.height = inputs.height; + params.frameIndex = inputs.frameIndex; + params.enabled = 1u; + params.temporalMaxAge = inputs.temporalStageRuns ? settings.restirDiTemporalMaxAge : 0u; + params.spatialRounds = settings.restirDiSpatialRounds; + params.spatialMaxM = settings.restirDiMaxM; + params.visibilityPolicy = settings.restirDiFinalVisibilityEnabled ? 1u : 0u; + params.spatialRadius = settings.restirDiSpatialRadius; + params.normalThreshold = kNormalCompatibilityThreshold; + params.depthThreshold = kDepthCompatibilityThreshold; + params.temporalLuminanceLimitFactor = + settings.restirDiProductionStabilizationEnabled ? kTemporalLuminanceLimitFactor : 0.0f; + params.confidenceDecay = kConfidenceDecay; + params.lumClampNeighborAvgFactor = kLuminanceClampNeighborAverageFactor; + params.lumClampNeighborMaxFactor = kLuminanceClampNeighborMaxFactor; + params.fireflyClamp = settings.fireflyClamp; + params.productionClampLuminance = settings.restirDiProductionStabilizationEnabled + ? settings.restirDiClampLuminance + : 0.0f; + params.mode = encodedMode(settings); + params.spatialResultValid = settings.restirDiSpatialEnabled ? 1u : 0u; + params.visibilityRayBudget = settings.restirDiVisibilityRayBudget; + params.historyValid = inputs.historyValid ? 1u : 0u; + params.materialVisibilityFlags = inputs.materialVisibilityEnabled ? 1u : 0u; + params.counterEnabled = inputs.counterEnabled ? 1u : 0u; + params.rawOutputIsCurrentSample = inputs.rawOutputIsCurrentSample ? 1u : 0u; + params.shadowDistanceBias = settings.shadowDistanceBias; + params.lightVersion = inputs.lightVersion; + params.environmentVersion = inputs.environmentVersion; + return params; + } + + struct SceneDescriptorBindings { + VkDescriptorBufferInfo lightRecords{}; + VkDescriptorBufferInfo meshParams{}; + VkDescriptorBufferInfo materials{}; + VkDescriptorBufferInfo rtTriangleMaterialIds{}; + VkDescriptorBufferInfo instanceRecords{}; + VkDescriptorBufferInfo meshRecords{}; + VkDescriptorBufferInfo localVertices{}; + VkDescriptorBufferInfo localIndices{}; + VkDescriptorBufferInfo geometryTriangleOffsets{}; + VkDescriptorBufferInfo meshGeometryRanges{}; + VkDescriptorBufferInfo tlasGeometryRanges{}; + VkAccelerationStructureKHR tlas = VK_NULL_HANDLE; + }; + + static void writeSceneDescriptors(DescriptorWriter& writer, const SceneDescriptorBindings& scene) { + writer + .writeBuffer(12, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.lightRecords) + .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene.meshParams) + .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.materials) + .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.rtTriangleMaterialIds) + .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.instanceRecords) + .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.meshRecords) + .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.localVertices) + .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.localIndices) + .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.geometryTriangleOffsets) + .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.meshGeometryRanges) + .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.tlasGeometryRanges) + .writeAccelerationStructure(10, scene.tlas); + } + + static bool requestsTemporalStage(const RendererSettings& settings) { + return settings.restirDiTemporalEnabled; + } + + static bool requestsSpatialStage(const RendererSettings& settings) { + return settings.restirDiSpatialEnabled; + } + + struct TemporalStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool currentReceiverReady = false; + bool initialReservoirReady = false; + bool previousReservoirReady = false; + bool previousReceiverReady = false; + bool tlasReady = false; + }; + + static bool canRunTemporalStage( + const RendererSettings& settings, + bool estimatorRequested, + const TemporalStageResources& resources) { + return estimatorRequested && + requestsTemporalStage(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.currentReceiverReady && + resources.initialReservoirReady && + resources.previousReservoirReady && + resources.previousReceiverReady && + resources.tlasReady; + } + + struct SpatialStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool temporalReservoirReady = false; + bool currentReceiverReady = false; + bool tlasReady = false; + }; + + static bool canRunSpatialStage( + const RendererSettings& settings, + bool estimatorRequested, + const SpatialStageResources& resources) { + return estimatorRequested && + requestsSpatialStage(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.temporalReservoirReady && + resources.currentReceiverReady && + resources.tlasReady; + } + + struct FinalStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool spatialReservoirReady = false; + bool currentReceiverReady = false; + bool rawImageReady = false; + bool pathDataReady = false; + bool accumulationReady = false; + bool tlasReady = false; + }; + + static bool canRunFinalStage( + bool estimatorRequested, + const FinalStageResources& resources) { + return estimatorRequested && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.spatialReservoirReady && + resources.currentReceiverReady && + resources.rawImageReady && + resources.pathDataReady && + resources.accumulationReady && + resources.tlasReady; + } + + static bool requestsLegacySpatialStage(const RendererSettings& settings, bool adaptiveSkipSpatial) { + return !settings.wavefrontFinalOutputEnabled && + !adaptiveSkipSpatial && + settings.restirDiMode == RestirDiMode::Legacy && + settings.restirMode != RestirMode::ClassicNee; + } + + static bool canAliasFinalReservoir(const RendererSettings& settings, bool resourceAliasingEnabled) { + return resourceAliasingEnabled && + settings.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong && + settings.restirDiTemporalEnabled && + settings.restirDiSpatialEnabled; + } + + static bool requestsSharedHistoryPingPong( + const RendererSettings& settings, + bool diHistoryActive, + bool giHistoryActive) { + return settings.restirHistoryCopyMode == RestirHistoryCopyMode::PingPong && + !settings.wavefrontFinalOutputEnabled && + (diHistoryActive || giHistoryActive); + } + + static RestirHistoryCopyMode effectiveHistoryCopyMode(bool pingPongReady) { + return pingPongReady ? RestirHistoryCopyMode::PingPong : RestirHistoryCopyMode::Copy; + } + + static const char* sharedHistoryPingPongFallbackReason( + const RendererSettings& settings, + bool pingPongReady, + bool diHistoryActive, + bool giHistoryActive) { + if (settings.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong || pingPongReady) { + return nullptr; + } + if (settings.wavefrontFinalOutputEnabled) { + return "wavefront final output is enabled"; + } + if (!diHistoryActive && !giHistoryActive) { + return "no production ReSTIR history path is active"; + } + return nullptr; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::RestirDI; + contract.role = RendererPassContractRole::LightingReuse; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/RestirDIPass.h (contract + legacy/estimator/light-candidate/stage-readiness/resource-sizing/parameter/debug/counter/history-slot/light-history/descriptor-binding policy), src/rtv/PathTracerRenderer.cpp (resource descriptor sources/scheduling)"; + contract.featureFlagsRequired = "restirDiMode != off"; + contract.inputs = rendererContractArray({"surface receiver buffer", "scene light records", "previous DI reservoirs", "ReGIR optional service"}); + contract.outputs = rendererContractArray({"initial reservoirs", "temporal reservoirs", "spatial reservoirs", "final reservoirs", "source pixel buffers", "DI counters"}); + contract.historyResources = rendererContractArray({"current/previous DI reservoirs", "current/previous DI receiver buffers", "deterministic previous-light remap table", "light identity generation"}); + contract.descriptorLayouts = rendererContractArray({"ReSTIR DI descriptor set", "scene/light descriptor sets"}); + contract.pushConstants = rendererContractArray({"RestirDiParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/restir_di_temporal.comp", "shaders/restir_di_spatial.comp", "shaders/restir_di_final.comp"}); + contract.rendergraphReads = rendererContractArray({"restir_di_receiver", "restir_di_previous", "scene_lights", "regir_reservoirs"}); + contract.rendergraphWrites = rendererContractArray({"restir_di_initial", "restir_di_temporal", "restir_di_spatial", "restir_di_final", "restir_di_counters"}); + contract.requiredBarriers = rendererContractArray({"initial to temporal", "temporal to spatial", "spatial to final", "final to history copy"}); + contract.cameraHistoryResetBehavior = "Reject or reset history on camera cut, light topology/generation change, scene reload, material reload, or incompatible surface reprojection."; + contract.debugOutputs = rendererContractArray({"restir-di-selected-light", "restir-di-age", "restir-di-m", "restir-di-confidence", "restir-di-rejection-reason", "restir-di-light-map-status"}); + contract.profilingSections = rendererContractArray({"restir_di_temporal", "restir_di_spatial", "restir_di_final", "restir_di_history_copy", "restir_di_counters_readback"}); + contract.validationChecks = rendererContractArray({"invalid reservoir count", "temporal accepted/rejected", "spatial accepted/rejected", "source PDF/target PDF parity", "current/previous stable-light identity remap"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/RestirGIPass.h b/include/rtv/passes/RestirGIPass.h new file mode 100644 index 0000000..a57163a --- /dev/null +++ b/include/rtv/passes/RestirGIPass.h @@ -0,0 +1,437 @@ +#pragma once + +#include "rtv/DescriptorWriter.h" +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +#include + +#include +#include + +namespace rtv::passes { + +struct RestirGIPass { + static constexpr const char* kContractId = "restir_gi"; + static constexpr const char* kPassName = "RestirGIPass"; + static constexpr const char* kRole = "lighting_reuse"; + static constexpr const char* kExtractionState = + "contract-module plus mode/runtime resource-sizing/parameter/stage-readiness/counter/history-slot/history-selection policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + enum class HistorySlot { + Primary, + Secondary, + }; + + enum class ProductionHistorySource { + SpatialHistory, + TemporalReservoir, + }; + + static constexpr uint32_t kActiveTileSize = 16u; + + static uint32_t reuseWidth(uint32_t renderWidth, bool halfResolution) { + return halfResolution ? (renderWidth + 1u) / 2u : renderWidth; + } + + static uint32_t reuseHeight(uint32_t renderHeight, bool halfResolution) { + return halfResolution ? (renderHeight + 1u) / 2u : renderHeight; + } + + static VkDeviceSize pixelByteSize(uint64_t pixelCount, VkDeviceSize strideBytes) { + return static_cast(pixelCount) * strideBytes; + } + + static VkDeviceSize productionReservoirByteSize( + uint32_t renderWidth, + uint32_t renderHeight, + bool halfResolution, + VkDeviceSize reservoirStrideBytes) { + return static_cast(reuseWidth(renderWidth, halfResolution)) * + static_cast(reuseHeight(renderHeight, halfResolution)) * + reservoirStrideBytes; + } + + static VkDeviceSize activeTileMaskByteSize(uint32_t renderWidth, uint32_t renderHeight) { + const uint32_t columns = (renderWidth + kActiveTileSize - 1u) / kActiveTileSize; + const uint32_t rows = (renderHeight + kActiveTileSize - 1u) / kActiveTileSize; + return std::max(1u, static_cast(columns) * rows) * sizeof(uint32_t); + } + + struct SpatialParameterInputs { + uint32_t width = 0; + uint32_t height = 0; + uint32_t frameIndex = 0; + bool legacySpatialReuseRuns = false; + bool giReservoirsRun = false; + bool giHalfResolution = false; + bool rawOutputIsCurrentSample = false; + }; + + template + static Params makeSpatialParams( + const RendererSettings& settings, + const SpatialParameterInputs& inputs, + const CameraPosition& cameraPosition) { + Params params{}; + params.width = inputs.width; + params.height = inputs.height; + params.frameCount = inputs.frameIndex; + const uint32_t restirReuseEnabled = (inputs.legacySpatialReuseRuns || inputs.giReservoirsRun) ? 1u : 0u; + params.enabled = restirReuseEnabled | + ((inputs.giReservoirsRun && settings.restirGiFinalStabilizationEnabled) ? 2u : 0u); + params.giSpatialRounds = settings.restirGiSpatialRounds; + params.giHalfResolution = inputs.giHalfResolution ? 1u : 0u; + params.giTemporalMaxAge = settings.restirGiTemporalMaxAge; + params.giVisibilityRayBudget = settings.restirGiVisibilityRayBudget; + params.giSpatialRadius = settings.restirGiSpatialRadius; + params.giDepthThresholdScale = settings.restirGiDepthThresholdScale; + params.giSpatialCompatibilityThreshold = settings.restirGiSpatialCompatibilityThreshold; + params.rawOutputIsCurrentSample = inputs.rawOutputIsCurrentSample ? 1.0f : 0.0f; + params.cameraPosition = cameraPosition; + return params; + } + + static constexpr HistorySlot currentHistorySlot(uint32_t temporalFrameIndex) { + return (temporalFrameIndex & 1u) == 0u ? HistorySlot::Primary : HistorySlot::Secondary; + } + + static constexpr HistorySlot previousHistorySlot(uint32_t temporalFrameIndex) { + return currentHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? HistorySlot::Secondary + : HistorySlot::Primary; + } + + template + static const Resource& selectCurrentHistoryResource( + bool pingPongHistory, + uint32_t temporalFrameIndex, + const Resource& primary, + const Resource& secondary) { + if (!pingPongHistory) { + return primary; + } + return currentHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? primary + : secondary; + } + + template + static const Resource& selectPreviousHistoryResource( + bool pingPongHistory, + uint32_t temporalFrameIndex, + const Resource& primary, + const Resource& secondary) { + if (!pingPongHistory) { + return secondary; + } + return previousHistorySlot(temporalFrameIndex) == HistorySlot::Primary + ? primary + : secondary; + } + + static constexpr ProductionHistorySource productionHistorySource(bool spatialStageRuns) { + return spatialStageRuns + ? ProductionHistorySource::SpatialHistory + : ProductionHistorySource::TemporalReservoir; + } + + static constexpr bool usesTemporalReservoirAsProductionHistorySource(bool spatialStageRuns) { + return productionHistorySource(spatialStageRuns) == ProductionHistorySource::TemporalReservoir; + } + + template + static const Resource& selectProductionHistorySource( + bool spatialStageRuns, + const Resource& spatialHistory, + const Resource& temporalReservoir) { + return productionHistorySource(spatialStageRuns) == ProductionHistorySource::SpatialHistory + ? spatialHistory + : temporalReservoir; + } + + static bool isActive(const RendererSettings& settings) { + return settings.restirGiEnabled && settings.restirGiMode != RestirGiMode::Off; + } + + static bool isLegacyCacheMode(const RendererSettings& settings) { + return settings.restirGiMode == RestirGiMode::LegacyCache; + } + + static bool isProductionMode(const RendererSettings& settings) { + return settings.restirGiMode == RestirGiMode::Production; + } + + static bool isReferenceValidationMode(const RendererSettings& settings) { + return settings.restirGiMode == RestirGiMode::ReferenceValidation; + } + + static bool isNewReservoirMode(const RendererSettings& settings) { + return isProductionMode(settings) || isReferenceValidationMode(settings); + } + + static constexpr bool usesSpatialDebugView(RendererDebugView view) { + return view == RendererDebugView::RestirGiSpatial || + view == RendererDebugView::RestirGiGrid || + view == RendererDebugView::RestirGiPathClass || + view == RendererDebugView::RestirGiTarget || + view == RendererDebugView::RestirGiSourcePdf || + view == RendererDebugView::RestirGiWeightSum || + view == RendererDebugView::RestirGiM || + view == RendererDebugView::RestirGiConfidence || + view == RendererDebugView::RestirGiVisibility; + } + + static constexpr bool usesDebugView(RendererDebugView view) { + return view == RendererDebugView::RestirGiValidity || + view == RendererDebugView::RestirGiAge || + view == RendererDebugView::RestirGiInitial || + view == RendererDebugView::RestirGiTemporal || + view == RendererDebugView::RestirGiSpatial || + view == RendererDebugView::RestirGiFinal || + view == RendererDebugView::RestirGiNormal || + view == RendererDebugView::RestirGiHitDistance || + view == RendererDebugView::RestirGiGrid || + view == RendererDebugView::RestirGiPathClass || + view == RendererDebugView::RestirGiTarget || + view == RendererDebugView::RestirGiSourcePdf || + view == RendererDebugView::RestirGiWeightSum || + view == RendererDebugView::RestirGiM || + view == RendererDebugView::RestirGiConfidence || + view == RendererDebugView::RestirGiVisibility || + view == RendererDebugView::WavefrontRestirGi; + } + + static constexpr bool usesLegacyFinalDebugView(RendererDebugView view) { + return view == RendererDebugView::RestirGiFinal || + usesSpatialDebugView(view); + } + + static bool usesUncompressedInitialReservoir( + const RendererSettings& settings, + bool environmentOverride) { + return environmentOverride || + isProductionMode(settings) || + settings.restirGiReservoirLayout == RestirGiReservoirLayout::ValidationFull; + } + + static bool effectiveHalfResolution(const RendererSettings& settings, uint32_t memoryPressureTier) { + return settings.restirGiHalfResolution || memoryPressureTier > 0u; + } + + static constexpr uint32_t kCounterUintCount = 64u; + + static constexpr VkDeviceSize counterSlotByteSize() { + return sizeof(uint32_t) * kCounterUintCount; + } + + static constexpr VkDeviceSize counterSlotByteOffset(uint32_t temporalFrameIndex, uint32_t framesInFlight) { + return (temporalFrameIndex % framesInFlight) * counterSlotByteSize(); + } + + struct SceneDescriptorBindings { + VkDescriptorBufferInfo meshParams{}; + VkDescriptorBufferInfo materials{}; + VkDescriptorBufferInfo rtTriangleMaterialIds{}; + VkDescriptorBufferInfo instanceRecords{}; + VkDescriptorBufferInfo meshRecords{}; + VkDescriptorBufferInfo localVertices{}; + VkDescriptorBufferInfo localIndices{}; + VkDescriptorBufferInfo geometryTriangleOffsets{}; + VkDescriptorBufferInfo meshGeometryRanges{}; + VkDescriptorBufferInfo tlasGeometryRanges{}; + VkAccelerationStructureKHR tlas = VK_NULL_HANDLE; + }; + + static void writeSceneDescriptors(DescriptorWriter& writer, const SceneDescriptorBindings& scene) { + writer + .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene.meshParams) + .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.materials) + .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.rtTriangleMaterialIds) + .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.instanceRecords) + .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.meshRecords) + .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.localVertices) + .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.localIndices) + .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.geometryTriangleOffsets) + .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.meshGeometryRanges) + .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene.tlasGeometryRanges) + .writeAccelerationStructure(33, scene.tlas); + } + + static bool requestsReservoirs(const RendererSettings& settings) { + return !settings.wavefrontFinalOutputEnabled && + (settings.restirGiMode != RestirGiMode::Off || + usesDebugView(settings.debugView)); + } + + static bool requestsLegacyFinal(const RendererSettings& settings) { + return !settings.wavefrontFinalOutputEnabled && + (isLegacyCacheMode(settings) || usesLegacyFinalDebugView(settings.debugView)); + } + + static bool requestsProductionTemporal(const RendererSettings& settings) { + return !settings.wavefrontFinalOutputEnabled && isNewReservoirMode(settings); + } + + struct TemporalStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool initialReservoirReady = false; + bool previousProductionHistoryReady = false; + bool temporalReservoirReady = false; + bool currentReceiverReady = false; + bool previousReceiverReady = false; + bool tlasReady = false; + }; + + static bool canRunProductionTemporal( + const RendererSettings& settings, + const TemporalStageResources& resources) { + return requestsProductionTemporal(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.initialReservoirReady && + resources.previousProductionHistoryReady && + resources.temporalReservoirReady && + resources.currentReceiverReady && + resources.previousReceiverReady && + resources.tlasReady; + } + + static bool requestsProductionSpatial(const RendererSettings& settings, bool referenceValidation) { + return requestsProductionTemporal(settings) && + requestsSpatialStage(settings, referenceValidation); + } + + struct SpatialStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool temporalReservoirReady = false; + bool currentProductionHistoryReady = false; + bool currentReceiverReady = false; + bool tlasReady = false; + }; + + static bool canRunProductionSpatial( + const RendererSettings& settings, + bool referenceValidation, + const SpatialStageResources& resources) { + return requestsProductionSpatial(settings, referenceValidation) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.temporalReservoirReady && + resources.currentProductionHistoryReady && + resources.currentReceiverReady && + resources.tlasReady; + } + + static bool requestsProductionFinal(const RendererSettings& settings) { + return requestsProductionTemporal(settings); + } + + struct FinalStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool upsampleStageRuns = false; + bool upsampledReservoirReady = false; + bool temporalReservoirReady = false; + bool currentProductionHistoryReady = false; + bool currentReceiverReady = false; + bool rawImageReady = false; + bool pathDataReady = false; + bool accumulationReady = false; + }; + + static bool canRunProductionFinal( + const RendererSettings& settings, + const FinalStageResources& resources) { + const bool finalReservoirReady = resources.upsampleStageRuns + ? resources.upsampledReservoirReady + : (resources.temporalReservoirReady && resources.currentProductionHistoryReady); + return requestsProductionFinal(settings) && + resources.pipelineReady && + resources.descriptorLayoutReady && + finalReservoirReady && + resources.currentReceiverReady && + resources.rawImageReady && + resources.pathDataReady && + resources.accumulationReady; + } + + static bool requestsUpsample( + const RendererSettings& settings, + bool /*referenceValidation*/, + bool halfResolution) { + return requestsProductionTemporal(settings) && + halfResolution; + } + + struct UpsampleStageResources { + bool pipelineReady = false; + bool descriptorLayoutReady = false; + bool temporalReservoirReady = false; + bool currentProductionHistoryReady = false; + bool upsampledReservoirReady = false; + bool currentReceiverReady = false; + }; + + static bool canRunUpsample( + const RendererSettings& settings, + bool referenceValidation, + bool halfResolution, + const UpsampleStageResources& resources) { + return requestsUpsample(settings, referenceValidation, halfResolution) && + resources.pipelineReady && + resources.descriptorLayoutReady && + resources.temporalReservoirReady && + resources.currentProductionHistoryReady && + resources.upsampledReservoirReady && + resources.currentReceiverReady; + } + + static bool requestsSpatialStage(const RendererSettings& settings, bool referenceValidation) { + return settings.restirGiSpatialRounds > 0u || + usesSpatialDebugView(settings.debugView) || + referenceValidation; + } + + static bool requestsActiveTileMask( + const RendererSettings& settings, + bool autoEnabled, + bool referenceValidation) { + const bool modeEnabled = settings.restirGiActiveTileMaskMode == RestirGiActiveTileMaskMode::On || + (settings.restirGiActiveTileMaskMode == RestirGiActiveTileMaskMode::Auto && autoEnabled); + return modeEnabled && !referenceValidation; + } + + static bool requestsCounterCollectionInAutoMode(const RendererSettings& settings) { + return usesDebugView(settings.debugView) || + settings.restirGiMode == RestirGiMode::ReferenceValidation; + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::RestirGI; + contract.role = RendererPassContractRole::LightingReuse; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/RestirGIPass.h (contract + mode/runtime/effective-resolution/resource-sizing/parameter/stage-readiness/counter/history-slot/descriptor-binding policy), src/rtv/PathTracerRenderer.cpp (resource descriptor sources/scheduling)"; + contract.featureFlagsRequired = "restirGiEnabled && restirGiMode != off"; + contract.inputs = rendererContractArray({"GI receiver buffer", "path-traced sample data", "previous GI reservoirs", "motion/depth/normal guides"}); + contract.outputs = rendererContractArray({"temporal GI reservoirs", "spatial GI reservoirs", "upsampled GI reservoirs", "GI final contribution", "GI counters"}); + contract.historyResources = rendererContractArray({"previous GI production reservoirs", "previous GI receiver buffer", "GI active tile mask"}); + contract.descriptorLayouts = rendererContractArray({"ReSTIR GI descriptor set", "guide image descriptor set"}); + contract.pushConstants = rendererContractArray({"RestirGiParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/restir_gi_temporal.comp", "shaders/restir_gi_spatial_production.comp", "shaders/restir_gi_final_production.comp"}); + contract.rendergraphReads = rendererContractArray({"restir_gi_previous", "restir_gi_receiver", "path sample data", "temporal guides"}); + contract.rendergraphWrites = rendererContractArray({"restir_gi_temporal", "restir_gi_spatial", "restir_gi_final", "restir_gi_counters"}); + contract.requiredBarriers = rendererContractArray({"temporal to spatial", "half-res upsample to full-res", "final to denoiser"}); + contract.cameraHistoryResetBehavior = "Reject history on camera cut, disocclusion, normal/depth/material/object mismatch, resolution change, or frame-count reset."; + contract.debugOutputs = rendererContractArray({"restir-gi-validity", "restir-gi-age", "restir-gi-confidence", "restir-gi-path-class", "restir-gi-hit-distance"}); + contract.profilingSections = rendererContractArray({"restir_gi_temporal", "restir_gi_spatial", "restir_gi_upsample", "restir_gi_final", "restir_gi_counters_readback"}); + contract.validationChecks = rendererContractArray({"invalid receiver count", "temporal rejection reasons", "spatial rejection reasons", "half-res grid score"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/include/rtv/passes/TemporalAAPass.h b/include/rtv/passes/TemporalAAPass.h new file mode 100644 index 0000000..a4d2704 --- /dev/null +++ b/include/rtv/passes/TemporalAAPass.h @@ -0,0 +1,62 @@ +#pragma once + +#include "rtv/RendererPassContractTypes.h" +#include "rtv/RendererSettings.h" + +namespace rtv::passes { + +struct TemporalAAPass { + static constexpr const char* kContractId = "temporal_aa"; + static constexpr const char* kPassName = "TemporalAAPass"; + static constexpr const char* kRole = "temporal"; + static constexpr const char* kExtractionState = + "contract-module plus temporal resolve run policy; scheduling and GPU resources remain coordinated by PathTracerRenderer"; + + static bool isActive(const RendererSettings& settings) { + return settings.taaEnabled && + (settings.temporalUpscaler == TemporalUpscaler::TaaTsr || + settings.temporalUpscaler == TemporalUpscaler::Nis); + } + + static constexpr bool supportsTemporalResolve(TemporalUpscaler effectiveUpscaler) { + return effectiveUpscaler == TemporalUpscaler::TaaTsr || + effectiveUpscaler == TemporalUpscaler::Nis; + } + + static bool requestsRun( + const RendererSettings& settings, + bool bypassForDebugView, + TemporalUpscaler effectiveUpscaler) { + return !settings.wavefrontFinalOutputEnabled && + settings.pathTracingEnabled && + settings.taaEnabled && + !bypassForDebugView && + supportsTemporalResolve(effectiveUpscaler); + } + + static RendererPassContract contract(const RendererSettings& settings) { + RendererPassContract contract; + contract.id = RendererPassContractId::TemporalAA; + contract.role = RendererPassContractRole::Temporal; + contract.name = kPassName; + contract.activeByCurrentSettings = isActive(settings); + contract.currentOwnerFile = "include/rtv/passes/TemporalAAPass.h (contract + run policy), src/rtv/PathTracerRenderer.cpp (resources/scheduling)"; + contract.featureFlagsRequired = "taaEnabled && temporalUpscaler in {taa-tsr, nis}"; + contract.inputs = rendererContractArray({"current color", "motion vectors", "depth", "reactive mask", "previous TAA history"}); + contract.outputs = rendererContractArray({"TAA/TSR color", "history weight", "reprojection confidence"}); + contract.historyResources = rendererContractArray({"taa history image"}); + contract.descriptorLayouts = rendererContractArray({"TAA descriptor set"}); + contract.pushConstants = rendererContractArray({"TaaParams"}); + contract.pipelineShaderDependencies = rendererContractArray({"shaders/taa.comp", "shaders/temporal_common.glsl"}); + contract.rendergraphReads = rendererContractArray({"denoised/raw color", "motion vectors", "depth", "taa history"}); + contract.rendergraphWrites = rendererContractArray({"taa output", "taa history copy"}); + contract.requiredBarriers = rendererContractArray({"denoiser to TAA", "TAA to history copy", "TAA to tonemap"}); + contract.cameraHistoryResetBehavior = "Reset on camera cut, resolution/render-scale change, jitter reset, scene/material/light reload, or explicit accumulation reset."; + contract.debugOutputs = rendererContractArray({"motion-vectors", "reprojection-confidence", "temporal-history-weight", "temporal-reactive-mask"}); + contract.profilingSections = rendererContractArray({"taa", "taa_history_copy"}); + contract.validationChecks = rendererContractArray({"static camera convergence", "slow pan preservation", "camera cut rejection", "disocclusion rejection"}); + return contract; + } +}; + +} // namespace rtv::passes diff --git a/shaders/denoiser.comp b/shaders/denoiser.comp index 59bff56..af2eef7 100644 --- a/shaders/denoiser.comp +++ b/shaders/denoiser.comp @@ -79,6 +79,7 @@ const int DENOISER_SHARED_COUNT = DENOISER_SHARED_SIZE_X * DENOISER_SHARED_SIZE_ shared vec4 shared_color_tile[DENOISER_SHARED_COUNT]; shared uvec4 shared_depth_normal_tile[DENOISER_SHARED_COUNT]; +shared vec2 shared_hit_distance_tile[DENOISER_SHARED_COUNT]; int denoiser_shared_index(ivec2 tileCoords) { return tileCoords.y * DENOISER_SHARED_SIZE_X + tileCoords.x; @@ -137,13 +138,17 @@ vec3 decode_world_position(uvec2 packedValue, vec3 origin) { } vec2 xy = unpack_snorm2x16(packedValue.x); float z = unpack_snorm2x16(packedValue.y).x; - return origin + vec3(xy.x, xy.y, z) * 8.0; + return origin + vec3(xy.x, xy.y, z) * 64.0; } vec3 reconstruct_current_world_position(ivec2 coords, float depth, ivec2 dims) { if (!is_surface_depth(depth)) { return vec3(0.0, 1.0e10, 0.0); } + vec3 packedWorld = decode_world_position(world_position[uint(coords.y) * uint(dims.x) + uint(coords.x)], prev_camera.current_pos.xyz); + if (packedWorld.y < 1.0e9) { + return packedWorld; + } vec2 uv = (vec2(coords) + vec2(0.5)) / vec2(dims); vec2 ndc = uv * 2.0 - 1.0; @@ -323,6 +328,7 @@ void main() { uint sample_index = uint(sample_coords.y) * uint(dims.x) + uint(sample_coords.x); shared_color_tile[i] = imageLoad(input_color, sample_coords); shared_depth_normal_tile[i] = input_depth_normal[sample_index]; + shared_hit_distance_tile[i] = path_data_buffer[sample_index].denoiser_hit_distance.xy; } barrier(); #endif @@ -347,21 +353,31 @@ void main() { } PathDataRecord path_data = path_data_buffer[pixel_index]; - float center_primary_hit_distance = path_data.denoiser_hit_distance.x; - float center_secondary_hit_distance = path_data.denoiser_hit_distance.y; + vec4 center_hit_distance = path_data.denoiser_hit_distance; + vec4 center_direct_diffuse = path_data.direct_diffuse; + vec4 center_direct_specular = path_data.direct_specular; + vec4 center_indirect_diffuse = path_data.indirect_diffuse; + vec4 center_indirect_specular = path_data.indirect_specular; + vec4 center_albedo_roughness = path_data.albedo_roughness_hit_confidence; + vec4 center_emissive_residual = path_data.emissive_residual; + float center_primary_hit_distance = center_hit_distance.x; + float center_secondary_hit_distance = center_hit_distance.y; float center_primary_hit_normalized = temporal_normalized_hit_distance(center_primary_hit_distance); float center_secondary_hit_normalized = temporal_normalized_hit_distance(center_secondary_hit_distance); - float specular_lum = luminance(path_data.direct_specular.rgb + path_data.indirect_specular.rgb); - float diffuse_lum = luminance(path_data.direct_diffuse.rgb + path_data.indirect_diffuse.rgb); + float specular_lum = luminance(center_direct_specular.rgb + center_indirect_specular.rgb); + float diffuse_lum = luminance(center_direct_diffuse.rgb + center_indirect_diffuse.rgb); float specular_signal = clamp(specular_lum / max(specular_lum + diffuse_lum, 1.0e-4), 0.0, 1.0); - float emissive_lum = max(path_data.direct_diffuse.a, 0.0); + float emissive_lum = max(center_direct_diffuse.a, 0.0); float emissive_log = log2(max(emissive_lum, 0.0) + 1.0) / 8.0; float emissive_signal = smoothstep(0.10, 0.55, emissive_log); float distance_kernel_bias = center_roughness * smoothstep(0.12, 0.55, center_primary_hit_normalized); float center_lum = luminance(center_color); float var_factor = sqrt(max(center_var, 0.0)); - vec2 velocity_pixels = temporal_unpack_velocity_pixels(velocity_buffer[pixel_index], TEMPORAL_VELOCITY_PACK_SCALE); + vec2 raw_velocity_pixels = temporal_unpack_velocity_pixels(velocity_buffer[pixel_index], TEMPORAL_VELOCITY_PACK_SCALE); + vec2 velocity_pixels = temporal_velocity_pixels_are_invalid(raw_velocity_pixels, TEMPORAL_VELOCITY_PACK_SCALE) + ? vec2(0.0) + : raw_velocity_pixels; float motion_filter_boost = smoothstep(0.75, 12.0, length(velocity_pixels)); float motion_history_available = params.reset_history == 0u ? motion_filter_boost : 0.0; float inv_strength = 1.0 / max(params.strength, 0.001); @@ -406,25 +422,28 @@ void main() { #if RTV_DENOISER_SHARED_TILE vec3 sample_color; vec4 sample_dn; + vec2 sample_hit_distance; if (iter == 0u) { ivec2 tile_coords = ivec2(gl_LocalInvocationID.xy) + ivec2(kx + DENOISER_TILE_RADIUS, ky + DENOISER_TILE_RADIUS); int tile_index = denoiser_shared_index(tile_coords); sample_color = adaptive_fill_input_color(sample_coords, shared_color_tile[tile_index], dims); sample_dn = decode_depth_normal(shared_depth_normal_tile[tile_index]); + sample_hit_distance = shared_hit_distance_tile[tile_index]; } else { sample_color = adaptive_fill_input_color(sample_coords, imageLoad(input_color, sample_coords), dims); sample_dn = decode_depth_normal(input_depth_normal[sample_index]); + sample_hit_distance = path_data_buffer[sample_index].denoiser_hit_distance.xy; } #else vec3 sample_color = adaptive_fill_input_color(sample_coords, imageLoad(input_color, sample_coords), dims); vec4 sample_dn = decode_depth_normal(input_depth_normal[sample_index]); + vec2 sample_hit_distance = path_data_buffer[sample_index].denoiser_hit_distance.xy; #endif - PathDataRecord sample_path_data = path_data_buffer[sample_index]; float sample_depth = sample_dn.r; vec3 sample_normal = sample_dn.gba; float sample_lum = luminance(sample_color); - float sample_primary_hit_normalized = temporal_normalized_hit_distance(sample_path_data.denoiser_hit_distance.x); - float sample_secondary_hit_normalized = temporal_normalized_hit_distance(sample_path_data.denoiser_hit_distance.y); + float sample_primary_hit_normalized = temporal_normalized_hit_distance(sample_hit_distance.x); + float sample_secondary_hit_normalized = temporal_normalized_hit_distance(sample_hit_distance.y); float kernel_w = atrous_kernel_weight(kx) * atrous_kernel_weight(ky); if (abs(kx) == 2 || abs(ky) == 2) { @@ -590,9 +609,11 @@ void main() { float lum_delta = abs(history_lum_before_clip - center_lum) / (max(center_lum, history_lum_before_clip) + 0.02); float clipped_lum_delta = abs(history_lum_before_clip - history_lum_after_clip) / (history_lum_before_clip + 0.02); float neighborhood_lum_delta = abs(history_lum_before_clip - lum_mean) / (lum_mean + lum_sigma + 0.02); + float color_reactive_mask = temporal_reactive_weight(lum_delta, clipped_lum_delta, neighborhood_lum_delta); + float static_reactive_damping = mix(0.45, 1.0, motion_filter_boost); float reactive_mask = max( - temporal_reactive_weight(lum_delta, clipped_lum_delta, neighborhood_lum_delta), - clamp(path_data.emissive_residual.a, 0.0, 1.0)); + color_reactive_mask * static_reactive_damping, + clamp(center_emissive_residual.a, 0.0, 1.0)); vec2 m_dd = imageLoad(direct_diffuse_moments, coords).rg; vec2 m_ds = imageLoad(direct_specular_moments, coords).rg; @@ -602,8 +623,8 @@ void main() { uint n_is = hl.a & 0x7FFFu; bool moment_valid = (hl.a & 0x8000u) != 0u; - float diffuse_mean_lum = m_dd.x * path_data.direct_diffuse.a + m_id.x * path_data.indirect_diffuse.a; - float specular_mean_lum = m_ds.x * path_data.direct_specular.a + m_is.x * path_data.indirect_specular.a; + float diffuse_mean_lum = m_dd.x + m_id.x; + float specular_mean_lum = m_ds.x + m_is.x; float diffuse_var = m_dd.y + m_id.y; float specular_var = m_ds.y + m_is.y; @@ -689,18 +710,18 @@ void main() { } else if (params.debug_view == 34u) { debug_color = vec3(history_weight); } else if (params.debug_view == 47u) { - debug_color = path_data.direct_diffuse.rgb; + debug_color = center_direct_diffuse.rgb; } else if (params.debug_view == 48u) { - debug_color = path_data.direct_specular.rgb; + debug_color = center_direct_specular.rgb; } else if (params.debug_view == 49u) { - debug_color = path_data.indirect_diffuse.rgb; + debug_color = center_indirect_diffuse.rgb; } else if (params.debug_view == 50u) { - debug_color = path_data.indirect_specular.rgb; + debug_color = center_indirect_specular.rgb; } else if (params.debug_view == 51u) { - debug_color = path_data.albedo_roughness_hit_confidence.rgb; + debug_color = center_albedo_roughness.rgb; } else if (params.debug_view == 52u) { debug_color = vec3( - clamp(path_data.albedo_roughness_hit_confidence.w, 0.0, 1.0), + clamp(center_albedo_roughness.w, 0.0, 1.0), center_primary_hit_normalized, center_secondary_hit_normalized); } else if (params.debug_view == 53u) { diff --git a/shaders/dlss_guide_visualize.comp b/shaders/dlss_guide_visualize.comp new file mode 100644 index 0000000..ed0752a --- /dev/null +++ b/shaders/dlss_guide_visualize.comp @@ -0,0 +1,78 @@ +#version 460 + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(set = 0, binding = 0) uniform texture2D source_texture; +layout(set = 0, binding = 1) uniform sampler source_sampler; +layout(set = 0, binding = 2, rgba8) writeonly uniform image2D output_image; + +layout(push_constant) uniform VisualizeParams { + uint mode; + float scale; + float bias; + float reserved0; +} params; + +const uint MODE_HDR_COLOR = 0u; +const uint MODE_LINEAR_COLOR = 1u; +const uint MODE_SCALAR_01 = 2u; +const uint MODE_LINEAR_DEPTH = 3u; +const uint MODE_HARDWARE_DEPTH = 4u; +const uint MODE_MOTION_PIXELS = 5u; +const uint MODE_DIRECTION = 6u; +const uint MODE_DISTANCE = 7u; + +vec3 linear_to_srgb(vec3 color) { + color = clamp(color, vec3(0.0), vec3(1.0)); + vec3 lo = color * 12.92; + vec3 hi = 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055; + return mix(hi, lo, lessThanEqual(color, vec3(0.0031308))); +} + +vec3 tone_map_debug_hdr(vec3 color) { + color = max(color, vec3(0.0)); + return color / (color + vec3(1.0)); +} + +vec3 visualize_scalar(float value) { + return vec3(clamp(value, 0.0, 1.0)); +} + +vec3 visualize_exponential(float value, float scale) { + float mapped = 1.0 - exp(-max(value, 0.0) * max(scale, 1.0e-6)); + return vec3(clamp(mapped, 0.0, 1.0)); +} + +void main() { + ivec2 outSize = imageSize(output_image); + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + if (p.x >= outSize.x || p.y >= outSize.y) { + return; + } + + vec2 uv = (vec2(p) + vec2(0.5)) / vec2(max(outSize, ivec2(1))); + vec4 sampleValue = texture(sampler2D(source_texture, source_sampler), uv); + + vec3 color; + if (params.mode == MODE_HDR_COLOR) { + color = tone_map_debug_hdr(sampleValue.rgb); + } else if (params.mode == MODE_LINEAR_COLOR) { + color = clamp(sampleValue.rgb, vec3(0.0), vec3(1.0)); + } else if (params.mode == MODE_SCALAR_01 || params.mode == MODE_HARDWARE_DEPTH) { + color = visualize_scalar(sampleValue.r); + } else if (params.mode == MODE_LINEAR_DEPTH || params.mode == MODE_DISTANCE) { + color = visualize_exponential(sampleValue.r, params.scale); + } else if (params.mode == MODE_MOTION_PIXELS) { + vec2 encoded = vec2(0.5) + sampleValue.xy * max(params.scale, 1.0e-6); + color = vec3(clamp(encoded, vec2(0.0), vec2(1.0)), 0.5); + } else if (params.mode == MODE_DIRECTION) { + vec3 dir = sampleValue.xyz; + float len2 = dot(dir, dir); + dir = len2 > 1.0e-10 ? normalize(dir) : vec3(0.0, 0.0, 1.0); + color = dir * 0.5 + vec3(0.5); + } else { + color = clamp(sampleValue.rgb, vec3(0.0), vec3(1.0)); + } + + imageStore(output_image, p, vec4(linear_to_srgb(color), 1.0)); +} diff --git a/shaders/dlss_rr_guides.comp b/shaders/dlss_rr_guides.comp index 72c56a2..cdf4457 100644 --- a/shaders/dlss_rr_guides.comp +++ b/shaders/dlss_rr_guides.comp @@ -2,6 +2,7 @@ #extension GL_GOOGLE_include_directive : require #include "temporal_common.glsl" +#include "psr_guides.glsl" layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; @@ -84,6 +85,8 @@ layout(set = 0, binding = 20, std140) uniform Camera { vec4 clip_controls; } camera; layout(set = 0, binding = 21, r32f) readonly uniform image2D adaptive_filled_mask; +layout(set = 0, binding = 22, std430) readonly buffer PsrGuideBuffer { PsrGuideRecord psr_guide_buffer[]; }; +layout(set = 0, binding = 23, std430) readonly buffer PreviousPsrGuideSignatureBuffer { uint previous_psr_guide_signature_buffer[]; }; const float WORLD_POSITION_PACK_RANGE = 64.0; @@ -207,6 +210,25 @@ float disocclusion_mask(ivec2 coords, ivec2 dims, bool surface, vec2 currentToPr return 0.0; } +float psr_disocclusion_mask(ivec2 coords, ivec2 dims, bool psrActive, PsrGuideRecord guide, vec2 currentToPreviousPixels) { + if (!psrActive) { + return 0.0; + } + if (params.reset_history != 0u) { + return 1.0; + } + if (temporal_velocity_pixels_are_invalid(currentToPreviousPixels, params.velocity_scale)) { + return 1.0; + } + vec2 historyPos = vec2(coords) - currentToPreviousPixels; + if (!temporal_history_pixel_valid(historyPos, dims, 0.0)) { + return 1.0; + } + ivec2 historyCoords = clamp(ivec2(round(historyPos)), ivec2(0), dims - ivec2(1)); + uint historyIndex = uint(historyCoords.y) * uint(dims.x) + uint(historyCoords.x); + return previous_psr_guide_signature_buffer[historyIndex] == psr_guide_signature(guide) ? 0.0 : 1.0; +} + void main() { ivec2 coords = ivec2(gl_GlobalInvocationID.xy); ivec2 dims = ivec2(int(params.render_width), int(params.render_height)); @@ -224,6 +246,8 @@ void main() { } PathDataRecord path = path_data_buffer[pixelIndex]; + PsrGuideRecord psrGuide = psr_guide_buffer[pixelIndex]; + bool psrActive = surface && psr_guide_active(psrGuide); vec3 normal = surface ? decode_octahedral_normal(packedDepthNormal.y) : vec3(0.0, 0.0, 1.0); float roughness = surface ? clamp(path.albedo_roughness_hit_confidence.w, 0.001, 1.0) : 1.0; vec3 diffuseAlbedo = surface ? clamp(path.albedo_roughness_hit_confidence.rgb, 0.0, 1.0) : vec3(0.5); @@ -231,8 +255,17 @@ void main() { float primaryHitDistance = path.direct_specular.a; float diffuseHitDistance = surface ? guide_hit_distance(path.denoiser_hit_distance.x, primaryHitDistance) : 0.0; float specularHitDistance = surface ? guide_hit_distance(path.denoiser_hit_distance.y, primaryHitDistance) : 0.0; + if (psrActive) { + linearDepth = clamp(psrGuide.distances.y, 0.0, 65504.0); + normal = psr_guide_normal(psrGuide); + roughness = clamp(psr_guide_roughness(psrGuide), 0.001, 1.0); + diffuseAlbedo = psr_guide_diffuse_albedo(psrGuide); + specularAlbedo = psr_guide_specular_f0(psrGuide); + specularHitDistance = max(psrGuide.distances.x, 0.0); + } - vec2 currentToPreviousPixels = temporal_unpack_velocity_pixels(velocity_buffer[pixelIndex], params.velocity_scale); + uint packedGuideVelocity = psrActive ? psrGuide.geometry.w : velocity_buffer[pixelIndex]; + vec2 currentToPreviousPixels = temporal_unpack_velocity_pixels(packedGuideVelocity, params.velocity_scale); vec2 previousMinusCurrentPixels = temporal_velocity_pixels_are_invalid(currentToPreviousPixels, params.velocity_scale) ? vec2(0.0) : -currentToPreviousPixels; @@ -242,8 +275,16 @@ void main() { vec3 specularRayDirection = surface ? guide_direction(reflect(fallbackRayDirection, orientedNormal), fallbackRayDirection) : fallbackRayDirection; float diffuseRayDirectionHitDistance = surface ? diffuseHitDistance : 0.0; float specularRayDirectionHitDistance = surface ? specularHitDistance : 0.0; + if (psrActive) { + diffuseRayDirection = normal; + specularRayDirection = psr_guide_ray_direction(psrGuide); + diffuseRayDirectionHitDistance = max(psrGuide.distances.x, 0.0); + specularRayDirectionHitDistance = max(psrGuide.distances.x, 0.0); + } float disocclusion = max( disocclusion_mask(coords, dims, surface, currentToPreviousPixels), + psr_disocclusion_mask(coords, dims, psrActive, psrGuide, currentToPreviousPixels)); + disocclusion = max(disocclusion, clamp(path.emissive_residual.a, 0.0, 1.0)); if (adaptive_sample_count_enabled()) { disocclusion = max(disocclusion, clamp(imageLoad(adaptive_filled_mask, coords).r, 0.0, 1.0)); @@ -257,7 +298,8 @@ void main() { imageStore(output_roughness, coords, vec4(roughness, 0.0, 0.0, 0.0)); imageStore(output_diffuse_hit_distance, coords, vec4(diffuseHitDistance, 0.0, 0.0, 0.0)); imageStore(output_specular_hit_distance, coords, vec4(specularHitDistance, 0.0, 0.0, 0.0)); - imageStore(output_reflected_albedo, coords, vec4(specularAlbedo, 1.0)); + imageStore(output_reflected_albedo, coords, + vec4(psrActive ? diffuseAlbedo : specularAlbedo, 1.0)); imageStore(output_disocclusion_mask, coords, vec4(disocclusion, 0.0, 0.0, 0.0)); imageStore(output_diffuse_ray_direction, coords, vec4(diffuseRayDirection, 1.0)); imageStore(output_specular_ray_direction, coords, vec4(specularRayDirection, 1.0)); diff --git a/shaders/moment_update.comp b/shaders/moment_update.comp index ea0b462..4e4d27e 100644 --- a/shaders/moment_update.comp +++ b/shaders/moment_update.comp @@ -111,13 +111,17 @@ vec3 decode_world_position(uvec2 packedValue, vec3 origin) { } vec2 xy = unpack_snorm2x16(packedValue.x); float z = unpack_snorm2x16(packedValue.y).x; - return origin + vec3(xy.x, xy.y, z) * 8.0; + return origin + vec3(xy.x, xy.y, z) * 64.0; } vec3 reconstruct_current_world_position(ivec2 coords, float depth, ivec2 dims) { if (!is_surface_depth(depth)) { return vec3(0.0, 1.0e10, 0.0); } + vec3 packedWorld = decode_world_position(world_position[uint(coords.y) * uint(dims.x) + uint(coords.x)], prev_camera.current_pos.xyz); + if (packedWorld.y < 1.0e9) { + return packedWorld; + } vec2 uv = (vec2(coords) + vec2(0.5)) / vec2(dims); vec2 ndc = uv * 2.0 - 1.0; vec4 world_far = prev_camera.inv_view_proj * vec4(ndc, 1.0, 1.0); diff --git a/shaders/nrd_confidence_filter.comp b/shaders/nrd_confidence_filter.comp new file mode 100644 index 0000000..ccfec7f --- /dev/null +++ b/shaders/nrd_confidence_filter.comp @@ -0,0 +1,120 @@ +#version 450 + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(set = 0, binding = 0, rgba16f) readonly uniform image2D raw_confidence_gradient; +layout(set = 0, binding = 1, std430) readonly buffer DepthNormalBuffer { uvec4 depth_normal_buffer[]; }; +layout(set = 0, binding = 2, rgba16f) writeonly uniform image2D filtered_confidence_gradient; + +layout(set = 0, binding = 3, std140) uniform TaaParams { + uint enabled; + uint frame_count; + uint width; + uint height; + float feedback; + float velocity_scale; + uint reset_history; + float sharpen_strength; + uint history_valid; + uint camera_moving; + uint render_width; + uint render_height; + float motion_feedback; + float reactive_feedback; +} params; + +const int NRD_CONFIDENCE_STRATUM_SIZE = 4; +const int NRD_CONFIDENCE_FILTER_RADIUS = 2; + +uint confidence_hash(uvec3 value) { + uint h = value.x * 0x8da6b343u ^ value.y * 0xd8163841u ^ value.z * 0xcb1ab31fu; + h ^= h >> 16u; + h *= 0x7feb352du; + h ^= h >> 15u; + h *= 0x846ca68bu; + return h ^ (h >> 16u); +} + +ivec2 confidence_stratum_sample(ivec2 stratum, uint frameIndex) { + uint h = confidence_hash(uvec3(stratum, frameIndex)); + return stratum * NRD_CONFIDENCE_STRATUM_SIZE + + ivec2(int(h & 3u), int((h >> 2u) & 3u)); +} + +vec2 unpack_snorm2x16(uint packedValue) { + ivec2 quantized = ivec2(int(packedValue & 0xffffu), int((packedValue >> 16u) & 0xffffu)); + if (quantized.x >= 32768) quantized.x -= 65536; + if (quantized.y >= 32768) quantized.y -= 65536; + return clamp(vec2(quantized) / 32767.0, vec2(-1.0), vec2(1.0)); +} + +vec3 decode_octahedral_normal(uint packedValue) { + vec2 f = unpack_snorm2x16(packedValue); + vec3 n = vec3(f, 1.0 - abs(f.x) - abs(f.y)); + float t = clamp(-n.z, 0.0, 1.0); + n.x += n.x >= 0.0 ? -t : t; + n.y += n.y >= 0.0 ? -t : t; + return normalize(n); +} + +uvec4 depth_normal_at(ivec2 coords, ivec2 dims) { + coords = clamp(coords, ivec2(0), dims - ivec2(1)); + uint pixelIndex = uint(coords.y) * uint(dims.x) + uint(coords.x); + return depth_normal_buffer[pixelIndex]; +} + +void main() { + ivec2 coords = ivec2(gl_GlobalInvocationID.xy); + ivec2 dims = ivec2(int(params.render_width), int(params.render_height)); + if (dims.x <= 0 || dims.y <= 0 || coords.x >= dims.x || coords.y >= dims.y) { + return; + } + + uvec4 centerPacked = depth_normal_at(coords, dims); + float centerDepth = uintBitsToFloat(centerPacked.x); + bool centerValid = centerDepth < 1.0e9; + if (!centerValid) { + imageStore(filtered_confidence_gradient, coords, vec4(1.0, 1.0, 0.0, 0.0)); + return; + } + + vec3 centerNormal = decode_octahedral_normal(centerPacked.y); + ivec2 centerStratum = coords / NRD_CONFIDENCE_STRATUM_SIZE; + vec2 gradientSum = vec2(0.0); + float sourceValiditySum = 0.0; + float weightSum = 0.0; + for (int y = -NRD_CONFIDENCE_FILTER_RADIUS; y <= NRD_CONFIDENCE_FILTER_RADIUS; ++y) { + for (int x = -NRD_CONFIDENCE_FILTER_RADIUS; x <= NRD_CONFIDENCE_FILTER_RADIUS; ++x) { + ivec2 sampleStratum = centerStratum + ivec2(x, y); + if (sampleStratum.x < 0 || sampleStratum.y < 0) { + continue; + } + ivec2 sampleCoords = confidence_stratum_sample(sampleStratum, params.frame_count); + if (sampleCoords.x >= dims.x || sampleCoords.y >= dims.y) { + continue; + } + vec4 sampleValue = imageLoad(raw_confidence_gradient, sampleCoords); + if (sampleValue.z <= 0.5) { + continue; + } + uvec4 samplePacked = depth_normal_at(sampleCoords, dims); + float sampleDepth = uintBitsToFloat(samplePacked.x); + vec3 sampleNormal = decode_octahedral_normal(samplePacked.y); + float relativeDepth = abs(sampleDepth - centerDepth) / + max(max(abs(sampleDepth), abs(centerDepth)), 0.05); + float depthWeight = exp(-relativeDepth * 80.0); + float normalWeight = pow(max(dot(centerNormal, sampleNormal), 0.0), 32.0); + vec2 sampleDelta = vec2(sampleCoords - coords) / float(NRD_CONFIDENCE_STRATUM_SIZE); + float spatialWeight = exp(-0.5 * dot(sampleDelta, sampleDelta)); + float weight = spatialWeight * depthWeight * normalWeight; + gradientSum += sampleValue.xy * weight; + sourceValiditySum += sampleValue.w * weight; + weightSum += weight; + } + } + + vec2 filtered = weightSum > 1.0e-6 ? gradientSum / weightSum : vec2(1.0); + float sourceValidity = weightSum > 1.0e-6 ? sourceValiditySum / weightSum : 0.0; + imageStore(filtered_confidence_gradient, coords, + vec4(clamp(filtered, vec2(0.0), vec2(1.0)), 1.0, clamp(sourceValidity, 0.0, 1.0))); +} diff --git a/shaders/nrd_confidence_gradient.comp b/shaders/nrd_confidence_gradient.comp new file mode 100644 index 0000000..f53730f --- /dev/null +++ b/shaders/nrd_confidence_gradient.comp @@ -0,0 +1,291 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require + +#include "temporal_common.glsl" + +#define RTV_RESTIR_DI_COMPUTE_ACCESSORS 1 +#include "restir_di_types_accessors.glsl" +#include "restir_di_reuse.glsl" +#include "restir_di_light_evaluation.glsl" + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(set = 0, binding = 0, std430) readonly buffer DepthNormalBuffer { uvec4 depth_normal_buffer[]; }; + +struct PathDataRecord { + vec4 direct_diffuse; + vec4 direct_specular; + vec4 indirect_diffuse; + vec4 indirect_specular; + vec4 albedo_roughness_hit_confidence; + vec4 material_specular_albedo; + vec4 denoiser_hit_distance; + vec4 diffuse_ray_direction_hit_distance; + vec4 specular_ray_direction_hit_distance; + vec4 emissive_residual; + vec4 restir_gi_fallback_reactive; +}; +layout(set = 0, binding = 1, std430) readonly buffer PathDataBuffer { PathDataRecord path_data_buffer[]; }; + +layout(set = 0, binding = 2, rgba16f) readonly uniform image2D previous_lighting_luminance; +layout(set = 0, binding = 3, r32f) readonly uniform image2D adaptive_filled_mask; +layout(set = 0, binding = 4, rgba16f) writeonly uniform image2D current_lighting_luminance; +layout(set = 0, binding = 5, rgba16f) writeonly uniform image2D raw_confidence_gradient; + +layout(set = 0, binding = 6, std140) uniform TaaParams { + uint enabled; + uint frame_count; + uint width; + uint height; + float feedback; + float velocity_scale; + uint reset_history; + float sharpen_strength; + uint history_valid; + uint camera_moving; + uint render_width; + uint render_height; + float motion_feedback; + float reactive_feedback; +} params; + +layout(set = 0, binding = 7, std430) readonly buffer RestirDiSourcePixels { + uint restir_di_source_pixels[]; +}; +layout(set = 0, binding = 8, std430) readonly buffer RestirDiCurrentReceivers { + RestirDiReceiver restir_di_current_receivers[]; +}; +layout(set = 0, binding = 9, std430) readonly buffer RestirDiPreviousReceivers { + RestirDiReceiver restir_di_previous_receivers[]; +}; +layout(set = 0, binding = 10, std430) readonly buffer RestirDiFinalReservoirs { + RestirDiReservoir restir_di_final_reservoirs[]; +}; +layout(set = 0, binding = 11, std430) readonly buffer RestirDiPreviousLightRecords { + RestirDiLightRecord restir_di_previous_light_records[]; +}; + +layout(push_constant, std430) uniform ConfidenceReplayParams { + uint restir_available; + uint current_light_count; + uint previous_light_count; + uint previous_light_history_valid; +} replay_params; + +const uint RESTIR_DI_SOURCE_PIXEL_INVALID = 0xffffffffu; +const int NRD_CONFIDENCE_STRATUM_SIZE = 4; + +uint confidence_hash(uvec3 value) { + uint h = value.x * 0x8da6b343u ^ value.y * 0xd8163841u ^ value.z * 0xcb1ab31fu; + h ^= h >> 16u; + h *= 0x7feb352du; + h ^= h >> 15u; + h *= 0x846ca68bu; + return h ^ (h >> 16u); +} + +ivec2 confidence_stratum_sample(ivec2 stratum, uint frameIndex) { + uint h = confidence_hash(uvec3(stratum, frameIndex)); + return stratum * NRD_CONFIDENCE_STRATUM_SIZE + + ivec2(int(h & 3u), int((h >> 2u) & 3u)); +} + +ivec2 unpack_source_pixel(uint packed) { + return ivec2(int(packed & 0xffffu), int((packed >> 16u) & 0xffffu)); +} + +float nrd_luminance(vec3 c) { + return dot(c, vec3(0.2126, 0.7152, 0.0722)); +} + +float relative_gradient(float currentValue, float previousValue) { + float denom = max(max(abs(currentValue), abs(previousValue)), 1.0e-3); + return clamp(abs(currentValue - previousValue) / denom, 0.0, 1.0); +} + +bool confidence_light_stable_identity_matches(RestirDiReservoir reservoir, RestirDiLightRecord light) { + return reservoir.sampleMetadata.x == restir_di_identity_hash(light.identity.xy) && + restir_di_light_kind(reservoir) == light.metadata.x; +} + +bool confidence_resolve_current_light(RestirDiReservoir reservoir, out RestirDiLightRecord light, out uint index) { + for (uint i = 0u; i < min(replay_params.current_light_count, restir_di_scene.lightCount); ++i) { + RestirDiLightRecord candidate = restir_di_light_records[i]; + if (confidence_light_stable_identity_matches(reservoir, candidate)) { + light = candidate; + index = i; + return true; + } + } + index = 0u; + return false; +} + +bool confidence_resolve_previous_light(RestirDiReservoir reservoir, out RestirDiLightRecord light, out uint index) { + for (uint i = 0u; i < replay_params.previous_light_count; ++i) { + RestirDiLightRecord candidate = restir_di_previous_light_records[i]; + if (confidence_light_stable_identity_matches(reservoir, candidate)) { + light = candidate; + index = i; + return true; + } + } + index = 0u; + return false; +} + +vec3 confidence_remap_sample_position( + RestirDiReservoir reservoir, + RestirDiLightRecord sourceLight, + RestirDiLightRecord targetLight) { + uint kind = restir_di_light_kind(reservoir); + if (kind == RESTIR_DI_LIGHT_POINT || kind == RESTIR_DI_LIGHT_SPOT) { + return targetLight.data1.xyz; + } + if (kind == RESTIR_DI_LIGHT_AREA) { + return reservoir.samplePosition_distance.xyz + targetLight.data1.xyz - sourceLight.data1.xyz; + } + return reservoir.samplePosition_distance.xyz; +} + +bool confidence_replay_selected_light( + uint pixelIndex, + uint sourceIndex, + out vec2 currentLuminance, + out vec2 previousLuminance) { + currentLuminance = vec2(0.0); + previousLuminance = vec2(0.0); + if (replay_params.restir_available == 0u || + replay_params.previous_light_history_valid == 0u) { + return false; + } + + RestirDiReservoir reservoir = restir_di_final_reservoirs[pixelIndex]; + if (!restir_di_reservoir_valid(reservoir) || + restir_di_light_kind_infinite(restir_di_light_kind(reservoir))) { + return false; + } + + RestirDiLightRecord currentLight; + RestirDiLightRecord previousLight; + uint currentLightIndex; + uint previousLightIndex; + if (!confidence_resolve_current_light(reservoir, currentLight, currentLightIndex) || + !confidence_resolve_previous_light(reservoir, previousLight, previousLightIndex)) { + currentLuminance = vec2(1.0); + previousLuminance = vec2(0.0); + return true; + } + + bool reservoirFromCurrent = reservoir.sampleMetadata.y == currentLight.identity.z; + RestirDiLightRecord sourceLight = reservoirFromCurrent ? currentLight : previousLight; + RestirDiReservoir currentSample = reservoir; + RestirDiReservoir previousSample = reservoir; + currentSample.samplePosition_distance.xyz = confidence_remap_sample_position(reservoir, sourceLight, currentLight); + previousSample.samplePosition_distance.xyz = confidence_remap_sample_position(reservoir, sourceLight, previousLight); + + RestirDiReceiver currentReceiver = restir_di_current_receivers[pixelIndex]; + RestirDiReceiver previousReceiver = restir_di_previous_receivers[sourceIndex]; + uint invalidFlags = RESTIR_DI_SURFACE_SKY | RESTIR_DI_SURFACE_INVALID | + RESTIR_DI_SURFACE_DELTA | RESTIR_DI_SURFACE_UNLIT | RESTIR_DI_SURFACE_UNSUPPORTED; + if ((restir_di_receiver_surface_flags(currentReceiver) & invalidFlags) != 0u || + (restir_di_receiver_surface_flags(previousReceiver) & invalidFlags) != 0u) { + return false; + } + + vec3 currentNormal = restir_di_light_normal(currentSample); + vec3 previousNormal = restir_di_light_normal(previousSample); + float currentPdf = restir_di_pdf_at_receiver( + currentLightIndex, + currentLight, + currentReceiver.worldPosition_depth.xyz, + currentSample.samplePosition_distance.xyz, + currentNormal); + float previousPdf = max(restir_di_source_pdf(reservoir), 1.0e-6); + if (currentPdf <= 1.0e-6) { + currentLuminance = vec2(1.0); + previousLuminance = vec2(0.0); + return true; + } + + vec3 direction; + float distanceToLight; + vec3 diffuseContribution; + vec3 specularContribution; + restir_di_evaluate_integrand_components( + currentSample, + currentReceiver, + currentLight, + currentPdf, + direction, + distanceToLight, + currentNormal, + diffuseContribution, + specularContribution); + currentLuminance = vec2(nrd_luminance(diffuseContribution), nrd_luminance(specularContribution)); + + restir_di_evaluate_integrand_components( + previousSample, + previousReceiver, + previousLight, + previousPdf, + direction, + distanceToLight, + previousNormal, + diffuseContribution, + specularContribution); + previousLuminance = vec2(nrd_luminance(diffuseContribution), nrd_luminance(specularContribution)); + return !any(isnan(currentLuminance)) && !any(isinf(currentLuminance)) && + !any(isnan(previousLuminance)) && !any(isinf(previousLuminance)); +} + +void main() { + ivec2 coords = ivec2(gl_GlobalInvocationID.xy); + ivec2 dims = ivec2(int(params.render_width), int(params.render_height)); + if (dims.x <= 0 || dims.y <= 0 || coords.x >= dims.x || coords.y >= dims.y) { + return; + } + + uint pixelIndex = uint(coords.y) * uint(dims.x) + uint(coords.x); + float pathDistance = uintBitsToFloat(depth_normal_buffer[pixelIndex].x); + bool surface = pathDistance < 1.0e9; + bool adaptiveFilled = imageLoad(adaptive_filled_mask, coords).r > 0.5; + + PathDataRecord path = path_data_buffer[pixelIndex]; + vec3 noisyDiffuse = max(path.direct_diffuse.rgb + path.indirect_diffuse.rgb + path.emissive_residual.rgb, vec3(0.0)); + vec3 noisySpecular = max(path.direct_specular.rgb + path.indirect_specular.rgb, vec3(0.0)); + float diffuseLuminance = surface && !adaptiveFilled ? nrd_luminance(noisyDiffuse) : 0.0; + float specularLuminance = surface && !adaptiveFilled ? nrd_luminance(noisySpecular) : 0.0; + + uint packedSourcePixel = restir_di_source_pixels[pixelIndex]; + ivec2 sourceCoords = packedSourcePixel == RESTIR_DI_SOURCE_PIXEL_INVALID + ? coords + : clamp(unpack_source_pixel(packedSourcePixel), ivec2(0), dims - ivec2(1)); + float valid = surface && !adaptiveFilled ? 1.0 : 0.0; + + imageStore(current_lighting_luminance, coords, vec4(diffuseLuminance, specularLuminance, valid, 1.0)); + + ivec2 stratum = coords / NRD_CONFIDENCE_STRATUM_SIZE; + ivec2 sparseSample = confidence_stratum_sample(stratum, params.frame_count); + if (coords != sparseSample) { + imageStore(raw_confidence_gradient, coords, vec4(0.0)); + return; + } + + vec4 previous = imageLoad(previous_lighting_luminance, sourceCoords); + bool historyValid = params.history_valid != 0u && params.reset_history == 0u && previous.z > 0.5; + vec2 replayCurrent; + vec2 replayPrevious; + uint sourceIndex = uint(sourceCoords.y) * uint(dims.x) + uint(sourceCoords.x); + bool replayValid = historyValid && confidence_replay_selected_light( + pixelIndex, sourceIndex, replayCurrent, replayPrevious); + float diffuseGradient = historyValid ? relative_gradient(diffuseLuminance, previous.x) : 1.0; + float specularGradient = historyValid ? relative_gradient(specularLuminance, previous.y) : 1.0; + if (replayValid) { + diffuseGradient = relative_gradient(replayCurrent.x, replayPrevious.x); + specularGradient = relative_gradient(replayCurrent.y, replayPrevious.y); + } + float sourceValid = packedSourcePixel == RESTIR_DI_SOURCE_PIXEL_INVALID ? 0.0 : 1.0; + imageStore(raw_confidence_gradient, coords, + vec4(diffuseGradient, specularGradient, valid, replayValid ? 1.0 : sourceValid * 0.5)); +} diff --git a/shaders/nrd_prepare.comp b/shaders/nrd_prepare.comp index 2b98e2c..ac1a394 100644 --- a/shaders/nrd_prepare.comp +++ b/shaders/nrd_prepare.comp @@ -2,6 +2,7 @@ #extension GL_GOOGLE_include_directive : require #include "temporal_common.glsl" +#include "psr_guides.glsl" layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; @@ -28,6 +29,8 @@ layout(set = 0, binding = 4, rgba16f) writeonly uniform image2D out_normal_rough layout(set = 0, binding = 5, r32f) writeonly uniform image2D out_view_z; layout(set = 0, binding = 6, rgba16f) writeonly uniform image2D out_diff_radiance_hitdist; layout(set = 0, binding = 7, rgba16f) writeonly uniform image2D out_spec_radiance_hitdist; +layout(set = 0, binding = 11, r16f) writeonly uniform image2D out_diff_confidence; +layout(set = 0, binding = 12, r16f) writeonly uniform image2D out_spec_confidence; layout(set = 0, binding = 8, std140) uniform TaaParams { uint enabled; uint frame_count; @@ -80,6 +83,8 @@ layout(set = 0, binding = 9, std140) uniform Camera { vec4 clip_controls; } camera; layout(set = 0, binding = 10, r32f) readonly uniform image2D adaptive_filled_mask; +layout(set = 0, binding = 13, rgba16f) readonly uniform image2D filtered_confidence_gradient; +layout(set = 0, binding = 14, std430) readonly buffer PsrGuideBuffer { PsrGuideRecord psr_guide_buffer[]; }; vec2 unpack_snorm2x16(uint packedValue) { ivec2 quantized = ivec2(int(packedValue & 0xffffu), int((packedValue >> 16u) & 0xffffu)); @@ -220,6 +225,14 @@ float valid_reblur_norm_hit_distance(float hitDistance, float viewZ, float rough return reblur_norm_hit_distance(hitDistance, viewZ, roughness); } +float nrd_history_confidence(bool validSurface, bool adaptiveFilled, float signalLuminance, float hitDistance) { + if (!validSurface || adaptiveFilled || signalLuminance <= 1.0e-5 || nrd_invalid_float(hitDistance)) { + return 0.0; + } + float hitConfidence = hitDistance > 0.001 ? 1.0 : 0.35; + return clamp(hitConfidence, 0.0, 1.0); +} + void main() { ivec2 coords = ivec2(gl_GlobalInvocationID.xy); ivec2 dims = ivec2(int(params.render_width), int(params.render_height)); @@ -238,14 +251,26 @@ void main() { vec3 normal = surface ? decode_octahedral_normal(packedDepthNormal.y) : vec3(0.0, 0.0, 1.0); PathDataRecord path = path_data_buffer[pixelIndex]; + PsrGuideRecord psrGuide = psr_guide_buffer[pixelIndex]; + bool psrActive = surface && psr_guide_active(psrGuide); + if (psrActive) { + viewZ = clamp(psrGuide.distances.y, 0.0, NRD_DENOISING_RANGE + 1.0); + normal = psr_guide_normal(psrGuide); + path.albedo_roughness_hit_confidence.rgb = psr_guide_diffuse_albedo(psrGuide); + path.material_specular_albedo.rgb = psr_guide_specular_f0(psrGuide); + } float roughness = clamp(path.albedo_roughness_hit_confidence.w, 0.001, 1.0); + if (psrActive) { + roughness = clamp(psr_guide_roughness(psrGuide), 0.001, 1.0); + } - vec2 currentToPreviousPixels = temporal_unpack_velocity_pixels(velocity_buffer[pixelIndex], params.velocity_scale); + uint packedGuideVelocity = psrActive ? psrGuide.geometry.w : velocity_buffer[pixelIndex]; + vec2 currentToPreviousPixels = temporal_unpack_velocity_pixels(packedGuideVelocity, params.velocity_scale); vec2 previousMinusCurrentPixels = temporal_velocity_pixels_are_invalid(currentToPreviousPixels, params.velocity_scale) ? vec2(0.0) : -currentToPreviousPixels; - vec3 view = -primary_ray_direction(coords, dims); + vec3 view = psrActive ? -psr_guide_ray_direction(psrGuide) : -primary_ray_direction(coords, dims); vec3 diffMaterialFactor; vec3 specMaterialFactor; nrd_material_factors(normal, view, path, roughness, diffMaterialFactor, specMaterialFactor); @@ -256,9 +281,22 @@ void main() { vec3 specRadiance = noisySpecular / specMaterialFactor; float diffHitDistance = path.denoiser_hit_distance.x; float specHitDistance = path.denoiser_hit_distance.y; - float diffNormHitDistance = valid_reblur_norm_hit_distance(diffHitDistance, viewZ, 1.0, nrd_luminance(noisyDiffuse)); - float specNormHitDistance = valid_reblur_norm_hit_distance(specHitDistance, viewZ, roughness, nrd_luminance(noisySpecular)); - if (adaptive_filled_pixel(coords)) { + if (psrActive) { + specHitDistance = max(psrGuide.distances.x, 0.0); + } + float diffLuminance = nrd_luminance(noisyDiffuse); + float specLuminance = nrd_luminance(noisySpecular); + float diffNormHitDistance = valid_reblur_norm_hit_distance(diffHitDistance, viewZ, 1.0, diffLuminance); + float specNormHitDistance = valid_reblur_norm_hit_distance(specHitDistance, viewZ, roughness, specLuminance); + bool adaptiveFilled = adaptive_filled_pixel(coords); + float diffConfidence = nrd_history_confidence(surface, adaptiveFilled, diffLuminance, diffHitDistance); + float specConfidence = nrd_history_confidence(surface, adaptiveFilled, specLuminance, specHitDistance); + vec4 confidenceGradient = imageLoad(filtered_confidence_gradient, coords); + if (confidenceGradient.z > 0.5) { + diffConfidence *= 1.0 - clamp(confidenceGradient.x, 0.0, 1.0); + specConfidence *= 1.0 - clamp(confidenceGradient.y, 0.0, 1.0); + } + if (adaptiveFilled) { diffRadiance = vec3(0.0); specRadiance = vec3(0.0); diffNormHitDistance = 0.0; @@ -276,4 +314,6 @@ void main() { imageStore(out_view_z, coords, vec4(viewZ, 0.0, 0.0, 0.0)); imageStore(out_diff_radiance_hitdist, coords, pack_reblur_radiance_hitdist(diffRadiance, diffNormHitDistance)); imageStore(out_spec_radiance_hitdist, coords, pack_reblur_radiance_hitdist(specRadiance, specNormHitDistance)); + imageStore(out_diff_confidence, coords, vec4(diffConfidence, 0.0, 0.0, 0.0)); + imageStore(out_spec_confidence, coords, vec4(specConfidence, 0.0, 0.0, 0.0)); } diff --git a/shaders/nrd_resolve.comp b/shaders/nrd_resolve.comp index 851113d..a4eed1f 100644 --- a/shaders/nrd_resolve.comp +++ b/shaders/nrd_resolve.comp @@ -6,6 +6,20 @@ layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; layout(set = 0, binding = 0, rgba16f) readonly uniform image2D raw_hdr; layout(set = 0, binding = 1, rgba16f) readonly uniform image2D nrd_diff_radiance_hitdist; layout(set = 0, binding = 2, rgba16f) readonly uniform image2D nrd_spec_radiance_hitdist; +layout(set = 0, binding = 8, r16f) readonly uniform image2D nrd_diff_confidence; +layout(set = 0, binding = 9, r16f) readonly uniform image2D nrd_spec_confidence; +layout(set = 0, binding = 10, rgba16f) readonly uniform image2D nrd_raw_confidence_gradient; +layout(set = 0, binding = 11, rgba16f) readonly uniform image2D nrd_filtered_confidence_gradient; + +layout(push_constant, std430) uniform PushConstants { + uint debug_view; +} push_constants; + +const uint DEBUG_VIEW_NRD_DIFFUSE_CONFIDENCE = 160u; +const uint DEBUG_VIEW_NRD_SPECULAR_CONFIDENCE = 161u; +const uint DEBUG_VIEW_NRD_RAW_CONFIDENCE_GRADIENT = 162u; +const uint DEBUG_VIEW_NRD_FILTERED_CONFIDENCE_GRADIENT = 163u; +const uint DEBUG_VIEW_NRD_CONFIDENCE_HISTORY = 164u; struct PathDataRecord { vec4 direct_diffuse; @@ -160,6 +174,35 @@ void main() { uint pixelIndex = uint(coords.y) * uint(dims.x) + uint(coords.x); vec3 raw = max(imageLoad(raw_hdr, coords).rgb, vec3(0.0)); + if (push_constants.debug_view == DEBUG_VIEW_NRD_DIFFUSE_CONFIDENCE) { + float confidence = clamp(imageLoad(nrd_diff_confidence, coords).r, 0.0, 1.0); + imageStore(out_denoised_hdr, coords, vec4(vec3(confidence), 1.0)); + return; + } + if (push_constants.debug_view == DEBUG_VIEW_NRD_SPECULAR_CONFIDENCE) { + float confidence = clamp(imageLoad(nrd_spec_confidence, coords).r, 0.0, 1.0); + imageStore(out_denoised_hdr, coords, vec4(vec3(confidence), 1.0)); + return; + } + if (push_constants.debug_view == DEBUG_VIEW_NRD_RAW_CONFIDENCE_GRADIENT) { + vec2 gradient = clamp(imageLoad(nrd_raw_confidence_gradient, coords).xy, vec2(0.0), vec2(1.0)); + imageStore(out_denoised_hdr, coords, vec4(gradient.x, gradient.y, max(gradient.x, gradient.y), 1.0)); + return; + } + if (push_constants.debug_view == DEBUG_VIEW_NRD_FILTERED_CONFIDENCE_GRADIENT) { + vec2 gradient = clamp(imageLoad(nrd_filtered_confidence_gradient, coords).xy, vec2(0.0), vec2(1.0)); + imageStore(out_denoised_hdr, coords, vec4(gradient.x, gradient.y, max(gradient.x, gradient.y), 1.0)); + return; + } + if (push_constants.debug_view == DEBUG_VIEW_NRD_CONFIDENCE_HISTORY) { + vec4 rawGradient = imageLoad(nrd_raw_confidence_gradient, coords); + vec2 filteredGradient = clamp(imageLoad(nrd_filtered_confidence_gradient, coords).xy, vec2(0.0), vec2(1.0)); + float effectiveHistoryConfidence = 1.0 - max(filteredGradient.x, filteredGradient.y); + float sourcePixelLookup = clamp(rawGradient.w, 0.0, 1.0); + float currentPixelValid = clamp(rawGradient.z, 0.0, 1.0); + imageStore(out_denoised_hdr, coords, vec4(effectiveHistoryConfidence, sourcePixelLookup, currentPixelValid, 1.0)); + return; + } float viewZ = uintBitsToFloat(depth_normal_buffer[pixelIndex].x); if (!(viewZ < 1.0e9)) { imageStore(out_denoised_hdr, coords, vec4(raw, 1.0)); diff --git a/shaders/pathtrace.rahit b/shaders/pathtrace.rahit index 0126a2d..fc3adf2 100644 --- a/shaders/pathtrace.rahit +++ b/shaders/pathtrace.rahit @@ -39,14 +39,16 @@ void main() { return; } - uint triIndex = globalTriangleIndex * 3u; - uint i0 = local_mesh_indices[triIndex + 0u]; - uint i1 = local_mesh_indices[triIndex + 1u]; - uint i2 = local_mesh_indices[triIndex + 2u]; + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + record_rt_counter(RT_DIAG_CAMERA_ANY_HIT_IGNORED); + ignoreIntersectionEXT; + return; + } uvec4 skinningBinding = ray_tracing_gpu_skinning_binding(meshIndex); - LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, i0); - LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, i1); - LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, i2); + LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.x); + LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.y); + LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.z); Material material = decode_material(materialIndex); vec3 bary = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); diff --git a/shaders/pathtrace.rchit b/shaders/pathtrace.rchit index 66fa9a1..5ef4671 100644 --- a/shaders/pathtrace.rchit +++ b/shaders/pathtrace.rchit @@ -30,15 +30,16 @@ void main() { MeshRecord mesh = mesh_records[meshIndex]; uint firstIndex = mesh.vertex_index_data.z; uint globalTriangleIndex = geometry_triangle_offset(meshIndex, tlasRecordIndex, gl_GeometryIndexEXT, firstIndex) + gl_PrimitiveID; - uint triIndex = globalTriangleIndex * 3u; - uint i0 = local_mesh_indices[triIndex + 0u]; - uint i1 = local_mesh_indices[triIndex + 1u]; - uint i2 = local_mesh_indices[triIndex + 2u]; + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + payload.hit = 0u; + return; + } uvec4 skinningBinding = ray_tracing_gpu_skinning_binding(meshIndex); - LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, i0); - LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, i1); - LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, i2); + LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.x); + LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.y); + LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.z); vec3 p0 = v0.position_uv_x.xyz; vec3 p1 = v1.position_uv_x.xyz; vec3 p2 = v2.position_uv_x.xyz; diff --git a/shaders/pathtrace.rgen b/shaders/pathtrace.rgen index 71887d2..7c9c860 100644 --- a/shaders/pathtrace.rgen +++ b/shaders/pathtrace.rgen @@ -4,6 +4,9 @@ #ifndef RTV_NATIVE2B_PIPELINE #define RTV_NATIVE2B_PIPELINE 0 #endif +#if !RTV_NATIVE2B_PIPELINE +#extension GL_EXT_ray_query : require +#endif #ifndef RTV_BEAUTY_OUTPUT_ONLY #define RTV_BEAUTY_OUTPUT_ONLY 0 #endif @@ -29,9 +32,13 @@ uint restir_gi_receiver_material_id(RestirGiReceiver r) { return r.materialIds.x #include "restir_gi_reconnection.glsl" layout(set = 0, binding = 33) uniform accelerationStructureEXT topLevelAS; +layout(set = 0, binding = 72, std430) buffer PsrGuideBuffer { PsrGuideRecord psr_guide_buffer[]; }; +layout(set = 0, binding = 73, std430) buffer PsrGuideSignatureBuffer { uint psr_guide_signature_buffer[]; }; layout(location = 0) rayPayloadEXT RayPayload payload; layout(location = 1) rayPayloadEXT uint shadow_occluded; +#if RTV_NATIVE2B_PIPELINE layout(location = 2) rayPayloadEXT TerminalRayPayload terminal_payload; +#endif const uint RAY_MASK_CAMERA = 0x01u; const uint RAY_MASK_SHADOW = 0x02u; @@ -85,6 +92,7 @@ struct PathComponents { float caustic_transmissive_hits; float caustic_visible_paths; float caustic_blocked_paths; +#if RTV_REGIR_TRACE_ENABLED uint regir_query_count; uint regir_selected_light; float regir_reservoir_weight; @@ -104,6 +112,7 @@ struct PathComponents { float regir_environment_mis_weight; float regir_environment_m; uint regir_environment_generation_mismatch; +#endif float first_specular_probability; float direct_light_hit_distance; float diffuse_hit_distance; @@ -136,6 +145,8 @@ struct PathComponents { uint primitive_id; uint packed_velocity; uint packed_velocity_valid; + uint psr_candidate; + PsrGuideRecord psr_guide; }; struct Ray { @@ -191,6 +202,8 @@ void main() { world_position_buffer[pixel_index] = uvec2(0u); velocity_buffer[pixel_index] = packedVelocity; path_data_buffer[pixel_index] = empty_adaptive_path_data(); + psr_guide_buffer[pixel_index] = psr_invalid_guide(); + psr_guide_signature_buffer[pixel_index] = 0u; entity_id_buffer[pixel_index] = 0xffffffffu; if (restir_mode() != 0u) { restir_reservoirs[pixel_index] = empty_restir_reservoir(); @@ -285,7 +298,14 @@ void main() { #endif vec4 previous = (camera.frame_count <= 1u || camera.path_tracing_enabled == 0u) ? vec4(0.0) : accumulation_buffer[pixel_index]; vec3 previous_average = previous.rgb / max(previous.a, 1.0); + vec3 unclamped_sample_color = max(sample_color, vec3(0.0)); sample_color = clamp_firefly_sample(sample_color, previous_average, camera.frame_count); + float sampleClampScale = 1.0; + float unclampedSampleLum = dot(unclamped_sample_color, vec3(0.2126, 0.7152, 0.0722)); + float clampedSampleLum = dot(max(sample_color, vec3(0.0)), vec3(0.2126, 0.7152, 0.0722)); + if (unclampedSampleLum > 1.0e-6 && clampedSampleLum < unclampedSampleLum) { + sampleClampScale = clamp(clampedSampleLum / unclampedSampleLum, 0.0, 1.0); + } vec3 rr_sample_color = sample_color; vec4 accumulated = previous + vec4(sample_color, 1.0); accumulation_buffer[pixel_index] = accumulated; @@ -302,10 +322,10 @@ void main() { ? clamp(1.0 - abs(lum - avgLum) / (max(max(lum, avgLum), 0.0) + 1.0), 0.0, 1.0) : 0.0; float primaryHitDistance = did_hit ? min(hit_depth, 65504.0) : 65504.0; - vec3 directDiffuseSignal = components.direct_light * pathDiffuseWeight; - vec3 directSpecularSignal = components.direct_light * pathSpecularWeight; - vec3 indirectDiffuseSignal = (components.indirect_light + components.environment_light) * pathDiffuseWeight; - vec3 indirectSpecularSignal = (components.indirect_light + components.environment_light) * pathSpecularWeight; + vec3 directDiffuseSignal = components.direct_light * pathDiffuseWeight * sampleClampScale; + vec3 directSpecularSignal = components.direct_light * pathSpecularWeight * sampleClampScale; + vec3 indirectDiffuseSignal = (components.indirect_light + components.environment_light) * pathDiffuseWeight * sampleClampScale; + vec3 indirectSpecularSignal = (components.indirect_light + components.environment_light) * pathSpecularWeight * sampleClampScale; float directDiffuseWeight = luminance(max(directDiffuseSignal, vec3(0.0))); float directSpecularWeight = luminance(max(directSpecularSignal, vec3(0.0))); float indirectDiffuseWeight = luminance(max(indirectDiffuseSignal, vec3(0.0))); @@ -339,7 +359,7 @@ void main() { fallbackRayDirection) : fallbackRayDirection; PathDataRecord pathData; - float emissiveSignal = luminance(components.emissive_light); + float emissiveSignal = luminance(components.emissive_light * sampleClampScale); pathData.direct_diffuse = vec4(directDiffuseSignal, emissiveSignal); pathData.direct_specular = vec4(directSpecularSignal, primaryHitDistance); pathData.indirect_diffuse = vec4(indirectDiffuseSignal, confidence); @@ -359,9 +379,9 @@ void main() { pathData.specular_ray_direction_hit_distance = vec4( components.specular_ray_direction, components.specular_hit_distance); - pathData.emissive_residual = vec4(max(components.visible_emissive_light, vec3(0.0)), 0.0); + pathData.emissive_residual = vec4(max(components.visible_emissive_light * sampleClampScale, vec3(0.0)), 0.0); vec3 restirGiCurrentFallback = components.restir_gi_candidate_valid != 0u - ? components.restir_gi_candidate_radiance / max(components.restir_gi_candidate_source_pdf, 1.0e-8) + ? (components.restir_gi_candidate_radiance * sampleClampScale) / max(components.restir_gi_candidate_source_pdf, 1.0e-8) : vec3(0.0); pathData.restir_gi_fallback_reactive = vec4(max(restirGiCurrentFallback, vec3(0.0)), pathSpecularWeight); path_data_buffer[pixel_index] = pathData; @@ -378,9 +398,23 @@ void main() { ivec2(size))) : compute_sky_velocity(ray.direction, ivec2(size)); velocity_buffer[pixel_index] = packedVelocity; + if (psr_guide_valid(components.psr_guide)) { + components.psr_guide.geometry.w = psr_guide_active(components.psr_guide) + ? components.psr_guide.geometry.w + : packedVelocity; + } + psr_guide_buffer[pixel_index] = components.psr_guide; + psr_guide_signature_buffer[pixel_index] = psr_guide_signature(components.psr_guide); store_initial_restir_reservoir(pixel_index, ivec2(pixel), ivec2(size), packedVelocity, components); if (!native2b_kernel_enabled() || restir_gi_enabled() || restir_gi_debug_view()) { - store_initial_restir_gi_reservoir(pixel_index, ivec2(pixel), ivec2(size), packedVelocity, components); + PathComponents restirGiComponents = components; + if (sampleClampScale < 0.999999 && restirGiComponents.restir_gi_candidate_valid != 0u) { + restirGiComponents.restir_gi_candidate_radiance *= sampleClampScale; + restirGiComponents.restir_gi_candidate_suffix_radiance *= sampleClampScale; + restirGiComponents.restir_gi_candidate_target_pdf = + max(luminance(max(restirGiComponents.restir_gi_candidate_radiance, vec3(0.0))), 1.0e-8); + } + store_initial_restir_gi_reservoir(pixel_index, ivec2(pixel), ivec2(size), packedVelocity, restirGiComponents); store_restir_gi_receiver(pixel_index, ivec2(pixel), packedVelocity, components, hit_position, hit_normal, hit_depth, did_hit); } if (restir_di_raygen_params.width != 0u) { @@ -637,12 +671,16 @@ void main() { output_value = vec3(previousWeight, 1.0 - previousWeight, clamp(restir_reservoirs[pixel_index].sample_value_confidence.a, 0.0, 1.0)); } else if (debug_params.view == 68u) { RestirGiReservoir reservoir = restir_gi_reservoirs[pixel_index]; - RestirGiReservoir previousReservoir = previous_restir_gi_reservoirs[pixel_index]; - RestirGiReservoir spatialReservoir = restir_gi_spatial_reservoirs[pixel_index]; - output_value = vec3( - restir_gi_reservoir_valid(reservoir) ? 1.0 : 0.0, - restir_gi_reservoir_valid(previousReservoir) ? 1.0 : 0.0, - restir_gi_reservoir_valid(spatialReservoir) ? 1.0 : 0.0); + if ((camera.restir_gi_controls.y & 8u) == 0u) { + output_value = vec3(restir_gi_reservoir_valid(reservoir) ? 1.0 : 0.0); + } else { + RestirGiReservoir previousReservoir = previous_restir_gi_reservoirs[pixel_index]; + RestirGiReservoir spatialReservoir = restir_gi_spatial_reservoirs[pixel_index]; + output_value = vec3( + restir_gi_reservoir_valid(reservoir) ? 1.0 : 0.0, + restir_gi_reservoir_valid(previousReservoir) ? 1.0 : 0.0, + restir_gi_reservoir_valid(spatialReservoir) ? 1.0 : 0.0); + } } else if (debug_params.view == 69u) { RestirGiReservoir reservoir = restir_gi_reservoirs[pixel_index]; output_value = vec3( @@ -714,6 +752,33 @@ void main() { temporal_normalized_hit_distance(pathData.denoiser_hit_distance.x), temporal_normalized_hit_distance(pathData.denoiser_hit_distance.y), clamp(pathData.indirect_diffuse.a, 0.0, 1.0)); + } else if (debug_params.view == 165u) { + output_value = psr_guide_active(components.psr_guide) + ? vec3(0.05, 1.0, 0.25) + : (psr_guide_valid(components.psr_guide) ? vec3(0.12) : vec3(0.0)); + } else if (debug_params.view == 166u) { + float viewZ = psr_guide_valid(components.psr_guide) ? components.psr_guide.distances.y : 0.0; + output_value = vec3(clamp(log2(max(viewZ, 0.0) + 1.0) / 16.0, 0.0, 1.0)); + } else if (debug_params.view == 167u) { + vec2 motion = psr_guide_motion_pixels(components.psr_guide); + output_value = vec3(clamp(abs(motion) / 32.0, vec2(0.0), vec2(1.0)), + psr_guide_active(components.psr_guide) ? 1.0 : 0.0); + } else if (debug_params.view == 168u) { + vec3 normal = psr_guide_normal(components.psr_guide); + output_value = psr_guide_valid(components.psr_guide) + ? vec3(normal.xy * 0.5 + 0.5, psr_guide_roughness(components.psr_guide)) + : vec3(0.0); + } else if (debug_params.view == 169u) { + float hitDistance = psr_guide_valid(components.psr_guide) ? components.psr_guide.distances.x : 0.0; + output_value = vec3(temporal_normalized_hit_distance(hitDistance)); + } else if (debug_params.view == 170u) { + output_value = pixel.x < size.x / 2u + ? psr_guide_diffuse_albedo(components.psr_guide) + : psr_guide_specular_f0(components.psr_guide); + } else if (debug_params.view == 171u) { + output_value = psr_guide_valid(components.psr_guide) + ? psr_guide_ray_direction(components.psr_guide) * 0.5 + 0.5 + : vec3(0.0); } imageStore(output_color, ivec2(pixel), vec4(output_value, 1.0)); #endif diff --git a/shaders/pathtrace_camera_volume.glsl b/shaders/pathtrace_camera_volume.glsl index 7518ecc..d98ce8d 100644 --- a/shaders/pathtrace_camera_volume.glsl +++ b/shaders/pathtrace_camera_volume.glsl @@ -169,11 +169,12 @@ bool restir_gi_debug_view() { #if RTV_NATIVE2B_PIPELINE return false; #else - return (debug_params.view >= 68u && debug_params.view <= 73u) || - debug_params.view == 90u || - debug_params.view == 91u || - debug_params.view == 127u || - debug_params.view == 128u; + uint debugView = renderer_debug_view(); + return (debugView >= 68u && debugView <= 73u) || + debugView == 90u || + debugView == 91u || + debugView == 127u || + debugView == 128u; #endif } @@ -181,7 +182,7 @@ bool native2b_kernel_enabled() { #if RTV_NATIVE2B_PIPELINE return true; #else - return (camera.path_trace_controls.z & 16u) != 0u; + return false; #endif } diff --git a/shaders/pathtrace_integrator.glsl b/shaders/pathtrace_integrator.glsl index a02a20d..07994c6 100644 --- a/shaders/pathtrace_integrator.glsl +++ b/shaders/pathtrace_integrator.glsl @@ -67,6 +67,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.caustic_transmissive_hits = 0.0; components.caustic_visible_paths = 0.0; components.caustic_blocked_paths = 0.0; +#if RTV_REGIR_TRACE_ENABLED components.regir_query_count = 0u; components.regir_selected_light = 0xffffffffu; components.regir_reservoir_weight = 0.0; @@ -86,6 +87,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.regir_environment_mis_weight = 0.0; components.regir_environment_m = 0.0; components.regir_environment_generation_mismatch = 0u; +#endif components.first_specular_probability = 0.0; components.direct_light_hit_distance = 65504.0; components.diffuse_hit_distance = 65504.0; @@ -118,6 +120,8 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.primitive_id = 0xffffffffu; components.packed_velocity = 0u; components.packed_velocity_valid = 0u; + components.psr_candidate = 0u; + components.psr_guide = psr_invalid_guide(); bool previousWasBrdfSample = false; uint previousEventType = PATH_EVENT_NONE; @@ -139,7 +143,8 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di uint rrStartDepth = clamp(camera.max_bounces / 2u, 3u, 5u); for (uint bounce = 0u; bounce < bounceLimit; ++bounce) { rng ^= sample_dimension_seed(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_PATH_SEED); - if (native2b_kernel_enabled() && bounce == 1u) { +#if RTV_NATIVE2B_PIPELINE + if (bounce == 1u) { TerminalRayPayload terminalHit = trace_terminal_surface(ray.origin, ray.direction, 0.001, 10000.0); components.secondary_ray_direction = normalize(prevRayDir); if (terminalHit.hit == 0u) { @@ -204,7 +209,9 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di #if RTV_NATIVE2B_COMPACT_PRIMARY_LIGHTS Material terminalMaterial = terminal_material_for_hit_fast(terminalRayHit, ray.direction); #else +#if RTV_MATERIAL_TEXTURES_ENABLED set_material_texture_lod(path_trace_material_texture_lod(1u, terminalHit.t)); +#endif Material terminalMaterial = material_for_hit(terminalRayHit, ray.direction); #endif const float terminalScale = camera.indirect_strength * @@ -260,6 +267,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di vec3 terminalDirectLightRadiance; vec3 terminalDirectLightNormal; vec3 terminalRestirCandidateDirect; +#if RTV_REGIR_TRACE_ENABLED uint terminalRegirQueryCount; uint terminalRegirSelectedLight; float terminalRegirReservoirWeight; @@ -279,6 +287,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di float terminalRegirEnvironmentMisWeight; float terminalRegirEnvironmentM; uint terminalRegirEnvironmentGenerationMismatch; +#endif terminalDirect = throughput * estimate_direct_lighting( directRng, terminalRayHit, @@ -302,6 +311,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di terminalDirectLightRadiance, terminalDirectLightNormal, terminalRestirCandidateDirect, +#if RTV_REGIR_TRACE_ENABLED terminalRegirQueryCount, terminalRegirSelectedLight, terminalRegirReservoirWeight, @@ -321,6 +331,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di terminalRegirEnvironmentMisWeight, terminalRegirEnvironmentM, terminalRegirEnvironmentGenerationMismatch, +#endif terminalCausticTransmissiveHits, terminalCausticVisiblePaths, terminalCausticBlockedPaths); @@ -342,6 +353,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di } break; } +#endif RayPayload hit = trace_surface(ray.origin, ray.direction, 0.001, 10000.0, ray.time); float segmentDistance = hit.hit != 0u ? hit.t : 10000.0; #if !RTV_NATIVE2B_PIPELINE @@ -380,7 +392,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di sky *= env_params.background_intensity; } vec3 env = throughput * (sky + sunDisk); - if (previousEventType == PATH_EVENT_BSDF && previousWasBrdfSample && env_params.enabled != 0u && debug_params.view != 27u) { + if (previousEventType == PATH_EVENT_BSDF && previousWasBrdfSample && env_params.enabled != 0u && renderer_debug_view() != 27u) { float envPdf = environment_light_sampling_pdf(ray.direction, bounce); if (envPdf > 1e-6) { float skyWeight = power_heuristic(previousBrdfPdf, envPdf); @@ -441,7 +453,9 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di } components.bounce_count += 1u; +#if RTV_MATERIAL_TEXTURES_ENABLED set_material_texture_lod(path_trace_material_texture_lod(bounce, hit.t)); +#endif Material material = material_for_hit(hit, ray.direction); if (bounce == 0u) { did_hit = true; @@ -491,9 +505,61 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.instance_id = hit.instance_id; components.mesh_id = hit.mesh_id; components.primitive_id = hit.primitive_id; + uvec2 psrPackedPosition = pack_world_position(hit.world_pos); + uint psrPrimaryVelocity = compute_surface_velocity( + hit.world_pos, + hit.local_pos, + hit.instance_id, + hit.mesh_id, + hit.primitive_id, + hit.barycentrics, + dims); + uint psrIdentity = pcg_hash(hit.instance_id ^ (hit.primitive_id * 0x9e3779b9u)); + components.psr_guide.geometry = uvec4( + psrPackedPosition, + psr_pack_direction(hit.normal), + psrPrimaryVelocity); + components.psr_guide.material = uvec4( + packUnorm4x8(vec4(clamp(components.first_albedo, 0.0, 1.0), 1.0)), + packUnorm4x8(vec4(clamp(components.restir_di_f0, 0.0, 1.0), 1.0)), + psr_pack_direction(ray.direction), + psr_pack_metadata(true, false, material.roughness, psrIdentity)); + components.psr_guide.distances = vec4( + min(hit.t, 65504.0), + max(dot(hit.world_pos - camera.pos.xyz, camera.forward.xyz), 0.0), + min(hit.t, 65504.0), + 0.0); + components.packed_velocity = psrPrimaryVelocity; + components.packed_velocity_valid = 1u; } else if (bounce == 1u) { components.secondary_hit_distance = min(hit.t, 65504.0); components.secondary_ray_direction = normalize(prevRayDir); + if (components.psr_candidate != 0u) { + uvec2 psrPackedPosition = pack_world_position(hit.world_pos); + uint psrMotion = compute_surface_velocity( + hit.world_pos, + hit.local_pos, + hit.instance_id, + hit.mesh_id, + hit.primitive_id, + hit.barycentrics, + dims); + uint psrIdentity = pcg_hash(hit.instance_id ^ (hit.primitive_id * 0x9e3779b9u)); + components.psr_guide.geometry = uvec4( + psrPackedPosition, + psr_pack_direction(hit.normal), + psrMotion); + components.psr_guide.material = uvec4( + packUnorm4x8(vec4(clamp(pbr_diffuse_reflectance(material), 0.0, 1.0), 1.0)), + packUnorm4x8(vec4(clamp(pbr_f0(material), 0.0, 1.0), 1.0)), + psr_pack_direction(prevRayDir), + psr_pack_metadata(true, true, material.roughness, psrIdentity)); + components.psr_guide.distances = vec4( + min(hit.t, 65504.0), + max(dot(hit.world_pos - camera.pos.xyz, camera.forward.xyz), 0.0), + components.psr_guide.distances.z, + 0.0); + } } const bool finalBounceFastPath = @@ -505,7 +571,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di previousWasBrdfSample && !restir_gi_enabled() && !homogeneous_volume_enabled() && - debug_params.view != 27u; + renderer_debug_view() != 27u; if (finalBounceFastPath) { Material terminalMaterial = decode_material(hit.material_id); const float terminalScale = camera.indirect_strength * @@ -531,7 +597,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di break; } - if (camera.sunlight_enabled != 0u && debug_params.view != 27u) { + if (camera.sunlight_enabled != 0u && renderer_debug_view() != 27u) { float segDist = length(hit.world_pos - prevHitPos); if (bounce > 0u && segDist > 0.1) { vec3 segTrans = atmosphere_segment_transmittance(prevHitPos, prevRayDir, segDist, 8); @@ -539,7 +605,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di } } - if (bounce == 0u && debug_params.view == 24u) { + if (bounce == 0u && renderer_debug_view() == 24u) { float testPdf; uint envTestRng = sample_dimension_seed(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_ENVIRONMENT); vec3 testDir = sample_cosine_hemisphere(envTestRng, hit.normal, testPdf); @@ -547,7 +613,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.secondary_env_miss = testHit.hit == 0u ? 1.0 : 0.0; return vec3(components.secondary_env_miss); } - if (bounce == 0u && debug_params.view == 26u) { + if (bounce == 0u && renderer_debug_view() == 26u) { float testPdf; uint envTestRng = sample_dimension_seed(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_ENVIRONMENT + 2u); vec3 testDir = sample_cosine_hemisphere(envTestRng, hit.normal, testPdf); @@ -622,6 +688,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di uint causticTransmissiveHits; uint causticVisiblePaths; uint causticBlockedPaths; +#if RTV_REGIR_TRACE_ENABLED uint regirQueryCount; uint regirSelectedLight; float regirReservoirWeight; @@ -641,6 +708,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di float regirEnvironmentMisWeight; float regirEnvironmentM; uint regirEnvironmentGenerationMismatch; +#endif vec3 direct = vec3(0.0); emissiveDirect = vec3(0.0); envDirect = vec3(0.0); @@ -661,6 +729,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di causticTransmissiveHits = 0u; causticVisiblePaths = 0u; causticBlockedPaths = 0u; +#if RTV_REGIR_TRACE_ENABLED regirQueryCount = 0u; regirSelectedLight = 0xffffffffu; regirReservoirWeight = 0.0; @@ -680,6 +749,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di regirEnvironmentMisWeight = 0.0; regirEnvironmentM = 0.0; regirEnvironmentGenerationMismatch = 0u; +#endif const bool allowSecondaryDirect = camera.restir_di_controls.y != 0u; if (!hitEmissiveSurface && (bounce == 0u || allowSecondaryDirect)) { uint directRng = sample_dimension_seed(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_LIGHT_SELECT); @@ -741,6 +811,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di directLightRadiance, directLightNormal, restirCandidateDirect, +#if RTV_REGIR_TRACE_ENABLED regirQueryCount, regirSelectedLight, regirReservoirWeight, @@ -760,6 +831,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di regirEnvironmentMisWeight, regirEnvironmentM, regirEnvironmentGenerationMismatch, +#endif causticTransmissiveHits, causticVisiblePaths, causticBlockedPaths); @@ -771,6 +843,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.caustic_transmissive_hits += float(causticTransmissiveHits); components.caustic_visible_paths += float(causticVisiblePaths); components.caustic_blocked_paths += float(causticBlockedPaths); +#if RTV_REGIR_TRACE_ENABLED if (regirQueryCount > 0u || regirEffectivePdf > 0.0) { components.regir_query_count += regirQueryCount; components.regir_selected_light = regirSelectedLight; @@ -794,6 +867,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di components.regir_environment_m = regirEnvironmentM; components.regir_environment_generation_mismatch = regirEnvironmentGenerationMismatch; } +#endif vec3 shadedDirect = direct; vec3 restirDirect = directSampleType != 0u ? throughput * restirCandidateDirect : vec3(0.0); vec3 nonRestirDirect = max(direct - restirDirect, vec3(0.0)); @@ -1016,6 +1090,8 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di throughput = clamp_path_throughput(throughput, bounce + 1u, material.roughness, true); if (bounce == 0u) { components.first_bounce_throughput = throughput; + components.psr_candidate = 1u; + components.psr_guide.material.z = psr_pack_direction(ray.direction); } previousEventType = PATH_EVENT_DELTA; continuedPath = true; @@ -1046,12 +1122,17 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di throughput = clamp_path_throughput(throughput, bounce + 1u, material.roughness, true); if (bounce == 0u) { components.first_bounce_throughput = throughput; + components.psr_candidate = reflected && material.roughness <= 0.2 ? 1u : 0u; + if (components.psr_candidate != 0u) { + components.psr_guide.material.z = psr_pack_direction(ray.direction); + } } previousEventType = PATH_EVENT_DELTA; continuedPath = true; } else { float bsdfPdf; uint bsdfRng = sample_dimension_seed(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_BSDF); + vec3 mirrorDirection = reflect(ray.direction, hit.normal); vec3 wi = sample_brdf(bsdfRng, material, wo, hit.normal, hit.tangent, hit.bitangent, bsdfPdf); rng ^= bsdfRng; float cosTheta = max(dot(hit.normal, wi), 0.0); @@ -1069,6 +1150,12 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di throughput = clamp_path_throughput(throughput, bounce + 1u, material.roughness, false); if (bounce == 0u) { components.first_bounce_throughput = throughput; + float mirrorAlignmentThreshold = mix(0.997, 0.90, clamp(material.roughness / 0.2, 0.0, 1.0)); + components.psr_candidate = material.roughness <= 0.2 && + dot(normalize(wi), normalize(mirrorDirection)) >= mirrorAlignmentThreshold ? 1u : 0u; + if (components.psr_candidate != 0u) { + components.psr_guide.material.z = psr_pack_direction(ray.direction); + } } continuedPath = true; } @@ -1081,7 +1168,7 @@ vec3 trace_path(Ray ray, inout uint rng, uint pixelIndex, ivec2 coords, ivec2 di prevRayDir = ray.direction; uint completedDepth = bounce + 1u; - if (completedDepth >= rrStartDepth && completedDepth < bounceLimit && debug_params.view != 27u) { + if (completedDepth >= rrStartDepth && completedDepth < bounceLimit && renderer_debug_view() != 27u) { float p = clamp(luminance(throughput), russian_roulette_min_survival(), 0.95); if (sample_dimension_1d(coords, camera.temporal_frame_index, bounce, SAMPLE_DIM_RUSSIAN_ROULETTE) > p) { break; diff --git a/shaders/pathtrace_lighting.glsl b/shaders/pathtrace_lighting.glsl index 494bcbd..db50c82 100644 --- a/shaders/pathtrace_lighting.glsl +++ b/shaders/pathtrace_lighting.glsl @@ -456,6 +456,38 @@ void mark_regir_hash_cell(uint cellIndex) { } #if RTV_REGIR_FINITE_LIGHT_TRACE_ENABLED +bool regir_reservoir_identity_matches(ReGIRReservoir reservoir, LightRecord light) { + return reservoir.metadata.y == light.metadata.x && + reservoir.light_identity.x == light.identity.x && + reservoir.light_identity.y == light.identity.y && + reservoir.light_identity.z == light.identity.z && + reservoir.light_identity.w == light.identity.w; +} + +bool resolve_regir_reservoir_light(ReGIRReservoir reservoir, out uint lightIndex) { + if (reservoir.metadata.w == 0u || mesh_params.light_count == 0u) { + lightIndex = 0u; + return false; + } + + uint cachedIndex = reservoir.metadata.x; + if (cachedIndex < mesh_params.light_count && + regir_reservoir_identity_matches(reservoir, light_records[cachedIndex])) { + lightIndex = cachedIndex; + return true; + } + + for (uint i = 0u; i < mesh_params.light_count; ++i) { + if (regir_reservoir_identity_matches(reservoir, light_records[i])) { + lightIndex = i; + return true; + } + } + + lightIndex = 0u; + return false; +} + bool sample_regir_emissive_light( inout uint rng, vec3 hitPos, @@ -564,13 +596,15 @@ bool sample_regir_emissive_light( uint r = (reservoirQueryStart + ri * reservoirQueryStride) % reservoirsPerCell; uint reservoirIndex = storageCellIndex * reservoirsPerCell + r; ReGIRReservoir inputReservoir = regir_input_reservoirs[reservoirIndex]; - if (inputReservoir.metadata.w != 0u && inputReservoir.metadata.x < mesh_params.light_count) { + uint inputLightIndex; + if (resolve_regir_reservoir_light(inputReservoir, inputLightIndex)) { inputSourceWeightSum += max(inputReservoir.sample_position_weight.w, 0.0); } ReGIRReservoir reservoir = regir_reservoirs[reservoirIndex]; ++queryCountOut; ++queriedReservoirSlots; - if (reservoir.metadata.w == 0u || reservoir.metadata.x >= mesh_params.light_count) { + uint resolvedLightIndex; + if (!resolve_regir_reservoir_light(reservoir, resolvedLightIndex)) { continue; } float reservoirSourceWeight = max(reservoir.sample_position_weight.w, 0.0); @@ -631,14 +665,17 @@ bool sample_regir_emissive_light( uint r = (reservoirQueryStart + ri * reservoirQueryStride) % reservoirsPerCell; ReGIRReservoir reservoir = regir_reservoirs[storageCellIndex * reservoirsPerCell + r]; ++queryCountOut; - if (reservoir.metadata.w == 0u || reservoir.metadata.x >= mesh_params.light_count) { + uint resolvedLightIndex; + if (!resolve_regir_reservoir_light(reservoir, resolvedLightIndex)) { continue; } float sourceWeight = max(reservoir.sample_position_weight.w, 0.0); if (sourceWeight <= 0.0) { continue; } - float sourcePdf = sourceWeight / sourceWeightSum; + float averageSourceWeight = max(reservoir.proposal_pdf_m.z, 1.0e-8); + float sourcePdf = sourceWeight / + max(averageSourceWeight * float(max(mesh_params.light_count, 1u)), 1.0e-8); vec3 candidateWi; vec3 candidateEmission; float candidatePdf; @@ -650,7 +687,7 @@ bool sample_regir_emissive_light( vec3 candidateNormal; if (!sample_emissive_light_index( rng, - reservoir.metadata.x, + resolvedLightIndex, sourcePdf, hitPos, candidateWi, @@ -703,8 +740,8 @@ bool sample_regir_emissive_light( ++validCandidateCount; risWeightSum += risWeight; if (risWeight > 0.0 && rand_f32(rng) * risWeightSum <= risWeight) { - selectedLight = reservoir.metadata.x; - selectedKind = reservoir.metadata.y; + selectedLight = resolvedLightIndex; + selectedKind = light_records[resolvedLightIndex].metadata.x; selectedWi = candidateWi; selectedEmission = candidateEmission; selectedPdf = candidatePdf; @@ -903,10 +940,14 @@ float environment_light_sampling_pdf(vec3 dir, uint bounce) { if (sourcePdf <= 0.0) { return 0.0; } +#if RTV_REGIR_TRACE_ENABLED float canonicalProbability; float regirProbability; regir_environment_sampling_probabilities(bounce, canonicalProbability, regirProbability); return sourcePdf * max(canonicalProbability + regirProbability, 0.0); +#else + return sourcePdf; +#endif } bool regir_sun_direct_available(uint bounce) { @@ -927,10 +968,14 @@ float sun_light_sampling_pdf(vec3 dir, uint bounce) { if (sourcePdf <= 0.0) { return 0.0; } +#if RTV_REGIR_TRACE_ENABLED float canonicalProbability; float regirProbability; regir_sun_sampling_probabilities(bounce, canonicalProbability, regirProbability); return sourcePdf * max(canonicalProbability + regirProbability, 0.0); +#else + return sourcePdf; +#endif } float emissive_hit_pdf(RayPayload hit, Material material, vec3 previousOrigin) { @@ -981,6 +1026,7 @@ vec3 estimate_direct_lighting( out vec3 sampledLightRadiance, out vec3 sampledLightNormal, out vec3 sampledRestirContribution, +#if RTV_REGIR_TRACE_ENABLED out uint regirQueryCount, out uint regirSelectedLight, out float regirReservoirWeight, @@ -1000,6 +1046,7 @@ vec3 estimate_direct_lighting( out float regirEnvironmentMisWeight, out float regirEnvironmentM, out uint regirEnvironmentGenerationMismatch, +#endif out uint causticTransmissiveHits, out uint causticVisiblePaths, out uint causticBlockedPaths) { @@ -1019,6 +1066,7 @@ vec3 estimate_direct_lighting( sampledLightRadiance = vec3(0.0); sampledLightNormal = vec3(0.0, 1.0, 0.0); sampledRestirContribution = vec3(0.0); +#if RTV_REGIR_TRACE_ENABLED regirQueryCount = 0u; regirSelectedLight = 0xffffffffu; regirReservoirWeight = 0.0; @@ -1038,11 +1086,12 @@ vec3 estimate_direct_lighting( regirEnvironmentMisWeight = 0.0; regirEnvironmentM = 0.0; regirEnvironmentGenerationMismatch = 0u; +#endif causticTransmissiveHits = 0u; causticVisiblePaths = 0u; causticBlockedPaths = 0u; bool isDelta = material_is_delta(material); - if (camera.direct_lighting_enabled == 0u || isDelta || debug_params.view == 27u) { + if (camera.direct_lighting_enabled == 0u || isDelta || renderer_debug_view() == 27u) { return vec3(0.0); } float secondaryDirectProbability = 1.0; @@ -1065,8 +1114,12 @@ vec3 estimate_direct_lighting( float secondaryDirectWeight = 1.0 / secondaryDirectProbability; vec3 lightingNormal = normalize(hit.geom_normal); +#if RTV_REGIR_TRACE_ENABLED bool regirCandidateAvailable = bounce >= 2u && regir_finite_light_enabled(); uint risCandidateCount = regirCandidateAvailable ? 1u : direct_light_ris_candidate_count(); +#else + uint risCandidateCount = direct_light_ris_candidate_count(); +#endif vec3 selectedWi = vec3(0.0, 1.0, 0.0); vec3 selectedEmission = vec3(0.0); vec3 selectedBsdf = vec3(0.0); @@ -1084,13 +1137,19 @@ vec3 estimate_direct_lighting( float sampledRestirProxy = 0.0; float proxyWeightSum = 0.0; bool selectedCandidate = false; +#if RTV_REGIR_TRACE_ENABLED bool selectedUsedCanonical = false; +#endif if (mesh_params.light_count != 0u && mesh_params.emissive_total_area > 1.0e-8) { +#if RTV_REGIR_TRACE_ENABLED float requestedCanonicalProbability = regirCandidateAvailable ? regir_canonical_mix() : 1.0; float regirProbability = regirCandidateAvailable ? (1.0 - requestedCanonicalProbability) * regir_finite_query_probability() : 0.0; float canonicalProbability = 1.0 - regirProbability; +#else + const float canonicalProbability = 1.0; +#endif for (uint candidate = 0u; candidate < risCandidateCount; ++candidate) { vec3 candidateWi; vec3 candidateEmission; @@ -1101,6 +1160,7 @@ vec3 estimate_direct_lighting( vec3 candidateLightPosition; vec3 candidateLightRadiance; vec3 candidateLightNormal; +#if RTV_REGIR_TRACE_ENABLED uint candidateRegirQueries = 0u; float candidateRegirWeight = 0.0; float candidateRegirSpatialInputWeight = 0.0; @@ -1109,17 +1169,22 @@ vec3 estimate_direct_lighting( uvec3 candidateRegirCell = uvec3(0u); float candidateRegirActiveCellOccupancy = 0.0; float candidateRegirHashCollisions = 0.0; +#endif vec3 candidateReusedVisibility = vec3(1.0); bool candidateReusedVisibilityKnown = false; uint candidateVisibilityRays = 0u; uint candidateVisibilityTransmissiveHits = 0u; uint candidateVisibilityVisiblePaths = 0u; uint candidateVisibilityBlockedPaths = 0u; +#if RTV_REGIR_TRACE_ENABLED bool candidateUsedCanonical = true; +#endif bool sampledCandidate = false; +#if RTV_REGIR_TRACE_ENABLED + uint regirChoiceRng = rng ^ 0x9e3779b9u; if (regirCandidateAvailable && regirProbability > 0.0 && - rand_f32(rng) >= canonicalProbability) { + rand_f32(regirChoiceRng) >= canonicalProbability) { sampledCandidate = sample_regir_emissive_light( rng, hit.world_pos, @@ -1160,6 +1225,7 @@ vec3 estimate_direct_lighting( candidateUsedCanonical = false; } } +#endif if (!sampledCandidate && canonicalProbability > 0.0) { sampledCandidate = sample_emissive_light( rng, @@ -1175,9 +1241,12 @@ vec3 estimate_direct_lighting( candidateLightNormal); if (sampledCandidate) { candidateLightPdf *= canonicalProbability; +#if RTV_REGIR_TRACE_ENABLED candidateUsedCanonical = true; +#endif } } +#if RTV_REGIR_TRACE_ENABLED regirQueryCount += candidateRegirQueries; if (candidateRegirQueries > 0u) { regirQueryCell = candidateRegirCell; @@ -1187,6 +1256,7 @@ vec3 estimate_direct_lighting( regirActiveCellOccupancy = candidateRegirActiveCellOccupancy; regirHashCollisions = candidateRegirHashCollisions; } +#endif if (!sampledCandidate) { continue; } @@ -1216,6 +1286,7 @@ vec3 estimate_direct_lighting( selectedReusedVisibilityKnown = candidateReusedVisibilityKnown; selectedProxy = candidateProxy; selectedCandidate = true; +#if RTV_REGIR_TRACE_ENABLED selectedUsedCanonical = candidateUsedCanonical; if (!candidateUsedCanonical) { regirSelectedLight = candidateLightIndex; @@ -1226,6 +1297,7 @@ vec3 estimate_direct_lighting( regirActiveCellOccupancy = candidateRegirActiveCellOccupancy; regirHashCollisions = candidateRegirHashCollisions; } +#endif } } } @@ -1270,6 +1342,7 @@ vec3 estimate_direct_lighting( sampledLightDirection = selectedWi; sampledLightRadiance = selectedLightRadiance * shadowT; sampledLightNormal = selectedLightNormal; +#if RTV_REGIR_TRACE_ENABLED if (regirCandidateAvailable) { regirMisWeight = weight; regirEffectivePdf = effectiveLightPdf; @@ -1278,6 +1351,7 @@ vec3 estimate_direct_lighting( regirSelectedLight = selectedLightIndex; } } +#endif emissiveContribution = selectedBsdf * selectedEmission * shadowT * cosSurface * weight / max(effectiveLightPdf, 1e-6); sampledRestirContribution = emissiveContribution; sampledRestirProxy = luminance(sampledRestirContribution); @@ -1306,13 +1380,16 @@ vec3 estimate_direct_lighting( uint envDirectSampleCount = sampleEnvironmentDirect ? (secondaryOneInfiniteLight ? 1u : clamp(camera.environment_direct_samples, 1u, 8u)) : 0u; +#if RTV_REGIR_TRACE_ENABLED float envCanonicalProbability; float envRegirProbability; regir_environment_sampling_probabilities(bounce, envCanonicalProbability, envRegirProbability); +#endif for (uint envSample = 0u; envSample < envDirectSampleCount; ++envSample) { vec3 envDir; float envPdf; vec3 envRadiance; +#if RTV_REGIR_TRACE_ENABLED uint envSourceKind; uint envReservoirSampleCount; uint envGenerationMismatch = 0u; @@ -1333,6 +1410,9 @@ vec3 estimate_direct_lighting( envSourceKind = 0u; envReservoirSampleCount = 0u; } +#else + envRadiance = sample_environment_direction(rng, envDir, envPdf); +#endif if (envPdf <= 0.0) { continue; } @@ -1365,6 +1445,7 @@ vec3 estimate_direct_lighting( } envEffectivePdf *= max(environmentTechniqueProbability, 1.0e-6); float weight = power_heuristic(envEffectivePdf, bsdfPdf); +#if RTV_REGIR_TRACE_ENABLED if (sampledEnvironmentBank) { regirEnvironmentSourceKind = envSourceKind; regirEnvironmentSourcePdf = envPdf; @@ -1373,6 +1454,7 @@ vec3 estimate_direct_lighting( regirEnvironmentMisWeight = weight; regirEnvironmentM = float(max(envReservoirSampleCount, 1u)); } +#endif vec3 envSampleContribution = bsdf * envRadiance * shadowT * cosSurface * weight / max(envEffectivePdf, 1e-6); if (camera.restir_di_controls.x != 0u && camera.restir_di_controls.w != 0u) { vec3 envRestirContribution = envSampleContribution / float(envDirectSampleCount); @@ -1409,6 +1491,7 @@ vec3 estimate_direct_lighting( vec3 sunWi; vec3 sunRadiance; float sunPdf; +#if RTV_REGIR_TRACE_ENABLED uint sunSourceKind = 0u; uint sunReservoirSampleCount = 0u; uint sunGenerationMismatch = 0u; @@ -1432,6 +1515,9 @@ vec3 estimate_direct_lighting( sunReservoirSampleCount = 0u; } bool sampledSun = sampledSunBank || sample_sun_light(rng, sunWi, sunRadiance, sunPdf); +#else + bool sampledSun = sample_sun_light(rng, sunWi, sunRadiance, sunPdf); +#endif if (sampledSun) { float ndl = max(dot(lightingNormal, sunWi), 0.0); if (ndl <= 0.0) { @@ -1464,6 +1550,7 @@ vec3 estimate_direct_lighting( float weight = power_heuristic(sunEffectivePdf, bsdfPdf); vec3 sunIncidentRadiance = sunRadiance * sun_transmittance(hit.world_pos, sunWi); vec3 sunCandidateContribution = bsdf * sunIncidentRadiance * shadowT * ndl * weight / max(sunEffectivePdf, 1e-6); +#if RTV_REGIR_TRACE_ENABLED if (sampledSunBank) { regirEnvironmentSourceKind = sunSourceKind; regirEnvironmentSourcePdf = sunPdf; @@ -1472,6 +1559,7 @@ vec3 estimate_direct_lighting( regirEnvironmentMisWeight = weight; regirEnvironmentM = float(max(sunReservoirSampleCount, 1u)); } +#endif if (camera.restir_di_controls.x != 0u && camera.restir_di_controls.z != 0u) { float sunRestirProxy = luminance(sunCandidateContribution); float sunCandidateProxySum = sampledRestirProxy + sunRestirProxy; @@ -1545,7 +1633,7 @@ vec3 estimate_direct_lighting_env_sun_only( causticVisiblePaths = 0u; causticBlockedPaths = 0u; bool isDelta = material_is_delta(material); - if (camera.direct_lighting_enabled == 0u || isDelta || debug_params.view == 27u) { + if (camera.direct_lighting_enabled == 0u || isDelta || renderer_debug_view() == 27u) { return vec3(0.0); } @@ -1692,7 +1780,7 @@ bool native2b_terminal_direct_fast_supported(Material material) { record_rt_counter(RT_DIAG_TERMINAL_FAST_DIRECT_VOLUME); supported = false; } - if (camera.path_trace_controls.w != 0u || debug_params.view == 27u) { + if (camera.path_trace_controls.w != 0u || renderer_debug_view() == 27u) { record_rt_counter(RT_DIAG_TERMINAL_FAST_DIRECT_DEBUG); supported = false; } @@ -1767,7 +1855,7 @@ vec3 estimate_native2b_terminal_env_sun_direct( out uint sampledType) { environmentContribution = vec3(0.0); sampledType = 0u; - if (camera.direct_lighting_enabled == 0u || material_is_delta(material) || debug_params.view == 27u) { + if (camera.direct_lighting_enabled == 0u || material_is_delta(material) || renderer_debug_view() == 27u) { return vec3(0.0); } diff --git a/shaders/pathtrace_ray_queries.glsl b/shaders/pathtrace_ray_queries.glsl index dde6595..86e5f98 100644 --- a/shaders/pathtrace_ray_queries.glsl +++ b/shaders/pathtrace_ray_queries.glsl @@ -1,6 +1,10 @@ #ifndef RTV_PATHTRACE_RAY_QUERIES_GLSL #define RTV_PATHTRACE_RAY_QUERIES_GLSL +#ifndef RTV_GENERIC_EXACT_SHADOW_RAY_QUERY +#define RTV_GENERIC_EXACT_SHADOW_RAY_QUERY 1 +#endif + // Surface, terminal, shadow, and transmittance ray helpers. void reset_payload() { payload.hit = 0u; @@ -100,6 +104,221 @@ RayPayload trace_shadow_surface(vec3 origin, vec3 direction, float tMin, float t return payload; } +#if RTV_GENERIC_EXACT_SHADOW_RAY_QUERY && !RTV_NATIVE2B_PIPELINE +RayPayload ray_query_surface_payload_from_parts( + float t, + uint tlasRecordIndex, + uint geometryIndex, + uint primitiveIndex, + vec2 bary2, + vec3 origin, + vec3 direction) { + RayPayload hit; + hit.hit = 1u; + hit.t = t; + hit.world_pos = origin + direction * hit.t; + hit.material_id = 0u; + hit.local_pos = vec3(0.0); + hit.geom_normal = vec3(0.0, 1.0, 0.0); + hit.front_face = 1u; + hit.normal = vec3(0.0, 1.0, 0.0); + hit.instance_id = 0xffffffffu; + hit.mesh_id = 0xffffffffu; + hit.primitive_id = 0xffffffffu; + hit.picking = 0u; + hit.barycentrics = vec3(1.0, 0.0, 0.0); + hit.uv = vec2(0.0); + hit.uv1 = vec2(0.0); + hit.tangent = vec3(1.0, 0.0, 0.0); + hit.bitangent = vec3(0.0, 0.0, 1.0); + hit.vertex_color = vec4(1.0); + + uint instanceIndex = scene_instance_index_from_tlas_record(tlasRecordIndex); + if (instanceIndex >= mesh_params.instance_count) { + return hit; + } + + InstanceRecord instance = instance_records[instanceIndex]; + uint meshIndex = instance.metadata.x; + MeshRecord mesh = mesh_records[meshIndex]; + uint firstIndex = mesh.vertex_index_data.z; + uint globalTriangleIndex = geometry_triangle_offset(meshIndex, tlasRecordIndex, geometryIndex, firstIndex) + primitiveIndex; + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + hit.hit = 0u; + return hit; + } + + uvec4 skinningBinding = ray_tracing_gpu_skinning_binding(meshIndex); + LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.x); + LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.y); + LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.z); + vec3 p0 = v0.position_uv_x.xyz; + vec3 p1 = v1.position_uv_x.xyz; + vec3 p2 = v2.position_uv_x.xyz; + + vec3 bary = vec3(1.0 - bary2.x - bary2.y, bary2.x, bary2.y); + vec3 localNormal = normalize(v0.normal_uv_y.xyz * bary.x + v1.normal_uv_y.xyz * bary.y + v2.normal_uv_y.xyz * bary.z); + vec3 localGeomNormal = normalize(cross(p1 - p0, p2 - p0)); + vec3 worldNormal = normalize(mat3(instance.normal_transform) * localNormal); + vec3 worldGeomNormal = normalize(mat3(instance.normal_transform) * localGeomNormal); + bool frontFace = dot(worldGeomNormal, direction) < 0.0; + if (!frontFace) { + worldGeomNormal = -worldGeomNormal; + worldNormal = -worldNormal; + } + + vec2 uv = vec2( + v0.position_uv_x.w * bary.x + v1.position_uv_x.w * bary.y + v2.position_uv_x.w * bary.z, + v0.normal_uv_y.w * bary.x + v1.normal_uv_y.w * bary.y + v2.normal_uv_y.w * bary.z); + vec2 uv1 = v0.texcoord1.xy * bary.x + v1.texcoord1.xy * bary.y + v2.texcoord1.xy * bary.z; + vec3 localTangent = normalize(v0.tangent.xyz * bary.x + v1.tangent.xyz * bary.y + v2.tangent.xyz * bary.z); + float tangentSign = v0.tangent.w * bary.x + v1.tangent.w * bary.y + v2.tangent.w * bary.z; + vec3 worldTangent = normalize(mat3(instance.transform) * localTangent); + vec3 worldBitangent = normalize(cross(worldNormal, worldTangent) * (tangentSign < 0.0 ? -1.0 : 1.0)); + + hit.material_id = material_for_triangle_index(globalTriangleIndex); + hit.local_pos = p0 * bary.x + p1 * bary.y + p2 * bary.z; + hit.geom_normal = worldGeomNormal; + hit.front_face = frontFace ? 1u : 0u; + hit.normal = worldNormal; + hit.instance_id = instanceIndex; + hit.mesh_id = meshIndex; + hit.primitive_id = globalTriangleIndex; + hit.barycentrics = bary; + hit.uv = uv; + hit.uv1 = uv1; + hit.tangent = worldTangent; + hit.bitangent = worldBitangent; + hit.vertex_color = clamp(v0.color * bary.x + v1.color * bary.y + v2.color * bary.z, vec4(0.0), vec4(1.0)); + return hit; +} + +RayPayload ray_query_candidate_surface_payload(rayQueryEXT rayQuery, vec3 origin, vec3 direction) { + return ray_query_surface_payload_from_parts( + rayQueryGetIntersectionTEXT(rayQuery, false), + rayQueryGetIntersectionInstanceCustomIndexEXT(rayQuery, false), + rayQueryGetIntersectionGeometryIndexEXT(rayQuery, false), + rayQueryGetIntersectionPrimitiveIndexEXT(rayQuery, false), + rayQueryGetIntersectionBarycentricsEXT(rayQuery, false), + origin, + direction); +} + +uint ray_query_material_index(uint tlasRecordIndex, uint geometryIndex, uint primitiveIndex) { + if (mesh_params.instance_count == 1u && mesh_params.mesh_count == 1u && + tlasRecordIndex == 0u && geometryIndex == 0u) { + uint triangleOffset = rt_geometry_triangle_offsets.length() > 0 + ? rt_geometry_triangle_offsets[0] + : mesh_records[0].vertex_index_data.z / 3u; + return material_for_triangle_index(triangleOffset + primitiveIndex); + } + + uint instanceIndex = scene_instance_index_from_tlas_record(tlasRecordIndex); + if (instanceIndex >= mesh_params.instance_count) { + return 0u; + } + + uint meshIndex = instance_records[instanceIndex].metadata.x; + uint firstIndex = mesh_records[meshIndex].vertex_index_data.z; + uint globalTriangleIndex = geometry_triangle_offset(meshIndex, tlasRecordIndex, geometryIndex, firstIndex) + primitiveIndex; + return material_for_triangle_index(globalTriangleIndex); +} + +bool ray_query_shadow_candidate_accepted(rayQueryEXT rayQuery, vec3 origin, vec3 direction) { + uint tlasRecordIndex = rayQueryGetIntersectionInstanceCustomIndexEXT(rayQuery, false); + uint geometryIndex = rayQueryGetIntersectionGeometryIndexEXT(rayQuery, false); + uint primitiveIndex = rayQueryGetIntersectionPrimitiveIndexEXT(rayQuery, false); + uint materialIndex = ray_query_material_index(tlasRecordIndex, geometryIndex, primitiveIndex); + MaterialRuntimeHeader materialHeader = decode_material_runtime_header(materialIndex); + if (materialHeader.double_sided == 0u && + !rayQueryGetIntersectionFrontFaceEXT(rayQuery, false)) { + return false; + } + if (materialHeader.alpha_mode == ALPHA_MODE_OPAQUE) { + return true; + } + + RayPayload candidate = ray_query_candidate_surface_payload(rayQuery, origin, direction); + if (candidate.hit == 0u) { + return true; + } + Material material = decode_material(materialIndex); + apply_material_alpha_texture(material, candidate.uv, candidate.uv1); + material.alpha_factor *= candidate.vertex_color.a; + return accept_material_alpha(material); +} + +struct ShadowSurfaceHit { + uint hit; + float t; + uint material_id; + uint tlas_record_index; + uint geometry_index; + uint primitive_index; + vec2 barycentrics; +}; + +ShadowSurfaceHit shadow_surface_miss() { + ShadowSurfaceHit hit; + hit.hit = 0u; + hit.t = 10000.0; + hit.material_id = 0u; + hit.tlas_record_index = 0xffffffffu; + hit.geometry_index = 0u; + hit.primitive_index = 0u; + hit.barycentrics = vec2(0.0); + return hit; +} + +RayPayload shadow_surface_payload(ShadowSurfaceHit compactHit, vec3 origin, vec3 direction) { + return ray_query_surface_payload_from_parts( + compactHit.t, + compactHit.tlas_record_index, + compactHit.geometry_index, + compactHit.primitive_index, + compactHit.barycentrics, + origin, + direction); +} +#endif + +#if RTV_GENERIC_EXACT_SHADOW_RAY_QUERY && !RTV_MOTION_BLUR_ENABLED && !RTV_NATIVE2B_PIPELINE +ShadowSurfaceHit trace_shadow_surface_exact(vec3 origin, vec3 direction, float tMin, float tMax) { + record_rt_counter(RT_DIAG_SURFACE_TRACE_RAYS); + record_rt_counter(RT_DIAG_SHADOW_SURFACE_TRACE_RAYS); + rayQueryEXT rayQuery; + rayQueryInitializeEXT( + rayQuery, + topLevelAS, + gl_RayFlagsNoneEXT, + RAY_MASK_SHADOW, + origin, + tMin, + direction, + tMax); + while (rayQueryProceedEXT(rayQuery)) { + if (rayQueryGetIntersectionTypeEXT(rayQuery, false) == gl_RayQueryCandidateIntersectionTriangleEXT && + ray_query_shadow_candidate_accepted(rayQuery, origin, direction)) { + rayQueryConfirmIntersectionEXT(rayQuery); + } + } + if (rayQueryGetIntersectionTypeEXT(rayQuery, true) == gl_RayQueryCommittedIntersectionNoneEXT) { + return shadow_surface_miss(); + } + + ShadowSurfaceHit hit; + hit.hit = 1u; + hit.t = rayQueryGetIntersectionTEXT(rayQuery, true); + hit.tlas_record_index = rayQueryGetIntersectionInstanceCustomIndexEXT(rayQuery, true); + hit.geometry_index = rayQueryGetIntersectionGeometryIndexEXT(rayQuery, true); + hit.primitive_index = rayQueryGetIntersectionPrimitiveIndexEXT(rayQuery, true); + hit.barycentrics = rayQueryGetIntersectionBarycentricsEXT(rayQuery, true); + hit.material_id = ray_query_material_index(hit.tlas_record_index, hit.geometry_index, hit.primitive_index); + return hit; +} +#endif + bool trace_shadow(vec3 origin, vec3 direction, float tMax, float rayTime) { record_rt_counter(RT_DIAG_SHADOW_TRACE_RAYS); shadow_occluded = 1u; @@ -138,6 +357,7 @@ bool trace_shadow(vec3 origin, vec3 direction, float tMax, float rayTime) { return shadow_occluded != 0u; } +#if RTV_NATIVE2B_PIPELINE TerminalRayPayload trace_terminal_surface(vec3 origin, vec3 direction, float tMin, float tMax) { record_rt_counter(RT_DIAG_SURFACE_TRACE_RAYS); record_rt_counter(RT_DIAG_TERMINAL_SURFACE_TRACE_RAYS); @@ -167,6 +387,7 @@ TerminalRayPayload trace_terminal_surface(vec3 origin, vec3 direction, float tMi 2); return terminal_payload; } +#endif vec3 shadow_transmittance(vec3 origin, vec3 direction, float tMax, float rayTime) { if (trace_shadow(origin, direction, tMax, rayTime)) { @@ -222,6 +443,7 @@ vec3 caustic_shadow_transmittance_stats( } return vec3(1.0); } + record_rt_counter(RT_DIAG_FULL_SHADOW_TRANSMITTANCE_USED); vec3 throughput = vec3(1.0); @@ -230,8 +452,17 @@ vec3 caustic_shadow_transmittance_stats( float remaining = tMax; for (uint interfaceIndex = 0u; interfaceIndex < 2u; ++interfaceIndex) { record_rt_counter(RT_DIAG_TRANSMISSIVE_SHADOW_SURFACE_TRACES); +#if RTV_GENERIC_EXACT_SHADOW_RAY_QUERY && !RTV_MOTION_BLUR_ENABLED && !RTV_NATIVE2B_PIPELINE + ShadowSurfaceHit compactShadowHit = trace_shadow_surface_exact( + rayOrigin, + rayDir, + shadow_self_hit_epsilon(), + remaining); + if (compactShadowHit.hit == 0u) { +#else RayPayload shadowHit = trace_shadow_surface(rayOrigin, rayDir, shadow_self_hit_epsilon(), remaining, rayTime); if (shadowHit.hit == 0u) { +#endif if (transmissiveHits > 0u) { if (causticDiagnosticsEnabled) { record_rt_counter(RT_DIAG_CAUSTIC_TRANSMISSIVE_VISIBLE); @@ -241,6 +472,28 @@ vec3 caustic_shadow_transmittance_stats( return throughput * vec3(homogeneous_transmittance_scalar(remaining)); } +#if RTV_GENERIC_EXACT_SHADOW_RAY_QUERY && !RTV_MOTION_BLUR_ENABLED && !RTV_NATIVE2B_PIPELINE + if (!material_static_may_be_transmissive(compactShadowHit.material_id)) { + MaterialRuntimeHeader blockerHeader = decode_material_runtime_header(compactShadowHit.material_id); +#else + if (!material_static_may_be_transmissive(shadowHit.material_id)) { + MaterialRuntimeHeader blockerHeader = decode_material_runtime_header(shadowHit.material_id); +#endif + record_rt_alpha_class_counter( + blockerHeader.alpha_mode, + RT_DIAG_CAUSTIC_BLOCKER_OPAQUE, + RT_DIAG_CAUSTIC_BLOCKER_ALPHA_TESTED, + RT_DIAG_CAUSTIC_BLOCKER_BLENDED); + if (causticDiagnosticsEnabled) { + record_rt_counter(RT_DIAG_CAUSTIC_SHADOW_BLOCKED); + } + blockedPath = 1u; + return vec3(0.0); + } + +#if RTV_GENERIC_EXACT_SHADOW_RAY_QUERY && !RTV_MOTION_BLUR_ENABLED && !RTV_NATIVE2B_PIPELINE + RayPayload shadowHit = shadow_surface_payload(compactShadowHit, rayOrigin, rayDir); +#endif Material material = material_for_hit(shadowHit, rayDir); if (!material_is_transmissive(material)) { MaterialRuntimeHeader blockerHeader = decode_material_runtime_header(shadowHit.material_id); @@ -331,7 +584,7 @@ bool direct_shadow_fast_visibility_supported(Material receiverMaterial) { (camera.path_trace_controls.z & 8u) != 0u && !homogeneous_volume_enabled() && camera.path_trace_controls.w == 0u && - debug_params.view != 27u; + renderer_debug_view() != 27u; #else return false; #endif diff --git a/shaders/pathtrace_restir.glsl b/shaders/pathtrace_restir.glsl index 42b469b..df715fa 100644 --- a/shaders/pathtrace_restir.glsl +++ b/shaders/pathtrace_restir.glsl @@ -72,7 +72,9 @@ void store_initial_restir_reservoir(uint pixelIndex, ivec2 coords, ivec2 dims, u } void store_new_restir_di(uint pixelIndex, PathComponents components, vec3 hit_position, vec3 hit_normal, float hit_depth, bool did_hit) { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_PIXELS], 1u); + if (restir_di_raygen_params.counterEnabled != 0u) { + atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_PIXELS], 1u); + } // Write receiver surface data RestirDiReceiver receiver; if (did_hit) { @@ -95,7 +97,7 @@ void store_new_restir_di(uint pixelIndex, PathComponents components, vec3 hit_po surfaceFlags |= RESTIR_DI_SURFACE_UNSUPPORTED; } receiver.worldPosition_depth = vec4(hit_position, hit_depth); - receiver.normal_roughness = vec4(components.first_geom_normal, components.first_roughness); + receiver.normal_roughness = vec4(normalize(hit_normal), components.first_roughness); #if RTV_RESTIR_DI_VALIDATION_FULL receiver.tangent_materialId = vec4(components.restir_di_base_color, float(components.first_material_id)); receiver.bitangent_instanceId = vec4(components.restir_di_f0, float(components.instance_id)); @@ -180,8 +182,8 @@ void store_new_restir_di(uint pixelIndex, PathComponents components, vec3 hit_po ? RESTIR_DI_ENVIRONMENT_ID_HASH : RESTIR_DI_SUN_ID_HASH; identityVersion = components.restir_di_light_kind == RESTIR_DI_LIGHT_ENVIRONMENT - ? RESTIR_DI_ENVIRONMENT_VERSION - : RESTIR_DI_SUN_VERSION; + ? camera.gi_version_controls.w + : camera.gi_version_controls.x; } else { LightRecord identityRecord = light_records[components.restir_di_light_index]; identityHash = restir_di_identity_hash(identityRecord.identity.xy); @@ -200,7 +202,7 @@ void store_new_restir_di(uint pixelIndex, PathComponents components, vec3 hit_po restir_di_set_target(initial, targetLum); restir_di_set_light_normal(initial, components.restir_di_sample_normal); #if !RTV_RESTIR_DI_VALIDATION_FULL - if (initialWeight > 65504.0) { + if (restir_di_raygen_params.counterEnabled != 0u && initialWeight > 65504.0) { atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_WEIGHT_OVERFLOW], 1u); } #endif @@ -212,27 +214,30 @@ void store_new_restir_di(uint pixelIndex, PathComponents components, vec3 hit_po restir_di_set_source_pdf(initial, components.first_light_pdf); restir_di_set_previous_weight(initial, 0.0); restir_di_set_confidence(initial, 1.0); - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_VALID], 1u); - uint lightKind = components.restir_di_light_kind; - uint classCounter = lightKind <= RESTIR_DI_LIGHT_EMISSIVE_SPHERE ? RESTIR_DI_COUNTER_INITIAL_EMISSIVE : - lightKind == RESTIR_DI_LIGHT_DIRECTIONAL ? RESTIR_DI_COUNTER_INITIAL_DIRECTIONAL : - lightKind == RESTIR_DI_LIGHT_POINT ? RESTIR_DI_COUNTER_INITIAL_POINT : - lightKind == RESTIR_DI_LIGHT_AREA ? RESTIR_DI_COUNTER_INITIAL_AREA : - lightKind == RESTIR_DI_LIGHT_SPOT ? RESTIR_DI_COUNTER_INITIAL_SPOT : - lightKind == RESTIR_DI_LIGHT_ENVIRONMENT ? RESTIR_DI_COUNTER_INITIAL_ENVIRONMENT : - lightKind == RESTIR_DI_LIGHT_SUN ? RESTIR_DI_COUNTER_INITIAL_SUN : - RESTIR_DI_COUNTER_INITIAL_INVALID_IDENTITY; - atomicAdd(restir_di_counters[classCounter], 1u); - } else if (!did_hit || !validSurface) { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_INVALID_SURFACE], 1u); - } else if (!finiteCandidate) { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_NON_FINITE], 1u); - } else if (!validPdf) { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_INVALID_PDF], 1u); - } else if (!validIdentity) { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_INVALID_IDENTITY], 1u); - } else { - atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_INVALID_TARGET], 1u); + if (restir_di_raygen_params.counterEnabled != 0u) { + atomicAdd(restir_di_counters[RESTIR_DI_COUNTER_INITIAL_VALID], 1u); + uint lightKind = components.restir_di_light_kind; + uint classCounter = lightKind <= RESTIR_DI_LIGHT_EMISSIVE_SPHERE ? RESTIR_DI_COUNTER_INITIAL_EMISSIVE : + lightKind == RESTIR_DI_LIGHT_DIRECTIONAL ? RESTIR_DI_COUNTER_INITIAL_DIRECTIONAL : + lightKind == RESTIR_DI_LIGHT_POINT ? RESTIR_DI_COUNTER_INITIAL_POINT : + lightKind == RESTIR_DI_LIGHT_AREA ? RESTIR_DI_COUNTER_INITIAL_AREA : + lightKind == RESTIR_DI_LIGHT_SPOT ? RESTIR_DI_COUNTER_INITIAL_SPOT : + lightKind == RESTIR_DI_LIGHT_ENVIRONMENT ? RESTIR_DI_COUNTER_INITIAL_ENVIRONMENT : + lightKind == RESTIR_DI_LIGHT_SUN ? RESTIR_DI_COUNTER_INITIAL_SUN : + RESTIR_DI_COUNTER_INITIAL_INVALID_IDENTITY; + atomicAdd(restir_di_counters[classCounter], 1u); + } + } else if (restir_di_raygen_params.counterEnabled != 0u) { + uint invalidCounter = (!did_hit || !validSurface) + ? RESTIR_DI_COUNTER_INITIAL_INVALID_SURFACE + : (!finiteCandidate + ? RESTIR_DI_COUNTER_INITIAL_NON_FINITE + : (!validPdf + ? RESTIR_DI_COUNTER_INITIAL_INVALID_PDF + : (!validIdentity + ? RESTIR_DI_COUNTER_INITIAL_INVALID_IDENTITY + : RESTIR_DI_COUNTER_INITIAL_INVALID_TARGET))); + atomicAdd(restir_di_counters[invalidCounter], 1u); } restir_di_initial_reservoirs[pixelIndex] = initial; } @@ -426,7 +431,7 @@ void store_initial_restir_gi_reservoir(uint pixelIndex, ivec2 coords, ivec2 dims if (restir_gi_legacy_cache_mode() && restirGiDebugView && - debug_params.view != 70u && + renderer_debug_view() != 70u && restir_gi_reservoir_valid(reservoir) && temporal_history_available() && !streaming_instance_reset_mask(components.instance_id, 2u)) { diff --git a/shaders/pathtrace_shadow.rahit b/shaders/pathtrace_shadow.rahit index d179cd6..37f44a2 100644 --- a/shaders/pathtrace_shadow.rahit +++ b/shaders/pathtrace_shadow.rahit @@ -54,14 +54,17 @@ void main() { return; } - uint triIndex = globalTriangleIndex * 3u; - uint i0 = local_mesh_indices[triIndex + 0u]; - uint i1 = local_mesh_indices[triIndex + 1u]; - uint i2 = local_mesh_indices[triIndex + 2u]; + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + record_rt_counter(RT_DIAG_SHADOW_ANY_HIT_ACCEPTED); + shadow_occluded = 1u; + terminateRayEXT; + return; + } uvec4 skinningBinding = ray_tracing_gpu_skinning_binding(meshIndex); - LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, i0); - LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, i1); - LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, i2); + LocalVertex v0 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.x); + LocalVertex v1 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.y); + LocalVertex v2 = ray_tracing_local_vertex_with_binding(skinningBinding, indices.z); vec3 bary = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); vec2 uv = vec2( diff --git a/shaders/pathtrace_terminal.rahit b/shaders/pathtrace_terminal.rahit index 7a0828f..380b2f3 100644 --- a/shaders/pathtrace_terminal.rahit +++ b/shaders/pathtrace_terminal.rahit @@ -19,10 +19,14 @@ void main() { MeshRecord mesh = mesh_records[meshIndex]; uint globalTriangleIndex = geometry_triangle_offset( meshIndex, tlasRecordIndex, gl_GeometryIndexEXT, mesh.vertex_index_data.z) + gl_PrimitiveID; - uint triIndex = globalTriangleIndex * 3u; - LocalVertex v0 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex]); - LocalVertex v1 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex + 1u]); - LocalVertex v2 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex + 2u]); + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + ignoreIntersectionEXT; + return; + } + LocalVertex v0 = ray_tracing_local_vertex(meshIndex, indices.x); + LocalVertex v1 = ray_tracing_local_vertex(meshIndex, indices.y); + LocalVertex v2 = ray_tracing_local_vertex(meshIndex, indices.z); vec3 p0 = v0.position_uv_x.xyz; vec3 p1 = v1.position_uv_x.xyz; vec3 p2 = v2.position_uv_x.xyz; diff --git a/shaders/pathtrace_terminal.rchit b/shaders/pathtrace_terminal.rchit index 7c72ccb..90d48a2 100644 --- a/shaders/pathtrace_terminal.rchit +++ b/shaders/pathtrace_terminal.rchit @@ -49,10 +49,14 @@ void main() { } record_rt_counter(RT_DIAG_TERMINAL_MATERIAL_FULL_DECODE); - uint triIndex = globalTriangleIndex * 3u; - LocalVertex v0 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex]); - LocalVertex v1 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex + 1u]); - LocalVertex v2 = ray_tracing_local_vertex(meshIndex, local_mesh_indices[triIndex + 2u]); + uvec3 indices; + if (!ray_tracing_triangle_indices(globalTriangleIndex, indices)) { + payload.hit = 0u; + return; + } + LocalVertex v0 = ray_tracing_local_vertex(meshIndex, indices.x); + LocalVertex v1 = ray_tracing_local_vertex(meshIndex, indices.y); + LocalVertex v2 = ray_tracing_local_vertex(meshIndex, indices.z); vec3 bary = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); vec3 p0 = v0.position_uv_x.xyz; vec3 p1 = v1.position_uv_x.xyz; diff --git a/shaders/psr_guides.glsl b/shaders/psr_guides.glsl new file mode 100644 index 0000000..fc80e26 --- /dev/null +++ b/shaders/psr_guides.glsl @@ -0,0 +1,99 @@ +#ifndef RTV_PSR_GUIDES_GLSL +#define RTV_PSR_GUIDES_GLSL + +struct PsrGuideRecord { + // xy = packed camera-relative world position, z = packed normal, w = packed motion. + uvec4 geometry; + // x = diffuse albedo, y = specular F0, z = packed ray direction, w = flags/roughness/identity. + uvec4 material; + // x = reflected segment hit distance, y = replacement viewZ, z = primary hit distance. + vec4 distances; +}; + +const uint PSR_FLAG_ACTIVE = 1u << 0u; +const uint PSR_FLAG_VALID = 1u << 1u; + +vec2 psr_oct_encode(vec3 value) { + vec3 n = normalize(value); + n /= max(abs(n.x) + abs(n.y) + abs(n.z), 1.0e-6); + return n.z >= 0.0 ? n.xy : (1.0 - abs(n.yx)) * sign(n.xy); +} + +vec3 psr_oct_decode(vec2 value) { + vec3 n = vec3(value, 1.0 - abs(value.x) - abs(value.y)); + if (n.z < 0.0) n.xy = (1.0 - abs(n.yx)) * sign(n.xy); + return normalize(n); +} + +uint psr_pack_direction(vec3 direction) { + return packSnorm2x16(psr_oct_encode(direction)); +} + +vec3 psr_unpack_direction(uint packed) { + return psr_oct_decode(unpackSnorm2x16(packed)); +} + +uint psr_pack_metadata(bool valid, bool isActive, float roughness, uint identityHash) { + uint packedRoughness = uint(round(clamp(roughness, 0.0, 1.0) * 255.0)); + return (isActive ? PSR_FLAG_ACTIVE : 0u) | + (valid ? PSR_FLAG_VALID : 0u) | + (packedRoughness << 2u) | + ((identityHash & 0x003fffffu) << 10u); +} + +bool psr_guide_valid(PsrGuideRecord guide) { + return (guide.material.w & PSR_FLAG_VALID) != 0u; +} + +bool psr_guide_active(PsrGuideRecord guide) { + return (guide.material.w & PSR_FLAG_ACTIVE) != 0u; +} + +float psr_guide_roughness(PsrGuideRecord guide) { + return float((guide.material.w >> 2u) & 0xffu) / 255.0; +} + +uint psr_guide_identity(PsrGuideRecord guide) { + return guide.material.w >> 10u; +} + +uint psr_guide_signature(PsrGuideRecord guide) { + if (!psr_guide_valid(guide)) { + return 0u; + } + uint signature = (psr_guide_identity(guide) << 2u) | 0x1u; + if (psr_guide_active(guide)) { + signature |= 0x2u; + } + return signature; +} + +vec3 psr_guide_normal(PsrGuideRecord guide) { + return psr_unpack_direction(guide.geometry.z); +} + +vec3 psr_guide_diffuse_albedo(PsrGuideRecord guide) { + return unpackUnorm4x8(guide.material.x).rgb; +} + +vec3 psr_guide_specular_f0(PsrGuideRecord guide) { + return unpackUnorm4x8(guide.material.y).rgb; +} + +vec3 psr_guide_ray_direction(PsrGuideRecord guide) { + return psr_unpack_direction(guide.material.z); +} + +vec2 psr_guide_motion_pixels(PsrGuideRecord guide) { + return unpackSnorm2x16(guide.geometry.w) * 512.0; +} + +PsrGuideRecord psr_invalid_guide() { + PsrGuideRecord guide; + guide.geometry = uvec4(0u); + guide.material = uvec4(0u); + guide.distances = vec4(65504.0, 65504.0, 65504.0, 0.0); + return guide; +} + +#endif // RTV_PSR_GUIDES_GLSL diff --git a/shaders/regir_build.comp b/shaders/regir_build.comp index 08e16e7..7182105 100644 --- a/shaders/regir_build.comp +++ b/shaders/regir_build.comp @@ -59,6 +59,8 @@ layout(set = 0, binding = 3, std140) uniform ReGIRParamsBlock { struct ReGIRReservoir { uvec4 metadata; vec4 sample_position_weight; + vec4 proposal_pdf_m; + uvec4 light_identity; }; layout(set = 0, binding = 4, std430) buffer ReGIRReservoirBuffer { @@ -121,6 +123,8 @@ void main() { ReGIRReservoir emptyReservoir; emptyReservoir.metadata = uvec4(0u, 0u, 0u, 0u); emptyReservoir.sample_position_weight = vec4(0.0); + emptyReservoir.proposal_pdf_m = vec4(0.0); + emptyReservoir.light_identity = uvec4(0u); if ((regir_params.controls.x & 1u) == 0u || mesh_params.light_count == 0u || mesh_params.emissive_total_area <= 1.0e-8) { regir_reservoirs[reservoirIndex] = emptyReservoir; @@ -175,6 +179,7 @@ void main() { vec3 selectedPosition = vec3(0.0); float selectedWeight = 0.0; float weightSum = 0.0; + uvec4 selectedIdentity = uvec4(0u); for (uint candidate = 0u; candidate < candidateCount; ++candidate) { const uint lightIndex = min(uint(rand01(rng) * float(mesh_params.light_count)), mesh_params.light_count - 1u); @@ -188,6 +193,7 @@ void main() { selectedKind = light.metadata.x; selectedPosition = center; selectedWeight = weight; + selectedIdentity = light.identity; } } @@ -199,5 +205,11 @@ void main() { ReGIRReservoir reservoir; reservoir.metadata = uvec4(selectedLight, selectedKind, candidateCount, 1u); reservoir.sample_position_weight = vec4(selectedPosition, selectedWeight); + reservoir.proposal_pdf_m = vec4( + weightSum, + selectedWeight / max(weightSum, 1.0e-8), + weightSum / float(candidateCount), + float(candidateCount)); + reservoir.light_identity = selectedIdentity; regir_reservoirs[reservoirIndex] = reservoir; } diff --git a/shaders/regir_spatial_reuse.comp b/shaders/regir_spatial_reuse.comp index 9f2734c..e817133 100644 --- a/shaders/regir_spatial_reuse.comp +++ b/shaders/regir_spatial_reuse.comp @@ -5,6 +5,8 @@ layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; struct ReGIRReservoir { uvec4 metadata; vec4 sample_position_weight; + vec4 proposal_pdf_m; + uvec4 light_identity; }; layout(set = 0, binding = 0, std140) uniform ReGIRParamsBlock { @@ -112,5 +114,11 @@ void main() { } selected.metadata.w = max(1u, selected.metadata.w) + neighborContributionCount; selected.sample_position_weight.w = max(selected.sample_position_weight.w, 0.0); + float selectedM = float(max(selected.metadata.w, 1u)); + selected.proposal_pdf_m = vec4( + weightSum, + selected.sample_position_weight.w / max(weightSum, 1.0e-8), + weightSum / selectedM, + selectedM); output_reservoirs[reservoirIndex] = selected; } diff --git a/shaders/regir_temporal_reuse.comp b/shaders/regir_temporal_reuse.comp index ac03e6c..4499d34 100644 --- a/shaders/regir_temporal_reuse.comp +++ b/shaders/regir_temporal_reuse.comp @@ -5,6 +5,8 @@ layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; struct ReGIRReservoir { uvec4 metadata; vec4 sample_position_weight; + vec4 proposal_pdf_m; + uvec4 light_identity; }; layout(set = 0, binding = 0, std140) uniform ReGIRParamsBlock { @@ -83,5 +85,11 @@ void main() { ReGIRReservoir selected = rand01(rng) * weightSum < previousWeight ? previous : current; selected.metadata.w = min(maxM, max(current.metadata.w, 1u) + max(previous.metadata.w, 1u)); selected.sample_position_weight.w = max(selected.sample_position_weight.w, 0.0); + float selectedM = float(max(selected.metadata.w, 1u)); + selected.proposal_pdf_m = vec4( + weightSum, + selected.sample_position_weight.w / max(weightSum, 1.0e-8), + weightSum / selectedM, + selectedM); output_reservoirs[reservoirIndex] = selected; } diff --git a/shaders/restir_di_final.comp b/shaders/restir_di_final.comp index e42eed2..0f12a8e 100644 --- a/shaders/restir_di_final.comp +++ b/shaders/restir_di_final.comp @@ -59,7 +59,9 @@ layout(set = 0, binding = 6, std140) uniform Params { uint materialVisibilityFlags; uint counterEnabled; uint rawOutputIsCurrentSample; - uint padding2; + float shadowDistanceBias; + uint lightVersion; + uint environmentVersion; } params; #define DI_COUNTER_ADD(counter, value) ((params.counterEnabled != 0u) ? atomicAdd(counter, value) : 0u) @@ -101,6 +103,29 @@ layout(set = 0, binding = 14, std430) buffer VarianceBuffer { uint variance_buffer[]; }; +layout(set = 0, binding = 24, std430) readonly buffer SelectedSourcePixels { + uint selected_source_pixels[]; +}; + +layout(set = 0, binding = 25, std430) writeonly buffer FinalSourcePixels { + uint final_source_pixels[]; +}; + +const uint RESTIR_DI_SOURCE_PIXEL_INVALID = 0xffffffffu; + +uint restir_di_pack_source_pixel(ivec2 p) { + if (p.x < 0 || p.y < 0 || p.x > 65534 || p.y > 65534) { + return RESTIR_DI_SOURCE_PIXEL_INVALID; + } + return uint(p.x) | (uint(p.y) << 16u); +} + +uint restir_di_selected_source_pixel(uint pixelIndex, ivec2 fallbackCoords) { + uint fallback = restir_di_pack_source_pixel(fallbackCoords); + uint source = selected_source_pixels[pixelIndex]; + return source == RESTIR_DI_SOURCE_PIXEL_INVALID ? fallback : source; +} + const uint DEBUG_RESTIR_DI_SELECTED_LIGHT = 106u; const uint DEBUG_RESTIR_DI_TARGET = 107u; const uint DEBUG_RESTIR_DI_SOURCE_PDF = 108u; @@ -122,6 +147,7 @@ const uint DEBUG_RESTIR_DI_LIGHT_CLASS = 123u; const uint DEBUG_RESTIR_DI_AGE = 124u; const uint DEBUG_RESTIR_DI_CONFIDENCE = 125u; const uint DEBUG_RESTIR_DI_REFERENCE_DIFF = 126u; +const uint DEBUG_RESTIR_DI_LIGHT_MAP_STATUS = 158u; const uint DEBUG_RESTIR_LEGACY_AGE = 35u; const uint DEBUG_RESTIR_LEGACY_CONFIDENCE = 36u; const uint DEBUG_RESTIR_LEGACY_M = 37u; @@ -170,6 +196,15 @@ void restir_di_atomic_add_u64(uint lowIndex, uint highIndex, uint value) { } } +bool restir_di_relative_mismatch(float storedValue, float evaluatedValue, float tolerance) { + if (isnan(storedValue) || isinf(storedValue) || + isnan(evaluatedValue) || isinf(evaluatedValue)) { + return true; + } + float scale = max(max(abs(storedValue), abs(evaluatedValue)), 1.0e-3); + return abs(storedValue - evaluatedValue) / scale > tolerance; +} + vec3 restir_di_id_color(uint id) { uint h = id * 747796405u + 2891336453u; h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u; @@ -187,11 +222,35 @@ vec3 restir_di_visibility_color(uint visibility) { return vec3(0.0, 0.2, 1.0); } +vec3 restir_di_light_map_status_color(RestirDiReservoir reservoir) { + RestirDiLightRecord light; + uint lightIndex; + uint mapStatus; + bool resolved = restir_di_resolve_light_with_status( + reservoir, + light, + lightIndex, + mapStatus, + params.lightVersion, + params.environmentVersion); + if (mapStatus == RESTIR_DI_LIGHT_MAP_CACHED) return vec3(0.0, 1.0, 0.0); + if (mapStatus == RESTIR_DI_LIGHT_MAP_REMAPPED) return vec3(0.1, 0.45, 1.0); + if (mapStatus == RESTIR_DI_LIGHT_MAP_CHANGED) return vec3(1.0, 0.75, 0.0); + if (mapStatus == RESTIR_DI_LIGHT_MAP_UNMAPPED) return vec3(1.0, 0.0, 0.0); + if (mapStatus == RESTIR_DI_LIGHT_MAP_INFINITE) return resolved ? vec3(0.0, 1.0, 1.0) : vec3(1.0, 0.0, 1.0); + return vec3(0.25); +} + vec3 restir_di_reservoir_estimate_at_receiver(RestirDiReservoir reservoir, RestirDiReceiver receiver) { if (!restir_di_reservoir_valid(reservoir)) return vec3(0.0); RestirDiLightRecord light; uint lightIndex; - if (!restir_di_resolve_light(reservoir, light, lightIndex)) return vec3(0.0); + if (!restir_di_resolve_light( + reservoir, + light, + lightIndex, + params.lightVersion, + params.environmentVersion)) return vec3(0.0); vec3 lightNormal = restir_di_light_normal(reservoir); float pdf = restir_di_pdf_at_receiver( lightIndex, light, receiver.worldPosition_depth.xyz, @@ -230,6 +289,9 @@ vec3 restir_di_reservoir_debug(RestirDiReservoir reservoir, uint view) { if (view == DEBUG_RESTIR_DI_LIGHT_VERSION) { return vec3(float(restir_di_light_version(reservoir) & 255u) / 255.0); } + if (view == DEBUG_RESTIR_DI_LIGHT_MAP_STATUS) { + return restir_di_light_map_status_color(reservoir); + } if (view == DEBUG_RESTIR_DI_WEIGHT_SUM) { return vec3(clamp(restir_di_weight_sum(reservoir) / 64.0, 0.0, 1.0)); } @@ -293,6 +355,7 @@ bool restir_di_write_debug( view == DEBUG_RESTIR_DI_VISIBILITY || view == DEBUG_RESTIR_DI_REJECTION || view == DEBUG_RESTIR_DI_LIGHT_VERSION || + view == DEBUG_RESTIR_DI_LIGHT_MAP_STATUS || view == DEBUG_RESTIR_DI_WEIGHT_SUM || view == DEBUG_RESTIR_DI_M || view == DEBUG_RESTIR_DI_LIGHT_CLASS || @@ -315,16 +378,57 @@ void replace_direct_lighting( vec3 initialContribution, vec3 finalContribution, RestirDiReservoir selected, - RestirDiReceiver receiver) { + RestirDiReceiver receiver, + bool useStoredInitialDirect) { if (debug_params.view != 0u) { return; } PathDataRecord path = path_data_buffer[pixelIndex]; - vec3 oldDiffuse = max(path.direct_diffuse.rgb, vec3(0.0)); - vec3 oldSpecular = max(path.direct_specular.rgb, vec3(0.0)); + vec4 directDiffuse = path.direct_diffuse; + vec4 directSpecular = path.direct_specular; + vec4 indirectDiffuse = path.indirect_diffuse; + vec4 indirectSpecular = path.indirect_specular; + vec4 emissiveResidual = path.emissive_residual; + vec3 oldDiffuse = max(directDiffuse.rgb, vec3(0.0)); + vec3 oldSpecular = max(directSpecular.rgb, vec3(0.0)); vec3 oldDirect = oldDiffuse + oldSpecular; - vec3 directDelta = finalContribution - max(initialContribution, vec3(0.0)); + if (useStoredInitialDirect) { + finalContribution = oldDirect; + initialContribution = oldDirect; + } else { + // Reused temporal/spatial samples are valuable when the current + // frame missed a light, but at hard visibility boundaries a shifted + // sample can be underweighted or classified differently than the + // raygen NEE sample that is already in the image. Do not let reuse + // carve persistent black gaps by subtracting a strong current-frame + // direct-light sample down to near zero; falling back here is just the + // unbiased current NEE sample for this pixel. + float oldDirectLumForReuse = luminance(oldDirect); + float finalLumForReuse = luminance(max(finalContribution, vec3(0.0))); + if (oldDirectLumForReuse > 1.0e-4 && + finalLumForReuse < oldDirectLumForReuse * 0.35) { + finalContribution = oldDirect; + initialContribution = oldDirect; + DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_FALLBACK], 1u); + } + } + // The image/accumulation buffers already contain the raygen direct signal + // stored in path_data_buffer. The reservoir's initial estimate is + // re-evaluated here only to identify the ReSTIR-owned part of that signal, + // but it can differ from the stored value after raygen firefly/direct-light + // clamping or material-path bookkeeping. If we subtract the re-evaluated + // initial estimate verbatim, Production ReSTIR DI can remove more light + // than raygen actually wrote, producing dark clipped regions around very + // bright emitters (Veach Ajar's doorway is the sharp repro). Clamp the + // replacement baseline to the direct signal that is actually present. + vec3 replacementBaseline = max(initialContribution, vec3(0.0)); + float oldDirectLum = luminance(oldDirect); + float baselineLum = luminance(replacementBaseline); + if (baselineLum > oldDirectLum && baselineLum > 1.0e-6) { + replacementBaseline *= oldDirectLum / baselineLum; + } + vec3 directDelta = finalContribution - replacementBaseline; vec3 newDirect = max(oldDirect + directDelta, vec3(0.0)); vec4 accumulated = accumulation_buffer[pixelIndex]; @@ -349,15 +453,18 @@ void replace_direct_lighting( float specularWeight = totalLum > 1.0e-6 ? clamp(specularLum / totalLum, 0.0, 1.0) : clamp(materialSpecularWeight, 0.0, 1.0); - path.direct_diffuse.rgb = newDirect * (1.0 - specularWeight); - path.direct_specular.rgb = newDirect * specularWeight; - path.direct_diffuse.a = luminance(max(path.direct_diffuse.rgb, vec3(0.0))); - path.direct_specular.a = luminance(max(path.direct_specular.rgb, vec3(0.0))); + directDiffuse.rgb = newDirect * (1.0 - specularWeight); + directSpecular.rgb = newDirect * specularWeight; RestirDiLightRecord guideLight; uint guideLightIndex; if (restir_di_reservoir_valid(selected) && - restir_di_resolve_light(selected, guideLight, guideLightIndex)) { + restir_di_resolve_light( + selected, + guideLight, + guideLightIndex, + params.lightVersion, + params.environmentVersion)) { uint kind = restir_di_light_kind(selected); vec3 guideDirection; float guideDistance; @@ -372,24 +479,26 @@ void replace_direct_lighting( guideDistance = length(toLight); guideDirection = toLight / max(guideDistance, 1.0e-6); } - float indirectDiffuseLum = luminance(max(path.indirect_diffuse.rgb, vec3(0.0))); - float indirectSpecularLum = luminance(max(path.indirect_specular.rgb, vec3(0.0))); - if (luminance(path.direct_diffuse.rgb) >= indirectDiffuseLum) { + float indirectDiffuseLum = luminance(max(indirectDiffuse.rgb, vec3(0.0))); + float indirectSpecularLum = luminance(max(indirectSpecular.rgb, vec3(0.0))); + if (luminance(directDiffuse.rgb) >= indirectDiffuseLum) { path.diffuse_ray_direction_hit_distance = vec4(guideDirection, guideDistance); path.denoiser_hit_distance.x = guideDistance; } - if (luminance(path.direct_specular.rgb) >= indirectSpecularLum) { + if (luminance(directSpecular.rgb) >= indirectSpecularLum) { path.specular_ray_direction_hit_distance = vec4(guideDirection, guideDistance); path.denoiser_hit_distance.y = guideDistance; } } - vec3 currentSignal = max(path.direct_diffuse.rgb, vec3(0.0)) + - max(path.direct_specular.rgb, vec3(0.0)) + - max(path.indirect_diffuse.rgb, vec3(0.0)) + - max(path.indirect_specular.rgb, vec3(0.0)) + - max(path.emissive_residual.rgb, vec3(0.0)); + vec3 currentSignal = max(directDiffuse.rgb, vec3(0.0)) + + max(directSpecular.rgb, vec3(0.0)) + + max(indirectDiffuse.rgb, vec3(0.0)) + + max(indirectSpecular.rgb, vec3(0.0)) + + max(emissiveResidual.rgb, vec3(0.0)); vec3 accumulatedAverage = max(accumulation_buffer[pixelIndex].rgb, vec3(0.0)) / accumulationSamples; variance_buffer[pixelIndex] = pack_variance(abs(luminance(currentSignal) - luminance(accumulatedAverage))); + path.direct_diffuse = directDiffuse; + path.direct_specular = directSpecular; path_data_buffer[pixelIndex] = path; } @@ -400,6 +509,7 @@ void main() { } uint pixelIndex = uint(coords.y) * params.width + uint(coords.x); + final_source_pixels[pixelIndex] = RESTIR_DI_SOURCE_PIXEL_INVALID; DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_PIXELS], 1u); // Load receiver @@ -425,11 +535,11 @@ void main() { uint rejectionFlags = restir_di_rejection_flags(selected) | restir_di_rejection_flags(temporalDebug); bool usedFallback = false; - // Hybrid compare keeps classic NEE on the left and applies the new DI // estimator on the right while still producing valid per-pixel history. if (params.mode == 4u && coords.x < int(params.width / 2u)) { final_reservoirs[pixelIndex] = initial; + final_source_pixels[pixelIndex] = restir_di_pack_source_pixel(coords); restir_di_write_debug(coords, receiver, initial, temporalDebug, spatialDebug, initial); return; } @@ -460,10 +570,20 @@ void main() { RestirDiLightRecord selectedLight; uint resolvedLightIndex; - if (!restir_di_resolve_light(selected, selectedLight, resolvedLightIndex)) { + if (!restir_di_resolve_light( + selected, + selectedLight, + resolvedLightIndex, + params.lightVersion, + params.environmentVersion)) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_LIGHT_REJECTED], 1u); if (!usedFallback && restir_di_reservoir_valid(initial) && - restir_di_resolve_light(initial, selectedLight, resolvedLightIndex)) { + restir_di_resolve_light( + initial, + selectedLight, + resolvedLightIndex, + params.lightVersion, + params.environmentVersion)) { selected = initial; usedFallback = true; DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_FALLBACK], 1u); @@ -521,7 +641,12 @@ void main() { } } - if (usedFallback && !restir_di_resolve_light(selected, selectedLight, resolvedLightIndex)) { + if (usedFallback && !restir_di_resolve_light( + selected, + selectedLight, + resolvedLightIndex, + params.lightVersion, + params.environmentVersion)) { final_reservoirs[pixelIndex] = restir_di_empty_reservoir(); restir_di_write_debug( coords, @@ -543,9 +668,38 @@ void main() { vec3 selectedLightNormal = restir_di_light_normal(selected); float selectedPdf = restir_di_pdf_at_receiver( resolvedLightIndex, selectedLight, receiverPos, samplePos, selectedLightNormal); +#if !RTV_RESTIR_DI_VALIDATION_FULL + selectedPdf = clamp(selectedPdf, 1.0e-6, 65504.0); +#endif vec3 radiance = restir_di_evaluate_integrand( selected, receiver, selectedLight, selectedPdf, sampleDir, distToLight, selectedLightNormal); + float evaluatedTarget = restir_di_target_function(radiance); +#if !RTV_RESTIR_DI_VALIDATION_FULL + evaluatedTarget = clamp(evaluatedTarget, 1.0e-6, 65504.0); +#endif + + // Final history must be expressed at the current receiver. Temporal and + // spatial reuse may carry a selected sample across pixels, so canonicalize + // the selected reservoir here before validating/exporting it. + restir_di_set_direction(selected, sampleDir); + selected.samplePosition_distance.w = distToLight; + restir_di_set_source_pdf(selected, selectedPdf); + restir_di_set_target(selected, evaluatedTarget); + restir_di_set_sample_radiance(selected, radiance); + restir_di_set_light_normal(selected, selectedLightNormal); + + const float packedReservoirParityTolerance = 5.0e-3; + const float fullReservoirParityTolerance = 1.0e-3; +#if RTV_RESTIR_DI_VALIDATION_FULL + float reservoirParityTolerance = fullReservoirParityTolerance; +#else + float reservoirParityTolerance = packedReservoirParityTolerance; +#endif + if (restir_di_relative_mismatch(restir_di_source_pdf(selected), selectedPdf, reservoirParityTolerance) || + restir_di_relative_mismatch(restir_di_target(selected), evaluatedTarget, reservoirParityTolerance)) { + DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_CONTRACT_INVALID], 1u); + } // Reference mode always validates visibility. Production validates any // shifted sample whose visibility is no longer current at this receiver. @@ -556,9 +710,10 @@ void main() { } bool needsVisibility = params.visibilityPolicy == 1u || visibility == RESTIR_DI_VISIBILITY_UNKNOWN; + float finiteLightDistanceBias = max(params.shadowDistanceBias, RESTIR_DI_VISIBILITY_EPSILON * 2.0); if (needsVisibility && params.visibilityRayBudget > 0u && - distToLight > RESTIR_DI_VISIBILITY_EPSILON * 2.0) { - float tmax = distToLight - RESTIR_DI_VISIBILITY_EPSILON * 2.0; + distToLight > finiteLightDistanceBias) { + float tmax = distToLight - finiteLightDistanceBias; vec3 shadowOrigin = receiverPos + sampleDir * RESTIR_DI_VISIBILITY_EPSILON + receiverNormal * RESTIR_DI_VISIBILITY_EPSILON; @@ -660,7 +815,24 @@ void main() { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_INVALID], 1u); return; } - replace_direct_lighting(coords, pixelIndex, initialContribution, finalContribution, selected, receiver); + float currentInitialDistanceTolerance = max( + 1.0e-4, + max(abs(selected.samplePosition_distance.w), abs(initial.samplePosition_distance.w)) * 1.0e-5); + bool selectedIsCurrentInitialSample = + restir_di_reservoir_valid(initial) && + all(equal(selected.sampleMetadata.xyz, initial.sampleMetadata.xyz)) && + distance(selected.samplePosition_distance.xyz, initial.samplePosition_distance.xyz) <= + currentInitialDistanceTolerance && + abs(selected.samplePosition_distance.w - initial.samplePosition_distance.w) <= + currentInitialDistanceTolerance; + replace_direct_lighting( + coords, + pixelIndex, + initialContribution, + finalContribution, + selected, + receiver, + selectedIsCurrentInitialSample); DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_ESTIMATE], 1u); DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_FINAL_VALID], 1u); @@ -696,7 +868,13 @@ void main() { historyReservoir.contribution_confidence.rgb = finalContribution; #endif + if (restir_di_valid_bit_contract_invalid(historyReservoir)) { + DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_CONTRACT_INVALID], 1u); + } final_reservoirs[pixelIndex] = historyReservoir; + final_source_pixels[pixelIndex] = (usedFallback || selectedIsCurrentInitialSample) + ? restir_di_pack_source_pixel(coords) + : restir_di_selected_source_pixel(pixelIndex, coords); restir_di_write_debug( coords, receiver, diff --git a/shaders/restir_di_light_evaluation.glsl b/shaders/restir_di_light_evaluation.glsl index f559f92..a9f3c27 100644 --- a/shaders/restir_di_light_evaluation.glsl +++ b/shaders/restir_di_light_evaluation.glsl @@ -2,13 +2,51 @@ #define RTV_RESTIR_DI_LIGHT_EVALUATION_GLSL // Compute-pass light resolution and target-integrand evaluation; requires restir_di_types_accessors.glsl. -bool restir_di_resolve_light( +const uint RESTIR_DI_LIGHT_MAP_INVALID = 0u; +const uint RESTIR_DI_LIGHT_MAP_CACHED = 1u; +const uint RESTIR_DI_LIGHT_MAP_REMAPPED = 2u; +const uint RESTIR_DI_LIGHT_MAP_CHANGED = 3u; +const uint RESTIR_DI_LIGHT_MAP_UNMAPPED = 4u; +const uint RESTIR_DI_LIGHT_MAP_INFINITE = 5u; +const uint RESTIR_DI_LIGHT_REMAP_SCAN_LIMIT = 2048u; + +bool restir_di_light_hash_kind_matches(RestirDiReservoir reservoir, RestirDiLightRecord light) { + return reservoir.sampleMetadata.x == restir_di_identity_hash(light.identity.xy) && + restir_di_light_kind(reservoir) == light.metadata.x; +} + +uvec4 restir_di_infinite_light_identity(uint reservoirKind, uint currentLightVersion, uint currentEnvironmentVersion) { + return reservoirKind == RESTIR_DI_LIGHT_ENVIRONMENT + ? uvec4(RESTIR_DI_ENVIRONMENT_ID_HASH, 0u, currentEnvironmentVersion, 0u) + : uvec4(RESTIR_DI_SUN_ID_HASH, 0u, currentLightVersion, 0u); +} + +bool restir_di_resolve_light_with_status( RestirDiReservoir reservoir, out RestirDiLightRecord light, - out uint resolvedIndex) { + out uint resolvedIndex, + out uint mapStatus, + uint currentLightVersion, + uint currentEnvironmentVersion) { + mapStatus = RESTIR_DI_LIGHT_MAP_INVALID; uint reservoirKind = restir_di_light_kind(reservoir); if (restir_di_light_kind_infinite(reservoirKind)) { resolvedIndex = RESTIR_DI_PSEUDO_LIGHT_INDEX; + uvec4 currentIdentity = restir_di_infinite_light_identity( + reservoirKind, + currentLightVersion, + currentEnvironmentVersion); + if (reservoir.sampleMetadata.x != currentIdentity.x || + reservoir.sampleMetadata.y != currentIdentity.z) { + light.metadata = uvec4(0u); + light.identity = currentIdentity; + light.data0 = vec4(0.0); + light.data1 = vec4(0.0); + light.data2 = vec4(0.0); + light.data3 = vec4(0.0); + mapStatus = RESTIR_DI_LIGHT_MAP_CHANGED; + return false; + } vec3 storedDirection = reservoir.samplePosition_distance.xyz; float storedDirectionLen2 = dot(storedDirection, storedDirection); vec3 direction = storedDirectionLen2 > 1.0e-8 @@ -17,13 +55,12 @@ bool restir_di_resolve_light( vec3 radiance = restir_di_sample_radiance(reservoir); float sourcePdf = restir_di_source_pdf(reservoir); light.metadata = uvec4(reservoirKind, 0u, 0u, 0u); - light.identity = reservoirKind == RESTIR_DI_LIGHT_ENVIRONMENT - ? uvec4(RESTIR_DI_ENVIRONMENT_ID_HASH, 0u, RESTIR_DI_ENVIRONMENT_VERSION, 0u) - : uvec4(RESTIR_DI_SUN_ID_HASH, 0u, RESTIR_DI_SUN_VERSION, 0u); + light.identity = currentIdentity; light.data0 = vec4(sourcePdf, sourcePdf, 0.0, 0.0); light.data1 = vec4(direction, 0.0); light.data2 = vec4(radiance, 0.0); light.data3 = vec4(0.0); + mapStatus = RESTIR_DI_LIGHT_MAP_INFINITE; return sourcePdf > 1.0e-6 && storedDirectionLen2 > 1.0e-8 && dot(radiance, vec3(0.2126, 0.7152, 0.0722)) > 0.0; @@ -34,10 +71,34 @@ bool restir_di_resolve_light( restir_di_light_identity_matches(reservoir, restir_di_light_records[cachedIndex])) { resolvedIndex = cachedIndex; light = restir_di_light_records[cachedIndex]; + mapStatus = RESTIR_DI_LIGHT_MAP_CACHED; return true; } - // Light/distribution changes invalidate DI history on the CPU. Avoid an - // unbounded per-pixel scan when a stale cached index does not match. + + if (restir_di_scene.lightCount > RESTIR_DI_LIGHT_REMAP_SCAN_LIMIT) { + resolvedIndex = 0u; + light.metadata = uvec4(0u); + light.identity = uvec4(0u); + light.data0 = vec4(0.0); + light.data1 = vec4(0.0); + light.data2 = vec4(0.0); + light.data3 = vec4(0.0); + mapStatus = RESTIR_DI_LIGHT_MAP_UNMAPPED; + return false; + } + + bool sawChangedIdentity = false; + for (uint i = 0u; i < restir_di_scene.lightCount; ++i) { + RestirDiLightRecord candidate = restir_di_light_records[i]; + if (restir_di_light_identity_matches(reservoir, candidate)) { + resolvedIndex = i; + light = candidate; + mapStatus = RESTIR_DI_LIGHT_MAP_REMAPPED; + return true; + } + sawChangedIdentity = sawChangedIdentity || restir_di_light_hash_kind_matches(reservoir, candidate); + } + resolvedIndex = 0u; light.metadata = uvec4(0u); light.identity = uvec4(0u); @@ -45,9 +106,48 @@ bool restir_di_resolve_light( light.data1 = vec4(0.0); light.data2 = vec4(0.0); light.data3 = vec4(0.0); + mapStatus = sawChangedIdentity ? RESTIR_DI_LIGHT_MAP_CHANGED : RESTIR_DI_LIGHT_MAP_UNMAPPED; return false; } +bool restir_di_resolve_light_with_status( + RestirDiReservoir reservoir, + out RestirDiLightRecord light, + out uint resolvedIndex, + out uint mapStatus) { + return restir_di_resolve_light_with_status( + reservoir, + light, + resolvedIndex, + mapStatus, + RESTIR_DI_SUN_VERSION, + RESTIR_DI_ENVIRONMENT_VERSION); +} + +bool restir_di_resolve_light( + RestirDiReservoir reservoir, + out RestirDiLightRecord light, + out uint resolvedIndex, + uint currentLightVersion, + uint currentEnvironmentVersion) { + uint ignoredMapStatus; + return restir_di_resolve_light_with_status( + reservoir, + light, + resolvedIndex, + ignoredMapStatus, + currentLightVersion, + currentEnvironmentVersion); +} + +bool restir_di_resolve_light( + RestirDiReservoir reservoir, + out RestirDiLightRecord light, + out uint resolvedIndex) { + uint ignoredMapStatus; + return restir_di_resolve_light_with_status(reservoir, light, resolvedIndex, ignoredMapStatus); +} + float restir_di_light_selection_pdf(uint lightIndex, RestirDiLightRecord light) { if (restir_di_light_kind_infinite(light.metadata.x)) { return max(light.data0.x, 0.0); @@ -91,14 +191,18 @@ float restir_di_pdf_at_receiver( vec3 restir_di_light_normal(RestirDiReservoir r); -vec3 restir_di_evaluate_integrand( +vec3 restir_di_evaluate_integrand_components( RestirDiReservoir reservoir, RestirDiReceiver current, RestirDiLightRecord light, float lightPdf, out vec3 currentDirection, out float currentDistance, - out vec3 currentLightNormal) { + out vec3 currentLightNormal, + out vec3 diffuseContribution, + out vec3 specularContribution) { + diffuseContribution = vec3(0.0); + specularContribution = vec3(0.0); uint kind = restir_di_light_kind(reservoir); vec3 samplePosition = reservoir.samplePosition_distance.xyz; if (kind == RESTIR_DI_LIGHT_DIRECTIONAL || restir_di_light_kind_infinite(kind)) { @@ -197,7 +301,8 @@ vec3 restir_di_evaluate_integrand( max(3.14159265 * eV * eL, 1.0e-8); bool pbrClosure = (restir_di_receiver_surface_flags(current) & RESTIR_DI_SURFACE_PBR) != 0u; - vec3 bsdf = pbrClosure ? diffuse + specular : baseColor * (orenFactor / 3.14159265); + vec3 diffuseBsdf = pbrClosure ? diffuse : baseColor * (orenFactor / 3.14159265); + vec3 specularBsdf = pbrClosure ? specular : vec3(0.0); float diffusePdf = nDotL / 3.14159265; float smithG1V = 2.0 * nDotV / @@ -211,7 +316,32 @@ vec3 restir_di_evaluate_integrand( float bsdfPdf = pbrClosure ? mix(diffusePdf, specularPdf, specularProbability) : diffusePdf; float lightPdf2 = lightPdf * lightPdf; float misWeight = lightPdf2 / max(lightPdf2 + bsdfPdf * bsdfPdf, 1.0e-8); - return max(bsdf * incidentRadiance * nDotL * misWeight, vec3(0.0)); + vec3 incidentScale = incidentRadiance * nDotL * misWeight; + diffuseContribution = max(diffuseBsdf * incidentScale, vec3(0.0)); + specularContribution = max(specularBsdf * incidentScale, vec3(0.0)); + return diffuseContribution + specularContribution; +} + +vec3 restir_di_evaluate_integrand( + RestirDiReservoir reservoir, + RestirDiReceiver current, + RestirDiLightRecord light, + float lightPdf, + out vec3 currentDirection, + out float currentDistance, + out vec3 currentLightNormal) { + vec3 diffuseContribution; + vec3 specularContribution; + return restir_di_evaluate_integrand_components( + reservoir, + current, + light, + lightPdf, + currentDirection, + currentDistance, + currentLightNormal, + diffuseContribution, + specularContribution); } diff --git a/shaders/restir_di_spatial.comp b/shaders/restir_di_spatial.comp index f3556ec..0de2782 100644 --- a/shaders/restir_di_spatial.comp +++ b/shaders/restir_di_spatial.comp @@ -55,9 +55,37 @@ layout(set = 0, binding = 4, std140) uniform Params { uint materialVisibilityFlags; uint counterEnabled; uint rawOutputIsCurrentSample; - uint padding2; + float shadowDistanceBias; + uint lightVersion; + uint environmentVersion; } params; +layout(set = 0, binding = 24, std430) readonly buffer TemporalSourcePixels { + uint temporal_source_pixels[]; +}; + +layout(set = 0, binding = 25, std430) writeonly buffer SpatialSourcePixels { + uint spatial_source_pixels[]; +}; + +const uint RESTIR_DI_SOURCE_PIXEL_INVALID = 0xffffffffu; + +uint restir_di_pack_source_pixel(ivec2 p) { + if (p.x < 0 || p.y < 0 || p.x > 65534 || p.y > 65534) { + return RESTIR_DI_SOURCE_PIXEL_INVALID; + } + return uint(p.x) | (uint(p.y) << 16u); +} + +uint restir_di_input_source_pixel(uint pixelIndex, ivec2 fallbackCoords) { + uint fallback = restir_di_pack_source_pixel(fallbackCoords); + if (params.temporalMaxAge == 0u) { + return fallback; + } + uint source = temporal_source_pixels[pixelIndex]; + return source == RESTIR_DI_SOURCE_PIXEL_INVALID ? fallback : source; +} + #define DI_COUNTER_ADD(counter, value) ((params.counterEnabled != 0u) ? atomicAdd(counter, value) : 0u) // Blue-noise neighbor offset @@ -126,11 +154,13 @@ void main() { RESTIR_DI_SURFACE_UNLIT | RESTIR_DI_SURFACE_UNSUPPORTED)) != 0u) { spatial_reservoirs[pixelIndex] = temporal_reservoirs[pixelIndex]; + spatial_source_pixels[pixelIndex] = restir_di_input_source_pixel(pixelIndex, coords); return; } // Load center temporal reservoir as starting point RestirDiReservoir merged = temporal_reservoirs[pixelIndex]; + uint mergedSourcePixel = restir_di_input_source_pixel(pixelIndex, coords); // Neighboring temporal reservoirs contain heavily overlapping histories. // Treat each input reservoir as one normalized representative estimate for // this spatial stage; importing every historical M again would count the @@ -147,6 +177,7 @@ void main() { if (params.spatialResultValid == 0u || params.spatialRounds == 0u) { spatial_reservoirs[pixelIndex] = merged; + spatial_source_pixels[pixelIndex] = mergedSourcePixel; return; } @@ -154,8 +185,18 @@ void main() { float maxNeighborLum = 0.0; uint neighborCount = 0u; bool selectedNeighbor = false; + uint localSpatialRounds = params.spatialRounds; + uint currentSourcePixel = restir_di_pack_source_pixel(coords); + bool sourceIsCurrent = mergedSourcePixel == currentSourcePixel || + mergedSourcePixel == RESTIR_DI_SOURCE_PIXEL_INVALID; + bool unstableHistory = sourceIsCurrent || + (restir_di_confidence(merged) < 0.35 && + restir_di_previous_weight(merged) < 0.15); + if (unstableHistory && localSpatialRounds < 8u) { + localSpatialRounds = min(localSpatialRounds + 1u, 8u); + } - for (uint roundIndex = 0u; roundIndex < params.spatialRounds; ++roundIndex) { + for (uint roundIndex = 0u; roundIndex < localSpatialRounds; ++roundIndex) { ivec2 sampleCoords = coords + blue_noise_offset(coords, roundIndex); if (sampleCoords.x < 0 || sampleCoords.y < 0 || sampleCoords.x >= int(params.width) || sampleCoords.y >= int(params.height)) { @@ -182,7 +223,12 @@ void main() { } RestirDiLightRecord candidateLight; uint resolvedLightIndex; - if (!restir_di_resolve_light(candidate, candidateLight, resolvedLightIndex)) { + if (!restir_di_resolve_light( + candidate, + candidateLight, + resolvedLightIndex, + params.lightVersion, + params.environmentVersion)) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_SPATIAL_LIGHT_REJECTED], 1u); continue; } @@ -206,14 +252,6 @@ void main() { vec3 centerPosition = centerReceiver.worldPosition_depth.xyz; float distToLight; uint candidateKind = restir_di_light_kind(candidate); - if (restir_di_light_kind_infinite(candidateKind) && params.visibilityPolicy == 0u) { - // Initial sampling still includes sun/environment. Do not spatially - // shift infinite-light reservoirs in production until final - // visibility/transmittance for shifted sky/sun samples is exact. - mergedMFloat += 1.0; - DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_SPATIAL_LIGHT_REJECTED], 1u); - continue; - } vec3 candidateLightNormal = restir_di_light_normal(candidate); if (candidateKind == RESTIR_DI_LIGHT_AREA || candidateKind == RESTIR_DI_LIGHT_SPOT) { candidateLightNormal = normalize(vec3(candidateLight.data1.w, candidateLight.data2.w, candidateLight.data3.x)); @@ -244,9 +282,10 @@ void main() { // Visibility check in ReferenceValidation mode bool visibilityChecked = false; + float finiteLightDistanceBias = max(params.shadowDistanceBias, RESTIR_DI_VISIBILITY_EPSILON * 2.0); if (params.visibilityPolicy == 1u && roundIndex < params.visibilityRayBudget && - distToLight > RESTIR_DI_VISIBILITY_EPSILON * 2.0) { - float tmax = distToLight - RESTIR_DI_VISIBILITY_EPSILON * 2.0; + distToLight > finiteLightDistanceBias) { + float tmax = distToLight - finiteLightDistanceBias; vec3 shadowOrigin = centerPosition + candidateDirection * RESTIR_DI_VISIBILITY_EPSILON + centerNormal * RESTIR_DI_VISIBILITY_EPSILON; @@ -318,6 +357,7 @@ void main() { mergedMFloat += 1.0; if (rngValue < selectionProbability) { selectedNeighbor = true; + mergedSourcePixel = restir_di_input_source_pixel(sampleIndex, sampleCoords); } neighborLuminanceSum += targetAtCenter; @@ -359,10 +399,11 @@ void main() { restir_di_set_valid(merged, restir_di_reservoir_valid(merged)); spatial_reservoirs[pixelIndex] = merged; + spatial_source_pixels[pixelIndex] = mergedSourcePixel; if (neighborCount > 0u) { DI_COUNTER_ADD(counters.values[selectedNeighbor ? RESTIR_DI_COUNTER_SPATIAL_SELECTED_NEIGHBOR : RESTIR_DI_COUNTER_SPATIAL_SELECTED_CENTER], 1u); } - DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_SPATIAL_ROUNDS], params.spatialRounds); + DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_SPATIAL_ROUNDS], localSpatialRounds); } diff --git a/shaders/restir_di_temporal.comp b/shaders/restir_di_temporal.comp index 7e8575c..c4ce523 100644 --- a/shaders/restir_di_temporal.comp +++ b/shaders/restir_di_temporal.comp @@ -61,7 +61,9 @@ layout(set = 0, binding = 6, std140) uniform Params { uint materialVisibilityFlags; uint counterEnabled; uint rawOutputIsCurrentSample; - uint padding2; + float shadowDistanceBias; + uint lightVersion; + uint environmentVersion; } params; #define DI_COUNTER_ADD(counter, value) ((params.counterEnabled != 0u) ? atomicAdd(counter, value) : 0u) @@ -70,6 +72,23 @@ layout(set = 0, binding = 7, std430) readonly buffer PreviousReceiverBuffer { RestirDiReceiver previous_receivers[]; }; +layout(set = 0, binding = 24, std430) writeonly buffer TemporalSourcePixels { + uint temporal_source_pixels[]; +}; + +const uint RESTIR_DI_SOURCE_PIXEL_INVALID = 0xffffffffu; + +uint restir_di_pack_source_pixel(ivec2 p) { + if (p.x < 0 || p.y < 0 || p.x > 65534 || p.y > 65534) { + return RESTIR_DI_SOURCE_PIXEL_INVALID; + } + return uint(p.x) | (uint(p.y) << 16u); +} + +void restir_di_store_temporal_source(uint pixelIndex, ivec2 sourcePixel) { + temporal_source_pixels[pixelIndex] = restir_di_pack_source_pixel(sourcePixel); +} + // Velocity unpacking vec2 unpack_velocity_pixels(uint packed) { ivec2 v = ivec2(packed & 0xffffu, (packed >> 16u) & 0xffffu); @@ -99,6 +118,7 @@ void main() { uint pixelIndex = uint(coords.y) * params.width + uint(coords.x); DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_PIXELS], 1u); + restir_di_store_temporal_source(pixelIndex, coords); // Load current receiver RestirDiReceiver receiver = receivers[pixelIndex]; @@ -107,8 +127,9 @@ void main() { uint surfaceFlags = restir_di_receiver_surface_flags(receiver); if ((surfaceFlags & (RESTIR_DI_SURFACE_SKY | RESTIR_DI_SURFACE_INVALID | RESTIR_DI_SURFACE_DELTA | RESTIR_DI_SURFACE_ALPHA | - RESTIR_DI_SURFACE_UNLIT | RESTIR_DI_SURFACE_UNSUPPORTED)) != 0u) { + RESTIR_DI_SURFACE_UNLIT | RESTIR_DI_SURFACE_UNSUPPORTED)) != 0u) { temporal_reservoirs[pixelIndex] = initial_reservoirs[pixelIndex]; + restir_di_store_temporal_source(pixelIndex, coords); DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_SURFACE_REJECTED], 1u); return; } @@ -120,6 +141,7 @@ void main() { // Try temporal reuse if (params.enabled == 0u || params.historyValid == 0u) { temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } @@ -134,40 +156,72 @@ void main() { if (!reprojectValid || length(velocityPixels) > 48.0) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_REPROJECT_INVALID], 1u); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_REPROJECT_VALID], 1u); - // Load previous reservoir - uint prevIndex = uint(reprojectedCoords.y) * params.width + uint(reprojectedCoords.x); - RestirDiReservoir previous = previous_reservoirs[prevIndex]; + uint prevIndex = 0u; + ivec2 previousCoords = reprojectedCoords; + RestirDiReceiver prevReceiver; + bool foundCompatibleHistory = false; + float bestHistoryScore = 1.0e30; + float receiverWorldLimit = max(0.05, max(abs(receiver.worldPosition_depth.w), 1.0) * params.depthThreshold); - // Load previous receiver for surface compatibility - RestirDiReceiver prevReceiver = previous_receivers[prevIndex]; + int historySearchRadius = length(velocityPixels) > 0.25 ? 1 : 0; + for (int oy = -historySearchRadius; oy <= historySearchRadius; ++oy) { + for (int ox = -historySearchRadius; ox <= historySearchRadius; ++ox) { + ivec2 candidateCoords = reprojectedCoords + ivec2(ox, oy); + if (candidateCoords.x < 0 || candidateCoords.y < 0 || + candidateCoords.x >= int(params.width) || candidateCoords.y >= int(params.height)) { + continue; + } - // Surface compatibility - if (!restir_di_surface_compatible(receiver, prevReceiver, params.normalThreshold, params.depthThreshold)) { - DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_SURFACE_REJECTED], 1u); - rejectionFlags |= RESTIR_DI_REJECT_SURFACE_MISMATCH; - restir_di_set_rejection_flags(current, rejectionFlags); - temporal_reservoirs[pixelIndex] = current; - return; + uint candidateIndex = uint(candidateCoords.y) * params.width + uint(candidateCoords.x); + RestirDiReceiver candidateReceiver = previous_receivers[candidateIndex]; + if (!restir_di_surface_compatible(receiver, candidateReceiver, params.normalThreshold, params.depthThreshold)) { + continue; + } + + float worldDelta = length(receiver.worldPosition_depth.xyz - candidateReceiver.worldPosition_depth.xyz); + if (worldDelta > receiverWorldLimit) { + continue; + } + + vec3 currentNormal = normalize(receiver.normal_roughness.xyz); + vec3 previousNormal = normalize(candidateReceiver.normal_roughness.xyz); + float normalPenalty = 1.0 - clamp(dot(currentNormal, previousNormal), 0.0, 1.0); + float offsetPenalty = 0.05 * float(ox * ox + oy * oy); + float depthPenalty = worldDelta / max(receiverWorldLimit, 1.0e-4); + float validBonus = restir_di_reservoir_valid(previous_reservoirs[candidateIndex]) ? -0.25 : 0.0; + float score = depthPenalty + normalPenalty + offsetPenalty + validBonus; + if (score < bestHistoryScore) { + bestHistoryScore = score; + prevIndex = candidateIndex; + previousCoords = candidateCoords; + prevReceiver = candidateReceiver; + foundCompatibleHistory = true; + } + } } - float receiverWorldDelta = length(receiver.worldPosition_depth.xyz - prevReceiver.worldPosition_depth.xyz); - float receiverWorldLimit = max(0.05, max(abs(receiver.worldPosition_depth.w), 1.0) * params.depthThreshold); - if (receiverWorldDelta > receiverWorldLimit) { + + if (!foundCompatibleHistory) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_SURFACE_REJECTED], 1u); rejectionFlags |= RESTIR_DI_REJECT_SURFACE_MISMATCH; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } + RestirDiReservoir previous = previous_reservoirs[prevIndex]; + // An invalid reservoir can still represent zero-weight proposals. Merge // its M only after verifying that both receivers share a target domain. if (!restir_di_reservoir_valid(previous)) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_PREVIOUS_INVALID], 1u); temporal_reservoirs[pixelIndex] = merge_zero_weight_history_m(current, previous); + restir_di_store_temporal_source(pixelIndex, coords); return; } DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_PREVIOUS_VALID], 1u); @@ -177,16 +231,23 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_AGE; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } RestirDiLightRecord previousLight; uint resolvedLightIndex; - if (!restir_di_resolve_light(previous, previousLight, resolvedLightIndex)) { + if (!restir_di_resolve_light( + previous, + previousLight, + resolvedLightIndex, + params.lightVersion, + params.environmentVersion)) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_LIGHT_VERSION_REJECTED], 1u); rejectionFlags |= RESTIR_DI_REJECT_LIGHT_VERSION; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } @@ -194,16 +255,6 @@ void main() { float distToLight; vec3 lightNormal = restir_di_light_normal(previous); uint previousKind = restir_di_light_kind(previous); - if (restir_di_light_kind_infinite(previousKind) && params.visibilityPolicy == 0u) { - // Sun/environment are valid initial DI candidates, but production - // temporal shifting needs a current-receiver transmittance model for - // infinite lights before it is safe. Keep the current frame's all-light - // sample and avoid feeding long-lived shifted sky/sun reservoirs into - // the first-frame/final visibility path. - DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_LIGHT_VERSION_REJECTED], 1u); - temporal_reservoirs[pixelIndex] = current; - return; - } if (previousKind == RESTIR_DI_LIGHT_AREA || previousKind == RESTIR_DI_LIGHT_SPOT) { lightNormal = normalize(vec3(previousLight.data1.w, previousLight.data2.w, previousLight.data3.x)); } @@ -231,6 +282,7 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_TARGET_ZERO; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = merge_zero_weight_history_m(current, previous); + restir_di_store_temporal_source(pixelIndex, coords); return; } @@ -240,6 +292,7 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_PDF_ZERO; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = merge_zero_weight_history_m(current, previous); + restir_di_store_temporal_source(pixelIndex, coords); return; } @@ -250,6 +303,7 @@ void main() { if (isnan(shiftedWeight) || isinf(shiftedWeight) || shiftedWeight < 0.0) { DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_NON_FINITE], 1u); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } @@ -260,6 +314,7 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_VISIBILITY; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = current; + restir_di_store_temporal_source(pixelIndex, coords); return; } // ReferenceValidation: trace actual shadow ray from current receiver to light @@ -272,9 +327,10 @@ void main() { : normalize(previousLight.data1.xyz)) : lightPos - receiver.worldPosition_depth.xyz; float dist = directionalSample ? previous.samplePosition_distance.w : length(toLight); - if (dist > RESTIR_DI_VISIBILITY_EPSILON * 2.0) { + float finiteLightDistanceBias = max(params.shadowDistanceBias, RESTIR_DI_VISIBILITY_EPSILON * 2.0); + if (dist > finiteLightDistanceBias) { vec3 shadowDir = directionalSample ? toLight : toLight / dist; - float tmax = dist - RESTIR_DI_VISIBILITY_EPSILON * 2.0; + float tmax = dist - finiteLightDistanceBias; vec3 shadowOrigin = receiver.worldPosition_depth.xyz + shadowDir * RESTIR_DI_VISIBILITY_EPSILON + receiverNormal * RESTIR_DI_VISIBILITY_EPSILON; @@ -288,6 +344,7 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_VISIBILITY; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = merge_zero_weight_history_m(current, previous); + restir_di_store_temporal_source(pixelIndex, coords); return; } DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_VISIBILITY_PASSED], 1u); @@ -302,6 +359,7 @@ void main() { rejectionFlags |= RESTIR_DI_REJECT_VISIBILITY; restir_di_set_rejection_flags(current, rejectionFlags); temporal_reservoirs[pixelIndex] = merge_zero_weight_history_m(current, previous); + restir_di_store_temporal_source(pixelIndex, coords); return; } DI_COUNTER_ADD(counters.values[RESTIR_DI_COUNTER_TEMPORAL_VISIBILITY_SKIPPED], 1u); @@ -401,4 +459,5 @@ void main() { restir_di_set_rejection_flags(selected, rejectionFlags); temporal_reservoirs[pixelIndex] = selected; + restir_di_store_temporal_source(pixelIndex, selectPrevious ? previousCoords : coords); } diff --git a/shaders/restir_di_types_accessors.glsl b/shaders/restir_di_types_accessors.glsl index a0ed926..39f01c4 100644 --- a/shaders/restir_di_types_accessors.glsl +++ b/shaders/restir_di_types_accessors.glsl @@ -176,6 +176,7 @@ const uint RESTIR_DI_COUNTER_FINAL_LIGHT_REJECTED = 59u; const uint RESTIR_DI_COUNTER_SUM_LUMINANCE_HIGH = 60u; const uint RESTIR_DI_COUNTER_INITIAL_ENVIRONMENT = 61u; const uint RESTIR_DI_COUNTER_INITIAL_SUN = 62u; +const uint RESTIR_DI_COUNTER_CONTRACT_INVALID = 63u; const uint RESTIR_DI_COUNTER_CAPACITY = 64u; const float RESTIR_DI_INVALID_ID_FLOAT = 4294967040.0; @@ -333,6 +334,25 @@ bool restir_di_reservoir_valid(RestirDiReservoir r) { !any(isnan(radiance)) && !any(isinf(radiance)); } +bool restir_di_valid_bit_contract_invalid(RestirDiReservoir r) { + bool stateValid = (r.reservoirMetadata.x & (1u << 18u)) != 0u; + if (!stateValid) return false; +#if RTV_RESTIR_DI_VALIDATION_FULL + float target = r.sampleRadiance_target.w; + float weightSum = r.sampleNormal_weightSum.w; + float sourcePdf = r.sampleDirection_pdf.w; +#else + vec2 targetWeight = unpackHalf2x16(r.reservoirMetadata.w); + float target = targetWeight.x; + float weightSum = targetWeight.y; + float sourcePdf = unpackHalf2x16(r.reservoirMetadata.y).x; +#endif + return target <= 0.0 || weightSum <= 0.0 || sourcePdf <= 0.0 || + isnan(target) || isinf(target) || + isnan(weightSum) || isinf(weightSum) || + isnan(sourcePdf) || isinf(sourcePdf); +} + uint restir_di_age(RestirDiReservoir r) { return r.reservoirMetadata.x & 0xffu; } @@ -491,6 +511,25 @@ bool restir_di_reservoir_valid(RestirDiReservoir r) { !any(isnan(r.samplePosition_distance.xyz)) && !any(isinf(r.samplePosition_distance.xyz)) && !any(isnan(radiance)) && !any(isinf(radiance)); } + +bool restir_di_valid_bit_contract_invalid(RestirDiReservoir r) { + bool validBit = (r.reservoirMetadata.x & (1u << 18u)) != 0u; + if (!validBit) return false; +#if RTV_RESTIR_DI_VALIDATION_FULL + float target = r.sampleRadiance_target.w; + float weight = r.sampleNormal_weightSum.w; + float pdf = r.sampleDirection_pdf.w; +#else + vec2 targetWeight = unpackHalf2x16(r.reservoirMetadata.w); + float target = targetWeight.x; + float weight = targetWeight.y; + float pdf = unpackHalf2x16(r.reservoirMetadata.y).x; +#endif + return target <= 0.0 || weight <= 0.0 || pdf <= 0.0 || + isnan(target) || isinf(target) || + isnan(weight) || isinf(weight) || + isnan(pdf) || isinf(pdf); +} uint restir_di_light_kind(RestirDiReservoir r) { return r.sampleMetadata.z & 0xffu; } uint restir_di_light_id(RestirDiReservoir r) { return r.sampleMetadata.x; } uint restir_di_light_index(RestirDiReservoir r) { return r.sampleMetadata.z >> 8u; } diff --git a/shaders/restir_di_visibility.glsl b/shaders/restir_di_visibility.glsl index b8c6664..594725a 100644 --- a/shaders/restir_di_visibility.glsl +++ b/shaders/restir_di_visibility.glsl @@ -27,7 +27,7 @@ struct RestirDiUniforms { uint materialVisibilityFlags; uint counterEnabled; uint padding1; - uint padding2; + float shadowDistanceBias; }; const float RESTIR_DI_VISIBILITY_EPSILON = 0.001; diff --git a/shaders/restir_gi_final.comp b/shaders/restir_gi_final.comp index 15f5a08..69acad8 100644 --- a/shaders/restir_gi_final.comp +++ b/shaders/restir_gi_final.comp @@ -39,6 +39,14 @@ layout(set = 0, binding = 4, std140) uniform DebugParams { float scale; } debug_params; +vec3 scalar_contract_color(float value, float range) { + if (isnan(value) || isinf(value) || value < 0.0) { + return vec3(1.0, 0.0, 0.8); + } + float normalizedValue = clamp(log2(max(value, 0.0) + 1.0) / max(range, 1.0e-6), 0.0, 1.0); + return vec3(normalizedValue, sqrt(normalizedValue), 1.0 - normalizedValue); +} + void main() { ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); if (pixel.x >= int(params.width) || pixel.y >= int(params.height)) return; @@ -75,6 +83,36 @@ void main() { imageStore(raw_image, pixel, vec4(tint, 1.0)); return; } + if (debug_params.view == 152u) { + imageStore(raw_image, pixel, vec4(scalar_contract_color(restir_gi_target_function(selectedRes), 16.0), 1.0)); + return; + } + if (debug_params.view == 153u) { + imageStore(raw_image, pixel, vec4(scalar_contract_color(restir_gi_target_pdf(selectedRes), 16.0), 1.0)); + return; + } + if (debug_params.view == 154u) { + imageStore(raw_image, pixel, vec4(scalar_contract_color(selectedRes.radiance_weight_sum.w, 16.0), 1.0)); + return; + } + if (debug_params.view == 155u) { + imageStore(raw_image, pixel, vec4(vec3(clamp(restir_gi_sample_count(selectedRes) / 255.0, 0.0, 1.0)), 1.0)); + return; + } + if (debug_params.view == 156u) { + float sampleFactor = restir_gi_reservoir_valid(selectedRes) + ? clamp(restir_gi_sample_count(selectedRes) / 16.0, 0.0, 1.0) + : 0.0; + float ageFactor = 1.0 - clamp(float(restir_gi_age(selectedRes)) / 32.0, 0.0, 0.75); + float visFactor = restir_gi_visible(selectedRes) ? 1.0 : 0.25; + imageStore(raw_image, pixel, vec4(vec3(sampleFactor * ageFactor * visFactor), 1.0)); + return; + } + if (debug_params.view == 157u) { + bool valid = restir_gi_reservoir_valid(selectedRes); + imageStore(raw_image, pixel, vec4(0.35, restir_gi_visible(selectedRes) ? 1.0 : 0.0, valid ? 0.0 : 1.0, 1.0)); + return; + } bool beautyView = debug_params.view == 0u; bool finalDebugView = debug_params.view == 73u; @@ -135,21 +173,23 @@ void main() { float rawDeltaScale = params.raw_output_is_current_sample > 0.5 ? 1.0 : 1.0 / max(accumulated.a, 1.0); imageStore(raw_image, pixel, vec4(max(rawBase.rgb + giDelta * rawDeltaScale, vec3(0.0)), 1.0)); - PathDataRecord pathData = path_data_buffer[pixelIndex]; - pathData.indirect_diffuse.rgb = max(pathData.indirect_diffuse.rgb + giDelta, vec3(0.0)); + vec4 indirectDiffuse = path_data_buffer[pixelIndex].indirect_diffuse; + indirectDiffuse.rgb = max(indirectDiffuse.rgb + giDelta, vec3(0.0)); float guideStrength = giConfidence * replacementStrength; - pathData.indirect_diffuse.a = max(pathData.indirect_diffuse.a, guideStrength); + indirectDiffuse.a = max(indirectDiffuse.a, guideStrength); + path_data_buffer[pixelIndex].indirect_diffuse = indirectDiffuse; if (restir_gi_reservoir_valid(selectedRes)) { float giHitDistance = clamp(restir_gi_hit_distance(selectedRes), 0.0, 65504.0); if (guideStrength > 0.04 && giHitDistance > 0.001 && giHitDistance < 65504.0) { float guideBlend = clamp(guideStrength, 0.0, 0.75); + float currentDiffuseHitDistance = path_data_buffer[pixelIndex].denoiser_hit_distance.x; float previousDiffuseHitDistance = - pathData.denoiser_hit_distance.x > 0.001 && pathData.denoiser_hit_distance.x < 65504.0 - ? pathData.denoiser_hit_distance.x + currentDiffuseHitDistance > 0.001 && currentDiffuseHitDistance < 65504.0 + ? currentDiffuseHitDistance : giHitDistance; - pathData.denoiser_hit_distance.x = mix(previousDiffuseHitDistance, giHitDistance, guideBlend); - pathData.diffuse_ray_direction_hit_distance.w = pathData.denoiser_hit_distance.x; + float resolvedHitDistance = mix(previousDiffuseHitDistance, giHitDistance, guideBlend); + path_data_buffer[pixelIndex].denoiser_hit_distance.x = resolvedHitDistance; + path_data_buffer[pixelIndex].diffuse_ray_direction_hit_distance.w = resolvedHitDistance; } } - path_data_buffer[pixelIndex] = pathData; } diff --git a/shaders/restir_gi_final_production.comp b/shaders/restir_gi_final_production.comp index 5e7d083..9e94691 100644 --- a/shaders/restir_gi_final_production.comp +++ b/shaders/restir_gi_final_production.comp @@ -39,6 +39,10 @@ const uint GI_COUNTER_VISIBILITY_RAYS = 42u; const uint GI_COUNTER_VISIBILITY_UNKNOWN = 43u; const uint GI_COUNTER_VERSION_REJECT = 44u; const uint GI_COUNTER_NON_FINITE = 45u; +const uint GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF = 48u; +const uint GI_COUNTER_CONTRACT_INVALID_TARGET_PDF = 49u; +const uint GI_COUNTER_SOURCE_PDF_PARITY_MISMATCH = 50u; +const uint GI_COUNTER_TARGET_PDF_PARITY_MISMATCH = 51u; const uint DEBUG_VIEW_RESTIR_GI_VALIDITY = 68u; const uint DEBUG_VIEW_RESTIR_GI_AGE = 69u; @@ -50,6 +54,12 @@ const uint DEBUG_VIEW_RESTIR_GI_NORMAL = 90u; const uint DEBUG_VIEW_RESTIR_GI_HIT_DISTANCE = 91u; const uint DEBUG_VIEW_RESTIR_GI_GRID = 127u; const uint DEBUG_VIEW_RESTIR_GI_PATH_CLASS = 128u; +const uint DEBUG_VIEW_RESTIR_GI_TARGET = 152u; +const uint DEBUG_VIEW_RESTIR_GI_SOURCE_PDF = 153u; +const uint DEBUG_VIEW_RESTIR_GI_WEIGHT_SUM = 154u; +const uint DEBUG_VIEW_RESTIR_GI_M = 155u; +const uint DEBUG_VIEW_RESTIR_GI_CONFIDENCE = 156u; +const uint DEBUG_VIEW_RESTIR_GI_VISIBILITY = 157u; layout(push_constant, std430) uniform PushConstants { uint width; @@ -72,6 +82,42 @@ layout(push_constant, std430) uniform PushConstants { const uint GI_ACTIVE_TILE_SIZE = 16u; +void record_prod_contract_violation(ProdRestirGiReservoir reservoir) { + if ((prod_unpack_flags(reservoir) & PROD_FLAG_VALID) == 0u) return; + if (prod_contract_invalid_source_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF], 1u); + } + if (prod_contract_invalid_target_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_TARGET_PDF], 1u); + } +} + +bool relative_mismatch(float storedValue, float evaluatedValue, float tolerance) { + if (!prod_contract_finite(storedValue) || !prod_contract_finite(evaluatedValue)) { + return true; + } + float scale = max(max(abs(storedValue), abs(evaluatedValue)), 1.0e-6); + return abs(storedValue - evaluatedValue) / scale > tolerance; +} + +void record_prod_parity_violation(ProdRestirGiReservoir reservoir, float storedTarget, float distance) { + bool environment = prod_unpack_path_class(reservoir) == PROD_PATH_CLASS_ENVIRONMENT_REUSABLE || + (prod_unpack_flags(reservoir) & PROD_FLAG_ENVIRONMENT) != 0u; + float sourceDistance = environment ? distance : max(abs(reservoir.x2PositionDistance.w), 1.0e-4); + float reconstructedSourcePdf = restir_gi_source_measure_pdf( + reservoir.sourceDirectionBsdfPdf.w, + reservoir.sourceDirectionBsdfPdf.xyz, + reservoir.x2NormalRoughness.xyz, + sourceDistance, + environment); + if (relative_mismatch(prod_unpack_source_pdf(reservoir), reconstructedSourcePdf, 1.0e-4)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_SOURCE_PDF_PARITY_MISMATCH], 1u); + } + if (relative_mismatch(storedTarget, prod_unpack_target(reservoir), 1.0e-4)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_TARGET_PDF_PARITY_MISMATCH], 1u); + } +} + bool active_tile_contains(ivec2 pixel) { if (!GI_ACTIVE_TILE_MASK_ENABLED) return true; uint tileColumns = (pc.width + GI_ACTIVE_TILE_SIZE - 1u) / GI_ACTIVE_TILE_SIZE; @@ -90,7 +136,8 @@ vec2 unpack_velocity_pixels(uint packed) { bool is_gi_debug(uint view) { return (view >= DEBUG_VIEW_RESTIR_GI_VALIDITY && view <= DEBUG_VIEW_RESTIR_GI_FINAL) || view == DEBUG_VIEW_RESTIR_GI_NORMAL || view == DEBUG_VIEW_RESTIR_GI_HIT_DISTANCE || - view == DEBUG_VIEW_RESTIR_GI_GRID || view == DEBUG_VIEW_RESTIR_GI_PATH_CLASS; + view == DEBUG_VIEW_RESTIR_GI_GRID || view == DEBUG_VIEW_RESTIR_GI_PATH_CLASS || + (view >= DEBUG_VIEW_RESTIR_GI_TARGET && view <= DEBUG_VIEW_RESTIR_GI_VISIBILITY); } bool trace_visibility(RestirGiReceiver receiver, ProdRestirGiReservoir reservoir) { @@ -117,6 +164,15 @@ bool trace_visibility(RestirGiReceiver receiver, ProdRestirGiReservoir reservoir return visibility == RESTIR_DI_VISIBILITY_VISIBLE; } +vec3 clamp_gi_final_luminance(vec3 value, float maxLum) { + value = max(value, vec3(0.0)); + float lum = restir_gi_luma(value); + if (maxLum <= 0.0 || lum <= maxLum || lum <= 1.0e-8) { + return value; + } + return value * (maxLum / lum); +} + vec3 evaluate_final( inout ProdRestirGiReservoir reservoir, RestirGiReceiver receiver, @@ -127,13 +183,17 @@ vec3 evaluate_final( { confidence = 0.0; visible = false; - if (!prod_reservoir_reusable(reservoir)) return vec3(0.0); + if (!prod_reservoir_reusable(reservoir)) { + record_prod_contract_violation(reservoir); + return vec3(0.0); + } if (prod_unpack_version_hash(reservoir) != restir_gi_receiver_version_hash(receiver)) { GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_VERSION_REJECT], 1u); return vec3(0.0); } float sourceTarget = prod_unpack_target(reservoir); if (!restir_gi_reconnect_sample(reservoir, receiver, pc.cameraPosition.xyz, direction, distance)) return vec3(0.0); + record_prod_parity_violation(reservoir, sourceTarget, distance); float shiftedWeight = prod_shifted_reservoir_weight( reservoir, sourceTarget, prod_unpack_target(reservoir)); if (!restir_gi_finite(shiftedWeight) || shiftedWeight <= 0.0) { @@ -188,6 +248,14 @@ vec3 class_color(uint c, float confidence) { return color * (0.25 + 0.75 * confidence); } +vec3 scalar_contract_color(float value, float range) { + if (!restir_gi_finite(value) || value < 0.0) { + return vec3(1.0, 0.0, 0.8); + } + float normalizedValue = clamp(log2(max(value, 0.0) + 1.0) / max(range, 1.0e-6), 0.0, 1.0); + return vec3(normalizedValue, sqrt(normalizedValue), 1.0 - normalizedValue); +} + void main() { ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); if (pixel.x >= int(pc.width) || pixel.y >= int(pc.height) || (pc.enabled & 1u) == 0u) return; @@ -214,11 +282,16 @@ void main() { ProdRestirGiReservoir selected = useSpatial ? spatial : current; if (!debugGi && pc.referenceValidation == 0u && (!restir_gi_receiver_valid_for_reuse(receiver) || !prod_reservoir_reusable(selected))) { + if (!prod_reservoir_reusable(selected)) record_prod_contract_violation(selected); GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_FINAL_FALLBACK], 1u); return; } PathDataRecord pathData = path_data_buffer[pixelIndex]; - vec3 currentFallback = max(pathData.restir_gi_fallback_reactive.rgb, vec3(0.0)); + vec4 giFallbackReactive = pathData.restir_gi_fallback_reactive; + vec3 currentFallback = max(giFallbackReactive.rgb, vec3(0.0)); + if (pc.referenceValidation == 0u && pc.fireflyClamp > 0.0) { + currentFallback = clamp_gi_final_luminance(currentFallback, pc.fireflyClamp); + } if (useSpatial) GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_FINAL_SPATIAL_USED], 1u); else GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_FINAL_CURRENT_USED], 1u); @@ -267,7 +340,7 @@ void main() { bool clamped = false; float finalLum = restir_gi_luma(finalSample); if (pc.referenceValidation == 0u && pc.fireflyClamp > 0.0 && finalLum > pc.fireflyClamp) { - finalSample *= pc.fireflyClamp / max(finalLum, 1.0e-8); + finalSample = clamp_gi_final_luminance(finalSample, pc.fireflyClamp); clamped = true; GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_FINAL_CLAMP], 1u); } @@ -317,6 +390,28 @@ void main() { if (pc.debugView == DEBUG_VIEW_RESTIR_GI_PATH_CLASS) { imageStore(rawImage, pixel, vec4(class_color(pathClass, confidence), 1.0)); return; } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_TARGET) { + imageStore(rawImage, pixel, vec4(scalar_contract_color(prod_unpack_target(selected), 16.0), 1.0)); return; + } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_SOURCE_PDF) { + imageStore(rawImage, pixel, vec4(scalar_contract_color(prod_unpack_source_pdf(selected), 16.0), 1.0)); return; + } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_WEIGHT_SUM) { + imageStore(rawImage, pixel, vec4(scalar_contract_color(prod_unpack_weight_sum(selected), 16.0), 1.0)); return; + } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_M) { + imageStore(rawImage, pixel, vec4(vec3(clamp(prod_unpack_sample_count(selected) / 255.0, 0.0, 1.0)), 1.0)); return; + } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_CONFIDENCE) { + imageStore(rawImage, pixel, vec4(vec3(clamp(prod_unpack_confidence(selected), 0.0, 1.0)), 1.0)); return; + } + if (pc.debugView == DEBUG_VIEW_RESTIR_GI_VISIBILITY) { + uint flags = prod_unpack_flags(selected); + bool known = (flags & PROD_FLAG_VISIBILITY_KNOWN) != 0u; + bool selectedVisible = (flags & PROD_FLAG_VISIBLE) != 0u; + imageStore(rawImage, pixel, vec4(known ? 0.0 : 0.35, selectedVisible ? 1.0 : 0.0, + prod_reservoir_valid(selected) ? 0.0 : 1.0, 1.0)); return; + } if (pc.debugView == DEBUG_VIEW_RESTIR_GI_GRID) { vec3 tint = ((pixel.x ^ pixel.y) & 1) == 0 ? vec3(0.2, 0.55, 1.0) : vec3(1.0, 0.45, 0.15); imageStore(rawImage, pixel, vec4(tint * clamp(restir_gi_luma(replacement) * 8.0, 0.0, 1.0), 1.0)); return; @@ -328,7 +423,7 @@ void main() { float rawDeltaScale = pc.rawOutputIsCurrentSample > 0.5 ? 1.0 : 1.0 / max(accumulated.a, 1.0); imageStore(rawImage, pixel, vec4(max(raw.rgb + delta * rawDeltaScale, vec3(0.0)), 1.0)); - float specularWeight = clamp(pathData.restir_gi_fallback_reactive.a, 0.0, 1.0); + float specularWeight = clamp(giFallbackReactive.a, 0.0, 1.0); pathData.indirect_diffuse.rgb = max(pathData.indirect_diffuse.rgb + delta * (1.0 - specularWeight), vec3(0.0)); pathData.indirect_specular.rgb = max(pathData.indirect_specular.rgb + delta * specularWeight, vec3(0.0)); pathData.indirect_diffuse.a = max(pathData.indirect_diffuse.a, confidence); @@ -336,7 +431,9 @@ void main() { if (estimatorValid) { pathData.denoiser_hit_distance.x = distance; pathData.diffuse_ray_direction_hit_distance = vec4(direction, distance); - if (specularWeight > 0.0) pathData.specular_ray_direction_hit_distance = vec4(direction, distance); + if (specularWeight > 0.0) { + pathData.specular_ray_direction_hit_distance = vec4(direction, distance); + } } path_data_buffer[pixelIndex] = pathData; } diff --git a/shaders/restir_gi_prod_packing.glsl b/shaders/restir_gi_prod_packing.glsl index fe10614..87cb5b4 100644 --- a/shaders/restir_gi_prod_packing.glsl +++ b/shaders/restir_gi_prod_packing.glsl @@ -56,6 +56,18 @@ float prod_unpack_target(ProdRestirGiReservoir r) { return max(r.selectedIntegra float prod_unpack_source_pdf(ProdRestirGiReservoir r) { return max(r.suffixRadianceSourcePdf.w, 0.0); } uint prod_unpack_version_hash(ProdRestirGiReservoir r) { return floatBitsToUint(r.reservoirData.w); } +bool prod_contract_finite(float v) { + return !isnan(v) && !isinf(v); +} + +bool prod_contract_invalid_target_pdf(ProdRestirGiReservoir r) { + return !prod_contract_finite(r.selectedIntegrandTarget.w) || r.selectedIntegrandTarget.w <= 0.0; +} + +bool prod_contract_invalid_source_pdf(ProdRestirGiReservoir r) { + return !prod_contract_finite(r.suffixRadianceSourcePdf.w) || r.suffixRadianceSourcePdf.w <= 0.0; +} + void prod_set_meta(inout ProdRestirGiReservoir r, uint age, uint flags, uint pathClass, float confidence) { r.reservoirData.z = uintBitsToFloat(prod_pack_meta(age, flags, pathClass, confidence)); } diff --git a/shaders/restir_gi_spatial_production.comp b/shaders/restir_gi_spatial_production.comp index 8831672..a759d59 100644 --- a/shaders/restir_gi_spatial_production.comp +++ b/shaders/restir_gi_spatial_production.comp @@ -30,6 +30,8 @@ const uint GI_COUNTER_VISIBILITY_RAYS = 42u; const uint GI_COUNTER_VISIBILITY_UNKNOWN = 43u; const uint GI_COUNTER_VERSION_REJECT = 44u; const uint GI_COUNTER_NON_FINITE = 45u; +const uint GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF = 48u; +const uint GI_COUNTER_CONTRACT_INVALID_TARGET_PDF = 49u; layout(push_constant, std430) uniform PushConstants { uint width; @@ -51,6 +53,16 @@ layout(push_constant, std430) uniform PushConstants { const uint GI_ACTIVE_TILE_SIZE = 16u; +void record_prod_contract_violation(ProdRestirGiReservoir reservoir) { + if ((prod_unpack_flags(reservoir) & PROD_FLAG_VALID) == 0u) return; + if (prod_contract_invalid_source_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF], 1u); + } + if (prod_contract_invalid_target_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_TARGET_PDF], 1u); + } +} + bool active_tile_contains(ivec2 fullPixel) { if (!GI_ACTIVE_TILE_MASK_ENABLED) return true; uint tileColumns = (pc.width + GI_ACTIVE_TILE_SIZE - 1u) / GI_ACTIVE_TILE_SIZE; @@ -120,6 +132,7 @@ void main() { uint(representative_full_pixel(pixel).x)]; ProdRestirGiReservoir selected = temporal_reservoirs[pixelIndex]; if (!restir_gi_receiver_valid_for_reuse(center) || !prod_reservoir_reusable(selected)) { + if (!prod_reservoir_reusable(selected)) record_prod_contract_violation(selected); if (!prod_reservoir_reusable(selected)) GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_PATH_SPATIAL_CENTER_CURRENT_ONLY], 1u); spatial_reservoirs[pixelIndex] = selected; return; @@ -153,6 +166,7 @@ void main() { ProdRestirGiReservoir candidate = temporal_reservoirs[neighborIndex]; if (!prod_reservoir_reusable(candidate)) { + record_prod_contract_violation(candidate); GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_SPATIAL_REJECT_INVALID], 1u); continue; } @@ -204,8 +218,9 @@ void main() { } #endif + float reuseMassScale = visibilityConfidence; float candidateWeight = prod_shifted_reservoir_weight( - candidate, candidateSourceTarget, prod_unpack_target(candidate)) * visibilityConfidence; + candidate, candidateSourceTarget, prod_unpack_target(candidate)) * reuseMassScale; if (!restir_gi_finite(candidateWeight) || candidateWeight <= 0.0) { GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_NON_FINITE], 1u); continue; @@ -219,7 +234,9 @@ void main() { GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_SPATIAL_SELECTED_NEIGHBOR], 1u); } totalWeight = newWeight; - totalM += prod_unpack_sample_count(candidate); + // Reliability damping changes the effective proposal mass. Keep W and M + // symmetric so visibility-confidence shortcuts do not darken the final estimator. + totalM += prod_unpack_sample_count(candidate) * reuseMassScale; } if (totalM > 255.0 || totalWeight > 65504.0) GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_SPATIAL_CLAMP], 1u); diff --git a/shaders/restir_gi_temporal.comp b/shaders/restir_gi_temporal.comp index 83cdea8..112aabc 100644 --- a/shaders/restir_gi_temporal.comp +++ b/shaders/restir_gi_temporal.comp @@ -73,6 +73,8 @@ const uint GI_COUNTER_VERSION_REJECT = 44u; const uint GI_COUNTER_NON_FINITE = 45u; const uint GI_COUNTER_ACTIVE_TILES = 46u; const uint GI_COUNTER_ACTIVE_PIXELS = 47u; +const uint GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF = 48u; +const uint GI_COUNTER_CONTRACT_INVALID_TARGET_PDF = 49u; layout(push_constant, std430) uniform PushConstants { uint width; @@ -94,6 +96,16 @@ layout(push_constant, std430) uniform PushConstants { const uint GI_ACTIVE_TILE_SIZE = 16u; shared uint s_activeTileBits[4]; +void record_prod_contract_violation(ProdRestirGiReservoir reservoir) { + if ((prod_unpack_flags(reservoir) & PROD_FLAG_VALID) == 0u) return; + if (prod_contract_invalid_source_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_SOURCE_PDF], 1u); + } + if (prod_contract_invalid_target_pdf(reservoir)) { + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_CONTRACT_INVALID_TARGET_PDF], 1u); + } +} + uint active_tile_index(ivec2 fullPixel) { uint tileColumns = (pc.width + GI_ACTIVE_TILE_SIZE - 1u) / GI_ACTIVE_TILE_SIZE; return uint(fullPixel.y) / GI_ACTIVE_TILE_SIZE * tileColumns + @@ -101,6 +113,9 @@ uint active_tile_index(ivec2 fullPixel) { } void mark_active_tile_workgroup(ivec2 fullPixel, bool activeCandidate) { + if (!GI_ACTIVE_TILE_MASK_ENABLED) { + return; + } if (gl_LocalInvocationIndex < 4u) { s_activeTileBits[gl_LocalInvocationIndex] = 0u; } @@ -294,6 +309,7 @@ void main() { GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_TEMPORAL_PIXELS], 1u); current = convert_current(current_reservoirs[sourceIndex], receiver, sourceIndex); currentValid = prod_reservoir_valid(current); + if (!currentValid) record_prod_contract_violation(current); currentActive = currentValid && restir_gi_receiver_valid_for_reuse(receiver); } mark_active_tile_workgroup(sourcePixel, currentActive); @@ -330,8 +346,13 @@ void main() { ? uint(historyFull.y / 2) * outWidth + uint(historyFull.x / 2) : uint(historyFull.y) * outWidth + uint(historyFull.x); ProdRestirGiReservoir previous = previous_reservoirs[historyIndex]; - if (!prod_reservoir_valid(previous) || - (prod_unpack_flags(previous) & PROD_FLAG_FINALIZED) == 0u || + if (!prod_reservoir_valid(previous)) { + record_prod_contract_violation(previous); + GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_TEMPORAL_REJECT_AGE], 1u); + store_temporal_reservoir(outputIndex, current); + return; + } + if ((prod_unpack_flags(previous) & PROD_FLAG_FINALIZED) == 0u || prod_unpack_age(previous) >= min(pc.temporalMaxAge, 255u)) { GI_COUNTER_ADD(restir_gi_counters[GI_COUNTER_TEMPORAL_REJECT_AGE], 1u); store_temporal_reservoir(outputIndex, current); diff --git a/shaders/rt_common.glsl b/shaders/rt_common.glsl index ce06486..033ecc7 100644 --- a/shaders/rt_common.glsl +++ b/shaders/rt_common.glsl @@ -2,6 +2,10 @@ #extension GL_EXT_nonuniform_qualifier : enable #extension GL_GOOGLE_include_directive : require +#ifndef RTV_BEAUTY_OUTPUT_ONLY +#define RTV_BEAUTY_OUTPUT_ONLY 0 +#endif + #include "atmosphere_phase.glsl" #define RTV_STBN_TEXTURE_ENABLED 1 #include "blue_noise.glsl" @@ -11,6 +15,15 @@ #endif #include "rt_resources.glsl" + +uint renderer_debug_view() { +#if RTV_BEAUTY_OUTPUT_ONLY + return 0u; +#else + return debug_params.view; +#endif +} + #include "rt_material.glsl" #include "rt_environment.glsl" #include "rt_scene_lighting.glsl" diff --git a/shaders/rt_material.glsl b/shaders/rt_material.glsl index 6934ff3..2cabb18 100644 --- a/shaders/rt_material.glsl +++ b/shaders/rt_material.glsl @@ -1,6 +1,10 @@ #ifndef RTV_RT_MATERIAL_GLSL #define RTV_RT_MATERIAL_GLSL +#ifndef RTV_MATERIAL_TEXTURES_ENABLED +#define RTV_MATERIAL_TEXTURES_ENABLED 1 +#endif + // Material records, texture evaluation, alpha handling, and terminal material sampling. struct Material { uint material_data_index; @@ -222,6 +226,7 @@ const uint MATERIAL_FLAG_MANUAL_EMISSIVE_SRGB = 1u << 1u; const uint MATERIAL_FLAG_NORMAL_MAP_DIRECTX = 1u << 2u; const uint MATERIAL_FLAG_SPECULAR_GLOSSINESS_WORKFLOW = 1u << 3u; const uint MATERIAL_FLAG_SPECULAR_ALPHA_GLOSSINESS = 1u << 4u; +const uint MATERIAL_FLAG_MAY_BE_TRANSMISSIVE = 1u << 5u; const uint ALPHA_MODE_OPAQUE = 0u; const uint ALPHA_MODE_MASK = 1u; const uint ALPHA_MODE_BLEND = 2u; @@ -717,13 +722,13 @@ uvec2 pack_world_position(vec3 world_pos) { } bool project_unjittered_to_pixels_checked(mat4 viewProj, vec2 jitterPixels, vec3 worldPos, ivec2 dims, out vec2 pixels) { + // PrevCamera matrices are uploaded without projection jitter; the jitter argument + // is retained for older call sites but must not be applied here. vec4 clip = viewProj * vec4(worldPos, 1.0); if (clip.w <= 0.0) { pixels = vec2(0.0); return false; } - vec2 invDims = 1.0 / vec2(max(dims, ivec2(1))); - clip.xy -= jitterPixels * 2.0 * invDims * clip.w; vec3 ndc = clip.xyz / clip.w; if (ndc.z < 0.0 || ndc.z > 1.0001) { pixels = vec2(0.0); @@ -846,7 +851,9 @@ Material decode_material(uint mat_idx) { vec4 d0 = mesh_materials[idx + 0u]; vec4 d1 = mesh_materials[idx + 1u]; vec4 d2 = mesh_materials[idx + 2u]; +#if RTV_MATERIAL_TEXTURES_ENABLED vec4 d3 = mesh_materials[idx + 3u]; +#endif vec4 d4 = mesh_materials[idx + 4u]; vec4 d5 = mesh_materials[idx + 5u]; vec4 d6 = mesh_materials[idx + 6u]; @@ -860,7 +867,9 @@ Material decode_material(uint mat_idx) { vec4 d14 = mesh_materials[idx + 14u]; vec4 d15 = mesh_materials[idx + 15u]; vec4 d16 = mesh_materials[idx + 16u]; +#if RTV_MATERIAL_TEXTURES_ENABLED vec4 d17 = mesh_materials[idx + 17u]; +#endif Material m; m.material_data_index = idx; m.color = d0.xyz; @@ -871,10 +880,17 @@ Material decode_material(uint mat_idx) { m.pad2 = d1.w; m.emissive = d2.xyz; m.alpha_factor = d2.w; +#if RTV_MATERIAL_TEXTURES_ENABLED m.base_color_texture = int(round(d3.x)); m.normal_texture = int(round(d3.y)); m.metallic_roughness_texture = int(round(d3.z)); m.emissive_texture = int(round(d3.w)); +#else + m.base_color_texture = -1; + m.normal_texture = -1; + m.metallic_roughness_texture = -1; + m.emissive_texture = -1; +#endif m.alpha_cutoff = d4.x; m.alpha_mode = uint(round(d4.y)); m.double_sided = uint(round(d4.z)); @@ -914,14 +930,26 @@ Material decode_material(uint mat_idx) { m.volume_attenuation_color = clamp(d15.xyz, vec3(0.0), vec3(1.0)); m.volume_attenuation_distance = d15.w; m.dispersion_factor = max(d16.x, 0.0); +#if RTV_MATERIAL_TEXTURES_ENABLED m.opacity_texture = int(round(d17.x)); m.height_texture = int(round(d17.y)); m.height_scale = d17.z; +#else + m.opacity_texture = -1; + m.height_texture = -1; + m.height_scale = 0.0; +#endif m.clearcoat_normal = vec3(0.0); m.clearcoat_normal_variance = 0.0; return m; } +bool material_static_may_be_transmissive(uint mat_idx) { + uint idx = min(mat_idx, max(mesh_params.material_count, 1u) - 1u) * MATERIAL_STRIDE; + vec4 d1 = mesh_materials[idx + 1u]; + return (uint(round(d1.w)) & MATERIAL_FLAG_MAY_BE_TRANSMISSIVE) != 0u; +} + uint material_for_triangle_index(uint triangleIndex) { uint triangleCount = mesh_params.local_index_count / 3u; if (triangleIndex >= triangleCount) { @@ -980,6 +1008,26 @@ LocalVertex ray_tracing_local_vertex(uint meshIndex, uint vertexIndex) { return ray_tracing_local_vertex_with_binding(ray_tracing_gpu_skinning_binding(meshIndex), vertexIndex); } +bool ray_tracing_triangle_indices(uint triangleIndex, out uvec3 indices) { + indices = uvec3(0u); + if (triangleIndex > 0x55555555u) { + return false; + } + + uint triIndex = triangleIndex * 3u; + if (triIndex + 2u >= mesh_params.local_index_count) { + return false; + } + + indices = uvec3( + local_mesh_indices[triIndex + 0u], + local_mesh_indices[triIndex + 1u], + local_mesh_indices[triIndex + 2u]); + return indices.x < mesh_params.local_vertex_count && + indices.y < mesh_params.local_vertex_count && + indices.z < mesh_params.local_vertex_count; +} + bool ray_tracing_mesh_has_gpu_skinning(uint meshIndex) { return meshIndex < mesh_params.mesh_count && gpu_skinning_rt_mesh_bindings[meshIndex].z != 0u; } @@ -1015,7 +1063,9 @@ vec2 apply_material_texture_transform(Material material, uint slot, vec2 uv0, ve float g_material_texture_lod = 0.0; void set_material_texture_lod(float lod) { +#if RTV_MATERIAL_TEXTURES_ENABLED g_material_texture_lod = clamp(lod, 0.0, 4.0); +#endif } vec4 sample_material_texture(int textureIndex, vec2 uv) { @@ -1037,6 +1087,7 @@ vec2 apply_material_height_parallax(Material material, vec2 uv0, vec2 uv1, vec3 } void apply_material_textures(inout Material material, vec2 uv0, vec2 uv1) { +#if RTV_MATERIAL_TEXTURES_ENABLED uint flags = uint(round(material.pad2)); if (material.base_color_texture >= 0 && material.base_color_texture < MATERIAL_TEXTURE_LIMIT) { int textureIndex = material.base_color_texture; @@ -1148,6 +1199,7 @@ void apply_material_textures(inout Material material, vec2 uv0, vec2 uv1) { float opacity = sample_material_texture(textureIndex, uv0).r; material.alpha_factor *= clamp(opacity, 0.0, 1.0); } +#endif } vec3 material_volume_transmittance(Material material, float distance) { @@ -1170,6 +1222,7 @@ void apply_material_vertex_color(inout Material material, vec4 vertexColor) { } void apply_material_alpha_texture(inout Material material, vec2 uv0, vec2 uv1) { +#if RTV_MATERIAL_TEXTURES_ENABLED if (material.base_color_texture >= 0 && material.base_color_texture < MATERIAL_TEXTURE_LIMIT) { int textureIndex = material.base_color_texture; vec2 sampleUv = apply_material_texture_transform(material, MATERIAL_TEXTURE_TRANSFORM_BASE_COLOR, uv0, uv1); @@ -1181,6 +1234,7 @@ void apply_material_alpha_texture(inout Material material, vec2 uv0, vec2 uv1) { float opacity = sample_material_texture(textureIndex, uv0).r; material.alpha_factor *= clamp(opacity, 0.0, 1.0); } +#endif } bool accept_material_alpha(Material material) { @@ -1259,7 +1313,7 @@ float material_specular_roughness(Material material) { } Material apply_debug_material_mode(Material material) { - if (debug_params.view == 22u || debug_params.view == 27u) { + if (renderer_debug_view() == 22u || renderer_debug_view() == 27u) { material.color = vec3(0.72, 0.70, 0.66); material.roughness = 0.85; material.metallic = 0.0; diff --git a/shaders/rt_resources.glsl b/shaders/rt_resources.glsl index 0c295be..957760f 100644 --- a/shaders/rt_resources.glsl +++ b/shaders/rt_resources.glsl @@ -1,6 +1,8 @@ #ifndef RTV_RT_RESOURCES_GLSL #define RTV_RT_RESOURCES_GLSL +#include "psr_guides.glsl" + // Renderer-wide descriptors and scene resource records. Descriptor bindings are ABI-stable. layout(set = 0, binding = 0, std430) buffer AccumulationBuffer { vec4 accumulation_buffer[]; }; @@ -274,6 +276,8 @@ layout(set = 0, binding = 66, std140) uniform ReGIRParamsBlock { struct ReGIRReservoir { uvec4 metadata; vec4 sample_position_weight; + vec4 proposal_pdf_m; + uvec4 light_identity; }; layout(set = 0, binding = 67, std430) readonly buffer ReGIRReservoirBuffer { ReGIRReservoir regir_reservoirs[]; diff --git a/shaders/rt_restir_di_bindings.glsl b/shaders/rt_restir_di_bindings.glsl index 967beb1..bb3f60a 100644 --- a/shaders/rt_restir_di_bindings.glsl +++ b/shaders/rt_restir_di_bindings.glsl @@ -24,10 +24,14 @@ layout(set = 0, binding = 61, std140) uniform RestirDiRaygenParams { float lumClampNeighborMaxFactor; float fireflyClamp; float productionClampLuminance; - uint useFallbackInitial; + uint mode; uint spatialResultValid; - uint lightVersion; - uint distributionVersion; + uint visibilityRayBudget; + uint historyValid; + uint materialVisibilityFlags; + uint counterEnabled; + uint rawOutputIsCurrentSample; + float shadowDistanceBias; } restir_di_raygen_params; diff --git a/shaders/rtxdi/rtxdi_contract_smoke.hlsl b/shaders/rtxdi/rtxdi_contract_smoke.hlsl new file mode 100644 index 0000000..649accb --- /dev/null +++ b/shaders/rtxdi/rtxdi_contract_smoke.hlsl @@ -0,0 +1,21 @@ +#include + +[[vk::binding(0, 0)]] RWStructuredBuffer outputBuffer; + +[numthreads(1, 1, 1)] +void main(uint3 dispatchThreadId : SV_DispatchThreadID) +{ + RTXDI_DIReservoir reservoir = RTXDI_EmptyDIReservoir(); + RTXDI_DIReservoir candidate = RTXDI_EmptyDIReservoir(); + candidate.lightData = RTXDI_DIReservoir_LightValidBit; + candidate.targetPdf = 1.0; + candidate.weightSum = 1.0; + candidate.M = 1.0; + RTXDI_CombineDIReservoirs(reservoir, candidate, 0.5, 1.0); + RTXDI_FinalizeResampling(reservoir, 1.0, max(reservoir.M, 1.0)); + outputBuffer[dispatchThreadId.x] = float4( + reservoir.weightSum, + reservoir.targetPdf, + reservoir.M, + RTXDI_IsValidDIReservoir(reservoir) ? 1.0 : 0.0); +} diff --git a/shaders/taa.comp b/shaders/taa.comp index fc56792..bcbaa0b 100644 --- a/shaders/taa.comp +++ b/shaders/taa.comp @@ -43,24 +43,59 @@ layout(set = 0, binding = 5, std140) uniform TaaParams { uint render_height; float motion_feedback; float reactive_feedback; + float input_pixel_offset_x; + float input_pixel_offset_y; + float clamping_factor; + float max_radiance; } params; float luminance(vec3 color) { return dot(color, vec3(0.2126, 0.7152, 0.0722)); } -vec3 load_history_bilinear(vec2 historyPos, ivec2 dims) { - ivec2 base = ivec2(floor(historyPos)); - vec2 fracValue = fract(historyPos); - ivec2 p00 = clamp(base, ivec2(0), dims - ivec2(1)); - ivec2 p10 = clamp(base + ivec2(1, 0), ivec2(0), dims - ivec2(1)); - ivec2 p01 = clamp(base + ivec2(0, 1), ivec2(0), dims - ivec2(1)); - ivec2 p11 = clamp(base + ivec2(1, 1), ivec2(0), dims - ivec2(1)); - vec3 c00 = imageLoad(history_color, p00).rgb; - vec3 c10 = imageLoad(history_color, p10).rgb; - vec3 c01 = imageLoad(history_color, p01).rgb; - vec3 c11 = imageLoad(history_color, p11).rgb; - return mix(mix(c00, c10, fracValue.x), mix(c01, c11, fracValue.x), fracValue.y); +const float PQ_M1 = 0.1593017578125; +const float PQ_M2 = 78.84375; +const float PQ_C1 = 0.8359375; +const float PQ_C2 = 18.8515625; +const float PQ_C3 = 18.6875; + +vec3 pq_encode(vec3 color) { + vec3 l = max(color, vec3(0.0)) / max(params.max_radiance, 1.0e-4); + vec3 lm = pow(l, vec3(PQ_M1)); + vec3 n = (vec3(PQ_C1) + PQ_C2 * lm) / (vec3(1.0) + PQ_C3 * lm); + return clamp(pow(n, vec3(PQ_M2)), vec3(0.0), vec3(1.0)); +} + +vec3 pq_decode(vec3 color) { + vec3 np = pow(max(color, vec3(0.0)), vec3(1.0 / PQ_M2)); + vec3 l = max(np - vec3(PQ_C1), vec3(0.0)) / max(vec3(PQ_C2) - PQ_C3 * np, vec3(1.0e-6)); + return pow(max(l, vec3(0.0)), vec3(1.0 / PQ_M1)) * max(params.max_radiance, 1.0e-4); +} + +vec3 load_history_texel(ivec2 coords, ivec2 dims) { + return imageLoad(history_color, clamp(coords, ivec2(0), dims - ivec2(1))).rgb; +} + +vec3 load_history_catmull_rom(vec2 historyPos, ivec2 dims) { + vec2 tc = floor(historyPos - vec2(0.5)) + vec2(0.5); + vec2 f = clamp(historyPos - tc, vec2(0.0), vec2(1.0)); + vec2 f2 = f * f; + vec2 f3 = f2 * f; + vec2 w0 = f2 - 0.5 * (f3 + f); + vec2 w1 = 1.5 * f3 - 2.5 * f2 + vec2(1.0); + vec2 w3 = 0.5 * (f3 - f2); + vec2 w2 = vec2(1.0) - w0 - w1 - w3; + vec2 base = tc - vec2(1.0); + + vec3 result = vec3(0.0); + for (int y = 0; y < 4; ++y) { + float wy = y == 0 ? w0.y : (y == 1 ? w1.y : (y == 2 ? w2.y : w3.y)); + for (int x = 0; x < 4; ++x) { + float wx = x == 0 ? w0.x : (x == 1 ? w1.x : (x == 2 ? w2.x : w3.x)); + result += load_history_texel(ivec2(base) + ivec2(x, y), dims) * (wx * wy); + } + } + return max(result, vec3(0.0)); } vec3 decode_octahedral_normal(uint packedValue) { @@ -88,55 +123,42 @@ bool geometry_history_valid(ivec2 coords, ivec2 dims, float centerDepth, vec3 ce if (centerDepth >= 9999.0) { return true; } - const ivec2 offsets[4] = ivec2[4](ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1)); - float rejection = 0.0; - for (int i = 0; i < 4; ++i) { - ivec2 p = clamp(coords + offsets[i], ivec2(0), dims - ivec2(1)); - vec4 sampleDn = decode_depth_normal(depth_normal_buffer[uint(p.y) * uint(dims.x) + uint(p.x)]); - if (sampleDn.x >= 9999.0) { - rejection = max(rejection, 1.0); - continue; - } - float relativeDepth = abs(sampleDn.x - centerDepth) / max(centerDepth, 0.01); - float normalDelta = 1.0 - clamp(dot(sampleDn.yzw, centerNormal), -1.0, 1.0); - rejection = max(rejection, relativeDepth * 5.0 + normalDelta * 2.0); - } - return rejection < 1.0; + float normalLengthSq = dot(centerNormal, centerNormal); + return centerDepth > 0.0 && + centerDepth < 1.0e9 && + normalLengthSq > 0.25 && + normalLengthSq < 1.25; } -void neighborhood_stats(ivec2 coords, ivec2 dims, vec3 center, out vec3 minColor, out vec3 maxColor, out float meanLum, out float sigmaLum) { - minColor = center; - maxColor = center; +void neighborhood_stats_pq(ivec2 coords, ivec2 dims, out vec3 meanColor, out vec3 sigmaColor, out float meanLum, out float sigmaLum) { + vec3 colorSum = vec3(0.0); + vec3 colorSqSum = vec3(0.0); float lumSum = 0.0; float lumSqSum = 0.0; float sampleCount = 0.0; for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { ivec2 sample_coords = clamp(coords + ivec2(x, y), ivec2(0), dims - ivec2(1)); - vec3 sample_color = texelFetch(sampler2D(input_color, input_sampler), sample_coords, 0).rgb; - float sampleLum = luminance(sample_color); - minColor = min(minColor, sample_color); - maxColor = max(maxColor, sample_color); + vec3 sample_linear = texelFetch(sampler2D(input_color, input_sampler), sample_coords, 0).rgb; + vec3 sample_color = pq_encode(sample_linear); + float sampleLum = luminance(sample_linear); + colorSum += sample_color; + colorSqSum += sample_color * sample_color; lumSum += sampleLum; lumSqSum += sampleLum * sampleLum; sampleCount += 1.0; } } - meanLum = lumSum / max(sampleCount, 1.0); - sigmaLum = sqrt(max(lumSqSum / max(sampleCount, 1.0) - meanLum * meanLum, 0.0)); -} - -vec3 clamp_history(vec3 history, vec3 minColor, vec3 maxColor) { - vec3 p_clip = 0.5 * (maxColor + minColor); - vec3 e_clip = 0.5 * (maxColor - minColor) + vec3(0.001); - vec3 v_clip = history - p_clip; - vec3 v_unit = v_clip / e_clip; - vec3 a_unit = abs(v_unit); - float ma_unit = max(a_unit.x, max(a_unit.y, a_unit.z)); - if (ma_unit > 1.0) { - return p_clip + v_clip / ma_unit; - } - return history; + float invCount = 1.0 / max(sampleCount, 1.0); + meanColor = colorSum * invCount; + sigmaColor = sqrt(max(colorSqSum * invCount - meanColor * meanColor, vec3(0.0))); + meanLum = lumSum * invCount; + sigmaLum = sqrt(max(lumSqSum * invCount - meanLum * meanLum, 0.0)); +} + +vec3 clamp_history_pq(vec3 history, vec3 meanColor, vec3 sigmaColor) { + vec3 sigma = max(sigmaColor * max(params.clamping_factor, 0.0), vec3(0.001)); + return clamp(history, meanColor - sigma, meanColor + sigma); } vec3 sharpen_resolved_color(vec3 center, vec3 up, vec3 down, vec3 left, vec3 right, float amount) { @@ -155,10 +177,42 @@ vec3 sample_current_display_color(ivec2 coords, ivec2 displayDims) { return texture(sampler2D(input_color, input_sampler), uv).rgb; } +vec2 output_to_input(ivec2 coords, ivec2 displayDims, ivec2 renderDims) { + return (vec2(coords) + vec2(0.5)) * (vec2(renderDims) / vec2(displayDims)) - + vec2(0.5) + + vec2(params.input_pixel_offset_x, params.input_pixel_offset_y); +} + +vec2 longest_valid_display_velocity(ivec2 centerCoords, ivec2 renderDims, vec2 displayScale, out bool centerVelocityValid) { + centerVelocityValid = false; + vec2 selectedVelocity = vec2(0.0); + float selectedLength = -1.0; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + ivec2 p = clamp(centerCoords + ivec2(x, y), ivec2(0), renderDims - ivec2(1)); + uint index = uint(p.y) * uint(renderDims.x) + uint(p.x); + vec2 velocityPixels = temporal_unpack_velocity_pixels(velocity_buffer[index], params.velocity_scale); + bool valid = !temporal_velocity_pixels_are_invalid(velocityPixels, params.velocity_scale); + if (x == 0 && y == 0) { + centerVelocityValid = valid; + } + if (!valid) { + continue; + } + float lenSq = dot(velocityPixels, velocityPixels); + if (lenSq > selectedLength) { + selectedLength = lenSq; + selectedVelocity = velocityPixels * displayScale; + } + } + } + return selectedVelocity; +} + vec3 resolve_taa_pixel(ivec2 coords, ivec2 displayDims, ivec2 renderDims, out float sharpenAmount) { - vec2 uv = (vec2(coords) + vec2(0.5)) / vec2(displayDims); - vec3 current = sample_current_display_color(coords, displayDims); - ivec2 renderCoords = clamp(ivec2(floor(uv * vec2(renderDims))), ivec2(0), renderDims - ivec2(1)); + vec2 inputPos = output_to_input(coords, displayDims, renderDims); + ivec2 renderCoords = clamp(ivec2(round(inputPos)), ivec2(0), renderDims - ivec2(1)); + vec3 current = texelFetch(sampler2D(input_color, input_sampler), renderCoords, 0).rgb; uint pixel_index = uint(renderCoords.y) * uint(renderDims.x) + uint(renderCoords.x); uint instance_id = entity_id_buffer[pixel_index]; bool streaming_temporal_reset = @@ -167,16 +221,23 @@ vec3 resolve_taa_pixel(ivec2 coords, ivec2 displayDims, ivec2 renderDims, out fl (streaming_reset_instance_masks[instance_id] & 1u) != 0u; vec4 centerDn = decode_depth_normal(depth_normal_buffer[pixel_index]); PathDataRecord pathData = path_data_buffer[pixel_index]; - float specularLum = luminance(pathData.direct_specular.rgb + pathData.indirect_specular.rgb); - float diffuseLum = luminance(pathData.direct_diffuse.rgb + pathData.indirect_diffuse.rgb + pathData.emissive_residual.rgb); + vec4 directSpecular = pathData.direct_specular; + vec4 indirectSpecular = pathData.indirect_specular; + vec4 directDiffuse = pathData.direct_diffuse; + vec4 indirectDiffuse = pathData.indirect_diffuse; + vec4 emissiveResidual = pathData.emissive_residual; + float specularLum = luminance(directSpecular.rgb + indirectSpecular.rgb); + float diffuseLum = luminance(directDiffuse.rgb + indirectDiffuse.rgb + emissiveResidual.rgb); float specularSignal = clamp(specularLum / max(specularLum + diffuseLum, 1.0e-4), 0.0, 1.0); - vec2 velocity_pixels = temporal_unpack_velocity_pixels(velocity_buffer[pixel_index], params.velocity_scale); - vec2 displayVelocityPixels = velocity_pixels * (vec2(displayDims) / max(vec2(renderDims), vec2(1.0))); + vec2 displayScale = vec2(displayDims) / max(vec2(renderDims), vec2(1.0)); + bool centerVelocityValid = false; + vec2 displayVelocityPixels = longest_valid_display_velocity(renderCoords, renderDims, displayScale, centerVelocityValid); vec2 history_pos = temporal_reproject_pixel(coords, displayVelocityPixels); bool history_valid = params.enabled != 0u && params.reset_history == 0u && !streaming_temporal_reset && + centerVelocityValid && params.history_valid != 0u && temporal_history_pixel_valid(history_pos, displayDims, 0.5) && geometry_history_valid(renderCoords, renderDims, centerDn.x, centerDn.yzw); @@ -184,39 +245,47 @@ vec3 resolve_taa_pixel(ivec2 coords, ivec2 displayDims, ivec2 renderDims, out fl vec3 output_value = current; sharpenAmount = params.camera_moving != 0u ? 0.0 : params.sharpen_strength * 0.35; if (history_valid) { - vec3 minColor; - vec3 maxColor; + vec3 meanColorPq; + vec3 sigmaColorPq; float meanLum; float sigmaLum; - neighborhood_stats(renderCoords, renderDims, current, minColor, maxColor, meanLum, sigmaLum); + neighborhood_stats_pq(renderCoords, renderDims, meanColorPq, sigmaColorPq, meanLum, sigmaLum); - vec3 historyRaw = load_history_bilinear(history_pos, displayDims); - vec3 history = clamp_history(historyRaw, minColor, maxColor); + vec3 currentPq = pq_encode(current); + vec3 historyRaw = load_history_catmull_rom(history_pos + vec2(0.5), displayDims); + vec3 historyRawPq = pq_encode(historyRaw); + vec3 historyPq = clamp_history_pq(historyRawPq, meanColorPq, sigmaColorPq); + vec3 history = pq_decode(historyPq); float currentLum = luminance(current); float historyLum = luminance(historyRaw); float clippedHistoryLum = luminance(history); float lumDelta = abs(currentLum - historyLum) / (max(currentLum, historyLum) + 0.02); float clipDelta = abs(historyLum - clippedHistoryLum) / (historyLum + 0.02); float neighborhoodDelta = abs(historyLum - meanLum) / (meanLum + sigmaLum + 0.02); - float motionConfidence = temporal_motion_confidence(displayVelocityPixels, 18.0, 0.12); - float motion = 1.0 - motionConfidence; + float motionWeight = smoothstep(0.0, 1.0, length(displayVelocityPixels)); + float colorReactive = temporal_reactive_weight(lumDelta, clipDelta, neighborhoodDelta); + float staticReactiveDamping = mix(0.35, 1.0, motionWeight); float reactive = max( - temporal_reactive_weight(lumDelta, clipDelta, neighborhoodDelta), - clamp(pathData.emissive_residual.a, 0.0, 1.0)); + colorReactive * staticReactiveDamping, + clamp(emissiveResidual.a, 0.0, 1.0)); + vec2 distanceToLowResPixel = inputPos - vec2(renderCoords); + float upscalingFactor = displayScale.x; + float sampleWeight = clamp(1.0 - upscalingFactor * dot(distanceToLowResPixel, distanceToLowResPixel), 0.0, 1.0); + float blendWeight = clamp(max(motionWeight, sampleWeight) * params.feedback, 0.0, 1.0); + float motion = motionWeight; float specularReactive = smoothstep(0.35, 0.85, specularSignal) * max(motion, reactive); + float adaptiveReactive = max(reactive, specularReactive); + float reactiveCurrentWeight = adaptiveReactive * params.reactive_feedback; + reactiveCurrentWeight = min(reactiveCurrentWeight, mix(0.18, params.reactive_feedback, motionWeight)); + blendWeight = clamp(max( + blendWeight, + max(motion * params.motion_feedback, + reactiveCurrentWeight)), 0.0, 1.0); sharpenAmount = params.camera_moving != 0u ? 0.0 - : params.sharpen_strength * (1.0 - clamp(max(motion, reactive), 0.0, 1.0) * 0.65); - - if (reactive >= 0.82 || lumDelta >= 0.85 || clipDelta >= 0.75 || specularReactive >= 0.72) { - float rejectAlpha = params.camera_moving != 0u ? params.reactive_feedback : 0.98; - output_value = mix(history, current, rejectAlpha); - } else { - float motionTarget = params.camera_moving != 0u ? params.motion_feedback : 0.95; - float alpha = mix(params.feedback, motionTarget, max(motion, reactive)); - alpha = max(alpha, mix(0.0, 0.22, specularReactive)); - output_value = mix(history, current, clamp(alpha, 0.06, 0.98)); - } + : params.sharpen_strength * (1.0 - clamp(max(motion, adaptiveReactive), 0.0, 1.0) * 0.65); + + output_value = pq_decode(mix(historyPq, currentPq, blendWeight)); } return output_value; } diff --git a/shaders/temporal_common.glsl b/shaders/temporal_common.glsl index 8b8843f..92ad4d1 100644 --- a/shaders/temporal_common.glsl +++ b/shaders/temporal_common.glsl @@ -50,7 +50,10 @@ float temporal_motion_confidence(vec2 velocityPixels, float fullRejectPixels, fl } float temporal_reactive_weight(float lumDelta, float clipDelta, float neighborhoodDelta) { - return clamp(max(max(lumDelta - 0.12, clipDelta), neighborhoodDelta - 1.8) * 2.5, 0.0, 1.0); + float luminanceReactive = lumDelta - 0.18; + float clipReactive = clipDelta - 0.02; + float neighborhoodReactive = neighborhoodDelta - 2.4; + return clamp(max(max(luminanceReactive, clipReactive), neighborhoodReactive) * 1.65, 0.0, 1.0); } float temporal_variance_confidence(float variance, float scale) { diff --git a/src/main.cpp b/src/main.cpp index 161fa6a..cdc0a4b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include "rtv/AnimationController.h" #include "rtv/AssetImport.h" #include "rtv/AssetRegistry.h" +#include "rtv/DescriptorWriteDiagnostics.h" #include "rtv/DiagnosticTools.h" #include "rtv/HeadlessDiagnostics.h" #include "rtv/NativeAssetMigration.h" @@ -14,6 +15,7 @@ #include "rtv/PathTracerRenderer.h" #include "rtv/Project.h" #include "rtv/RendererDebug.h" +#include "rtv/RendererCoreRegressionTests.h" #include "rtv/RendererSettings.h" #include "rtv/RenderGraphDump.h" #include "rtv/RenderGraph.h" @@ -27,12 +29,15 @@ #include "rtv/TextureLoader.h" #include "rtv/GpuSceneStreamingState.h" #include "rtv/IncrementalGpuSceneUpdateQueue.h" +#include "rtv/KnownCommandLineOptions.h" #include "rtv/GpuProfiler.h" +#include "rtv/GpuValidation.h" #include "rtv/GpuCrashDiagnostics.h" #include "rtv/NsightPerfMarkers.h" #include "rtv/GpuUploadTicket.h" #include "rtv/MainThreadApplyTicket.h" #include "rtv/TopologyRebuildTicket.h" +#include "rtv/VulkanContext.h" #include #include @@ -154,6 +159,73 @@ static void initRenderDoc() { namespace { +nlohmann::json backendComparisonPolicyJson() { + return { + {"schema_version", 1}, + {"policy_name", "RTXDI Q5D backend comparison policy"}, + {"promotion_gate", "scripts/backend_comparison_matrix.ps1"}, + {"current_safe_default", { + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"reason", "Engine denoiser plus TAA/TSR remains the safe default until Q5D evidence promotes another backend."}, + }}, + {"fallback_order", nlohmann::json::array({ + { + {"rank", 1}, + {"mode", "engine_taa"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Always available safe default."}, + }, + { + {"rank", 2}, + {"mode", "nrd_taa"}, + {"denoiser_backend", "nrd"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use only when direct NRD is available and the Q5D matrix passes quality/stability gates."}, + }, + { + {"rank", 3}, + {"mode", "engine_dlss"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use when DLSS is available/requested; fall back to TAA/TSR when DLSS is unavailable."}, + }, + { + {"rank", 4}, + {"mode", "nrd_dlss"}, + {"denoiser_backend", "nrd"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use only when NRD and DLSS are both available and Q5D evidence beats/ties lower-risk modes."}, + }, + { + {"rank", 5}, + {"mode", "dlss_rr"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", true}, + {"condition", "Opt-in only; requires DLSS RR availability plus valid depth, motion, disocclusion, ray-direction, hit-distance, and reflected-albedo guides."}, + }, + { + {"rank", 6}, + {"mode", "reference_no_temporal"}, + {"denoiser_backend", "off"}, + {"temporal_upscaler", "off"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Reference/diagnostic accumulation only; never a realtime fallback."}, + }, + })}, + {"promotion_rule", "Do not change defaults until the candidate wins or ties equal-time quality/stability, has valid guide/profile diagnostics, and records a rollback CLI setting."}, + {"failure_policy", "Backend-specific failures must be visible in profile JSON, validation logs, and debug guide exports; tone mapping, auto exposure, final-output clamps, or TAA history must not hide them."}, + {"cli_query", "--print-backend-policy"}, + }; +} + std::filesystem::path resolveProjectPath(const std::filesystem::path& root, const std::string& value) { if (value.empty()) { return {}; @@ -2861,8 +2933,16 @@ int cookAnimationControllerCommand( int main(int argc, char** argv) { try { + rtv::validateCommandLineArguments(argc, argv); const bool viewerExecutable = argc > 0 && std::filesystem::path(argv[0]).stem().string() == "rtviewer"; + for (int i = 1; i < argc; ++i) { + const std::string_view arg(argv[i]); + if (arg == "--print-backend-policy" || arg == "--backend-policy") { + std::cout << backendComparisonPolicyJson().dump(2) << '\n'; + return 0; + } + } uint32_t maxFrames = 0; rtv::RendererDebugView debugView = rtv::RendererDebugView::Beauty; bool debugViewProvided = false; @@ -2973,6 +3053,8 @@ int main(int argc, char** argv) { std::optional sppLimiterOverride; bool validationCameraMotion = false; bool validationObjectMotion = false; + bool validationLightReorder = false; + bool validationLightFlicker = false; bool rendererOnly = viewerExecutable; uint32_t captureReadyAfterFrames = 60; bool captureReadyLog = false; @@ -2994,6 +3076,7 @@ int main(int argc, char** argv) { std::optional compareImageSequenceBaselinePath; std::optional compareImageSequenceCurrentPath; std::optional compareImageOutputPath; + rtv::ImageCompareThresholds compareImageThresholds; bool updateBaseline = false; bool checkBaseline = false; std::filesystem::path baselineRoot = "baselines"; @@ -3073,6 +3156,7 @@ int main(int argc, char** argv) { bool validateGpuLabels = false; bool shaderHotReloadReport = false; bool selfTestStreamingTransfer = false; + bool selfTestRendererCore = false; rtv::StreamingRuntimeOptions streamingOptions; std::optional dumpStreamingPath; std::optional streamingValidationScenePath; @@ -3564,7 +3648,13 @@ int main(int argc, char** argv) { } if (arg == "--restir-di" && i + 1 < argc) { - restirDiModeOverride = rtv::parseRestirDiMode(argv[++i]); + const std::string_view value(argv[++i]); + rtv::RestirDiMode parsedMode{}; + if (!rtv::tryParseRestirDiMode(value, parsedMode)) { + throw std::runtime_error("Invalid --restir-di value '" + std::string(value) + + "'. Expected off, legacy, production, reference-validation, or hybrid-compare."); + } + restirDiModeOverride = parsedMode; continue; } if (arg == "--restir-di-temporal" && i + 1 < argc) { @@ -3627,7 +3717,13 @@ int main(int argc, char** argv) { continue; } if ((arg == "--restir-gi" || arg == "--restir-gi-mode") && i + 1 < argc) { - restirGiModeOverride = rtv::parseRestirGiMode(argv[++i]); + const std::string_view value(argv[++i]); + rtv::RestirGiMode parsedMode{}; + if (!rtv::tryParseRestirGiMode(value, parsedMode)) { + throw std::runtime_error("Invalid " + std::string(arg) + " value '" + std::string(value) + + "'. Expected off, legacy-cache, production, or reference-validation."); + } + restirGiModeOverride = parsedMode; restirGiOverride = *restirGiModeOverride != rtv::RestirGiMode::Off; continue; } @@ -3834,10 +3930,14 @@ int main(int argc, char** argv) { } else if ((arg == "--spp-limit" || arg == "--limit-spp") && i + 1 < argc) { const std::string_view value(argv[++i]); sppLimiterOverride = !(value == "off" || value == "false" || value == "0"); - } else if (arg == "--validation-camera-motion") { - validationCameraMotion = true; - } else if (arg == "--validation-object-motion") { - validationObjectMotion = true; + } else if (arg == "--validation-camera-motion" || + arg == "--validation-object-motion" || + arg == "--validation-light-reorder" || + arg == "--validation-light-flicker") { + validationCameraMotion = validationCameraMotion || arg == "--validation-camera-motion"; + validationObjectMotion = validationObjectMotion || arg == "--validation-object-motion"; + validationLightReorder = validationLightReorder || arg == "--validation-light-reorder"; + validationLightFlicker = validationLightFlicker || arg == "--validation-light-flicker"; } else if (arg == "--headless") { diagConfig.headless = true; } else if (arg == "--warmup-frames" && i + 1 < argc) { @@ -3978,6 +4078,8 @@ int main(int argc, char** argv) { nativeStoreReleases.push_back(value); } else if (arg == "--native-store-unmount-package") { nativeStoreUnmountPackages.push_back(std::filesystem::path(value)); + } else { + throw std::runtime_error("Unknown command-line argument: " + std::string(arg)); } } else if (arg == "--load-native-runtime-assets" && i + 1 < argc) { loadNativeRuntimeAssetsPath = std::filesystem::path(argv[++i]); @@ -4015,16 +4117,27 @@ int main(int argc, char** argv) { } else if ((arg == "--native2b-terminal-direct-rate" || arg == "--native2b-terminal-direct-sample-probability") && i + 1 < argc) { native2BTerminalDirectSampleProbabilityOverride = std::stof(argv[++i]); + } else if (arg == "--compare-min-psnr" && i + 1 < argc) { + compareImageThresholds.minPsnr = std::stod(argv[++i]); + } else if (arg == "--compare-min-ssim" && i + 1 < argc) { + compareImageThresholds.minSsim = std::stod(argv[++i]); + } else if (arg == "--compare-max-changed-pixels" && i + 1 < argc) { + compareImageThresholds.maxChangedPixelPercentage = std::stod(argv[++i]); } } for (int i = 1; i < argc; ++i) { if (std::string(argv[i]) == "--selftest-streaming-transfer") { selfTestStreamingTransfer = true; - break; + } else if (std::string(argv[i]) == "--selftest-renderer-core") { + selfTestRendererCore = true; } } + if (selfTestRendererCore) { + return rtv::runRendererCoreRegressionTests(std::cout); + } + if (compareProfileOldPath.has_value() || compareProfileNewPath.has_value()) { if (!compareProfileOldPath.has_value() || !compareProfileNewPath.has_value()) { throw std::runtime_error("--compare-profile requires old.json and new.json"); @@ -4035,7 +4148,11 @@ int main(int argc, char** argv) { if (!compareImageBaselinePath.has_value() || !compareImageCurrentPath.has_value()) { throw std::runtime_error("--compare-image requires baseline.png and current.png"); } - return rtv::compareImageCommand(*compareImageBaselinePath, *compareImageCurrentPath, compareImageOutputPath); + return rtv::compareImageCommand( + *compareImageBaselinePath, + *compareImageCurrentPath, + compareImageOutputPath, + compareImageThresholds); } if (compareImageSequenceBaselinePath.has_value() || compareImageSequenceCurrentPath.has_value()) { if (!compareImageSequenceBaselinePath.has_value() || !compareImageSequenceCurrentPath.has_value()) { @@ -4302,6 +4419,16 @@ int main(int argc, char** argv) { diagConfig.fixedSeed = *frameIndex; } + const bool needsManualBarrierDiagnostics = + needsProfile || + needsRenderGraph || + dumpMemoryPath.has_value() || + dumpFrameTimelinePath.has_value() || + dumpResourceLifetimesPath.has_value() || + dumpBindingsPath.has_value() || + dumpShaderReportPath.has_value() || + validateGpuLabels; + if (nativePackageScenePath.has_value() && (scenePath.has_value() || gltfPath.has_value())) { throw std::runtime_error("--native-package-scene is mutually exclusive with --scene and --gltf"); } @@ -4328,6 +4455,22 @@ int main(int argc, char** argv) { throw std::runtime_error("--descriptor-lifetime-stress requires --headless"); } + if (restirModeOverride.has_value() && !restirDiModeOverride.has_value()) { + switch (*restirModeOverride) { + case rtv::RestirMode::ClassicNee: + restirDiModeOverride = rtv::RestirDiMode::Off; + break; + case rtv::RestirMode::RestirOnly: + restirDiModeOverride = rtv::RestirDiMode::Production; + break; + case rtv::RestirMode::HybridCompare: + restirDiModeOverride = rtv::RestirDiMode::HybridCompare; + break; + } + } + const std::optional startupRestirModeOverride = + restirDiModeOverride.has_value() ? std::nullopt : restirModeOverride; + #ifdef RTV_HAS_RENDERDOC if (diagConfig.captureRenderDocPath.has_value()) { rdocCaptureRequested = true; @@ -4347,14 +4490,23 @@ int main(int argc, char** argv) { (void)gpuCrashDiagnostics.enable(gpuCrashDumpDir); } + if (dumpBindingsPath.has_value()) { + rtv::resetDescriptorWriteDiagnostics(); + } + rtv::setManualBarrierEscapeDiagnosticsEnabled(needsManualBarrierDiagnostics); + if (needsManualBarrierDiagnostics) { + rtv::resetManualBarrierEscapeDiagnostics(); + } + rtv::Application app(debugView, gltfPath, hdrPath, scenePath, nativePackageScenePath, nativePackageAnimationSelection, - denoiserOverride, restirModeOverride, renderPresetOverride, restirGiOverride, + denoiserOverride, startupRestirModeOverride, renderPresetOverride, restirGiOverride, opacityMicromapOverride, opacityMicromapBlendOverride, hardwareBackfaceCullingOverride, opacityMicromapSubdivisionOverride, - debugViewProvided, validationCameraMotion, validationObjectMotion, + debugViewProvided, validationCameraMotion, validationObjectMotion, validationLightReorder, + validationLightFlicker, diagConfig.headless, rendererOnly ? rtv::ApplicationMode::RendererOnly : rtv::ApplicationMode::Editor, diagConfig.headlessWidth, @@ -4621,7 +4773,8 @@ int main(int argc, char** argv) { << nvidiaStatus.dlssFrameGenerationUnavailableReason << ".\n"; } } - if (restirDiModeOverride.has_value() || + if (restirModeOverride.has_value() || + restirDiModeOverride.has_value() || restirDiTemporalOverride.has_value() || restirDiSpatialOverride.has_value() || restirDiFinalVisibilityOverride.has_value() || @@ -4636,6 +4789,9 @@ int main(int argc, char** argv) { restirDiStabilizationOverride.has_value() || restirDiReservoirLayoutOverride.has_value()) { rtv::RendererSettings settings = renderer->settings(); + if (restirModeOverride.has_value()) { + settings.restirMode = *restirModeOverride; + } if (restirDiModeOverride.has_value()) { settings.restirDiMode = *restirDiModeOverride; } @@ -4701,6 +4857,11 @@ int main(int argc, char** argv) { settings.restirDiFinalVisibilityEnabled = true; settings.restirDiProductionStabilizationEnabled = false; } + if (settings.restirDiMode == rtv::RestirDiMode::Production || + settings.restirDiMode == rtv::RestirDiMode::HybridCompare) { + settings.restirDiFinalVisibilityEnabled = true; + settings.restirDiVisibilityRayBudget = std::max(settings.restirDiVisibilityRayBudget, 1u); + } settings.renderPreset = rtv::RenderPreset::Custom; renderer->applySettings(settings); } @@ -5046,15 +5207,29 @@ int main(int argc, char** argv) { return 1; } std::string selfTestError; - const bool ok = executor.runSelfTest(selfTestError); + const bool functionalPass = executor.runSelfTest(selfTestError); + const auto selfTestStats = executor.stats(); + executor.shutdown(); + const bool validationEnabled = context->validationEnabled(); + const uint64_t validationErrors = context->validationErrorCount(); + const uint64_t validationWarnings = context->validationWarningCount(); + const bool validationPass = validationEnabled && validationErrors == 0u; + const bool ok = functionalPass && validationPass; nlohmann::json report; report["self_test"] = "streaming_gpu_transfer_executor"; report["passed"] = ok; - if (!ok) { + report["functional_passed"] = functionalPass; + report["validation_enabled"] = validationEnabled; + report["validation_error_count"] = validationErrors; + report["validation_warning_count"] = validationWarnings; + if (!functionalPass) { report["error"] = selfTestError; + } else if (!validationEnabled) { + report["error"] = "Vulkan validation was not enabled for the streaming self-test"; + } else if (!validationPass) { + report["error"] = "Vulkan validation reported errors during the streaming self-test"; } - report["stats"] = rtv::streamingGpuTransferExecutorStatsJson(executor.stats()); - executor.shutdown(); + report["stats"] = rtv::streamingGpuTransferExecutorStatsJson(selfTestStats); const std::string serialized = report.dump(2); if (inspectionJsonPath.has_value()) { std::ofstream out(*inspectionJsonPath); diff --git a/src/rtv/Application.cpp b/src/rtv/Application.cpp index 1447844..cf42d74 100644 --- a/src/rtv/Application.cpp +++ b/src/rtv/Application.cpp @@ -99,6 +99,25 @@ bool mainLoopTraceEnabled() { return enabled; } +bool editorTicketProbesEnabled() { + static const bool enabled = [] { +#if defined(_WIN32) + char* value = nullptr; + size_t length = 0; + if (_dupenv_s(&value, &length, "RTV_ENABLE_EDITOR_TICKET_PROBES") != 0 || value == nullptr) { + return false; + } + const bool result = value[0] != '\0' && value[0] != '0'; + std::free(value); + return result; +#else + const char* value = std::getenv("RTV_ENABLE_EDITOR_TICKET_PROBES"); + return value != nullptr && value[0] != '\0' && value[0] != '0'; +#endif + }(); + return enabled; +} + void traceMainLoopPhase(uint32_t frame, const char* phase) { if (!mainLoopTraceEnabled()) { return; @@ -1801,6 +1820,7 @@ void syncDocumentRenderSettings(SceneDocument& document, const RendererSettings& render.dlssFrameGenerationEnabled = settings.dlssFrameGenerationEnabled; render.dlssRayReconstructionEnabled = settings.dlssRayReconstructionEnabled; render.streamlineReflexEnabled = settings.streamlineReflexEnabled; + render.streamlineNvPerfEnabled = settings.streamlineNvPerfEnabled; render.dlssSharpeningStrength = settings.dlssSharpeningStrength; render.taaFeedback = settings.taaFeedback; render.taaMotionFeedback = settings.taaMotionFeedback; @@ -1948,6 +1968,7 @@ RendererSettings rendererSettingsFromDocument(const SceneDocument& document, Ren settings.dlssFrameGenerationEnabled = render.dlssFrameGenerationEnabled; settings.dlssRayReconstructionEnabled = render.dlssRayReconstructionEnabled; settings.streamlineReflexEnabled = render.streamlineReflexEnabled; + settings.streamlineNvPerfEnabled = render.streamlineNvPerfEnabled; settings.dlssSharpeningStrength = render.dlssSharpeningStrength; settings.taaFeedback = render.taaFeedback; settings.taaMotionFeedback = render.taaMotionFeedback; @@ -3951,6 +3972,8 @@ Application::Application( bool debugViewOverride, bool validationCameraMotion, bool validationObjectMotion, + bool validationLightReorder, + bool validationLightFlicker, bool headless, ApplicationMode mode, uint32_t headlessWidth, @@ -3976,6 +3999,8 @@ Application::Application( debugViewOverride_(debugViewOverride), validationCameraMotion_(validationCameraMotion), validationObjectMotion_(validationObjectMotion), + validationLightReorder_(validationLightReorder), + validationLightFlicker_(validationLightFlicker), disableAsyncCompute_(disableAsyncCompute), singleQueueFallback_(singleQueueFallback), disableResourceAliasing_(disableResourceAliasing), @@ -3990,22 +4015,24 @@ Application::Application( } initVulkan(); (void)initializeNsightGraphicsRuntime(); - frameWorkProbeJobId_ = frameWorkScheduler_.enqueue(FrameWorkJobDesc{ - .queue = FrameWorkQueue::MainThreadApply, - .title = "Editor main loop scheduler probe", - .status = "waiting for fence", - .estimatedCostMs = 0.0, - .estimatedUploadBytes = 0, - .callback = [](FrameWorkJobContext&) { - return FrameWorkJobStepResult{ - .complete = false, - .waitingForFence = true, - .progress = 0.25f, - }; - }, - }); - frameWorkProbeCompletionPending_ = frameWorkProbeJobId_ != 0; - initializeEditorTicketProbeQueues(); + if (editorTicketProbesEnabled()) { + frameWorkProbeJobId_ = frameWorkScheduler_.enqueue(FrameWorkJobDesc{ + .queue = FrameWorkQueue::MainThreadApply, + .title = "Editor main loop scheduler probe", + .status = "waiting for fence", + .estimatedCostMs = 0.0, + .estimatedUploadBytes = 0, + .callback = [](FrameWorkJobContext&) { + return FrameWorkJobStepResult{ + .complete = false, + .waitingForFence = true, + .progress = 0.25f, + }; + }, + }); + frameWorkProbeCompletionPending_ = frameWorkProbeJobId_ != 0; + initializeEditorTicketProbeQueues(); + } } Application::~Application() { @@ -4124,7 +4151,9 @@ void Application::runHeadless(uint32_t warmupFrames, uint32_t totalFrames) { stepStreamingGpuSceneUpdateQueue(); pollProgressiveRuntimeLoadJob(); applyValidationObjectMotion(nextDiagnosticFrameIndex_); - applyValidationCameraMotion(nextDiagnosticFrameIndex_++); + applyValidationCameraMotion(nextDiagnosticFrameIndex_); + applyValidationLightReorder(nextDiagnosticFrameIndex_); + applyValidationLightFlicker(nextDiagnosticFrameIndex_++); updateAnimationPlayers(deltaSeconds); if (beginFrameCapture_) { beginFrameCapture_(frameCount + 1u); @@ -4180,7 +4209,9 @@ void Application::renderFrames(uint32_t count) { stepStreamingGpuSceneUpdateQueue(); pollProgressiveRuntimeLoadJob(); applyValidationObjectMotion(nextDiagnosticFrameIndex_); - applyValidationCameraMotion(nextDiagnosticFrameIndex_++); + applyValidationCameraMotion(nextDiagnosticFrameIndex_); + applyValidationLightReorder(nextDiagnosticFrameIndex_); + applyValidationLightFlicker(nextDiagnosticFrameIndex_++); updateAnimationPlayers(deltaSeconds); if (beginFrameCapture_) { beginFrameCapture_(i + 1u); @@ -4545,7 +4576,8 @@ std::optional queryMeshBlasBuildSizing(VkDevice device, con VkAccelerationStructureBuildGeometryInfoKHR buildInfo{}; buildInfo.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_GEOMETRY_INFO_KHR; buildInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR; - buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR; + buildInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR | + VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR; buildInfo.geometryCount = 1; buildInfo.pGeometries = &geometry; @@ -4881,6 +4913,8 @@ nlohmann::json Application::streamingRuntimeReport() const { {"last_frame", incrementalGpuSceneApplyFrameResultJson(lastStreamingGpuSceneApply_)}, {"operations", incrementalGpuSceneUpdateSnapshotsJson(streamingGpuSceneUpdateQueue_.snapshots())}, }; + report["streaming_async_compute"] = + streamingAsyncComputeBudgeterStateJson(streamingAsyncComputeBudgeter_.state()); if (pathTracer_ != nullptr) { const PathTracerRenderer::StreamingResetMaskReport& resetMasks = pathTracer_->streamingResetMaskReport(); report["streaming_reset_masks"] = { @@ -6417,9 +6451,7 @@ void Application::processRendererOnlyRequests(const RendererOnlyRequests& reques cameraController_.setFastMoveSpeed(std::clamp(*requests.cameraFastMoveSpeed, 0.05f, 250.0f)); } if (requests.settings.has_value()) { - if (pathTracer_->applySettings(*requests.settings)) { - syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); - } + applyRendererSettingsSafely(*requests.settings, true); } if (requests.resetAccumulation.has_value()) { pathTracer_->resetAccumulation(*requests.resetAccumulation); @@ -6555,6 +6587,22 @@ void Application::initWindow() { << "Files: drop .hdr for environment maps or .gltf/.glb for scene reload.\n"; } +void Application::configureStreamingAsyncComputeBudgeter() { + const bool asyncComputeAvailable = + context_ != nullptr && + !disableAsyncCompute_ && + !singleQueueFallback_ && + context_->computeQueue() != VK_NULL_HANDLE && + context_->hasIndependentComputeQueue() && + context_->queueFamilies().compute.has_value() && + context_->supportsTimelineSemaphore(); + streamingAsyncComputeBudgeter_.configure( + streamingOptions_.asyncComputeFrameBudgetUs, + streamingOptions_.asyncComputeMinStreamingHeadroomUs, + asyncComputeAvailable, + streamingOptions_.asyncComputeStreamingEnabled); +} + void Application::initVulkan() { if (headless_) { context_ = VulkanContext::createHeadless(); @@ -6571,6 +6619,7 @@ void Application::initVulkan() { } commandSystem_ = std::make_unique(*context_, *swapchain_, disableAsyncCompute_, singleQueueFallback_); commandSystem_->setHeadless(headless_); + configureStreamingAsyncComputeBudgeter(); if (!headless_) { uiOverlay_ = std::make_unique(window_, *context_, *swapchain_, *allocator_, *uploader_); @@ -6666,7 +6715,6 @@ void Application::initVulkan() { } sceneUnsavedDirty_ = false; sceneDocument_.setSourceHdrPath(hdrPath_); - rebuildGpuSceneAsset(); RendererSettings startupSettings{}; startupSettings.debugView = debugView_; startupSettings = rendererSettingsFromDocument(sceneDocument_, startupSettings); @@ -6724,6 +6772,8 @@ void Application::initVulkan() { startupSettings.opacityMicromapSubdivisionLevel = *opacityMicromapSubdivisionOverride_; startupSettings.renderPreset = RenderPreset::Custom; } + startupSettings = PathTracerRenderer::normalizeSettingsForDevice(startupSettings, *context_); + rebuildGpuSceneAsset(&startupSettings); traceStartupPhase("initial_create_path_tracer_begin"); createPathTracer(&startupSettings); traceStartupPhase("initial_create_path_tracer_end"); @@ -6819,6 +6869,8 @@ void Application::mainLoop(uint32_t maxFrames) { traceMainLoopPhase(frameCount, "runtime_controls_end"); applyValidationObjectMotion(frameCount); applyValidationCameraMotion(frameCount); + applyValidationLightReorder(frameCount); + applyValidationLightFlicker(frameCount); notifications_.update(deltaSeconds); updateAutosave(deltaSeconds); EditorRequests editorRequests; @@ -6837,14 +6889,13 @@ void Application::mainLoop(uint32_t maxFrames) { editorRequests.saveSceneAs = *path; } } + bool shaderReloadRequested = false; if (pendingReloadShaders_) { pendingReloadShaders_ = false; - editorRequests.reloadShaders = true; - editorRequests.resetAccumulation = AccumulationResetReason::ShaderReloaded; + shaderReloadRequested = true; } if (pathTracer_ && pathTracer_->shadersNeedReload()) { - editorRequests.reloadShaders = true; - editorRequests.resetAccumulation = AccumulationResetReason::ShaderReloaded; + shaderReloadRequested = true; } frameWorkScheduler_.tick(); traceMainLoopPhase(frameCount, "job_snapshots_begin"); @@ -7113,6 +7164,10 @@ void Application::mainLoop(uint32_t maxFrames) { ¬ifications_); traceMainLoopPhase(frameCount, "ui_build_project_manager_end"); } + if (shaderReloadRequested) { + editorRequests.reloadShaders = true; + editorRequests.resetAccumulation = AccumulationResetReason::ShaderReloaded; + } if (!rendererOnly_) { traceMainLoopPhase(frameCount, "apply_requests_pre_render_begin"); pollMountedNativePackageChanges(editorRequests); @@ -7742,9 +7797,10 @@ void Application::captureProjectThumbnailIfReady() { } try { + constexpr uint32_t kProjectThumbnailMaxDimension = 768u; DiagnosticImageExport exporter(*context_, *allocator_); if (!exporter.initialize(swapchain_->format(), extent) || - !exporter.exportView(*pathTracer_, RendererDebugView::Beauty, *pendingProjectThumbnailPath_, 0)) { + !exporter.exportView(*pathTracer_, RendererDebugView::Beauty, *pendingProjectThumbnailPath_, 0, kProjectThumbnailMaxDimension)) { pendingProjectThumbnailFrame_ = frameSerial_ + 1u; if (++pendingProjectThumbnailAttempts_ > 8u) { notifications_.notify("Project thumbnail capture failed", NotificationType::Warning, NotificationAction::OpenProjectManager, "Project Manager", 5.0f); @@ -8878,6 +8934,9 @@ bool Application::applyReplacementSceneResult(SceneLoadResult&& result, bool sce &replacementSettings); traceStartupPhase("renderer_create_make_end"); } + syncDocumentRenderSettings( + nextDocument, + reuseExistingFallbackRenderer ? pathTracer_->settings() : nextPathTracer->settings()); rendererCreateMs = elapsedMs(rendererCreateStart); const auto stateSwapStart = std::chrono::steady_clock::now(); @@ -15954,6 +16013,53 @@ void Application::applyValidationObjectMotion(uint32_t frameIndex) { (void)applyPendingSceneUpdate(false); } +void Application::applyValidationLightReorder(uint32_t frameIndex) { + if (!validationLightReorder_ || + validationLightReorderApplied_ || + pathTracer_ == nullptr || + frameIndex == 0u) { + return; + } + + validationLightReorderApplied_ = true; + SceneAsset scene = sceneDocument_.toSceneAsset(); + if (scene.lights.size() < 2u) { + std::cout << "Validation light reorder skipped: fewer than two authored lights\n"; + return; + } + + std::reverse(scene.lights.begin(), scene.lights.end()); + const bool updated = pathTracer_->updateSceneLights(scene, true); + std::cout << "Validation light reorder " << (updated ? "applied" : "skipped") + << ": authored_lights=" << scene.lights.size() << '\n'; +} + +void Application::applyValidationLightFlicker(uint32_t frameIndex) { + if (!validationLightFlicker_ || pathTracer_ == nullptr) { + return; + } + + SceneAsset scene = sceneDocument_.toSceneAsset(); + if (scene.lights.empty()) { + if (!validationLightFlickerUnavailableLogged_) { + std::cout << "Validation light flicker skipped: no authored lights\n"; + validationLightFlickerUnavailableLogged_ = true; + } + return; + } + + // Two bright frames, two near-off frames, then two bright frames. Keeping + // the source present exercises generation remapping without deleting it. + const uint32_t phase = frameIndex % 6u; + const bool nearOff = phase == 2u || phase == 3u; + const float baseIntensity = std::max(scene.lights.front().intensity, 0.001f); + scene.lights.front().intensity = nearOff ? baseIntensity * 0.02f : baseIntensity; + const bool updated = pathTracer_->updateSceneLights(scene, true); + std::cout << "Validation light flicker frame=" << frameIndex + << " state=" << (nearOff ? "near-off" : "bright") + << " update=" << (updated ? "applied" : "unchanged") << '\n'; +} + void Application::beginSunDragArm(bool dragEligible) { if (window_ == nullptr || sunDrag_.phase != SunDragPhase::Idle) { return; @@ -16225,6 +16331,7 @@ bool Application::applyPendingSceneUpdate(bool allowResourceRebuild, bool intera applyActiveSceneCamera(); pathTracer_->resetAccumulation(route.resetReason); commandSystem_->setPathTracer(pathTracer_.get()); + syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); }; auto syncDerivedSceneSettings = [&]() { @@ -16396,26 +16503,57 @@ bool Application::applyPendingSceneUpdate(bool allowResourceRebuild, bool intera } void Application::applyRendererSettingsSafely(const RendererSettings& settings, bool allowRenderResolutionChange) { - if (pathTracer_ == nullptr) { + if (pathTracer_ == nullptr || context_ == nullptr) { return; } const RendererSettings current = pathTracer_->settings(); + const RendererSettings requested = PathTracerRenderer::normalizeSettingsForDevice(settings, *context_); + const bool gpuSceneConfigurationChanged = + requested.opacityMicromapsEnabled != current.opacityMicromapsEnabled || + requested.opacityMicromapSubdivisionLevel != current.opacityMicromapSubdivisionLevel || + requested.opacityMicromapBlendEnabled != current.opacityMicromapBlendEnabled || + requested.compactImportedEmissiveTriangleSampling != current.compactImportedEmissiveTriangleSampling; const bool renderResolutionChanged = - std::abs(settings.renderResolutionScale - current.renderResolutionScale) > 0.0001f; - if (!renderResolutionChanged || allowRenderResolutionChange) { - if (pathTracer_->applySettings(settings)) { - syncDocumentRenderSettings(sceneDocument_, settings); + std::abs(requested.renderResolutionScale - current.renderResolutionScale) > 0.0001f; + + if (gpuSceneConfigurationChanged) { + if (!allowRenderResolutionChange) { + pendingPostFrameSettings_ = requested; + return; + } + preparePathTracerForRendererReplacement(current); + std::unique_ptr nextPathTracer = makePathTracer( + gpuSceneAsset_.has_value() && !gpuSceneAsset_->meshes.empty() ? &*gpuSceneAsset_ : nullptr, + gpuSceneAsset_.has_value() && !gpuSceneAsset_->meshes.empty() ? &assets_ : nullptr, + currentSceneCachePolicyForRenderer(), + &requested); + if (uiOverlay_ != nullptr) { + uiOverlay_->invalidateRendererTextures(); + uiOverlay_->editor().invalidateAssetThumbnails(); + } + retirePathTracer(std::move(pathTracer_)); + pathTracer_ = std::move(nextPathTracer); + applyActiveSceneCamera(); + pathTracer_->resetAccumulation(AccumulationResetReason::RenderSettingsChanged); + if (commandSystem_ != nullptr) { + commandSystem_->setPathTracer(pathTracer_.get()); } + syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); return; } - RendererSettings immediate = settings; - immediate.renderResolutionScale = current.renderResolutionScale; - if (pathTracer_->applySettings(immediate)) { - syncDocumentRenderSettings(sceneDocument_, settings); + if (!renderResolutionChanged || allowRenderResolutionChange) { + (void)pathTracer_->applySettings(requested); + syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); + return; } - pendingPostFrameSettings_ = settings; + + RendererSettings immediate = requested; + immediate.renderResolutionScale = current.renderResolutionScale; + (void)pathTracer_->applySettings(immediate); + syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); + pendingPostFrameSettings_ = requested; } bool Application::applyRendererSettingsFromEditor(const RendererSettings& settings, bool allowRenderResolutionChange) { @@ -16631,6 +16769,13 @@ void Application::shutdownStreamingRuntime() { } void Application::stepStreamingGpuWorkQueue() { + const float previousGpuMs = pathTracer_ != nullptr ? pathTracer_->timings().totalMs() : 0.0f; + const uint64_t previousGpuUs = + previousGpuMs > 0.0f && std::isfinite(previousGpuMs) + ? static_cast(previousGpuMs * 1000.0f) + : 0ull; + (void)streamingAsyncComputeBudgeter_.beginFrame(previousGpuUs, frameSerial_); + // Gate work-queue completion on real device-timeline progress when a live // transfer executor is available. The work queue remains the accounting // layer; live completion advances only after the executor's device timeline @@ -16671,6 +16816,8 @@ void Application::stepStreamingGpuWorkQueue() { std::unordered_map ownerComplete; std::unordered_map ownerFailed; std::unordered_map ownerPayloadBacked; + std::unordered_map textureUploadsComplete; + std::unordered_map textureUploadsPayloadBacked; std::unordered_map nativeSnapshotByGuid; for (const NativeGpuAssetSnapshot& snapshot : nativeGpuAssetCache_.snapshots()) { if (!snapshot.guid.empty()) { @@ -16682,6 +16829,12 @@ void Application::stepStreamingGpuWorkQueue() { if (ticket.ownerGuid.empty()) { continue; } + if (ticket.kind == StreamingGpuWorkKind::ImageMipUpload) { + auto completeIt = textureUploadsComplete.emplace(ticket.ownerGuid, true).first; + completeIt->second = completeIt->second && ticket.state == StreamingGpuWorkState::Complete; + auto payloadIt = textureUploadsPayloadBacked.emplace(ticket.ownerGuid, true).first; + payloadIt->second = payloadIt->second && ticket.payloadBacked; + } if (ticket.payloadBacked && ticket.kind == StreamingGpuWorkKind::ImageMipUpload && ticket.state == StreamingGpuWorkState::Complete && @@ -16780,6 +16933,39 @@ void Application::stepStreamingGpuWorkQueue() { streamingGpuBlasCompactionPayloads_.erase(ticket.id); } } + const std::vector currentNativeSnapshots = nativeGpuAssetCache_.snapshots(); + for (const auto& [guid, uploadsComplete] : textureUploadsComplete) { + if (!uploadsComplete || !textureUploadsPayloadBacked[guid] || pathTracer_ == nullptr || !gpuSceneAsset_.has_value()) { + continue; + } + const auto currentNativeIt = std::find_if( + currentNativeSnapshots.begin(), + currentNativeSnapshots.end(), + [&](const NativeGpuAssetSnapshot& snapshot) { return snapshot.guid == guid; }); + if (currentNativeIt == currentNativeSnapshots.end() || + currentNativeIt->kind != NativeGpuAssetKind::Texture || + currentNativeIt->descriptorPatchComplete || + currentNativeIt->mipCount == 0 || + currentNativeIt->residentMipCount < currentNativeIt->mipCount) { + continue; + } + TextureAssetHandle textureHandle{}; + const std::vector& textures = assets_.textures(); + for (uint32_t textureIndex = 0; textureIndex < textures.size(); ++textureIndex) { + if (textures[textureIndex].nativeGuid == guid) { + textureHandle = TextureAssetHandle{textureIndex}; + break; + } + } + Image* image = nativeGpuAssetCache_.imageResource(guid); + if (!textureHandle.valid() || image == nullptr || + !pathTracer_->patchStreamedMaterialTexture(*gpuSceneAsset_, textureHandle, *image)) { + continue; + } + (void)nativeGpuAssetCache_.markDescriptorPatchComplete(guid); + streamingRuntimeState_.setAssetState(guid, StreamingAssetState::GpuResident); + streamingRuntimeState_.pushEvent("published streamed texture descriptor for " + guid); + } for (const auto& [guid, complete] : ownerComplete) { const bool payloadBacked = ownerPayloadBacked[guid]; const auto nativeIt = nativeSnapshotByGuid.find(guid); @@ -17303,14 +17489,17 @@ void Application::initializeRendererFromCurrentScene(const RendererSettings* set lastStartupPhase = now; }; - rebuildGpuSceneAsset(); - logStartupPhase("rebuild_gpu_scene_asset"); RendererSettings startupSettings = settingsToRestore != nullptr ? *settingsToRestore : RendererSettings{}; if (settingsToRestore == nullptr) { startupSettings.debugView = debugView_; startupSettings = rendererSettingsFromDocument(sceneDocument_, startupSettings); } + if (context_ != nullptr) { + startupSettings = PathTracerRenderer::normalizeSettingsForDevice(startupSettings, *context_); + } logStartupPhase("resolve_startup_settings"); + rebuildGpuSceneAsset(&startupSettings); + logStartupPhase("rebuild_gpu_scene_asset"); createPathTracer(&startupSettings); logStartupPhase("create_path_tracer"); syncDocumentRenderSettings(sceneDocument_, pathTracer_->settings()); @@ -17388,8 +17577,10 @@ void Application::syncActiveSceneCameraFromController() { sceneDocument_.markDirty(SceneUpdateKind::CameraOnly); } -void Application::rebuildGpuSceneAsset() { - const RendererSettings settings = pathTracer_ != nullptr ? pathTracer_->settings() : RendererSettings{}; +void Application::rebuildGpuSceneAsset(const RendererSettings* settingsOverride) { + const RendererSettings settings = settingsOverride != nullptr + ? *settingsOverride + : (pathTracer_ != nullptr ? pathTracer_->settings() : RendererSettings{}); (void)SunController::migrateLegacyDirectionalSun(sceneDocument_); (void)SunController::repairPrimarySunTransform(sceneDocument_); applyDocumentMaterialAssignments(sceneDocument_, assets_); diff --git a/src/rtv/AssetBrowserPanel.cpp b/src/rtv/AssetBrowserPanel.cpp index 0870b7e..578b798 100644 --- a/src/rtv/AssetBrowserPanel.cpp +++ b/src/rtv/AssetBrowserPanel.cpp @@ -6816,7 +6816,7 @@ bool AssetBrowserPanel::drawRasterThumbnail(const std::filesystem::path& path, I ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(min, max, IM_COL32(16, 18, 22, 255), EditorUiMetric::cardRounding); const ImVec2 innerMin(min.x + 4.0f, min.y + 4.0f); - const ImVec2 innerMax(max.x - 4.0f, max.y - 17.0f); + const ImVec2 innerMax(max.x - 4.0f, max.y - 4.0f); const float cellW = (innerMax.x - innerMin.x) / static_cast(thumbnail.columns); const float cellH = (innerMax.y - innerMin.y) / static_cast(thumbnail.rows); for (int row = 0; row < thumbnail.rows; ++row) { @@ -6827,9 +6827,6 @@ bool AssetBrowserPanel::drawRasterThumbnail(const std::filesystem::path& path, I } } dl->AddRect(innerMin, innerMax, IM_COL32(255, 255, 255, 42), 1.0f); - const std::string badge = std::to_string(thumbnail.width) + "x" + std::to_string(thumbnail.height); - dl->AddRectFilled(ImVec2(min.x + 4.0f, max.y - 15.0f), ImVec2(max.x - 4.0f, max.y - 4.0f), IM_COL32(12, 15, 19, 205), 1.0f); - dl->AddText(ImVec2(min.x + 8.0f, max.y - 15.0f), IM_COL32(178, 188, 202, 255), badge.c_str()); dl->AddRect(min, max, selected ? ImGui::GetColorU32(editorActiveRowColor()) : IM_COL32(54, 62, 72, 255), EditorUiMetric::cardRounding); return true; } @@ -6855,11 +6852,9 @@ bool AssetBrowserPanel::drawGpuSceneTextureThumbnail(const EditorRuntimeState& s ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(min, max, IM_COL32(16, 18, 22, 255), EditorUiMetric::cardRounding); const ImVec2 imageMin(min.x + 4.0f, min.y + 4.0f); - const ImVec2 imageMax(max.x - 4.0f, max.y - 17.0f); + const ImVec2 imageMax(max.x - 4.0f, max.y - 4.0f); dl->AddImage(static_cast(reinterpret_cast(texture)), imageMin, imageMax); dl->AddRect(imageMin, imageMax, IM_COL32(255, 255, 255, 42), 1.0f); - dl->AddRectFilled(ImVec2(min.x + 4.0f, max.y - 15.0f), ImVec2(max.x - 4.0f, max.y - 4.0f), IM_COL32(12, 15, 19, 205), 1.0f); - dl->AddText(ImVec2(min.x + 8.0f, max.y - 15.0f), IM_COL32(160, 210, 255, 255), "GPU texture"); dl->AddRect(min, max, ImGui::GetColorU32(editorActiveRowColor()), EditorUiMetric::cardRounding); return true; } @@ -6875,14 +6870,9 @@ bool AssetBrowserPanel::drawStandaloneGpuAssetPreview(const EditorRuntimeState& ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(min, max, IM_COL32(16, 18, 22, 255), EditorUiMetric::cardRounding); const ImVec2 imageMin(min.x + 4.0f, min.y + 4.0f); - const ImVec2 imageMax(max.x - 4.0f, max.y - 17.0f); + const ImVec2 imageMax(max.x - 4.0f, max.y - 4.0f); dl->AddImage(static_cast(reinterpret_cast(texture)), imageMin, imageMax); dl->AddRect(imageMin, imageMax, IM_COL32(255, 255, 255, 42), 1.0f); - dl->AddRectFilled(ImVec2(min.x + 4.0f, max.y - 15.0f), ImVec2(max.x - 4.0f, max.y - 4.0f), IM_COL32(12, 15, 19, 205), 1.0f); - const std::string badge = width > 0 && height > 0 - ? "GPU preview " + std::to_string(width) + "x" + std::to_string(height) - : std::string("GPU preview"); - dl->AddText(ImVec2(min.x + 8.0f, max.y - 15.0f), IM_COL32(160, 210, 255, 255), badge.c_str()); dl->AddRect(min, max, selected ? ImGui::GetColorU32(editorActiveRowColor()) : IM_COL32(56, 66, 82, 210), EditorUiMetric::cardRounding); return true; } @@ -7614,16 +7604,32 @@ void AssetBrowserPanel::drawPathList(const EditorRuntimeState& state, EditorRequ const std::filesystem::path path = entry->path; const bool selected = selectedPath_ == path; ImGui::PushID(path.string().c_str()); - const ImVec2 thumbSize(EditorUiMetric::contentGridThumbWidth, EditorUiMetric::contentGridThumbHeight); - ImGui::InvisibleButton("ContentGridThumb", thumbSize); - const ImVec2 thumbMin = ImGui::GetItemRectMin(); - const ImVec2 thumbMax = ImGui::GetItemRectMax(); + const ImVec2 cellSize(EditorUiMetric::contentGridCellWidth - 10.0f, EditorUiMetric::contentGridCellHeight); + ImGui::InvisibleButton("ContentGridTile", cellSize); + const ImVec2 cellMin = ImGui::GetItemRectMin(); + const ImVec2 cellMax = ImGui::GetItemRectMax(); ImDrawList* dl = ImGui::GetWindowDrawList(); - dl->AddRectFilled(thumbMin, thumbMax, selected ? ImGui::GetColorU32(editorSelectedRowColor()) : IM_COL32(24, 27, 32, 255), EditorUiMetric::cardRounding); - dl->AddRect(thumbMin, thumbMax, selected ? ImGui::GetColorU32(editorActiveRowColor()) : IM_COL32(54, 62, 72, 255), EditorUiMetric::cardRounding); - if (!drawGpuSceneTextureThumbnail(state, path, thumbMin, thumbMax) && - !drawStandaloneGpuAssetPreview(state, path, thumbMin, thumbMax, selected) && - !drawRasterThumbnail(path, thumbMin, thumbMax, selected)) { + const bool hovered = ImGui::IsItemHovered(); + const ImU32 cellBg = selected + ? ImGui::GetColorU32(ImVec4(0.090f, 0.185f, 0.315f, 0.95f)) + : ImGui::GetColorU32(hovered ? ImVec4(0.105f, 0.125f, 0.150f, 1.0f) : editorCardBgColor()); + dl->AddRectFilled(cellMin, cellMax, cellBg, EditorUiMetric::cardRounding); + dl->AddRect(cellMin, cellMax, selected ? ImGui::GetColorU32(editorAccentColor()) : ImGui::GetColorU32(editorToolbarBorderColor()), EditorUiMetric::cardRounding); + const ImVec2 thumbSize(EditorUiMetric::contentGridThumbWidth, EditorUiMetric::contentGridThumbHeight); + const ImVec2 thumbMin( + cellMin.x + (cellSize.x - thumbSize.x) * 0.5f, + cellMin.y + 10.0f); + const ImVec2 thumbMax(thumbMin.x + thumbSize.x, thumbMin.y + thumbSize.y); + dl->AddRectFilled(thumbMin, thumbMax, IM_COL32(22, 26, 32, 255), EditorUiMetric::cardRounding); + dl->AddRect(thumbMin, thumbMax, IM_COL32(54, 62, 72, 255), EditorUiMetric::cardRounding); + if (entry->isDirectory) { + drawContentGlyph( + path, + ImVec2(thumbMin.x + thumbSize.x * 0.28f, thumbMin.y + thumbSize.y * 0.18f), + ImVec2(thumbMax.x - thumbSize.x * 0.28f, thumbMax.y - thumbSize.y * 0.18f)); + } else if (!drawGpuSceneTextureThumbnail(state, path, thumbMin, thumbMax) && + !drawStandaloneGpuAssetPreview(state, path, thumbMin, thumbMax, selected) && + !drawRasterThumbnail(path, thumbMin, thumbMax, selected)) { drawContentGlyph( path, ImVec2(thumbMin.x + thumbSize.x * 0.34f, thumbMin.y + thumbSize.y * 0.22f), @@ -7647,7 +7653,17 @@ void AssetBrowserPanel::drawPathList(const EditorRuntimeState& state, EditorRequ drawPathContextMenu(state, path, entry->isDirectory, requests); ImGui::EndPopup(); } - ImGui::TextWrapped("%s%s", selected ? "> " : "", path.filename().string().c_str()); + std::string label = path.filename().empty() ? path.string() : path.filename().string(); + const float labelWidth = cellSize.x - 14.0f; + while (label.size() > 6 && ImGui::CalcTextSize(label.c_str()).x > labelWidth) { + label.erase(label.size() - 4, 1); + label.replace(label.size() - 3, 3, "..."); + } + const ImVec2 labelSize = ImGui::CalcTextSize(label.c_str()); + dl->AddText( + ImVec2(cellMin.x + std::max(7.0f, (cellSize.x - labelSize.x) * 0.5f), thumbMax.y + 10.0f), + ImGui::GetColorU32(selected ? ImVec4(0.90f, 0.95f, 1.0f, 1.0f) : ImVec4(0.72f, 0.76f, 0.82f, 1.0f)), + label.c_str()); ImGui::NextColumn(); ImGui::PopID(); } @@ -9833,6 +9849,25 @@ void AssetBrowserPanel::draw(const EditorRuntimeState& state, EditorSelection& s syncBrowserRoot(state); refreshImportOperations(state); + if (!presentationPreferencesLoaded_ && state.editorPrefs != nullptr) { + viewMode_ = std::clamp(state.editorPrefs->contentBrowserMode, 0, 1); + gridView_ = state.editorPrefs->contentBrowserGridView; + showDetails_ = state.editorPrefs->contentBrowserShowDetails; + presentationPreferencesLoaded_ = true; + } + auto persistPresentation = [&]() { + if (state.editorPrefs == nullptr) { + return; + } + state.editorPrefs->contentBrowserMode = std::clamp(viewMode_, 0, 1); + state.editorPrefs->contentBrowserGridView = gridView_; + state.editorPrefs->contentBrowserShowDetails = showDetails_; + const std::filesystem::path prefsPath = state.editorPreferencesPath.empty() + ? EditorPreferences::defaultPath() + : state.editorPreferencesPath; + (void)state.editorPrefs->save(prefsPath); + }; + ImGui::BeginGroup(); if (editorIconButton("ContentAdd", EditorGlyphIcon::Add, false)) { ImGui::OpenPopup("ContentAddMenu"); @@ -9879,6 +9914,16 @@ void AssetBrowserPanel::draw(const EditorRuntimeState& state, EditorSelection& s ImGui::EndPopup(); } ImGui::SameLine(); + if (editorToolbarTextButton("ContentBrowseMode", EditorGlyphIcon::Grid, "Browse", viewMode_ == 0)) { + viewMode_ = 0; + persistPresentation(); + } + ImGui::SameLine(); + if (editorToolbarTextButton("ContentRegistryMode", EditorGlyphIcon::Details, "Registry", viewMode_ == 1)) { + viewMode_ = 1; + persistPresentation(); + } + ImGui::SameLine(); const bool canValidateProject = state.assetRegistry != nullptr; if (!canValidateProject) { ImGui::BeginDisabled(); @@ -9896,36 +9941,55 @@ void AssetBrowserPanel::draw(const EditorRuntimeState& state, EditorSelection& s if (!canValidateProject) { ImGui::EndDisabled(); } - ImGui::SameLine(); - ImGui::SetNextItemWidth(220.0f); - ImGui::InputTextWithHint("##contentFilter", "Filter in selected folder...", search_.data(), search_.size()); - ImGui::SameLine(); - ImGui::BeginDisabled(backStack_.empty()); - if (editorIconButton("ContentBack", EditorGlyphIcon::Back, false)) { - forwardStack_.push_back(currentPath_); - const std::filesystem::path previous = backStack_.back(); - backStack_.pop_back(); - navigateTo(previous, false); - } - ImGui::EndDisabled(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { - ImGui::SetTooltip("Back"); - } - ImGui::SameLine(); - ImGui::BeginDisabled(forwardStack_.empty()); - if (editorIconButton("ContentForward", EditorGlyphIcon::Forward, false)) { - backStack_.push_back(currentPath_); - const std::filesystem::path next = forwardStack_.back(); - forwardStack_.pop_back(); - navigateTo(next, false); - } - ImGui::EndDisabled(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { - ImGui::SetTooltip("Forward"); + if (viewMode_ == 0) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(220.0f); + ImGui::InputTextWithHint("##contentFilter", "Search assets...", search_.data(), search_.size()); + ImGui::SameLine(); + if (editorIconButton("ContentGridView", EditorGlyphIcon::Grid, gridView_)) { + gridView_ = true; + persistPresentation(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("Thumbnail grid"); + } + ImGui::SameLine(); + if (editorIconButton("ContentListView", EditorGlyphIcon::List, !gridView_)) { + gridView_ = false; + persistPresentation(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("Compact list"); + } + ImGui::SameLine(); + if (editorIconButton("ContentDetailsToggle", EditorGlyphIcon::Details, showDetails_)) { + showDetails_ = !showDetails_; + persistPresentation(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("Toggle details"); + } + ImGui::SameLine(); + ImGui::BeginDisabled(backStack_.empty()); + if (editorIconButton("ContentBack", EditorGlyphIcon::Back, false)) { + forwardStack_.push_back(currentPath_); + const std::filesystem::path previous = backStack_.back(); + backStack_.pop_back(); + navigateTo(previous, false); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::BeginDisabled(forwardStack_.empty()); + if (editorIconButton("ContentForward", EditorGlyphIcon::Forward, false)) { + backStack_.push_back(currentPath_); + const std::filesystem::path next = forwardStack_.back(); + forwardStack_.pop_back(); + navigateTo(next, false); + } + ImGui::EndDisabled(); } - showDetails_ = true; - if (!browserRoot_.empty()) { + if (viewMode_ == 0 && !browserRoot_.empty()) { ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); @@ -9976,6 +10040,13 @@ void AssetBrowserPanel::draw(const EditorRuntimeState& state, EditorSelection& s ImGui::TextWrapped("%s", sceneLoadStatus.c_str()); } + if (viewMode_ == 1) { + drawRegistryTable(state, requests); + drawImportOperations(); + ImGui::End(); + return; + } + const float browserHeight = ImGui::GetContentRegionAvail().y; if (browserHeight > ImGui::GetTextLineHeightWithSpacing()) { const float browserWidth = ImGui::GetContentRegionAvail().x; @@ -10138,8 +10209,6 @@ void AssetBrowserPanel::draw(const EditorRuntimeState& state, EditorSelection& s ImGui::SameLine(); ImGui::BeginChild("ContentItems", ImVec2(-(detailsWidth + (showDetails_ ? sectionSpacing : 0.0f)), 0.0f), true); drawPathList(state, requests); - drawRegistryTable(state, requests); - drawImportOperations(); ImGui::EndChild(); if (showDetails_) { ImGui::SameLine(); diff --git a/src/rtv/BindlessResources.cpp b/src/rtv/BindlessResources.cpp index 5fd8167..e35edce 100644 --- a/src/rtv/BindlessResources.cpp +++ b/src/rtv/BindlessResources.cpp @@ -2,10 +2,12 @@ #include "rtv/Buffer.h" #include "rtv/Check.h" +#include "rtv/DescriptorWriteDiagnostics.h" #include "rtv/Image.h" #include #include +#include #include #include @@ -27,12 +29,43 @@ BindlessCapabilities queryBindlessCapabilities(VkPhysicalDevice physicalDevice) props.pNext = &indexingProps; vkGetPhysicalDeviceProperties2(physicalDevice, &props); + const uint32_t maxPerStageSampledImages = indexingProps.maxPerStageDescriptorUpdateAfterBindSampledImages; + const uint32_t maxPerStageSamplers = indexingProps.maxPerStageDescriptorUpdateAfterBindSamplers; + const uint32_t maxDescriptorSetSampledImages = indexingProps.maxDescriptorSetUpdateAfterBindSampledImages; + const uint32_t maxDescriptorSetSamplers = indexingProps.maxDescriptorSetUpdateAfterBindSamplers; + const uint32_t maxUpdateAfterBindDescriptorsInAllPools = indexingProps.maxUpdateAfterBindDescriptorsInAllPools; + constexpr uint32_t kPerStagePassDescriptorReserve = 32; + // Keep room for the default pass-local DescriptorAllocator pool. + constexpr uint32_t kPassLocalPoolDescriptorReserve = 256u * (4u + 32u + 16u + 8u + 8u + 4u + 2u); + const uint32_t perStageSampledImageLimit = maxPerStageSampledImages > kPerStagePassDescriptorReserve + ? maxPerStageSampledImages - kPerStagePassDescriptorReserve + : 0; + const uint32_t perStageSamplerLimit = maxPerStageSamplers > kPerStagePassDescriptorReserve + ? maxPerStageSamplers - kPerStagePassDescriptorReserve + : 0; + const uint32_t heapPoolBudget = maxUpdateAfterBindDescriptorsInAllPools > kPassLocalPoolDescriptorReserve + ? maxUpdateAfterBindDescriptorsInAllPools - kPassLocalPoolDescriptorReserve + : 0; + const uint32_t perHeapPoolLimit = heapPoolBudget / kBindlessTextureHeapVersionCount; + const uint32_t maxCombinedImageSamplers = std::min({ + perStageSampledImageLimit, + perStageSamplerLimit, + maxDescriptorSetSampledImages, + maxDescriptorSetSamplers, + perHeapPoolLimit, + }); + return { .descriptorIndexing = indexing.shaderSampledImageArrayNonUniformIndexing == VK_TRUE, .runtimeDescriptorArray = indexing.runtimeDescriptorArray == VK_TRUE, .partiallyBound = indexing.descriptorBindingPartiallyBound == VK_TRUE, .updateAfterBind = indexing.descriptorBindingSampledImageUpdateAfterBind == VK_TRUE, - .maxSampledImages = indexingProps.maxDescriptorSetUpdateAfterBindSampledImages, + .maxSampledImages = maxCombinedImageSamplers, + .maxPerStageSampledImages = maxPerStageSampledImages, + .maxPerStageSamplers = maxPerStageSamplers, + .maxDescriptorSetSampledImages = maxDescriptorSetSampledImages, + .maxDescriptorSetSamplers = maxDescriptorSetSamplers, + .maxUpdateAfterBindDescriptorsInAllPools = maxUpdateAfterBindDescriptorsInAllPools, }; } @@ -48,7 +81,8 @@ uint32_t fullBindlessTextureCapacityOrThrow(const BindlessCapabilities& caps) { if (!supportsFullBindlessTextures(caps)) { throw std::runtime_error( "Full bindless textures require descriptor indexing, runtime descriptor arrays, " - "partially-bound sampled image descriptors, and sampled-image update-after-bind support"); + "partially-bound sampled image descriptors, sampled-image update-after-bind support, " + "and non-zero combined image-sampler limits for the versioned heap"); } return maxMaterialTextureSlots(caps); } @@ -66,16 +100,23 @@ void BindlessTextureHeap::init(VkDevice device, const BindlessCapabilities& caps (void)fullBindlessTextureCapacityOrThrow(caps); } capacity_ = std::max(1u, capacity); + const uint32_t supportedCapacity = maxMaterialTextureSlots(caps); + if (capacity_ > supportedCapacity) { + throw std::runtime_error("Bindless texture heap capacity exceeds combined image-sampler descriptor limits"); + } device_ = device; + descriptors_.resize(capacity_); + descriptorValid_.assign(capacity_, 0); + appliedGenerations_.fill(std::numeric_limits::max()); VkDescriptorPoolSize poolSize{}; poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = capacity_; + poolSize.descriptorCount = capacity_ * kBindlessTextureHeapVersionCount; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; - poolInfo.maxSets = 1; + poolInfo.maxSets = kBindlessTextureHeapVersionCount; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; checkVk(vkCreateDescriptorPool(device_, &poolInfo, nullptr, &pool_), "vkCreateDescriptorPool(bindless texture heap)"); @@ -105,9 +146,10 @@ void BindlessTextureHeap::init(VkDevice device, const BindlessCapabilities& caps VkDescriptorSetAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorPool = pool_; - allocInfo.descriptorSetCount = 1; - allocInfo.pSetLayouts = &layout_; - checkVk(vkAllocateDescriptorSets(device_, &allocInfo, &descriptorSet_), "vkAllocateDescriptorSets(bindless texture heap)"); + const std::array layouts{layout_, layout_, layout_}; + allocInfo.descriptorSetCount = kBindlessTextureHeapVersionCount; + allocInfo.pSetLayouts = layouts.data(); + checkVk(vkAllocateDescriptorSets(device_, &allocInfo, descriptorSets_.data()), "vkAllocateDescriptorSets(bindless texture heap)"); } void BindlessTextureHeap::destroy() { @@ -122,49 +164,106 @@ void BindlessTextureHeap::destroy() { device_ = VK_NULL_HANDLE; pool_ = VK_NULL_HANDLE; layout_ = VK_NULL_HANDLE; - descriptorSet_ = VK_NULL_HANDLE; + descriptorSets_.fill(VK_NULL_HANDLE); + appliedGenerations_.fill(0); + descriptors_.clear(); + descriptorValid_.clear(); + descriptorGeneration_ = 0; + currentFrameSlot_ = 0; capacity_ = 0; descriptorCount_ = 0; patchCount_ = 0; + frameSetSelected_ = false; } void BindlessTextureHeap::updateAll(const std::vector& descriptors) { - if (descriptorSet_ == VK_NULL_HANDLE || descriptors.empty()) { + if (descriptorSets_.front() == VK_NULL_HANDLE) { return; } if (descriptors.size() > capacity_) { throw std::runtime_error("Bindless texture heap descriptor update exceeds heap capacity"); } - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = descriptorSet_; - write.dstBinding = 0; - write.dstArrayElement = 0; - write.descriptorCount = static_cast(descriptors.size()); - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = descriptors.data(); - vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr); + std::fill(descriptorValid_.begin(), descriptorValid_.end(), 0); + std::copy(descriptors.begin(), descriptors.end(), descriptors_.begin()); + std::fill_n(descriptorValid_.begin(), descriptors.size(), 1); + ++descriptorGeneration_; descriptorCount_ = static_cast(descriptors.size()); ++patchCount_; } void BindlessTextureHeap::patch(uint32_t slot, const VkDescriptorImageInfo& descriptor) { - if (descriptorSet_ == VK_NULL_HANDLE || slot >= capacity_) { + if (descriptorSets_.front() == VK_NULL_HANDLE || slot >= capacity_) { return; } - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = descriptorSet_; - write.dstBinding = 0; - write.dstArrayElement = slot; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &descriptor; - vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr); + descriptors_[slot] = descriptor; + descriptorValid_[slot] = 1; + ++descriptorGeneration_; descriptorCount_ = std::max(descriptorCount_, slot + 1u); ++patchCount_; } +void BindlessTextureHeap::beginFrame(uint32_t frameIndex) { + if (descriptorSets_.front() == VK_NULL_HANDLE) { + throw std::runtime_error("Bindless texture heap is not initialized"); + } + // CommandSystem waits this frame slot's fence before the renderer selects it. + currentFrameSlot_ = frameIndex % kBindlessTextureHeapVersionCount; + frameSetSelected_ = true; + if (appliedGenerations_[currentFrameSlot_] == descriptorGeneration_) { + return; + } + + VkDescriptorSet descriptorSet = descriptorSets_[currentFrameSlot_]; + uint32_t first = 0; + while (first < capacity_) { + while (first < capacity_ && descriptorValid_[first] == 0) { + ++first; + } + if (first == capacity_) { + break; + } + uint32_t end = first + 1; + while (end < capacity_ && descriptorValid_[end] != 0) { + ++end; + } + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = descriptorSet; + write.dstBinding = 0; + write.dstArrayElement = first; + write.descriptorCount = end - first; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = descriptors_.data() + first; + recordDescriptorWriteUpdate({{ + .descriptorSet = descriptorSet, + .descriptorSetLayout = layout_, + .binding = 0, + .arrayElement = first, + .count = write.descriptorCount, + .type = write.descriptorType, + .kind = "image_array", + .source = "BindlessTextureHeap::beginFrame", + .owner = { + .owner = "BindlessTextureHeap", + .pass = "bindless_texture_heap", + .setName = "bindless_texture_heap_set_2", + .setIndex = 2, + }, + }}); + vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr); + first = end; + } + appliedGenerations_[currentFrameSlot_] = descriptorGeneration_; +} + +VkDescriptorSet BindlessTextureHeap::descriptorSet() const { + if (!frameSetSelected_) { + throw std::runtime_error("Bindless texture heap descriptor set requested before beginFrame"); + } + return descriptorSets_[currentFrameSlot_]; +} + BindlessTextureHeapStats BindlessTextureHeap::stats() const { return { .capacity = capacity_, diff --git a/src/rtv/CommandSystem.cpp b/src/rtv/CommandSystem.cpp index c596291..03c80b8 100644 --- a/src/rtv/CommandSystem.cpp +++ b/src/rtv/CommandSystem.cpp @@ -1,6 +1,8 @@ #include "rtv/CommandSystem.h" +#include "rtv/BindlessResources.h" #include "rtv/Check.h" +#include "rtv/GpuValidation.h" #include "rtv/NsightMarkers.h" #include "rtv/NsightPerfMarkers.h" #include "rtv/PathTracerRenderer.h" @@ -15,8 +17,11 @@ #include #include #include +#include namespace rtv { +static_assert(CommandSystem::framesInFlight == kBindlessTextureHeapVersionCount); + namespace { void beginCommandBufferLabel(VkCommandBuffer commandBuffer, const char* name) { @@ -105,6 +110,7 @@ void CommandSystem::drawFrame(float clearPhase, float deltaSeconds) { uint32_t imageIndex = 0; VkResult acquireResult = VK_SUCCESS; + bool acquireSuboptimal = false; if (headless_) { imageIndex = headlessImageIndex_; @@ -124,10 +130,21 @@ void CommandSystem::drawFrame(float clearPhase, float deltaSeconds) { recreateSwapchainResources(); return; } - checkVk(acquireResult, "vkAcquireNextImageKHR"); + acquireSuboptimal = acquireResult == VK_SUBOPTIMAL_KHR; + if (acquireResult != VK_SUCCESS && !acquireSuboptimal) { + checkVk(acquireResult, "vkAcquireNextImageKHR"); + } traceCommandPhase("acquire_image_end"); } + if (imageIndex >= imageInFlightFences_.size()) { + throw std::runtime_error("Swapchain returned an image index outside the tracked image set"); + } + const VkFence imageFence = imageInFlightFences_[imageIndex]; + if (imageFence != VK_NULL_HANDLE && imageFence != frame.inFlight) { + checkVk(vkWaitForFences(context_.device(), 1, &imageFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(swapchain image)"); + } + traceCommandPhase("reset_begin"); checkVk(vkResetFences(context_.device(), 1, &frame.inFlight), "vkResetFences"); checkVk(vkResetCommandPool(context_.device(), frame.commandPool, 0), "vkResetCommandPool"); @@ -185,6 +202,7 @@ void CommandSystem::drawFrame(float clearPhase, float deltaSeconds) { traceCommandPhase("begin_nvperf_end"); traceCommandPhase("submit_begin"); submitFrame(frame, imageIndex, asyncComputeRecorded); + imageInFlightFences_[imageIndex] = frame.inFlight; traceCommandPhase("submit_end"); endNsightPerfFrame(); if (pathTracer_ != nullptr) { @@ -218,7 +236,7 @@ void CommandSystem::drawFrame(float clearPhase, float deltaSeconds) { if (pathTracer_ != nullptr) { pathTracer_->markStreamlineReflexPresentEnd(); } - if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR) { + if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR || acquireSuboptimal) { recreateSwapchainResources(); } else { checkVk(presentResult, "vkQueuePresentKHR"); @@ -298,6 +316,7 @@ void CommandSystem::createFrameResources() { } void CommandSystem::createPresentSemaphores() { + imageInFlightFences_.assign(swapchain_.imageCount(), VK_NULL_HANDLE); imageRenderFinished_.resize(swapchain_.imageCount()); for (VkSemaphore& semaphore : imageRenderFinished_) { VkSemaphoreCreateInfo semaphoreInfo{}; @@ -331,6 +350,7 @@ void CommandSystem::destroyPresentSemaphores() { } } imageRenderFinished_.clear(); + imageInFlightFences_.clear(); } void CommandSystem::recreateSwapchainResources() { @@ -426,7 +446,8 @@ void CommandSystem::recordPresentationCommands(VkCommandBuffer commandBuffer, ui } vkCmdEndRendering(commandBuffer); - if (resourceDemo_ != nullptr && pipelineDemo_ == nullptr && pathTracer_ == nullptr) { + if (resourceDemo_ != nullptr && pipelineDemo_ == nullptr && pathTracer_ == nullptr && + swapchain_.supportsImageUsage(VK_IMAGE_USAGE_TRANSFER_DST_BIT)) { transitionImage( commandBuffer, swapchainImage, @@ -648,6 +669,7 @@ void CommandSystem::transitionImage( dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; + recordManualBarrierEscape("CommandSystem", "transitionImage", dependency); vkCmdPipelineBarrier2(commandBuffer, &dependency); } diff --git a/src/rtv/DescriptorWriter.cpp b/src/rtv/DescriptorWriter.cpp index aeceeb5..a8c3bbd 100644 --- a/src/rtv/DescriptorWriter.cpp +++ b/src/rtv/DescriptorWriter.cpp @@ -1,6 +1,57 @@ #include "rtv/DescriptorWriter.h" +#include "rtv/DescriptorWriteDiagnostics.h" + +#include +#include +#include +#include +#include +#include + namespace rtv { +namespace { + +constexpr uint32_t kRecentDescriptorWriteLimit = 4096; + +struct DescriptorWriteRecorderState { + uint64_t nextSequence = 1; + uint64_t updateCallCount = 0; + uint64_t writeCount = 0; + uint64_t droppedRecentWriteCount = 0; + std::deque recentWrites; + std::map aggregates; + std::mutex mutex; +}; + +DescriptorWriteRecorderState& descriptorWriteRecorderState() { + static DescriptorWriteRecorderState state; + return state; +} + +template +uint64_t handleToUint64(Handle handle) { + uint64_t value = 0; + static_assert(sizeof(handle) <= sizeof(value), "Vulkan handle is larger than diagnostic storage"); + std::memcpy(&value, &handle, sizeof(handle)); + return value; +} + +std::string descriptorWriteAggregateKey(const DescriptorWriteDiagnosticRecord& record) { + std::ostringstream key; + key << record.descriptorSetLayout + << ':' << record.binding + << ':' << static_cast(record.type) + << ':' << record.kind + << ':' << record.source + << ':' << record.owner + << ':' << record.pass + << ':' << record.setName + << ':' << record.setIndex; + return key.str(); +} + +} // namespace DescriptorWriter& DescriptorWriter::writeBuffer(uint32_t binding, VkDescriptorType type, const VkDescriptorBufferInfo& bufferInfo) { buffers_.push_back(bufferInfo); @@ -35,9 +86,20 @@ DescriptorWriter& DescriptorWriter::writeAccelerationStructure(uint32_t binding, return *this; } -void DescriptorWriter::update(VkDevice device, DescriptorSet set) const { +void DescriptorWriter::update(VkDevice device, DescriptorSet set, DescriptorWriteOwner owner) const { + auto descriptorWriteKindName = [](PendingWrite::Kind kind) { + switch (kind) { + case PendingWrite::Kind::Buffer: return "buffer"; + case PendingWrite::Kind::Image: return "image"; + case PendingWrite::Kind::AccelerationStructure: return "acceleration_structure"; + default: return "unknown"; + } + }; + std::vector patched; patched.reserve(writes_.size()); + std::vector diagnosticWrites; + diagnosticWrites.reserve(writes_.size()); std::vector accelerationStructureWrites; accelerationStructureWrites.reserve(accelerationStructures_.size()); for (const PendingWrite& pending : writes_) { @@ -59,9 +121,96 @@ void DescriptorWriter::update(VkDevice device, DescriptorSet set) const { accelerationStructureWrites.push_back(asWrite); write.pNext = &accelerationStructureWrites.back(); } + diagnosticWrites.push_back({ + .descriptorSet = set.handle(), + .descriptorSetLayout = set.layout(), + .binding = pending.binding, + .arrayElement = 0, + .count = pending.count, + .type = pending.type, + .kind = descriptorWriteKindName(pending.kind), + .source = "DescriptorWriter", + .owner = owner, + }); patched.push_back(write); } + recordDescriptorWriteUpdate(diagnosticWrites); vkUpdateDescriptorSets(device, static_cast(patched.size()), patched.data(), 0, nullptr); } +void recordDescriptorWriteUpdate(const std::vector& entries) { + DescriptorWriteRecorderState& state = descriptorWriteRecorderState(); + std::lock_guard lock(state.mutex); + ++state.updateCallCount; + for (const DescriptorWriteDiagnosticEntry& entry : entries) { + DescriptorWriteDiagnosticRecord record{ + .sequence = state.nextSequence++, + .descriptorSet = handleToUint64(entry.descriptorSet), + .descriptorSetLayout = handleToUint64(entry.descriptorSetLayout), + .binding = entry.binding, + .arrayElement = entry.arrayElement, + .count = entry.count, + .type = entry.type, + .kind = entry.kind != nullptr ? entry.kind : "", + .source = entry.source != nullptr ? entry.source : "", + .owner = entry.owner.owner != nullptr ? entry.owner.owner : "", + .pass = entry.owner.pass != nullptr ? entry.owner.pass : "", + .setName = entry.owner.setName != nullptr ? entry.owner.setName : "", + .setIndex = entry.owner.setIndex, + }; + ++state.writeCount; + if (state.recentWrites.size() >= kRecentDescriptorWriteLimit) { + state.recentWrites.pop_front(); + ++state.droppedRecentWriteCount; + } + state.recentWrites.push_back(record); + + auto& aggregate = state.aggregates[descriptorWriteAggregateKey(record)]; + if (aggregate.occurrenceCount == 0) { + aggregate.descriptorSetLayout = record.descriptorSetLayout; + aggregate.binding = record.binding; + aggregate.type = record.type; + aggregate.kind = record.kind; + aggregate.source = record.source; + aggregate.owner = record.owner; + aggregate.pass = record.pass; + aggregate.setName = record.setName; + aggregate.setIndex = record.setIndex; + aggregate.minCount = record.count; + aggregate.maxCount = record.count; + } else { + aggregate.minCount = std::min(aggregate.minCount, record.count); + aggregate.maxCount = std::max(aggregate.maxCount, record.count); + } + ++aggregate.occurrenceCount; + } +} + +void resetDescriptorWriteDiagnostics() { + DescriptorWriteRecorderState& state = descriptorWriteRecorderState(); + std::lock_guard lock(state.mutex); + state.nextSequence = 1; + state.updateCallCount = 0; + state.writeCount = 0; + state.droppedRecentWriteCount = 0; + state.recentWrites.clear(); + state.aggregates.clear(); +} + +DescriptorWriteDiagnosticsSnapshot descriptorWriteDiagnosticsSnapshot() { + DescriptorWriteRecorderState& state = descriptorWriteRecorderState(); + std::lock_guard lock(state.mutex); + DescriptorWriteDiagnosticsSnapshot snapshot; + snapshot.updateCallCount = state.updateCallCount; + snapshot.writeCount = state.writeCount; + snapshot.droppedRecentWriteCount = state.droppedRecentWriteCount; + snapshot.recentWriteLimit = kRecentDescriptorWriteLimit; + snapshot.recentWrites.assign(state.recentWrites.begin(), state.recentWrites.end()); + for (const auto& [key, aggregate] : state.aggregates) { + (void)key; + snapshot.aggregates.push_back(aggregate); + } + return snapshot; +} + } // namespace rtv diff --git a/src/rtv/DiagnosticImageExport.cpp b/src/rtv/DiagnosticImageExport.cpp index ec746a8..3b68894 100644 --- a/src/rtv/DiagnosticImageExport.cpp +++ b/src/rtv/DiagnosticImageExport.cpp @@ -2,6 +2,7 @@ #include "rtv/Buffer.h" #include "rtv/Check.h" +#include "rtv/GpuValidation.h" #include "rtv/Image.h" #include "rtv/PathTracerRenderer.h" #include "rtv/RendererDebug.h" @@ -81,7 +82,8 @@ bool DiagnosticImageExport::exportView( PathTracerRenderer& renderer, RendererDebugView view, const std::filesystem::path& outputPath, - uint32_t warmupFrames) { + uint32_t warmupFrames, + uint32_t maxOutputDimension) { (void)view; (void)warmupFrames; if (!initialized_) { @@ -100,8 +102,8 @@ bool DiagnosticImageExport::exportView( VkImageMemoryBarrier2 preBarrier{}; preBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; - preBarrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; - preBarrier.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; + preBarrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + preBarrier.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT; preBarrier.dstStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; preBarrier.dstAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; preBarrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; @@ -119,6 +121,7 @@ bool DiagnosticImageExport::exportView( preDependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; preDependency.imageMemoryBarrierCount = 1; preDependency.pImageMemoryBarriers = &preBarrier; + recordManualBarrierEscape("DiagnosticImageExport", "presentation_to_transfer_src", preDependency); vkCmdPipelineBarrier2(commandBuffer_, &preDependency); VkBufferImageCopy region{}; @@ -139,8 +142,8 @@ bool DiagnosticImageExport::exportView( postBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; postBarrier.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; postBarrier.srcAccessMask = VK_ACCESS_2_TRANSFER_READ_BIT; - postBarrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; - postBarrier.dstAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; + postBarrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; + postBarrier.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT; postBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; postBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; postBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; @@ -156,6 +159,7 @@ bool DiagnosticImageExport::exportView( postDependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; postDependency.imageMemoryBarrierCount = 1; postDependency.pImageMemoryBarriers = &postBarrier; + recordManualBarrierEscape("DiagnosticImageExport", "transfer_src_to_presentation", postDependency); vkCmdPipelineBarrier2(commandBuffer_, &postDependency); checkVk(vkEndCommandBuffer(commandBuffer_), "vkEndCommandBuffer(export)"); @@ -185,13 +189,43 @@ bool DiagnosticImageExport::exportView( std::filesystem::create_directories(dir); } + const unsigned char* pngData = data; + uint32_t pngWidth = extent_.width; + uint32_t pngHeight = extent_.height; + std::vector thumbnailPixels; + const uint32_t sourceMaxDimension = std::max(extent_.width, extent_.height); + if (maxOutputDimension > 0u && sourceMaxDimension > maxOutputDimension) { + pngWidth = std::max(1u, static_cast( + (static_cast(extent_.width) * maxOutputDimension + sourceMaxDimension / 2u) / sourceMaxDimension)); + pngHeight = std::max(1u, static_cast( + (static_cast(extent_.height) * maxOutputDimension + sourceMaxDimension / 2u) / sourceMaxDimension)); + thumbnailPixels.resize(static_cast(pngWidth) * pngHeight * 4u); + for (uint32_t y = 0; y < pngHeight; ++y) { + const uint32_t sourceY = std::min( + extent_.height - 1u, + static_cast((static_cast(y) * extent_.height + pngHeight / 2u) / pngHeight)); + for (uint32_t x = 0; x < pngWidth; ++x) { + const uint32_t sourceX = std::min( + extent_.width - 1u, + static_cast((static_cast(x) * extent_.width + pngWidth / 2u) / pngWidth)); + const size_t sourceIndex = (static_cast(sourceY) * extent_.width + sourceX) * 4u; + const size_t destIndex = (static_cast(y) * pngWidth + x) * 4u; + thumbnailPixels[destIndex + 0u] = data[sourceIndex + 0u]; + thumbnailPixels[destIndex + 1u] = data[sourceIndex + 1u]; + thumbnailPixels[destIndex + 2u] = data[sourceIndex + 2u]; + thumbnailPixels[destIndex + 3u] = data[sourceIndex + 3u]; + } + } + pngData = thumbnailPixels.data(); + } + int result = stbi_write_png( outputPath.string().c_str(), - static_cast(extent_.width), - static_cast(extent_.height), + static_cast(pngWidth), + static_cast(pngHeight), 4, - data, - static_cast(extent_.width * 4)); + pngData, + static_cast(pngWidth * 4u)); return result != 0; } @@ -303,6 +337,35 @@ std::vector DiagnosticImageExport::allExportViews() { RendererDebugView::MomentHistoryKindValid, RendererDebugView::DenoiserDiffuseRawVariance, RendererDebugView::DenoiserSpecularRawVariance, + RendererDebugView::NrdValidation, + RendererDebugView::NrdDiffuseConfidence, + RendererDebugView::NrdSpecularConfidence, + RendererDebugView::NrdRawConfidenceGradient, + RendererDebugView::NrdFilteredConfidenceGradient, + RendererDebugView::NrdConfidenceHistory, + RendererDebugView::PsrActiveMask, + RendererDebugView::PsrDepth, + RendererDebugView::PsrMotion, + RendererDebugView::PsrNormalRoughness, + RendererDebugView::PsrHitDistance, + RendererDebugView::PsrAlbedoF0, + RendererDebugView::PsrRayDirection, + RendererDebugView::DlssDepth, + RendererDebugView::DlssMotionVectors, + RendererDebugView::DlssInputColor, + RendererDebugView::DlssOutputColor, + RendererDebugView::DlssRrDiffuseAlbedo, + RendererDebugView::DlssRrSpecularAlbedo, + RendererDebugView::DlssRrNormals, + RendererDebugView::DlssRrRoughness, + RendererDebugView::DlssRrDiffuseHitDistance, + RendererDebugView::DlssRrSpecularHitDistance, + RendererDebugView::DlssRrReflectedAlbedo, + RendererDebugView::DlssRrDisocclusionMask, + RendererDebugView::DlssRrDiffuseRayDirection, + RendererDebugView::DlssRrSpecularRayDirection, + RendererDebugView::DlssRrDiffuseRayDirectionHitDistance, + RendererDebugView::DlssRrSpecularRayDirectionHitDistance, RendererDebugView::RestirPairwiseMis, RendererDebugView::RestirGiValidity, RendererDebugView::RestirGiAge, @@ -314,6 +377,12 @@ std::vector DiagnosticImageExport::allExportViews() { RendererDebugView::RestirGiHitDistance, RendererDebugView::RestirGiGrid, RendererDebugView::RestirGiPathClass, + RendererDebugView::RestirGiTarget, + RendererDebugView::RestirGiSourcePdf, + RendererDebugView::RestirGiWeightSum, + RendererDebugView::RestirGiM, + RendererDebugView::RestirGiConfidence, + RendererDebugView::RestirGiVisibility, RendererDebugView::AdaptiveDensityMap, RendererDebugView::AdaptiveSampleCount, RendererDebugView::AdaptiveUnsampledPixels, @@ -348,6 +417,7 @@ std::vector DiagnosticImageExport::allExportViews() { RendererDebugView::RestirDiReceiverPosition, RendererDebugView::RestirDiReceiverNormal, RendererDebugView::RestirDiLightVersion, + RendererDebugView::RestirDiLightMapStatus, RendererDebugView::RestirDiInitialReservoir, RendererDebugView::RestirDiTemporalReservoir, RendererDebugView::RestirDiSpatialReservoir, diff --git a/src/rtv/DiagnosticTools.cpp b/src/rtv/DiagnosticTools.cpp index e91e071..dafd176 100644 --- a/src/rtv/DiagnosticTools.cpp +++ b/src/rtv/DiagnosticTools.cpp @@ -1,6 +1,8 @@ #include "rtv/DiagnosticTools.h" +#include "rtv/DescriptorWriteDiagnostics.h" #include "rtv/RenderGraphDump.h" +#include "rtv/ShaderReflection.h" #include #include @@ -10,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +23,7 @@ #include #include #include +#include namespace rtv { namespace { @@ -695,6 +699,375 @@ std::vector readBinary(const std::filesystem::path& path) { std::istreambuf_iterator()); } +std::vector bytesToWords(const std::vector& bytes) { + if (bytes.size() % sizeof(uint32_t) != 0) { + throw std::runtime_error("SPIR-V byte size is not aligned to 32-bit words"); + } + std::vector words(bytes.size() / sizeof(uint32_t)); + if (!words.empty()) { + std::memcpy(words.data(), bytes.data(), bytes.size()); + } + return words; +} + +std::string descriptorTypeName(VkDescriptorType type) { + switch (type) { + case VK_DESCRIPTOR_TYPE_SAMPLER: return "sampler"; + case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: return "combined_image_sampler"; + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return "sampled_image"; + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return "storage_image"; + case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: return "uniform_texel_buffer"; + case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: return "storage_texel_buffer"; + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: return "uniform_buffer"; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: return "storage_buffer"; + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: return "uniform_buffer_dynamic"; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: return "storage_buffer_dynamic"; + case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: return "input_attachment"; + case VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR: return "acceleration_structure"; + default: return "unknown"; + } +} + +std::string shaderStageFlagsName(VkShaderStageFlags stages) { + std::vector names; + if ((stages & VK_SHADER_STAGE_VERTEX_BIT) != 0) { + names.push_back("vertex"); + } + if ((stages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0) { + names.push_back("fragment"); + } + if ((stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0) { + names.push_back("compute"); + } + if ((stages & VK_SHADER_STAGE_RAYGEN_BIT_KHR) != 0) { + names.push_back("raygen"); + } + if ((stages & VK_SHADER_STAGE_ANY_HIT_BIT_KHR) != 0) { + names.push_back("any_hit"); + } + if ((stages & VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR) != 0) { + names.push_back("closest_hit"); + } + if ((stages & VK_SHADER_STAGE_MISS_BIT_KHR) != 0) { + names.push_back("miss"); + } + if ((stages & VK_SHADER_STAGE_INTERSECTION_BIT_KHR) != 0) { + names.push_back("intersection"); + } + if ((stages & VK_SHADER_STAGE_CALLABLE_BIT_KHR) != 0) { + names.push_back("callable"); + } + if (names.empty()) { + return "none"; + } + std::ostringstream stream; + for (size_t i = 0; i < names.size(); ++i) { + if (i > 0) { + stream << "|"; + } + stream << names[i]; + } + return stream.str(); +} + +struct DescriptorExpectation { + const char* setName = ""; + uint32_t set = 0; + uint32_t binding = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_MAX_ENUM; + const char* role = ""; +}; + +std::vector rendererDescriptorExpectations() { + return { + {"raytracing_set_0", 0, 38, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReSTIR DI current reservoir storage buffer"}, + {"raytracing_set_0", 0, 39, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReSTIR DI previous reservoir storage buffer"}, + {"raytracing_set_0", 0, 40, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReGIR light BVH nodes storage buffer"}, + {"raytracing_set_0", 0, 42, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "path data storage buffer"}, + {"raytracing_set_0", 0, 43, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReSTIR GI current reservoir storage buffer"}, + {"raytracing_set_0", 0, 44, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReSTIR GI previous reservoir storage buffer"}, + {"raytracing_set_0", 0, 45, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, "ReSTIR GI spatial reservoir storage buffer"}, + {"bindless_texture_heap_set_2", 2, 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, "bindless combined image sampler heap"}, + }; +} + +json descriptorExpectationJson(const DescriptorExpectation& expectation) { + return { + {"set_name", expectation.setName}, + {"set", expectation.set}, + {"binding", expectation.binding}, + {"type", descriptorTypeName(expectation.type)}, + {"role", expectation.role}, + {"required", true}, + }; +} + +json descriptorExpectationsJson() { + json rows = json::array(); + for (const DescriptorExpectation& expectation : rendererDescriptorExpectations()) { + rows.push_back(descriptorExpectationJson(expectation)); + } + return rows; +} + +json reflectedBindingJson(const ReflectedBinding& binding) { + return { + {"set", binding.set}, + {"binding", binding.binding}, + {"count", binding.count}, + {"type", descriptorTypeName(binding.type)}, + {"stage_flags", binding.stages}, + {"stages", shaderStageFlagsName(binding.stages)}, + }; +} + +struct ReflectedBindingRecord { + ReflectedBinding binding; + std::string source; +}; + +struct ReflectedBindingSummary { + uint32_t set = 0; + uint32_t binding = 0; + uint32_t minCount = 0; + uint32_t maxCount = 0; + uint32_t occurrenceCount = 0; + std::set types; + std::set stages; + std::set sources; +}; + +json reflectedBindingSummaryJson(const ReflectedBindingSummary& summary) { + json sources = json::array(); + uint32_t sourceCount = 0; + for (const std::string& source : summary.sources) { + if (sourceCount >= 16) { + break; + } + sources.push_back(source); + ++sourceCount; + } + return { + {"set", summary.set}, + {"binding", summary.binding}, + {"min_count", summary.minCount}, + {"max_count", summary.maxCount}, + {"occurrence_count", summary.occurrenceCount}, + {"types", summary.types}, + {"stages", summary.stages}, + {"sources", sources}, + {"truncated_sources", summary.sources.size() > sources.size()}, + }; +} + +json descriptorReflectionAuditJson( + const std::vector& reflectedBindings, + bool reflectionPassed) { + std::map, ReflectedBindingSummary> summaries; + std::set reflectedSets; + for (const ReflectedBindingRecord& record : reflectedBindings) { + reflectedSets.insert(record.binding.set); + auto& summary = summaries[{record.binding.set, record.binding.binding}]; + if (summary.occurrenceCount == 0) { + summary.set = record.binding.set; + summary.binding = record.binding.binding; + summary.minCount = record.binding.count; + summary.maxCount = record.binding.count; + } else { + summary.minCount = std::min(summary.minCount, record.binding.count); + summary.maxCount = std::max(summary.maxCount, record.binding.count); + } + ++summary.occurrenceCount; + summary.types.insert(descriptorTypeName(record.binding.type)); + summary.stages.insert(shaderStageFlagsName(record.binding.stages)); + summary.sources.insert(record.source); + } + + json reflectedBindingRows = json::array(); + for (const auto& [key, summary] : summaries) { + (void)key; + reflectedBindingRows.push_back(reflectedBindingSummaryJson(summary)); + } + + json expectations = json::array(); + json failures = json::array(); + for (const DescriptorExpectation& expectation : rendererDescriptorExpectations()) { + json matches = json::array(); + bool hasBinding = false; + bool hasTypeMatch = false; + for (const ReflectedBindingRecord& record : reflectedBindings) { + if (record.binding.set != expectation.set || record.binding.binding != expectation.binding) { + continue; + } + hasBinding = true; + const bool typeMatches = record.binding.type == expectation.type; + hasTypeMatch = hasTypeMatch || typeMatches; + matches.push_back({ + {"source", record.source}, + {"count", record.binding.count}, + {"type", descriptorTypeName(record.binding.type)}, + {"stages", shaderStageFlagsName(record.binding.stages)}, + {"type_matches", typeMatches}, + }); + } + const bool passed = hasBinding && hasTypeMatch; + json expectationJson = descriptorExpectationJson(expectation); + expectationJson["reflected"] = hasBinding; + expectationJson["type_matched"] = hasTypeMatch; + expectationJson["match_count"] = matches.size(); + expectationJson["matches"] = matches; + expectationJson["passed"] = passed; + expectations.push_back(expectationJson); + if (!passed) { + failures.push_back({ + {"set_name", expectation.setName}, + {"set", expectation.set}, + {"binding", expectation.binding}, + {"expected_type", descriptorTypeName(expectation.type)}, + {"role", expectation.role}, + {"reason", hasBinding ? "descriptor type mismatch" : "descriptor binding not reflected by compiled shaders"}, + }); + } + } + + return { + {"schema_version", 1}, + {"reflection_passed", reflectionPassed}, + {"reflected_binding_occurrence_count", reflectedBindings.size()}, + {"reflected_unique_binding_count", summaries.size()}, + {"reflected_descriptor_set_count", reflectedSets.size()}, + {"reflected_descriptor_bindings", reflectedBindingRows}, + {"known_set_expectation_count", rendererDescriptorExpectations().size()}, + {"known_set_expectations", expectations}, + {"failure_count", failures.size()}, + {"failures", failures}, + {"passed", reflectionPassed && !reflectedBindings.empty() && failures.empty()}, + {"notes", json::array({ + "Descriptor count 0 is used by SPIR-V reflection for runtime-sized descriptor arrays.", + "This audit verifies the renderer's documented ray tracing and bindless descriptor contracts against compiled SPIR-V.", + })}, + }; +} + +json descriptorWriteRecordJson(const DescriptorWriteDiagnosticRecord& record) { + return { + {"sequence", record.sequence}, + {"descriptor_set", record.descriptorSet}, + {"descriptor_set_layout", record.descriptorSetLayout}, + {"binding", record.binding}, + {"array_element", record.arrayElement}, + {"count", record.count}, + {"type", descriptorTypeName(record.type)}, + {"kind", record.kind}, + {"source", record.source}, + {"owner", record.owner}, + {"pass", record.pass}, + {"set_name", record.setName}, + {"set_index", record.setIndex}, + }; +} + +json descriptorWriteAggregateJson(const DescriptorWriteDiagnosticAggregate& aggregate) { + return { + {"descriptor_set_layout", aggregate.descriptorSetLayout}, + {"binding", aggregate.binding}, + {"type", descriptorTypeName(aggregate.type)}, + {"kind", aggregate.kind}, + {"source", aggregate.source}, + {"owner", aggregate.owner}, + {"pass", aggregate.pass}, + {"set_name", aggregate.setName}, + {"set_index", aggregate.setIndex}, + {"min_count", aggregate.minCount}, + {"max_count", aggregate.maxCount}, + {"occurrence_count", aggregate.occurrenceCount}, + }; +} + +json descriptorWriteDiagnosticsJson(const DescriptorWriteDiagnosticsSnapshot& snapshot) { + json recentWrites = json::array(); + for (const DescriptorWriteDiagnosticRecord& record : snapshot.recentWrites) { + recentWrites.push_back(descriptorWriteRecordJson(record)); + } + json aggregates = json::array(); + for (const DescriptorWriteDiagnosticAggregate& aggregate : snapshot.aggregates) { + aggregates.push_back(descriptorWriteAggregateJson(aggregate)); + } + return { + {"schema_version", 1}, + {"update_call_count", snapshot.updateCallCount}, + {"write_count", snapshot.writeCount}, + {"unique_write_count", snapshot.aggregates.size()}, + {"recent_write_limit", snapshot.recentWriteLimit}, + {"dropped_recent_write_count", snapshot.droppedRecentWriteCount}, + {"recent_writes", recentWrites}, + {"unique_writes", aggregates}, + }; +} + +json actualDescriptorWriteAuditJson(const DescriptorWriteDiagnosticsSnapshot& snapshot) { + json expectations = json::array(); + json failures = json::array(); + for (const DescriptorExpectation& expectation : rendererDescriptorExpectations()) { + bool matched = false; + uint64_t matchCount = 0; + uint64_t occurrenceCount = 0; + json matches = json::array(); + for (const DescriptorWriteDiagnosticAggregate& aggregate : snapshot.aggregates) { + if (aggregate.binding != expectation.binding || + aggregate.type != expectation.type || + aggregate.setName != expectation.setName) { + continue; + } + matched = true; + ++matchCount; + occurrenceCount += aggregate.occurrenceCount; + matches.push_back(descriptorWriteAggregateJson(aggregate)); + } + + json expectationJson = descriptorExpectationJson(expectation); + expectationJson["actual_write_matched"] = matched; + expectationJson["match_count"] = matchCount; + expectationJson["occurrence_count"] = occurrenceCount; + expectationJson["matches"] = matches; + expectationJson["passed"] = matched; + expectations.push_back(expectationJson); + if (!matched) { + failures.push_back({ + {"set_name", expectation.setName}, + {"set", expectation.set}, + {"binding", expectation.binding}, + {"expected_type", descriptorTypeName(expectation.type)}, + {"role", expectation.role}, + {"reason", "expected descriptor binding was not observed in actual descriptor writes"}, + }); + } + } + return { + {"schema_version", 1}, + {"write_count", snapshot.writeCount}, + {"unique_write_count", snapshot.aggregates.size()}, + {"known_set_expectation_count", rendererDescriptorExpectations().size()}, + {"known_set_expectations", expectations}, + {"failure_count", failures.size()}, + {"failures", failures}, + {"passed", snapshot.writeCount > 0 && failures.empty()}, + {"notes", json::array({ + "This audit is based on actual Vulkan descriptor updates recorded through DescriptorWriter and BindlessTextureHeap.", + "Expected bindings must match explicit owner-provided descriptor set names.", + })}, + }; +} + +json reflectedPushConstantJson(const ReflectedPushConstant& pushConstant) { + return { + {"offset", pushConstant.offset}, + {"size", pushConstant.size}, + {"stage_flags", pushConstant.stages}, + {"stages", shaderStageFlagsName(pushConstant.stages)}, + }; +} + json renderGraphOrEmpty(const std::optional& path) { if (!path.has_value() || !std::filesystem::exists(*path)) { return json::object(); @@ -824,14 +1197,35 @@ ImageDiffMetrics compareImages( int compareImageCommand( const std::filesystem::path& baselinePath, const std::filesystem::path& currentPath, - const std::optional& diffOutputPath) { + const std::optional& diffOutputPath, + const ImageCompareThresholds& thresholds) { const ImageDiffMetrics metrics = compareImages(baselinePath, currentPath, diffOutputPath); json result = imageMetricsJson(metrics); if (diffOutputPath.has_value()) { result["diff_image"] = diffOutputPath->string(); } + std::vector failures; + if (thresholds.minPsnr.has_value() && metrics.psnr < *thresholds.minPsnr) { + failures.push_back("psnr below threshold"); + } + if (thresholds.minSsim.has_value() && metrics.ssim < *thresholds.minSsim) { + failures.push_back("ssim below threshold"); + } + if (thresholds.maxChangedPixelPercentage.has_value() && + metrics.changedPixelPercentage > *thresholds.maxChangedPixelPercentage) { + failures.push_back("changed pixel percentage above threshold"); + } + result["status"] = failures.empty() ? "pass" : "fail"; + result["failures"] = failures; + result["thresholds"] = { + {"min_psnr", thresholds.minPsnr.has_value() ? json(*thresholds.minPsnr) : json(nullptr)}, + {"min_ssim", thresholds.minSsim.has_value() ? json(*thresholds.minSsim) : json(nullptr)}, + {"max_changed_pixel_percentage", thresholds.maxChangedPixelPercentage.has_value() + ? json(*thresholds.maxChangedPixelPercentage) + : json(nullptr)}, + }; std::cout << result.dump(2) << "\n"; - return 0; + return failures.empty() ? 0 : 1; } SequenceComparisonReport compareImageSequences( @@ -1389,6 +1783,13 @@ void writeShaderReport( const std::filesystem::path& shaderSourceDir, const std::filesystem::path& shaderOutputDir) { json shaders = json::array(); + json reflectionFailures = json::array(); + uint32_t shaderCount = 0; + uint32_t spirvPresentCount = 0; + uint32_t reflectedShaderCount = 0; + uint32_t totalDescriptorBindingCount = 0; + uint32_t totalPushConstantRangeCount = 0; + std::vector reflectedBindingRecords; if (std::filesystem::exists(shaderSourceDir)) { for (const auto& entry : std::filesystem::directory_iterator(shaderSourceDir)) { if (!entry.is_regular_file()) { @@ -1399,6 +1800,7 @@ void writeShaderReport( if (ext != ".rgen" && ext != ".rchit" && ext != ".rahit" && ext != ".rmiss" && ext != ".comp" && ext != ".vert" && ext != ".frag") { continue; } + ++shaderCount; const std::filesystem::path spirv = shaderOutputDir / (source.filename().string() + ".spv"); json shader = { {"source", source.string()}, @@ -1412,16 +1814,80 @@ void writeShaderReport( {"compile_time_ms", nullptr}, {"hash", nullptr}, {"spirv_size_bytes", 0}, + {"reflection", { + {"available", false}, + {"descriptor_binding_count", 0}, + {"push_constant_range_count", 0}, + {"bindings", json::array()}, + {"descriptor_sets", json::object()}, + {"push_constants", json::array()}, + {"error", nullptr}, + }}, }; if (std::filesystem::exists(spirv)) { const std::vector bytes = readBinary(spirv); shader["hash"] = hexHashBytes(bytes); shader["spirv_size_bytes"] = bytes.size(); + ++spirvPresentCount; + try { + const ShaderReflectionData reflection = ShaderReflection::reflect(bytesToWords(bytes)); + json bindingRows = json::array(); + json descriptorSets = json::object(); + for (const ReflectedBinding& binding : reflection.bindings) { + reflectedBindingRecords.push_back({binding, source.string()}); + json bindingRow = reflectedBindingJson(binding); + bindingRows.push_back(bindingRow); + descriptorSets[std::to_string(binding.set)].push_back(std::move(bindingRow)); + } + json pushRows = json::array(); + for (const ReflectedPushConstant& pushConstant : reflection.pushConstants) { + pushRows.push_back(reflectedPushConstantJson(pushConstant)); + } + shader["reflection"] = { + {"available", true}, + {"stage", shaderStageFlagsName(static_cast(reflection.stage))}, + {"stage_flags", static_cast(reflection.stage)}, + {"descriptor_binding_count", reflection.bindings.size()}, + {"push_constant_range_count", reflection.pushConstants.size()}, + {"bindings", std::move(bindingRows)}, + {"descriptor_sets", std::move(descriptorSets)}, + {"push_constants", std::move(pushRows)}, + {"error", nullptr}, + }; + ++reflectedShaderCount; + totalDescriptorBindingCount += static_cast(reflection.bindings.size()); + totalPushConstantRangeCount += static_cast(reflection.pushConstants.size()); + } catch (const std::exception& e) { + shader["reflection"]["error"] = e.what(); + reflectionFailures.push_back({ + {"source", source.string()}, + {"spirv", spirv.string()}, + {"error", e.what()}, + }); + } } shaders.push_back(shader); } } - writeJsonFile(outputPath, {{"shaders", shaders}}); + const json reflectionValidation = { + {"schema_version", 1}, + {"shader_count", shaderCount}, + {"spirv_present_count", spirvPresentCount}, + {"reflected_shader_count", reflectedShaderCount}, + {"descriptor_binding_count", totalDescriptorBindingCount}, + {"push_constant_range_count", totalPushConstantRangeCount}, + {"failure_count", reflectionFailures.size()}, + {"failures", reflectionFailures}, + {"passed", shaderCount > 0 && spirvPresentCount > 0 && reflectedShaderCount == spirvPresentCount && reflectionFailures.empty()}, + }; + const json descriptorAudit = descriptorReflectionAuditJson( + reflectedBindingRecords, + reflectionValidation.value("passed", false)); + writeJsonFile(outputPath, { + {"shaders", shaders}, + {"reflection_validation", reflectionValidation}, + {"descriptor_audit", descriptorAudit}, + }); } void writeBindingsReport( @@ -1453,7 +1919,14 @@ void writeBindingsReport( {"binding_0", "full bindless combined image sampler heap"}, }}, }; - writeJsonFile(outputPath, {{"passes", passes}, {"known_descriptor_sets", knownSets}}); + const DescriptorWriteDiagnosticsSnapshot descriptorWrites = descriptorWriteDiagnosticsSnapshot(); + writeJsonFile(outputPath, { + {"passes", passes}, + {"known_descriptor_sets", knownSets}, + {"known_descriptor_binding_expectations", descriptorExpectationsJson()}, + {"descriptor_write_diagnostics", descriptorWriteDiagnosticsJson(descriptorWrites)}, + {"actual_descriptor_write_audit", actualDescriptorWriteAuditJson(descriptorWrites)}, + }); } void writeCrashDumpPackage( @@ -1575,7 +2048,13 @@ int checkBudget(const std::filesystem::path& budgetPath, const ProfileReport& pr } if (budget.contains("validation_error_count")) { const uint64_t maxValidationErrors = bytesFromBudgetValue(budget["validation_error_count"]); - if (profile.validationErrorCount > maxValidationErrors) { + if (!profile.validationEnabled) { + failures.push_back({ + {"metric", "validation_enabled"}, + {"actual", false}, + {"budget", true}, + }); + } else if (profile.validationErrorCount > maxValidationErrors) { failures.push_back({ {"metric", "validation_error_count"}, {"actual", profile.validationErrorCount}, @@ -1651,6 +2130,111 @@ int checkBudget(const std::filesystem::path& budgetPath, const ProfileReport& pr "require_gpu_crash_dumps_enabled", profile.nvidiaIntegrations.gpuCrashDumps.requested && profile.nvidiaIntegrations.gpuCrashDumps.enabled, "nvidia_integrations.gpu_crash_dumps.enabled"); + + auto counterAt = [](const std::vector& counters, size_t index) -> uint64_t { + return index < counters.size() ? counters[index] : 0ull; + }; + auto addReservoirContractFailure = [&failures]( + const char* metric, + bool active, + bool checked, + uint64_t sourcePdfViolations, + uint64_t targetPdfViolations, + uint64_t sourcePdfParityMismatches, + uint64_t targetPdfParityMismatches, + uint64_t nonFiniteViolations, + uint64_t budgetLimit) { + const uint64_t violations = + sourcePdfViolations + + targetPdfViolations + + sourcePdfParityMismatches + + targetPdfParityMismatches + + nonFiniteViolations; + if ((active && !checked) || violations > budgetLimit) { + failures.push_back({ + {"metric", metric}, + {"active", active}, + {"checked", checked}, + {"invalid_source_pdf_count", sourcePdfViolations}, + {"invalid_target_pdf_count", targetPdfViolations}, + {"source_pdf_parity_mismatch_count", sourcePdfParityMismatches}, + {"target_pdf_parity_mismatch_count", targetPdfParityMismatches}, + {"non_finite_count", nonFiniteViolations}, + {"actual", violations}, + {"budget", budgetLimit}, + }); + } + }; + const bool requireAnyReservoirContract = + budget.value("require_restir_reservoir_contract_validation", false); + const bool diContractActive = + profile.settings.restirDiMode == RestirDiMode::Production || + profile.settings.restirDiMode == RestirDiMode::ReferenceValidation || + profile.settings.restirDiMode == RestirDiMode::HybridCompare; + const bool diContractChecked = diContractActive && !profile.restirDiCounters.empty(); + const uint64_t diContractInvalidSourcePdf = 0ull; + const uint64_t diContractInvalidTargetPdf = counterAt(profile.restirDiCounters, 63); + const uint64_t diSourcePdfParityMismatch = 0ull; + const uint64_t diTargetPdfParityMismatch = 0ull; + const uint64_t diContractNonFinite = 0ull; + const bool giContractActive = + !profile.settings.wavefrontFinalOutputEnabled && + (profile.settings.restirGiMode == RestirGiMode::Production || + profile.settings.restirGiMode == RestirGiMode::ReferenceValidation); + const bool giContractChecked = giContractActive && !profile.restirGiCounters.empty(); + const uint64_t giContractInvalidSourcePdf = counterAt(profile.restirGiCounters, 48); + const uint64_t giContractInvalidTargetPdf = counterAt(profile.restirGiCounters, 49); + const uint64_t giSourcePdfParityMismatch = counterAt(profile.restirGiCounters, 50); + const uint64_t giTargetPdfParityMismatch = counterAt(profile.restirGiCounters, 51); + const uint64_t giContractNonFinite = counterAt(profile.restirGiCounters, 45); + if (requireAnyReservoirContract || budget.value("require_restir_di_reservoir_contract_validation", false)) { + addReservoirContractFailure( + "restir_di.reservoir_contract_validation", + diContractActive, + diContractChecked, + diContractInvalidSourcePdf, + diContractInvalidTargetPdf, + diSourcePdfParityMismatch, + diTargetPdfParityMismatch, + diContractNonFinite, + 0ull); + } + if (requireAnyReservoirContract || budget.value("require_restir_gi_reservoir_contract_validation", false)) { + addReservoirContractFailure( + "restir_gi.reservoir_contract_validation", + giContractActive, + giContractChecked, + giContractInvalidSourcePdf, + giContractInvalidTargetPdf, + giSourcePdfParityMismatch, + giTargetPdfParityMismatch, + giContractNonFinite, + 0ull); + } + if (budget.contains("max_restir_di_reservoir_contract_violation_count")) { + addReservoirContractFailure( + "restir_di.reservoir_contract_validation.violation_count", + diContractActive, + diContractChecked, + diContractInvalidSourcePdf, + diContractInvalidTargetPdf, + diSourcePdfParityMismatch, + diTargetPdfParityMismatch, + diContractNonFinite, + bytesFromBudgetValue(budget["max_restir_di_reservoir_contract_violation_count"])); + } + if (budget.contains("max_restir_gi_reservoir_contract_violation_count")) { + addReservoirContractFailure( + "restir_gi.reservoir_contract_validation.violation_count", + giContractActive, + giContractChecked, + giContractInvalidSourcePdf, + giContractInvalidTargetPdf, + giSourcePdfParityMismatch, + giTargetPdfParityMismatch, + giContractNonFinite, + bytesFromBudgetValue(budget["max_restir_gi_reservoir_contract_violation_count"])); + } if (budget.value("require_no_streaming_uploads", false)) { const uint64_t streamingUploads = profile.textureDiagnostics.is_object() && diff --git a/src/rtv/EditorDockspace.cpp b/src/rtv/EditorDockspace.cpp index d65ea04..ddb118e 100644 --- a/src/rtv/EditorDockspace.cpp +++ b/src/rtv/EditorDockspace.cpp @@ -12,12 +12,17 @@ #include #include #include +#include +#include +#include #include namespace rtv { namespace { +constexpr int kEditorLayoutVersion = 4; + std::string activeSceneTitle(const EditorRuntimeState& state) { std::filesystem::path path; if (state.scenePath != nullptr && state.scenePath->has_value()) { @@ -75,6 +80,41 @@ void drawEditorSaveStateLabel(const EditorRuntimeState& state, const std::string } } +std::filesystem::path layoutVersionPath(const std::filesystem::path& layoutPath) { + std::filesystem::path versionPath = layoutPath; + versionPath += ".version"; + return versionPath; +} + +int readLayoutVersion(const std::filesystem::path& layoutPath) { + std::ifstream file(layoutVersionPath(layoutPath)); + int version = 0; + if (file >> version) { + return version; + } + return 0; +} + +void writeLayoutVersion(const std::filesystem::path& layoutPath) { + std::ofstream file(layoutVersionPath(layoutPath)); + if (file.is_open()) { + file << kEditorLayoutVersion << '\n'; + } +} + +void backupLegacyLayout(const std::filesystem::path& layoutPath) { + if (layoutPath.empty() || !std::filesystem::exists(layoutPath)) { + return; + } + std::filesystem::path backupPath = layoutPath; + backupPath += ".pre-ui-redesign.bak"; + if (std::filesystem::exists(backupPath)) { + return; + } + std::error_code ec; + std::filesystem::copy_file(layoutPath, backupPath, std::filesystem::copy_options::none, ec); +} + void menuItemTooltip(const char* description, const char* disabledReason = nullptr) { if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { return; @@ -523,6 +563,8 @@ void EditorDockspace::begin(EditorRuntimeState& state, EditorPanelVisibility& vi ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground; ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); @@ -532,7 +574,13 @@ void EditorDockspace::begin(EditorRuntimeState& state, EditorPanelVisibility& vi ImGui::PopStyleVar(3); const ImGuiID dockspaceId = ImGui::GetID("EditorDockspace"); - ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + drawTopToolbar(state, visibility, requests); + ImVec2 dockspaceSize = ImGui::GetContentRegionAvail(); + // DockSpace advances the parent cursor by ItemSpacing before the status bar. + const float statusBarReservation = EditorUiMetric::statusBarHeight + ImGui::GetStyle().ItemSpacing.y; + dockspaceSize.y = std::max(1.0f, dockspaceSize.y - statusBarReservation); + ImGui::DockSpace(dockspaceId, dockspaceSize, ImGuiDockNodeFlags_None); + drawBottomStatusBar(state); if (layoutResetRequested_) { buildDefaultLayout(); layoutResetRequested_ = false; @@ -597,6 +645,7 @@ void EditorDockspace::saveLayout() const { std::filesystem::create_directories(parent, ec); } ImGui::SaveIniSettingsToDisk(profilePath_.string().c_str()); + writeLayoutVersion(profilePath_); } } @@ -610,8 +659,13 @@ void EditorDockspace::setProfileFile(const std::filesystem::path& layoutPath) { void EditorDockspace::loadLayout() { if (!profilePath_.empty() && std::filesystem::exists(profilePath_)) { - ImGui::LoadIniSettingsFromDisk(profilePath_.string().c_str()); - layoutResetRequested_ = false; + if (readLayoutVersion(profilePath_) == kEditorLayoutVersion) { + ImGui::LoadIniSettingsFromDisk(profilePath_.string().c_str()); + layoutResetRequested_ = false; + } else { + backupLegacyLayout(profilePath_); + layoutResetRequested_ = true; + } } } @@ -619,23 +673,36 @@ void EditorDockspace::buildDefaultLayout() { ImGuiID dockspaceId = ImGui::GetID("EditorDockspace"); ImGui::DockBuilderRemoveNode(dockspaceId); ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace); - ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->WorkSize); + ImVec2 dockSize = ImGui::GetMainViewport()->WorkSize; + dockSize.y = std::max(1.0f, dockSize.y - EditorUiMetric::mainToolbarHeight - EditorUiMetric::statusBarHeight); + ImGui::DockBuilderSetNodeSize(dockspaceId, dockSize); + + const float workWidth = std::max(1.0f, dockSize.x); + const float workHeight = std::max(1.0f, dockSize.y); + const float leftWidth = std::clamp(workWidth * EditorUiMetric::dockLeftPanelRatio, 260.0f, 340.0f); + const float rightWidth = std::clamp(workWidth * EditorUiMetric::dockRightPanelRatio, 380.0f, 480.0f); + const float bottomHeight = std::clamp(workHeight * EditorUiMetric::dockBottomPanelRatio, 250.0f, 330.0f); ImGuiID center = dockspaceId; + ImGuiID left = 0; ImGuiID right = 0; ImGuiID bottom = 0; - ImGuiID rightBottom = 0; - ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, EditorUiMetric::dockRightPanelRatio, &right, ¢er); - ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, EditorUiMetric::dockBottomPanelRatio, &bottom, ¢er); - ImGui::DockBuilderSplitNode(right, ImGuiDir_Down, EditorUiMetric::dockRightInspectorRatio, &rightBottom, &right); + ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, leftWidth / workWidth, &left, ¢er); + ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, rightWidth / std::max(1.0f, workWidth - leftWidth), &right, ¢er); + ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, bottomHeight / workHeight, &bottom, ¢er); + + ImGuiID hierarchy = left; + ImGuiID sceneExplorer = 0; + ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.30f, &sceneExplorer, &hierarchy); ImGui::DockBuilderDockWindow("Scene", center); - ImGui::DockBuilderDockWindow("Hierarchy", right); + ImGui::DockBuilderDockWindow("Hierarchy", hierarchy); + ImGui::DockBuilderDockWindow("Scene Explorer", sceneExplorer); + ImGui::DockBuilderDockWindow("Inspector", right); ImGui::DockBuilderDockWindow("Render Settings", right); - ImGui::DockBuilderDockWindow("Inspector", rightBottom); - ImGui::DockBuilderDockWindow("Content", bottom); - ImGui::DockBuilderDockWindow("Timeline", bottom); ImGui::DockBuilderDockWindow("Log", bottom); + ImGui::DockBuilderDockWindow("Console", bottom); + ImGui::DockBuilderDockWindow("Content", bottom); ImGui::DockBuilderFinish(dockspaceId); } @@ -897,15 +964,178 @@ void EditorDockspace::executeCommand(EditorCommandId id, EditorRuntimeState& sta } } +void EditorDockspace::drawTopToolbar(EditorRuntimeState& state, EditorPanelVisibility& visibility, EditorRequests& requests) { + const RendererSettings& settings = state.renderer.settings(); + const float toolbarHeight = EditorUiMetric::mainToolbarHeight; + ImGui::PushStyleColor(ImGuiCol_ChildBg, editorToolbarBgColor()); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 0.0f)); + ImGui::BeginChild("EditorTopToolbar", ImVec2(0.0f, toolbarHeight), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec2 min = ImGui::GetWindowPos(); + const ImVec2 max(min.x + ImGui::GetWindowWidth(), min.y + toolbarHeight); + dl->AddRectFilled(min, max, ImGui::GetColorU32(editorToolbarBgColor())); + dl->AddLine(ImVec2(min.x, max.y - 1.0f), ImVec2(max.x, max.y - 1.0f), ImGui::GetColorU32(editorToolbarBorderColor())); + + ImGui::SetCursorPosY((toolbarHeight - EditorUiMetric::toolbarButtonHeight) * 0.5f); + auto commandButton = [&](const char* id, EditorGlyphIcon icon, EditorCommandId command, const char* tooltip, bool active = false) { + if (editorToolbarIconButton(id, icon, active)) { + executeCommand(command, state, visibility, requests); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("%s", tooltip); + } + ImGui::SameLine(); + }; + + commandButton("TopSaveAll", EditorGlyphIcon::Save, EditorCommandId::SaveAll, "Save all"); + commandButton("TopUndo", EditorGlyphIcon::Undo, EditorCommandId::Undo, "Undo", state.undoStack != nullptr && state.undoStack->canUndo()); + commandButton("TopRedo", EditorGlyphIcon::Redo, EditorCommandId::Redo, "Redo", state.undoStack != nullptr && state.undoStack->canRedo()); + + auto viewportCommandButton = [&](const char* id, EditorGlyphIcon icon, EditorCommandId command, const char* tooltipText, bool active) { + if (editorToolbarIconButton(id, icon, active)) { + requests.viewportCommand = command; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + const std::string shortcut = editorCommandShortcutDisplay(command, state.editorPrefs); + ImGui::SetTooltip(shortcut.empty() ? "%s" : "%s %s", tooltipText, shortcut.c_str()); + } + ImGui::SameLine(); + }; + + ImGui::Dummy(ImVec2(EditorUiMetric::toolbarGroupGap, 1.0f)); + ImGui::SameLine(); + viewportCommandButton("TopViewportSelect", EditorGlyphIcon::Select, EditorCommandId::ViewportSelect, "Select", state.viewport.activeTool < 0); + viewportCommandButton("TopViewportMove", EditorGlyphIcon::Move, EditorCommandId::ViewportMove, "Move", state.viewport.activeTool == 0); + viewportCommandButton("TopViewportRotate", EditorGlyphIcon::Rotate, EditorCommandId::ViewportRotate, "Rotate", state.viewport.activeTool == 1); + viewportCommandButton("TopViewportScale", EditorGlyphIcon::Scale, EditorCommandId::ViewportScale, "Scale", state.viewport.activeTool == 2); + viewportCommandButton( + "TopViewportSpace", + state.viewport.localTransform ? EditorGlyphIcon::LocalSpace : EditorGlyphIcon::WorldSpace, + EditorCommandId::ViewportToggleLocal, + state.viewport.localTransform ? "Local coordinates" : "World coordinates", + state.viewport.localTransform); + viewportCommandButton("TopViewportSnap", EditorGlyphIcon::Snap, EditorCommandId::ViewportToggleSnap, "Snapping", state.viewport.snapEnabled); + + ImGui::Dummy(ImVec2(EditorUiMetric::toolbarGroupGap, 1.0f)); + ImGui::SameLine(); + if (editorToolbarTextButton("TopRenderViewport", EditorGlyphIcon::Play, "Render", false)) { + executeCommand(EditorCommandId::RenderCurrentViewport, state, visibility, requests); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("Render current viewport"); + } + ImGui::SameLine(); + if (editorToolbarTextButton("TopRenderMode", EditorGlyphIcon::Render, settings.pathTracingEnabled ? "Path Tracing" : "Preview", settings.pathTracingEnabled)) { + ImGui::OpenPopup("TopRenderModePopup"); + } + if (ImGui::BeginPopup("TopRenderModePopup")) { + RendererSettings next = settings; + bool changed = false; + changed |= ImGui::Checkbox("Path Tracing", &next.pathTracingEnabled); + changed |= ImGui::Checkbox("Denoiser", &next.denoiserEnabled); + changed |= ImGui::Checkbox("TAA", &next.taaEnabled); + changed |= ImGui::Checkbox("Limit to 1 SPP", &next.limitSamplesPerPixel); + if (changed) { + requestSettings(requests, next); + } + ImGui::EndPopup(); + } + + const char* readyLabel = state.sceneLoadRunning ? "Loading" : "Ready"; + std::ostringstream spp; + spp << state.renderer.sampleCount() << " spp"; + std::ostringstream fps; + if (state.cpuFrameMs > 0.0f) { + fps << std::fixed << std::setprecision(0) << (1000.0f / std::max(0.001f, state.cpuFrameMs)) << " FPS"; + } else { + fps << "-- FPS"; + } + + const float itemSpacing = ImGui::GetStyle().ItemSpacing.x; + const float compactRightWidth = editorStatusPillSize("PT").x + editorStatusPillSize(readyLabel).x + editorToolbarTextButtonSize("View").x + itemSpacing * 2.0f; + const float detailedRightWidth = compactRightWidth + editorStatusPillSize(spp.str().c_str()).x + editorStatusPillSize(fps.str().c_str()).x + itemSpacing * 2.0f; + ImGui::SameLine(); + const float minimumRightX = ImGui::GetCursorPosX() + EditorUiMetric::toolbarGroupGap; + const float rightEdge = ImGui::GetWindowContentRegionMax().x; + const bool compactStatus = rightEdge - minimumRightX < detailedRightWidth; + const float rightWidth = compactStatus ? compactRightWidth : detailedRightWidth; + const float rightX = std::max(minimumRightX, rightEdge - rightWidth); + ImGui::SetCursorPosX(rightX); + + editorStatusPill("PT", settings.pathTracingEnabled, EditorGlyphIcon::Render); + ImGui::SameLine(); + editorStatusPill(readyLabel, !state.sceneLoadRunning, EditorGlyphIcon::Stats); + ImGui::SameLine(); + if (!compactStatus) { + editorStatusPill(spp.str().c_str(), false, EditorGlyphIcon::Stats); + ImGui::SameLine(); + editorStatusPill(fps.str().c_str(), false, EditorGlyphIcon::Stats); + ImGui::SameLine(); + } + if (editorToolbarTextButton("TopViewMenu", EditorGlyphIcon::ViewSettings, "View", false)) { + ImGui::OpenPopup("TopViewPopup"); + } + if (ImGui::BeginPopup("TopViewPopup")) { + ImGui::MenuItem("Hierarchy", nullptr, &visibility.sceneHierarchy); + ImGui::MenuItem("Content", nullptr, &visibility.assetBrowser); + ImGui::MenuItem("Render Settings", nullptr, &visibility.renderSettings); + ImGui::MenuItem("Inspector", nullptr, &visibility.inspector); + ImGui::MenuItem("Log", nullptr, &visibility.log); + ImGui::MenuItem("Timeline", nullptr, &visibility.timeline); + ImGui::EndPopup(); + } + + ImGui::EndChild(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); +} + +void EditorDockspace::drawBottomStatusBar(EditorRuntimeState& state) { + ImGui::PushStyleColor(ImGuiCol_ChildBg, editorToolbarBgColor()); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 3.0f)); + ImGui::BeginChild("EditorBottomStatusBar", ImVec2(0.0f, EditorUiMetric::statusBarHeight), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec2 min = ImGui::GetWindowPos(); + const ImVec2 max(min.x + ImGui::GetWindowWidth(), min.y + EditorUiMetric::statusBarHeight); + dl->AddLine(ImVec2(min.x, min.y), ImVec2(max.x, min.y), ImGui::GetColorU32(editorToolbarBorderColor())); + + const std::string sceneTitle = activeSceneTitle(state); + const std::string saveLabel = editorSaveStateLabel(state); + ImGui::TextDisabled("Ready"); + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + ImGui::Text("Scene: %s", sceneTitle.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + drawEditorSaveStateLabel(state, saveLabel); + + const float rightWidth = 360.0f; + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX() + 10.0f, ImGui::GetWindowContentRegionMax().x - rightWidth)); + ImGui::TextDisabled("Project: %s", state.project != nullptr ? "Open" : "None"); + ImGui::SameLine(); + ImGui::TextDisabled("| Vulkan | PT"); + + ImGui::EndChild(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); +} + void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibility& visibility, EditorRequests& requests) { if (!ImGui::BeginMainMenuBar()) { return; } static std::array fileSearch{}; + static std::array editSearch{}; static std::array createSearch{}; static std::array engineSearch{}; static std::array windowSearch{}; + static std::array toolsSearch{}; static std::array renderSearch{}; static std::array layoutSearch{}; const EditorPreferences* prefs = state.editorPrefs; @@ -915,13 +1145,13 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil visibility.inspector = true; visibility.assetBrowser = true; visibility.renderWorldSettings = false; - visibility.timeline = true; + visibility.timeline = false; visibility.log = true; - visibility.console = false; + visibility.console = true; visibility.materialEditor = false; visibility.renderSettings = true; visibility.debugProfiler = false; - visibility.sceneStats = false; + visibility.sceneStats = true; visibility.gpuDiagnostics = false; requests.resetLayout = true; requestResetLayout(); @@ -1013,8 +1243,32 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil if (filteredMenuItem("Close Scene", fileSearch.data(), nullptr, false, state.scenePath != nullptr && state.scenePath->has_value(), "No saved scene is currently open.", EditorGlyphIcon::SceneFile)) { requests.closeScene = true; } - menuSection("EDIT"); - if (menuFilterMatches(fileSearch.data(), "Undo")) { + menuSection("IMPORT / EXPORT"); + if (filteredCommandMenuItem(EditorCommandId::ImportAsset, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportAsset, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ImportAndPlace, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportAndPlace, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ImportSceneAsNewScene, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportSceneAsNewScene, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::MergeScene, prefs, fileSearch.data())) { executeCommand(EditorCommandId::MergeScene, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ImportHdri, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportHdri, state, visibility, requests); } + filteredPlaceholderMenuItem("Import Texture", fileSearch.data(), EditorGlyphIcon::Texture); + filteredPlaceholderMenuItem("Import IES Profile", fileSearch.data(), EditorGlyphIcon::IesProfile); + filteredPlaceholderMenuItem("Export All...", fileSearch.data(), EditorGlyphIcon::SceneFile); + filteredPlaceholderMenuItem("Export Selected...", fileSearch.data(), EditorGlyphIcon::Entity); + menuSection("PROJECT"); + if (filteredCommandMenuItem(EditorCommandId::ProjectManager, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ProjectManager, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::CloseProject, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::CloseProject, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ProjectSettings, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::ProjectSettings, state, visibility, requests); } + filteredPlaceholderMenuItem("Zip Project", fileSearch.data(), EditorGlyphIcon::ProjectFile); + if (filteredCommandMenuItem(EditorCommandId::OpenProjectDirectory, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::OpenProjectDirectory, state, visibility, requests); } + filteredPlaceholderMenuItem("Recent Projects", fileSearch.data(), EditorGlyphIcon::ProjectFile); + menuSection("APPLICATION"); + if (filteredCommandMenuItem(EditorCommandId::Exit, prefs, fileSearch.data())) { executeCommand(EditorCommandId::Exit, state, visibility, requests); } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Edit")) { + drawMenuSearch("##EditMenuSearch", editSearch); + menuSection("HISTORY"); + if (menuFilterMatches(editSearch.data(), "Undo")) { const std::string undoLabel = state.undoStack != nullptr && state.undoStack->canUndo() ? std::string("Undo ") + state.undoStack->undoLabel() : std::string("Undo"); @@ -1026,7 +1280,7 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil } drawMenuItemGlyph(EditorGlyphIcon::Undo, undoEnabled); } - if (menuFilterMatches(fileSearch.data(), "Redo")) { + if (menuFilterMatches(editSearch.data(), "Redo")) { const std::string redoLabel = state.undoStack != nullptr && state.undoStack->canRedo() ? std::string("Redo ") + state.undoStack->redoLabel() : std::string("Redo"); @@ -1038,25 +1292,8 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil } drawMenuItemGlyph(EditorGlyphIcon::Redo, redoEnabled); } - menuSection("IMPORT / EXPORT"); - if (filteredCommandMenuItem(EditorCommandId::ImportAsset, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportAsset, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::ImportAndPlace, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportAndPlace, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::ImportSceneAsNewScene, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportSceneAsNewScene, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::MergeScene, prefs, fileSearch.data())) { executeCommand(EditorCommandId::MergeScene, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::ImportHdri, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ImportHdri, state, visibility, requests); } - filteredPlaceholderMenuItem("Import Texture", fileSearch.data(), EditorGlyphIcon::Texture); - filteredPlaceholderMenuItem("Import IES Profile", fileSearch.data(), EditorGlyphIcon::IesProfile); - filteredPlaceholderMenuItem("Export All...", fileSearch.data(), EditorGlyphIcon::SceneFile); - filteredPlaceholderMenuItem("Export Selected...", fileSearch.data(), EditorGlyphIcon::Entity); - menuSection("PROJECT"); - if (filteredCommandMenuItem(EditorCommandId::ProjectManager, prefs, fileSearch.data())) { executeCommand(EditorCommandId::ProjectManager, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::CloseProject, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::CloseProject, state, visibility, requests); } - if (filteredCommandMenuItem(EditorCommandId::ProjectSettings, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::ProjectSettings, state, visibility, requests); } - filteredPlaceholderMenuItem("Zip Project", fileSearch.data(), EditorGlyphIcon::ProjectFile); - if (filteredCommandMenuItem(EditorCommandId::OpenProjectDirectory, prefs, fileSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::OpenProjectDirectory, state, visibility, requests); } - filteredPlaceholderMenuItem("Recent Projects", fileSearch.data(), EditorGlyphIcon::ProjectFile); - menuSection("APPLICATION"); - if (filteredCommandMenuItem(EditorCommandId::Exit, prefs, fileSearch.data())) { executeCommand(EditorCommandId::Exit, state, visibility, requests); } + menuSection("COMMANDS"); + if (filteredCommandMenuItem(EditorCommandId::CommandPalette, prefs, editSearch.data())) { executeCommand(EditorCommandId::CommandPalette, state, visibility, requests); } ImGui::EndMenu(); } @@ -1148,6 +1385,20 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil ImGui::EndMenu(); } + if (ImGui::BeginMenu("Tools")) { + drawMenuSearch("##ToolsMenuSearch", toolsSearch); + menuSection("PROJECT"); + if (filteredCommandMenuItem(EditorCommandId::ProjectSettings, prefs, toolsSearch.data(), state.project != nullptr, "No project is currently open.")) { executeCommand(EditorCommandId::ProjectSettings, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::JobCenter, prefs, toolsSearch.data())) { executeCommand(EditorCommandId::JobCenter, state, visibility, requests); } + menuSection("DEVELOPER"); + if (filteredCommandMenuItem(EditorCommandId::ReloadShaders, prefs, toolsSearch.data())) { executeCommand(EditorCommandId::ReloadShaders, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::OpenLogFolder, prefs, toolsSearch.data())) { executeCommand(EditorCommandId::OpenLogFolder, state, visibility, requests); } + menuSection("REFERENCE"); + if (filteredCommandMenuItem(EditorCommandId::ShowControls, prefs, toolsSearch.data())) { executeCommand(EditorCommandId::ShowControls, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ShowRendererInfo, prefs, toolsSearch.data())) { executeCommand(EditorCommandId::ShowRendererInfo, state, visibility, requests); } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Render")) { drawMenuSearch("##RenderMenuSearch", renderSearch); menuSection("OUTPUT"); @@ -1204,6 +1455,12 @@ void EditorDockspace::drawMainMenu(EditorRuntimeState& state, EditorPanelVisibil ImGui::EndMenu(); } + if (ImGui::BeginMenu("Help")) { + if (filteredCommandMenuItem(EditorCommandId::ShowControls, prefs, "")) { executeCommand(EditorCommandId::ShowControls, state, visibility, requests); } + if (filteredCommandMenuItem(EditorCommandId::ShowRendererInfo, prefs, "")) { executeCommand(EditorCommandId::ShowRendererInfo, state, visibility, requests); } + ImGui::EndMenu(); + } + ImGui::Separator(); if (drawSceneTabChrome(activeSceneTitle(state))) { requests.closeScene = true; diff --git a/src/rtv/EditorLayer.cpp b/src/rtv/EditorLayer.cpp index c1d5a81..9923b24 100644 --- a/src/rtv/EditorLayer.cpp +++ b/src/rtv/EditorLayer.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -2512,7 +2513,14 @@ EditorRequests EditorLayer::draw(EditorRuntimeState& state) { } } updateJobCenterHistory(state); + state.viewport.activeTool = viewportPanel_.activeToolMode(); + state.viewport.localTransform = viewportPanel_.localTransformMode(); + state.viewport.snapEnabled = viewportPanel_.snapEnabled(); dockspace_.begin(state, visibility_, requests); + if (requests.viewportCommand.has_value()) { + viewportPanel_.executeCommand(*requests.viewportCommand); + requests.viewportCommand.reset(); + } if (requests.showProjectManager) { showProjectManager_ = true; @@ -2608,7 +2616,7 @@ EditorRequests EditorLayer::draw(EditorRuntimeState& state) { debugProfilerPanel_.draw(state, requests); } if (visibility_.sceneStats) { - sceneStatsPanel_.draw(state); + sceneStatsPanel_.draw(state, requests); } if (visibility_.gpuDiagnostics) { gpuDiagnosticsPanel_.draw(state); @@ -3996,21 +4004,23 @@ void EditorLayer::drawProjectManager(const ProjectManagerRuntimeState& state, Ed void EditorLayer::applyThemePreset() { const int preset = std::clamp(editorPrefs_.themePreset, 0, 2); - if (appliedThemePreset_ == preset) { + const float uiScale = std::clamp(editorPrefs_.uiScale, 0.75f, 1.75f); + if (appliedThemePreset_ == preset && std::abs(appliedUiScale_ - uiScale) < 0.001f) { return; } appliedThemePreset_ = preset; + appliedUiScale_ = uiScale; if (preset == 1) { ImGui::StyleColorsDark(); } else { ImGui::StyleColorsDark(); ImGuiStyle& style = ImGui::GetStyle(); - style.WindowPadding = ImVec2(EditorUiMetric::panelPaddingX, EditorUiMetric::panelPaddingY); - style.FramePadding = ImVec2(EditorUiMetric::rowPaddingX, EditorUiMetric::rowPaddingY); - style.ItemSpacing = ImVec2(5.0f, 3.0f); - style.ItemInnerSpacing = ImVec2(4.0f, 2.0f); - style.ScrollbarSize = 10.0f; + style.WindowPadding = ImVec2(EditorUiMetric::panelPaddingX * uiScale, EditorUiMetric::panelPaddingY * uiScale); + style.FramePadding = ImVec2(EditorUiMetric::rowPaddingX * uiScale, EditorUiMetric::rowPaddingY * uiScale); + style.ItemSpacing = ImVec2(6.0f * uiScale, 4.0f * uiScale); + style.ItemInnerSpacing = ImVec2(5.0f * uiScale, 3.0f * uiScale); + style.ScrollbarSize = 11.0f * uiScale; style.WindowRounding = 0.0f; style.FrameRounding = preset == 2 ? 0.0f : EditorUiMetric::compactButtonRounding; style.GrabRounding = preset == 2 ? 0.0f : EditorUiMetric::compactButtonRounding; @@ -4129,13 +4139,13 @@ void EditorLayer::applyWorkspacePreset() { visibility_.sceneHierarchy = preset != 3; visibility_.inspector = preset != 3; visibility_.assetBrowser = preset != 3; - visibility_.timeline = preset == 0; + visibility_.timeline = false; visibility_.log = preset != 3; - visibility_.console = false; + visibility_.console = preset == 0; visibility_.materialEditor = preset == 2; visibility_.renderSettings = preset != 2; visibility_.debugProfiler = false; - visibility_.sceneStats = preset == 1; + visibility_.sceneStats = preset == 0 || preset == 1; visibility_.gpuDiagnostics = false; visibility_.renderWorldSettings = false; } diff --git a/src/rtv/EditorPanels.cpp b/src/rtv/EditorPanels.cpp index 68c653c..30afd7c 100644 --- a/src/rtv/EditorPanels.cpp +++ b/src/rtv/EditorPanels.cpp @@ -37,8 +37,8 @@ void debugViewTooltip(RendererDebugView view) { } // namespace -const std::array& editorDebugViews() { - static constexpr std::array views = { +const std::array& editorDebugViews() { + static constexpr std::array views = { RendererDebugView::Beauty, RendererDebugView::Variance, RendererDebugView::Normals, @@ -118,6 +118,13 @@ const std::array& editorDebugViews() { RendererDebugView::RestirGiFinal, RendererDebugView::RestirGiNormal, RendererDebugView::RestirGiHitDistance, + RendererDebugView::RestirGiTarget, + RendererDebugView::RestirGiSourcePdf, + RendererDebugView::RestirGiWeightSum, + RendererDebugView::RestirGiM, + RendererDebugView::RestirGiConfidence, + RendererDebugView::RestirGiVisibility, + RendererDebugView::RestirDiLightMapStatus, RendererDebugView::AdaptiveDensityMap, RendererDebugView::AdaptiveSampleCount, RendererDebugView::AdaptiveUnsampledPixels, @@ -154,6 +161,35 @@ const std::array& editorDebugViews() { RendererDebugView::MomentHistoryKindValid, RendererDebugView::DenoiserDiffuseRawVariance, RendererDebugView::DenoiserSpecularRawVariance, + RendererDebugView::NrdValidation, + RendererDebugView::NrdDiffuseConfidence, + RendererDebugView::NrdSpecularConfidence, + RendererDebugView::NrdRawConfidenceGradient, + RendererDebugView::NrdFilteredConfidenceGradient, + RendererDebugView::NrdConfidenceHistory, + RendererDebugView::PsrActiveMask, + RendererDebugView::PsrDepth, + RendererDebugView::PsrMotion, + RendererDebugView::PsrNormalRoughness, + RendererDebugView::PsrHitDistance, + RendererDebugView::PsrAlbedoF0, + RendererDebugView::PsrRayDirection, + RendererDebugView::DlssDepth, + RendererDebugView::DlssMotionVectors, + RendererDebugView::DlssInputColor, + RendererDebugView::DlssOutputColor, + RendererDebugView::DlssRrDiffuseAlbedo, + RendererDebugView::DlssRrSpecularAlbedo, + RendererDebugView::DlssRrNormals, + RendererDebugView::DlssRrRoughness, + RendererDebugView::DlssRrDiffuseHitDistance, + RendererDebugView::DlssRrSpecularHitDistance, + RendererDebugView::DlssRrReflectedAlbedo, + RendererDebugView::DlssRrDisocclusionMask, + RendererDebugView::DlssRrDiffuseRayDirection, + RendererDebugView::DlssRrSpecularRayDirection, + RendererDebugView::DlssRrDiffuseRayDirectionHitDistance, + RendererDebugView::DlssRrSpecularRayDirectionHitDistance, RendererDebugView::WavefrontQueueOccupancy, RendererDebugView::WavefrontPathDepth, RendererDebugView::WavefrontLiveRays, @@ -241,6 +277,39 @@ void editorDebugViewCombo(const char* label, RendererSettings& settings, bool& c selectable(RendererDebugView::DenoiserSpecularHistoryLength); selectable(RendererDebugView::DenoiserDiffuseRawVariance); selectable(RendererDebugView::DenoiserSpecularRawVariance); + selectable(RendererDebugView::NrdValidation); + selectable(RendererDebugView::NrdDiffuseConfidence); + selectable(RendererDebugView::NrdSpecularConfidence); + selectable(RendererDebugView::NrdRawConfidenceGradient); + selectable(RendererDebugView::NrdFilteredConfidenceGradient); + selectable(RendererDebugView::NrdConfidenceHistory); + + ImGui::SeparatorText("Primary Surface Replacement"); + selectable(RendererDebugView::PsrActiveMask); + selectable(RendererDebugView::PsrDepth); + selectable(RendererDebugView::PsrMotion); + selectable(RendererDebugView::PsrNormalRoughness); + selectable(RendererDebugView::PsrHitDistance); + selectable(RendererDebugView::PsrAlbedoF0); + selectable(RendererDebugView::PsrRayDirection); + + ImGui::SeparatorText("DLSS Guides"); + selectable(RendererDebugView::DlssDepth); + selectable(RendererDebugView::DlssMotionVectors); + selectable(RendererDebugView::DlssInputColor); + selectable(RendererDebugView::DlssOutputColor); + selectable(RendererDebugView::DlssRrDiffuseAlbedo); + selectable(RendererDebugView::DlssRrSpecularAlbedo); + selectable(RendererDebugView::DlssRrNormals); + selectable(RendererDebugView::DlssRrRoughness); + selectable(RendererDebugView::DlssRrDiffuseHitDistance); + selectable(RendererDebugView::DlssRrSpecularHitDistance); + selectable(RendererDebugView::DlssRrReflectedAlbedo); + selectable(RendererDebugView::DlssRrDisocclusionMask); + selectable(RendererDebugView::DlssRrDiffuseRayDirection); + selectable(RendererDebugView::DlssRrSpecularRayDirection); + selectable(RendererDebugView::DlssRrDiffuseRayDirectionHitDistance); + selectable(RendererDebugView::DlssRrSpecularRayDirectionHitDistance); ImGui::SeparatorText("Moments"); selectable(RendererDebugView::MomentUpdateValidity); @@ -328,6 +397,13 @@ void editorDebugViewCombo(const char* label, RendererSettings& settings, bool& c selectable(RendererDebugView::RestirGiFinal); selectable(RendererDebugView::RestirGiNormal); selectable(RendererDebugView::RestirGiHitDistance); + selectable(RendererDebugView::RestirGiTarget); + selectable(RendererDebugView::RestirGiSourcePdf); + selectable(RendererDebugView::RestirGiWeightSum); + selectable(RendererDebugView::RestirGiM); + selectable(RendererDebugView::RestirGiConfidence); + selectable(RendererDebugView::RestirGiVisibility); + selectable(RendererDebugView::RestirDiLightMapStatus); ImGui::SeparatorText("Wavefront Queue"); selectable(RendererDebugView::WavefrontQueueOccupancy); diff --git a/src/rtv/EditorPreferences.cpp b/src/rtv/EditorPreferences.cpp index 9ac8ef0..f4b27a1 100644 --- a/src/rtv/EditorPreferences.cpp +++ b/src/rtv/EditorPreferences.cpp @@ -204,6 +204,9 @@ bool EditorPreferences::save(const std::filesystem::path& path) const { json["themePreset"] = themePreset; json["workspacePreset"] = workspacePreset; json["layoutVersion"] = layoutVersion; + json["contentBrowserMode"] = std::clamp(contentBrowserMode, 0, 1); + json["contentBrowserGridView"] = contentBrowserGridView; + json["contentBrowserShowDetails"] = contentBrowserShowDetails; json["cookEmitNativeTextureTargetSets"] = cookEmitNativeTextureTargetSets; json["cookNativeTextureTargetSetProfile"] = std::clamp(cookNativeTextureTargetSetProfile, 0, 5); json["cookNativeTextureTargetSetName"] = cookNativeTextureTargetSetName; @@ -324,6 +327,9 @@ void EditorPreferences::load(const std::filesystem::path& path) { if (json.contains("themePreset")) themePreset = json["themePreset"].get(); if (json.contains("workspacePreset")) workspacePreset = json["workspacePreset"].get(); if (json.contains("layoutVersion")) layoutVersion = json["layoutVersion"].get(); + if (json.contains("contentBrowserMode")) contentBrowserMode = std::clamp(json["contentBrowserMode"].get(), 0, 1); + if (json.contains("contentBrowserGridView")) contentBrowserGridView = json["contentBrowserGridView"].get(); + if (json.contains("contentBrowserShowDetails")) contentBrowserShowDetails = json["contentBrowserShowDetails"].get(); if (json.contains("cookEmitNativeTextureTargetSets")) cookEmitNativeTextureTargetSets = json["cookEmitNativeTextureTargetSets"].get(); if (json.contains("cookNativeTextureTargetSetProfile")) cookNativeTextureTargetSetProfile = std::clamp(json["cookNativeTextureTargetSetProfile"].get(), 0, 5); if (json.contains("cookNativeTextureTargetSetName")) cookNativeTextureTargetSetName = json["cookNativeTextureTargetSetName"].get(); diff --git a/src/rtv/GpuProfiler.cpp b/src/rtv/GpuProfiler.cpp index beaaf45..5de9b61 100644 --- a/src/rtv/GpuProfiler.cpp +++ b/src/rtv/GpuProfiler.cpp @@ -10,16 +10,6 @@ #include #include -#ifndef VK_QUERY_PIPELINE_STATISTIC_RAY_INVOCATIONS_BIT_KHR -#define VK_QUERY_PIPELINE_STATISTIC_RAY_INVOCATIONS_BIT_KHR 0x00000001ull -#endif -#ifndef VK_QUERY_PIPELINE_STATISTIC_RAY_TRIANGLES_HIT_BIT_KHR -#define VK_QUERY_PIPELINE_STATISTIC_RAY_TRIANGLES_HIT_BIT_KHR 0x00000002ull -#endif -#ifndef VK_QUERY_PIPELINE_STATISTIC_RAY_AABBS_HIT_BIT_KHR -#define VK_QUERY_PIPELINE_STATISTIC_RAY_AABBS_HIT_BIT_KHR 0x00000004ull -#endif - namespace rtv { namespace { @@ -60,6 +50,10 @@ const char* gpuMarkerBeginLabel(GpuProfiler::Query query) { case GpuProfiler::SkipDenoiserCopyStart: return "Skip Denoiser Copy"; case GpuProfiler::TaaStart: return "TAA/TSR"; case GpuProfiler::TaaHistoryCopyStart: return "TAA History Copy"; + case GpuProfiler::DlssGuidesStart: return "DLSS Guides"; + case GpuProfiler::DlssStart: return "DLSS"; + case GpuProfiler::DlssRayReconstructionGuidesStart: return "DLSS RR Guides"; + case GpuProfiler::DlssRayReconstructionStart: return "DLSS RR"; case GpuProfiler::AutoExposureHistogramClearStart: return "AutoExposure Histogram Clear"; case GpuProfiler::AutoExposureHistogramStart: return "AutoExposure Histogram"; case GpuProfiler::AutoExposureReduceStart: return "AutoExposure Reduce"; @@ -118,6 +112,10 @@ bool gpuMarkerEndsLabel(GpuProfiler::Query query) { case GpuProfiler::SkipDenoiserCopyEnd: case GpuProfiler::TaaEnd: case GpuProfiler::TaaHistoryCopyEnd: + case GpuProfiler::DlssGuidesEnd: + case GpuProfiler::DlssEnd: + case GpuProfiler::DlssRayReconstructionGuidesEnd: + case GpuProfiler::DlssRayReconstructionEnd: case GpuProfiler::AutoExposureHistogramClearEnd: case GpuProfiler::AutoExposureHistogramEnd: case GpuProfiler::AutoExposureReduceEnd: @@ -218,25 +216,14 @@ void GpuProfiler::create(VkDevice device, VkPhysicalDevice physicalDevice) { void GpuProfiler::createPipelineStatsQuery(VkDevice device, bool rayTracingAvailable) { if (statsQueryPool_ != VK_NULL_HANDLE) { vkDestroyQueryPool(device_, statsQueryPool_, nullptr); + statsQueryPool_ = VK_NULL_HANDLE; } device_ = device; - - VkQueryPipelineStatisticFlags statsFlags = - VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT; - if (rayTracingAvailable) { - statsFlags |= - VK_QUERY_PIPELINE_STATISTIC_RAY_INVOCATIONS_BIT_KHR | - VK_QUERY_PIPELINE_STATISTIC_RAY_TRIANGLES_HIT_BIT_KHR | - VK_QUERY_PIPELINE_STATISTIC_RAY_AABBS_HIT_BIT_KHR; - } - - VkQueryPoolCreateInfo info{}; - info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; - info.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; - info.queryCount = 1; - info.pipelineStatistics = statsFlags; - checkVk(vkCreateQueryPool(device_, &info, nullptr, &statsQueryPool_), - "vkCreateQueryPool(gpu pipeline stats)"); + (void)rayTracingAvailable; + // Vulkan has no portable pipeline-statistics bits for ray invocations or hits. + pipelineStats_ = {}; + smoothedPipelineStats_ = {}; + statsSubmitted_ = false; } void GpuProfiler::destroy() { @@ -252,6 +239,8 @@ void GpuProfiler::destroy() { submitted_ = false; statsSubmitted_ = false; activeQueries_.fill(false); + pipelineStats_ = {}; + smoothedPipelineStats_ = {}; } void GpuProfiler::collectCompletedFrame() { @@ -341,6 +330,10 @@ void GpuProfiler::collectCompletedFrame() { updateTiming(timings_.skipDenoiserCopyMs, SkipDenoiserCopyStart, SkipDenoiserCopyEnd); updateTiming(timings_.taaMs, TaaStart, TaaEnd); updateTiming(timings_.taaHistoryCopyMs, TaaHistoryCopyStart, TaaHistoryCopyEnd); + updateTiming(timings_.dlssGuidesMs, DlssGuidesStart, DlssGuidesEnd); + updateTiming(timings_.dlssMs, DlssStart, DlssEnd); + updateTiming(timings_.dlssRayReconstructionGuidesMs, DlssRayReconstructionGuidesStart, DlssRayReconstructionGuidesEnd); + updateTiming(timings_.dlssRayReconstructionMs, DlssRayReconstructionStart, DlssRayReconstructionEnd); updateTiming(timings_.autoExposureHistogramClearMs, AutoExposureHistogramClearStart, AutoExposureHistogramClearEnd); updateTiming(timings_.autoExposureHistogramMs, AutoExposureHistogramStart, AutoExposureHistogramEnd); updateTiming(timings_.autoExposureReduceMs, AutoExposureReduceStart, AutoExposureReduceEnd); @@ -394,6 +387,10 @@ void GpuProfiler::collectCompletedFrame() { timings_.skipDenoiserCopyMs + timings_.taaMs + timings_.taaHistoryCopyMs + + timings_.dlssGuidesMs + + timings_.dlssMs + + timings_.dlssRayReconstructionGuidesMs + + timings_.dlssRayReconstructionMs + timings_.autoExposureMs + timings_.toneMapMs + timings_.selectionOutlineMs + diff --git a/src/rtv/GpuScene.cpp b/src/rtv/GpuScene.cpp index 49c465d..83ea3f4 100644 --- a/src/rtv/GpuScene.cpp +++ b/src/rtv/GpuScene.cpp @@ -65,6 +65,7 @@ constexpr uint32_t materialFlagManualEmissiveSrgb = 1u << 1u; constexpr uint32_t materialFlagNormalMapDirectX = 1u << 2u; constexpr uint32_t materialFlagSpecularGlossinessWorkflow = 1u << 3u; constexpr uint32_t materialFlagSpecularAlphaGlossiness = 1u << 4u; +constexpr uint32_t materialFlagMayBeTransmissive = 1u << 5u; constexpr uint32_t materialParameterVec4Stride = 18u; constexpr uint32_t materialTextureTransformCount = 17u; constexpr uint32_t materialVec4Stride = materialParameterVec4Stride + materialTextureTransformCount * 2u; @@ -793,6 +794,30 @@ void appendOpacityHeightTextureIndices(std::vector& materialData, flo materialData.push_back({opacityTexture, heightTexture, heightScale, 0.0f}); } +bool materialRowsMayBeTransmissive(const std::vector& materialData, size_t materialIndex) { + const size_t base = materialIndex * materialVec4Stride; + if (base + 12u >= materialData.size()) { + return false; + } + const glm::vec4& typeAndFlags = materialData[base + 1u]; + const glm::vec4& transmission = materialData[base + 12u]; + return static_cast(std::lround(typeAndFlags.y)) == materialTypeDielectric || + transmission.y > 1.0e-5f || + static_cast(std::lround(transmission.z)) >= 0; +} + +void encodeMaterialClassificationFlags(std::vector& materialData) { + const size_t materialCount = materialData.size() / materialVec4Stride; + for (size_t i = 0; i < materialCount; ++i) { + glm::vec4& typeAndFlags = materialData[i * materialVec4Stride + 1u]; + uint32_t flags = static_cast(std::lround(typeAndFlags.w)); + if (materialRowsMayBeTransmissive(materialData, i)) { + flags |= materialFlagMayBeTransmissive; + } + typeAndFlags.w = static_cast(flags); + } +} + void appendConductorOptics(std::vector& materialData, const CachedMaterialData& material) { const bool enabled = material.useConductorOptics != 0u; glm::vec3 eta = enabled ? nonnegativeRgb(material.conductorEta) : glm::vec3{0.0f}; @@ -864,15 +889,14 @@ std::vector buildCachedMaterialData(const CachedScene& cached) { appendOpacityHeightTextureIndices(materialData); appendMaterialTextureTransforms(materialData, nullptr); } + encodeMaterialClassificationFlags(materialData); return materialData; } bool materialDataContainsTransmission(const std::vector& materialData) { const size_t materialCount = materialData.size() / materialVec4Stride; for (size_t i = 0; i < materialCount; ++i) { - const size_t transmissionSlot = i * materialVec4Stride + 12u; - if (transmissionSlot < materialData.size() && - materialData[transmissionSlot].y > materialTransmissionDielectricThreshold) { + if (materialRowsMayBeTransmissive(materialData, i)) { return true; } } @@ -1607,8 +1631,7 @@ std::vector transmissiveMaterialMaskFromRows(const std::vector mask(materialCount, 0u); for (size_t i = 0; i < materialCount; ++i) { - const glm::vec4& transmissionRow = materialData[i * materialVec4Stride + 12u]; - if (transmissionRow.y > materialTransmissionDielectricThreshold) { + if (materialRowsMayBeTransmissive(materialData, i)) { mask[i] = 1u; } } @@ -1645,6 +1668,29 @@ bool primitivesContainSidedness( return false; } +bool materialDataReferencesTextures(const std::vector& materialData) { + constexpr std::array, 17> textureFields{{ + {3u, 0u}, {3u, 1u}, {3u, 2u}, {3u, 3u}, + {7u, 2u}, {9u, 0u}, {9u, 1u}, {10u, 2u}, {10u, 3u}, + {11u, 2u}, {11u, 3u}, {12u, 0u}, {12u, 2u}, {13u, 3u}, + {14u, 0u}, {14u, 1u}, {14u, 3u}, + }}; + const size_t materialCount = materialData.size() / materialVec4Stride; + for (size_t i = 0; i < materialCount; ++i) { + const size_t base = i * materialVec4Stride; + for (const auto& [row, component] : textureFields) { + if (materialData[base + row][component] >= 0.0f) { + return true; + } + } + const glm::vec4& opacityHeight = materialData[base + 17u]; + if (opacityHeight.x >= 0.0f || opacityHeight.y >= 0.0f) { + return true; + } + } + return false; +} + float triangleArea(glm::vec3 v0, glm::vec3 v1, glm::vec3 v2) { return 0.5f * glm::length(glm::cross(v1 - v0, v2 - v0)); } @@ -2020,7 +2066,7 @@ GpuScene::GpuScene( if (cached.has_value()) { if (sceneCachePolicy_.mode == SceneCacheMode::FullReadWrite && importedScene->lights.empty()) { if (hasValidGpuCache(*cached, *importedScene)) { - createImportedSceneFromCache(uploader, *cached, importedScene->lights); + createImportedSceneFromCache(uploader, *cached, *importedScene); usedGpuCache = true; } else { std::cout << "GPU cache miss: full cache rejected for active scene signature.\n"; @@ -2283,6 +2329,27 @@ VkDescriptorImageInfo GpuScene::materialCombinedDescriptor(uint32_t slot) const return combined; } +bool GpuScene::streamedMaterialTextureDescriptor( + const SceneAsset& scene, + TextureAssetHandle texture, + const Image& image, + uint32_t& slotOut, + VkDescriptorImageInfo& descriptorOut) const { + const uint32_t slot = textureSlotIndexFor(scene, texture, materialTextureSlotCapacity_); + if (slot == UINT32_MAX || image.view() == VK_NULL_HANDLE) { + return false; + } + const VkSampler sampler = slot < materialTextureSamplers_.size() + ? materialTextureSamplers_[slot] + : materialSampler_; + if (sampler == VK_NULL_HANDLE) { + return false; + } + slotOut = slot; + descriptorOut = image.sampledDescriptor(sampler); + return descriptorOut.imageView != VK_NULL_HANDLE; +} + bool GpuScene::setEnvironmentControls(bool enabled, float intensity, float rotation, float backgroundIntensity) { const uint32_t enabledValue = enabled ? 1u : 0u; const bool changed = @@ -2685,6 +2752,7 @@ bool GpuScene::updateImportedMaterials(BufferUploader& uploader, const SceneAsse appendMaterialTextureTransforms(materialData, material); } + encodeMaterialClassificationFlags(materialData); const VkDeviceSize byteSize = sizeof(glm::vec4) * materialData.size(); if (byteSize == 0 || byteSize > materials_->size()) { return false; @@ -2730,6 +2798,7 @@ bool GpuScene::updateImportedMaterials(BufferUploader& uploader, const SceneAsse primitiveRecordCpu_, transmissiveMaterialMaskFromRows(materialData)); hasTransmissiveMaterials_ = materialDataContainsTransmission(materialData); + hasMaterialTextures_ = materialDataReferencesTextures(materialData); if (meshParamsBuffer_ != nullptr) { meshParamsBuffer_->write(&meshParams_, sizeof(meshParams_)); meshParamsBuffer_->flush(sizeof(meshParams_)); @@ -2963,11 +3032,19 @@ void GpuScene::createCornellBox(BufferUploader& uploader) { {{0.95f, 0.96f, 0.97f}, 0.08f, 1.5f, 3, 1.0f, {}}, {{0.95f, 0.93f, 0.88f}, 0.12f, 1.5f, 4, 0.0f, {}}, }; - const std::vector materialDoubleSided(mats.size(), true); + const std::vector materialDoubleSided = { + true, // white walls are thin debug shell planes, so keep them visible from outside + true, // red wall + true, // green wall + true, // ceiling light + true, // glass sphere needs interior exits for refraction + true, // metal sphere uses two-sided fallback hits for stable glossy reflections + true, // dielectric/clearcoat sphere uses two-sided fallback hits for stable glossy reflections + }; auto pushSphereMesh = [&](glm::vec3 center, float radius, uint32_t material) { - constexpr uint32_t longitude = 160; - constexpr uint32_t latitude = 80; + constexpr uint32_t longitude = 96; + constexpr uint32_t latitude = 48; const uint32_t base = static_cast(vertices.size()); for (uint32_t y = 0; y <= latitude; ++y) { const float v = static_cast(y) / static_cast(latitude); @@ -3103,12 +3180,14 @@ void GpuScene::createCornellBox(BufferUploader& uploader) { std::vector materialData; std::vector materialEmissive; materialEmissive.reserve(mats.size()); - for (const auto& m : mats) { + for (size_t materialIndex = 0; materialIndex < mats.size(); ++materialIndex) { + const auto& m = mats[materialIndex]; materialData.push_back({m.color, m.roughness}); materialData.push_back({m.ior, static_cast(m.type), m.metallic, 0.0f}); materialData.push_back({m.emissive, 1.0f}); materialData.push_back({-1.0f, -1.0f, -1.0f, -1.0f}); - materialData.push_back({0.5f, 0.0f, 1.0f, 0.0f}); + const float doubleSided = materialIndex < materialDoubleSided.size() && materialDoubleSided[materialIndex] ? 1.0f : 0.0f; + materialData.push_back({0.5f, 0.0f, doubleSided, 0.0f}); appendConductorOptics(materialData, nullptr); appendGltfMaterialExtensionData( materialData, @@ -3126,7 +3205,9 @@ void GpuScene::createCornellBox(BufferUploader& uploader) { appendMaterialTextureTransforms(materialData, nullptr); materialEmissive.push_back(m.emissive); } + encodeMaterialClassificationFlags(materialData); hasTransmissiveMaterials_ = materialDataContainsTransmission(materialData); + hasMaterialTextures_ = materialDataReferencesTextures(materialData); std::vector sphereData; @@ -3186,7 +3267,7 @@ void GpuScene::createCornellBox(BufferUploader& uploader) { uploadVectorBatched(batch, primitiveRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, primitiveRecords, "scene primitive records"); uploadVectorBatched(batch, instanceRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceRecords, "scene instance records"); uploadVectorBatched(batch, rtTriangleMaterialIds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, rtTriangleMaterialIds, "scene rt triangle material ids"); - uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, lightRecords, "scene emissive light records"); + uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, lightRecords, "scene emissive light records"); uploadVectorBatched(batch, localVertices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localVertexData, "scene local mesh vertices"); uploadVectorBatched(batch, localIndices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localIndices, "scene local mesh indices"); uploadVectorBatched(batch, instanceBounds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceBounds, "scene instance bounds"); @@ -3358,7 +3439,9 @@ void GpuScene::createImportedScene(BufferUploader& uploader, const SceneAsset& i materialDoubleSided.push_back(false); materialAlphaClasses.push_back(kPrimitiveAlphaClassOpaque); } + encodeMaterialClassificationFlags(materialData); hasTransmissiveMaterials_ = materialDataContainsTransmission(materialData); + hasMaterialTextures_ = materialDataReferencesTextures(materialData); std::unordered_map materialIndexForAsset; materialIndexForAsset.reserve(materialHandles.size()); @@ -3807,7 +3890,7 @@ void GpuScene::createImportedScene(BufferUploader& uploader, const SceneAsset& i uploadVectorBatched(batch, primitiveRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, primitiveRecords, "imported primitive records"); uploadVectorBatched(batch, instanceRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceRecords, "imported instance records"); uploadVectorBatched(batch, rtTriangleMaterialIds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, rtTriangleMaterialIds, "imported rt triangle material ids"); - uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, lightRecords, "imported emissive light records"); + uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, lightRecords, "imported emissive light records"); uploadVectorBatched(batch, localVertices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localVertexData, "imported local mesh vertices"); uploadVectorBatched(batch, localIndices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localIndices, "imported local mesh indices"); uploadVectorBatched(batch, instanceBounds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceBounds, "imported instance bounds"); @@ -4122,7 +4205,7 @@ void GpuScene::createImportedScene(BufferUploader& uploader, const SceneAsset& i } } -void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const CachedScene& cached, const std::vector& activeSceneLights) { +void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const CachedScene& cached, const SceneAsset& activeScene) { std::cout << "GPU cache hit: restoring cached BVH data for " << cached.meshGpuRecords.size() << " meshes.\n"; createCachedMaterialTextures(uploader, cached); @@ -4164,7 +4247,8 @@ void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const Cach } annotatePrimitiveAlphaClasses(primitiveRecords, materialAlphaClasses, &materialDoubleSided); - for (const auto& cachedMesh : cached.meshGpuRecords) { + for (size_t meshIndex = 0; meshIndex < cached.meshGpuRecords.size(); ++meshIndex) { + const CachedMeshGpuRecord& cachedMesh = cached.meshGpuRecords[meshIndex]; GpuMeshRecord rec{}; rec.vertexIndexData = cachedMesh.vertexIndexData; rec.primitiveData = cachedMesh.primitiveData; @@ -4177,6 +4261,9 @@ void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const Cach meshRecords.push_back(rec); rayTracingMeshes_.push_back(RayTracingMeshBuildInput{ .meshIndex = static_cast(meshRecords.size() - 1), + .sourceMeshHandleIndex = meshIndex < activeScene.meshes.size() + ? activeScene.meshes[meshIndex].index + : 0xffffffffu, .firstVertex = rec.vertexIndexData.x, .vertexCount = rec.vertexIndexData.y, .firstIndex = rec.vertexIndexData.z, @@ -4282,7 +4369,7 @@ void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const Cach float lightSelectionWeight = emissiveTotalWeight; lightRecords = combineLightRecords( emissiveLightRecords_, - activeSceneLights, + activeScene.lights, emissiveTotalWeight, lightSelectionWeight); lightRecordCpu_ = lightRecords; @@ -4304,6 +4391,7 @@ void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const Cach primitiveRecords, transmissiveMaterialMaskFromRows(materialData)); hasTransmissiveMaterials_ = materialDataContainsTransmission(materialData); + hasMaterialTextures_ = materialDataReferencesTextures(materialData); applyLightRecordMetadataToMeshParams(meshParams_, lightRecords, lightSelectionWeight); rayTracingGeometryStats_ = computeRayTracingGeometryStats(meshRecords, primitiveRecords); primitiveRecordCpu_ = primitiveRecords; @@ -4341,7 +4429,7 @@ void GpuScene::createImportedSceneFromCache(BufferUploader& uploader, const Cach uploadVectorBatched(batch, primitiveRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, primitiveRecords, "imported primitive records (cached)"); uploadVectorBatched(batch, instanceRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceRecords, "imported instance records (cached)"); uploadVectorBatched(batch, rtTriangleMaterialIds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, rtTriangleMaterialIds, "imported rt triangle material ids (cached)"); - uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, lightRecords, "imported emissive light records (cached)"); + uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, lightRecords, "imported emissive light records (cached)"); uploadVectorBatched(batch, localVertices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localVertexData, "imported local mesh vertices (cached)"); uploadVectorBatched(batch, localIndices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localIndices, "imported local mesh indices (cached)"); @@ -4617,6 +4705,7 @@ void GpuScene::createImportedSceneGeometryFromCache(BufferUploader& uploader, co transmissiveMaterialMaskFromRows(materialData)); applyLightRecordMetadataToMeshParams(meshParams_, lightRecords, lightSelectionWeight); hasTransmissiveMaterials_ = materialDataContainsTransmission(materialData); + hasMaterialTextures_ = materialDataReferencesTextures(materialData); rayTracingGeometryStats_ = computeRayTracingGeometryStats(meshRecords, primitiveRecords); primitiveRecordCpu_ = primitiveRecords; localVertexCpu_ = localVertexData; @@ -4658,7 +4747,7 @@ void GpuScene::createImportedSceneGeometryFromCache(BufferUploader& uploader, co uploadVectorBatched(batch, primitiveRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, primitiveRecords, "imported primitive records (geometry cache)"); uploadVectorBatched(batch, instanceRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceRecords, "imported instance records (geometry cache)"); uploadVectorBatched(batch, rtTriangleMaterialIds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, rtTriangleMaterialIds, "imported rt triangle material ids (geometry cache)"); - uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, lightRecords, "imported emissive light records (geometry cache)"); + uploadVectorBatched(batch, lightRecords_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, lightRecords, "imported emissive light records (geometry cache)"); uploadVectorBatched(batch, localVertices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localVertexData, "imported local mesh vertices (geometry cache)"); uploadVectorBatched(batch, localIndices_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, localIndices, "imported local mesh indices (geometry cache)"); uploadVectorBatched(batch, instanceBounds_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, instanceBounds, "imported instance bounds (geometry cache)"); @@ -4770,7 +4859,7 @@ void GpuScene::uploadLightRecords( allocator_, uploader, lightRecords_, - VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, lightRecords.data(), sizeof(GpuLightRecord) * lightRecords.size(), "scene light records"), retireFrame); diff --git a/src/rtv/GpuValidation.cpp b/src/rtv/GpuValidation.cpp index 592fd15..31d480f 100644 --- a/src/rtv/GpuValidation.cpp +++ b/src/rtv/GpuValidation.cpp @@ -1,10 +1,170 @@ #include "rtv/GpuValidation.h" +#include #include +#include #include +#include +#include namespace rtv { +namespace { + +constexpr size_t kManualBarrierRecentEventLimit = 4096; + +struct ManualBarrierEscapeDiagnosticsState { + std::mutex mutex; + bool enabled = false; + uint64_t dependencyCallCount = 0; + uint64_t barrierCount = 0; + uint64_t droppedRecentEventCount = 0; + std::vector recentEvents; + std::vector aggregates; +}; + +ManualBarrierEscapeDiagnosticsState& manualBarrierState() { + static ManualBarrierEscapeDiagnosticsState state; + return state; +} + +template +uint64_t vulkanHandleValue(Handle handle) { + if constexpr (std::is_pointer_v) { + return reinterpret_cast(handle); + } else { + return static_cast(handle); + } +} + +bool matchesAggregate(const ManualBarrierEscapeAggregate& aggregate, const ManualBarrierEscapeEvent& event) { + return aggregate.source == event.source && + aggregate.label == event.label && + aggregate.resourceKind == event.resourceKind && + aggregate.srcStage == event.srcStage && + aggregate.srcAccess == event.srcAccess && + aggregate.dstStage == event.dstStage && + aggregate.dstAccess == event.dstAccess && + aggregate.oldLayout == event.oldLayout && + aggregate.newLayout == event.newLayout; +} + +void recordManualBarrierEscapeLocked( + ManualBarrierEscapeDiagnosticsState& state, + ManualBarrierEscapeEvent event) { + event.sequence = ++state.barrierCount; + if (state.recentEvents.size() < kManualBarrierRecentEventLimit) { + state.recentEvents.push_back(event); + } else { + ++state.droppedRecentEventCount; + } + + auto aggregateIt = std::find_if( + state.aggregates.begin(), + state.aggregates.end(), + [&](const ManualBarrierEscapeAggregate& aggregate) { + return matchesAggregate(aggregate, event); + }); + if (aggregateIt != state.aggregates.end()) { + ++aggregateIt->count; + return; + } + + state.aggregates.push_back(ManualBarrierEscapeAggregate{ + .source = event.source, + .label = event.label, + .resourceKind = event.resourceKind, + .srcStage = event.srcStage, + .srcAccess = event.srcAccess, + .dstStage = event.dstStage, + .dstAccess = event.dstAccess, + .oldLayout = event.oldLayout, + .newLayout = event.newLayout, + .count = 1, + }); +} + +} // namespace + +void setManualBarrierEscapeDiagnosticsEnabled(bool enabled) { + auto& state = manualBarrierState(); + std::lock_guard lock(state.mutex); + state.enabled = enabled; +} + +void resetManualBarrierEscapeDiagnostics() { + auto& state = manualBarrierState(); + std::lock_guard lock(state.mutex); + state.dependencyCallCount = 0; + state.barrierCount = 0; + state.droppedRecentEventCount = 0; + state.recentEvents.clear(); + state.aggregates.clear(); +} + +void recordManualBarrierEscape(std::string source, std::string label, const VkDependencyInfo& dependency) { + auto& state = manualBarrierState(); + std::lock_guard lock(state.mutex); + if (!state.enabled) { + return; + } + ++state.dependencyCallCount; + + for (uint32_t i = 0; i < dependency.memoryBarrierCount; ++i) { + const VkMemoryBarrier2& barrier = dependency.pMemoryBarriers[i]; + recordManualBarrierEscapeLocked(state, ManualBarrierEscapeEvent{ + .source = source, + .label = label, + .resourceKind = "memory", + .srcStage = barrier.srcStageMask, + .srcAccess = barrier.srcAccessMask, + .dstStage = barrier.dstStageMask, + .dstAccess = barrier.dstAccessMask, + }); + } + for (uint32_t i = 0; i < dependency.bufferMemoryBarrierCount; ++i) { + const VkBufferMemoryBarrier2& barrier = dependency.pBufferMemoryBarriers[i]; + recordManualBarrierEscapeLocked(state, ManualBarrierEscapeEvent{ + .source = source, + .label = label, + .resourceKind = "buffer", + .resourceHandle = vulkanHandleValue(barrier.buffer), + .srcStage = barrier.srcStageMask, + .srcAccess = barrier.srcAccessMask, + .dstStage = barrier.dstStageMask, + .dstAccess = barrier.dstAccessMask, + }); + } + for (uint32_t i = 0; i < dependency.imageMemoryBarrierCount; ++i) { + const VkImageMemoryBarrier2& barrier = dependency.pImageMemoryBarriers[i]; + recordManualBarrierEscapeLocked(state, ManualBarrierEscapeEvent{ + .source = source, + .label = label, + .resourceKind = "image", + .resourceHandle = vulkanHandleValue(barrier.image), + .srcStage = barrier.srcStageMask, + .srcAccess = barrier.srcAccessMask, + .dstStage = barrier.dstStageMask, + .dstAccess = barrier.dstAccessMask, + .oldLayout = barrier.oldLayout, + .newLayout = barrier.newLayout, + }); + } +} + +ManualBarrierEscapeDiagnosticsSnapshot manualBarrierEscapeDiagnosticsSnapshot() { + auto& state = manualBarrierState(); + std::lock_guard lock(state.mutex); + ManualBarrierEscapeDiagnosticsSnapshot snapshot; + snapshot.enabled = state.enabled; + snapshot.dependencyCallCount = state.dependencyCallCount; + snapshot.barrierCount = state.barrierCount; + snapshot.droppedRecentEventCount = state.droppedRecentEventCount; + snapshot.recentEvents = state.recentEvents; + snapshot.aggregates = state.aggregates; + return snapshot; +} + void RendererValidationLog::recordBarrier(std::string label, VkPipelineStageFlags2 srcStage, VkAccessFlags2 srcAccess, VkPipelineStageFlags2 dstStage, VkAccessFlags2 dstAccess) { std::ostringstream stream; stream << label << ": stage " << srcStage << "/" << dstStage << " access " << srcAccess << "/" << dstAccess; diff --git a/src/rtv/HeadlessDiagnostics.cpp b/src/rtv/HeadlessDiagnostics.cpp index 0617204..3036e3f 100644 --- a/src/rtv/HeadlessDiagnostics.cpp +++ b/src/rtv/HeadlessDiagnostics.cpp @@ -9,14 +9,17 @@ #include "rtv/PathTracerRenderer.h" #include "rtv/RenderGraphDump.h" #include "rtv/RenderGraph.h" +#include "rtv/RendererPassContracts.h" #include "rtv/ResourceAllocator.h" #include "rtv/Swapchain.h" +#include "rtv/TemporalSystem.h" #include "rtv/UiOverlay.h" #include "rtv/VulkanContext.h" #include #include +#include #include #include #include @@ -24,8 +27,10 @@ #include #include #include +#include #include #include +#include #include namespace rtv { @@ -43,6 +48,12 @@ bool isRestirGiExportView(RendererDebugView view) { view == RendererDebugView::RestirGiHitDistance || view == RendererDebugView::RestirGiGrid || view == RendererDebugView::RestirGiPathClass || + view == RendererDebugView::RestirGiTarget || + view == RendererDebugView::RestirGiSourcePdf || + view == RendererDebugView::RestirGiWeightSum || + view == RendererDebugView::RestirGiM || + view == RendererDebugView::RestirGiConfidence || + view == RendererDebugView::RestirGiVisibility || view == RendererDebugView::WavefrontRestirGi; } @@ -58,6 +69,7 @@ bool isRestirDiExportView(RendererDebugView view) { view == RendererDebugView::RestirDiReceiverPosition || view == RendererDebugView::RestirDiReceiverNormal || view == RendererDebugView::RestirDiLightVersion || + view == RendererDebugView::RestirDiLightMapStatus || view == RendererDebugView::RestirDiInitialReservoir || view == RendererDebugView::RestirDiTemporalReservoir || view == RendererDebugView::RestirDiSpatialReservoir || @@ -71,6 +83,28 @@ bool isRestirDiExportView(RendererDebugView view) { view == RendererDebugView::WavefrontRestirDi; } +bool isDlssExportView(RendererDebugView view) { + return view == RendererDebugView::DlssDepth || + view == RendererDebugView::DlssMotionVectors || + view == RendererDebugView::DlssInputColor || + view == RendererDebugView::DlssOutputColor; +} + +bool isDlssRayReconstructionExportView(RendererDebugView view) { + return view == RendererDebugView::DlssRrDiffuseAlbedo || + view == RendererDebugView::DlssRrSpecularAlbedo || + view == RendererDebugView::DlssRrNormals || + view == RendererDebugView::DlssRrRoughness || + view == RendererDebugView::DlssRrDiffuseHitDistance || + view == RendererDebugView::DlssRrSpecularHitDistance || + view == RendererDebugView::DlssRrReflectedAlbedo || + view == RendererDebugView::DlssRrDisocclusionMask || + view == RendererDebugView::DlssRrDiffuseRayDirection || + view == RendererDebugView::DlssRrSpecularRayDirection || + view == RendererDebugView::DlssRrDiffuseRayDirectionHitDistance || + view == RendererDebugView::DlssRrSpecularRayDirectionHitDistance; +} + bool shouldExportDebugViewForSettings(const RendererSettings& settings, RendererDebugView view) { if (settings.restirGiMode == RestirGiMode::Off && isRestirGiExportView(view)) { return false; @@ -78,9 +112,96 @@ bool shouldExportDebugViewForSettings(const RendererSettings& settings, Renderer if (settings.restirDiMode == RestirDiMode::Off && isRestirDiExportView(view)) { return false; } + if (isDlssExportView(view) && + settings.temporalUpscaler != TemporalUpscaler::Dlss && + !settings.dlssRayReconstructionEnabled) { + return false; + } + if (isDlssRayReconstructionExportView(view) && !settings.dlssRayReconstructionEnabled) { + return false; + } return true; } +std::vector exportableRendererDebugViewNames() { + const std::vector views = DiagnosticImageExport::allExportViews(); + std::vector names; + names.reserve(views.size()); + for (RendererDebugView view : views) { + names.emplace_back(rendererDebugViewName(view)); + } + return names; +} + +std::vector reservoirContractDebugViews(const RendererSettings& settings) { + std::vector views; + if (settings.restirDiMode != RestirDiMode::Off) { + views.insert(views.end(), { + RendererDebugView::RestirDiSelectedLight, + RendererDebugView::RestirDiTarget, + RendererDebugView::RestirDiSourcePdf, + RendererDebugView::RestirDiWeightSum, + RendererDebugView::RestirDiM, + RendererDebugView::RestirDiAge, + RendererDebugView::RestirDiConfidence, + RendererDebugView::RestirDiVisibility, + RendererDebugView::RestirDiRejectionReason, + }); + } + if (settings.restirGiMode != RestirGiMode::Off) { + views.insert(views.end(), { + RendererDebugView::RestirGiTarget, + RendererDebugView::RestirGiSourcePdf, + RendererDebugView::RestirGiWeightSum, + RendererDebugView::RestirGiM, + RendererDebugView::RestirGiAge, + RendererDebugView::RestirGiConfidence, + RendererDebugView::RestirGiVisibility, + RendererDebugView::RestirGiPathClass, + }); + } + std::vector names; + names.reserve(views.size()); + for (RendererDebugView view : views) { + names.emplace_back(rendererDebugViewName(view)); + } + return names; +} + +std::vector dlssGuideContractDebugViews(const RendererSettings& settings) { + std::vector views; + if (settings.temporalUpscaler == TemporalUpscaler::Dlss || settings.dlssRayReconstructionEnabled) { + views.insert(views.end(), { + RendererDebugView::DlssDepth, + RendererDebugView::DlssMotionVectors, + RendererDebugView::DlssInputColor, + RendererDebugView::DlssOutputColor, + }); + } + if (settings.dlssRayReconstructionEnabled) { + views.insert(views.end(), { + RendererDebugView::DlssRrDiffuseAlbedo, + RendererDebugView::DlssRrSpecularAlbedo, + RendererDebugView::DlssRrNormals, + RendererDebugView::DlssRrRoughness, + RendererDebugView::DlssRrDiffuseHitDistance, + RendererDebugView::DlssRrSpecularHitDistance, + RendererDebugView::DlssRrReflectedAlbedo, + RendererDebugView::DlssRrDisocclusionMask, + RendererDebugView::DlssRrDiffuseRayDirection, + RendererDebugView::DlssRrSpecularRayDirection, + RendererDebugView::DlssRrDiffuseRayDirectionHitDistance, + RendererDebugView::DlssRrSpecularRayDirectionHitDistance, + }); + } + std::vector names; + names.reserve(views.size()); + for (RendererDebugView view : views) { + names.emplace_back(rendererDebugViewName(view)); + } + return names; +} + } // namespace void to_json(nlohmann::json& j, const ProfileReport::Resolution& r) { @@ -132,6 +253,10 @@ void to_json(nlohmann::json& j, const ProfileReport::PerPassGpuMs& p) { j["skip_denoiser_copy"] = p.skipDenoiserCopy; j["taa"] = p.taa; j["taa_history_copy"] = p.taaHistoryCopy; + j["dlss_guides"] = p.dlssGuides; + j["dlss"] = p.dlss; + j["dlss_rr_guides"] = p.dlssRayReconstructionGuides; + j["dlss_rr"] = p.dlssRayReconstruction; j["auto_exposure_histogram_clear"] = p.autoExposureHistogramClear; j["auto_exposure_histogram"] = p.autoExposureHistogram; j["auto_exposure_reduce"] = p.autoExposureReduce; @@ -509,6 +634,9 @@ void appendCompletedTopologySchedulerReport(ProfileReport& report) { } void to_json(nlohmann::json& j, const ProfileReport::PipelineStatistics& s) { + j["supported"] = s.supported; + j["valid"] = s.valid; + j["unavailable_reason"] = s.unavailableReason; j["ray_invocations"] = s.rayInvocations; j["triangle_hits"] = s.triangleHits; j["aabb_hits"] = s.aabbHits; @@ -1124,11 +1252,137 @@ void to_json(nlohmann::json& j, const ProfileReport::NvidiaIntegrationReport& n) j["nrd_requestable"] = n.nrdRequestable; j["nrd_available"] = n.nrdAvailable; j["nrd_unavailable_reason"] = n.nrdUnavailableReason; + j["nrd_direct_runtime_resources_ready"] = n.nrdDirectRuntimeResourcesReady; + j["nrd_history_confidence_inputs_allocated"] = n.nrdHistoryConfidenceInputsAllocated; + j["nrd_history_confidence_available"] = n.nrdHistoryConfidenceAvailable; + j["nrd_validation_output_allocated"] = n.nrdValidationOutputAllocated; + j["nrd_validation_output_enabled"] = n.nrdValidationOutputEnabled; + j["nrd_guide_contract_reason"] = n.nrdGuideContractReason; + j["nrd_guide_contract"] = { + {"schema_version", 1}, + {"motion_vectors", true}, + {"normal_roughness", true}, + {"view_z", true}, + {"diffuse_radiance_hit_distance", true}, + {"specular_radiance_hit_distance", true}, + {"diffuse_history_confidence", n.nrdHistoryConfidenceAvailable}, + {"specular_history_confidence", n.nrdHistoryConfidenceAvailable}, + {"history_confidence_common_setting", n.nrdHistoryConfidenceAvailable}, + {"restir_source_pixel_lookup", n.nrdHistoryConfidenceAvailable}, + {"validation_output_allocated", n.nrdValidationOutputAllocated}, + {"validation_output_enabled", n.nrdValidationOutputEnabled}, + {"validation_debug_view", "nrd-validation"}, + {"confidence_debug_views", { + "nrd-diffuse-confidence", + "nrd-specular-confidence", + "nrd-raw-confidence-gradient", + "nrd-filtered-confidence-gradient", + "nrd-confidence-history", + }}, + {"history_confidence_format", "r16f"}, + {"gradient_format", "rgba16f"}, + {"gradient_sampling", "rotating-4x4-strata"}, + {"gradient_filter", "5x5-strata-wide-depth-normal-bilateral"}, + {"selected_light_replay", "current-and-previous-receiver-evaluation"}, + {"light_history_mapping", "stable-identity-current-previous-gpu-tables"}, + {"primary_surface_replacement", { + {"schema_version", 1}, + {"guide_record_bytes", 48}, + {"activation", "first-bounce-delta-or-near-mirror-reflection"}, + {"primary_fallback_populated", true}, + {"capture_precedes_russian_roulette", true}, + {"nrd_replacement_guides", {"motion", "view_z", "normal", "roughness", "diffuse_albedo", "specular_f0", "hit_distance", "ray_direction"}}, + {"dlss_rr_replacement_guides", {"depth", "motion", "normal", "roughness", "diffuse_albedo", "specular_f0", "reflected_albedo", "hit_distance", "ray_direction"}}, + {"debug_views", { + "psr-active-mask", + "psr-depth", + "psr-motion", + "psr-normal-roughness", + "psr-hit-distance", + "psr-albedo-f0", + "psr-ray-direction", + }}, + }}, + {"confidence_source", n.nrdHistoryConfidenceAvailable + ? "restir-selected-light-opposite-frame-replay" + : "not-generated"}, + }; j["nrd_backend_policy"] = n.nrdBackendPolicy; j["nrd_backend_policy_reason"] = n.nrdBackendPolicyReason; j["nrd_backends_mutually_exclusive"] = n.nrdBackendsMutuallyExclusive; j["requested_denoiser_backend"] = n.requestedDenoiserBackend; j["effective_denoiser_backend"] = n.effectiveDenoiserBackend; + j["backend_comparison_policy"] = { + {"schema_version", 1}, + {"policy_name", "RTXDI Q5D backend comparison policy"}, + {"promotion_gate", "scripts/backend_comparison_matrix.ps1"}, + {"active_mode", { + {"requested_denoiser_backend", n.requestedDenoiserBackend}, + {"effective_denoiser_backend", n.effectiveDenoiserBackend}, + {"requested_temporal_upscaler", n.requestedTemporalUpscaler}, + {"effective_temporal_upscaler", n.effectiveTemporalUpscaler}, + {"requested_dlss_ray_reconstruction", n.requestedDlssRayReconstruction}, + {"effective_dlss_ray_reconstruction", n.effectiveDlssRayReconstruction}, + }}, + {"current_safe_default", { + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"reason", "Engine denoiser plus TAA/TSR remains the safe default until Q5D evidence promotes another backend."}, + }}, + {"fallback_order", { + { + {"rank", 1}, + {"mode", "engine_taa"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Always available safe default."}, + }, + { + {"rank", 2}, + {"mode", "nrd_taa"}, + {"denoiser_backend", "nrd"}, + {"temporal_upscaler", "taa-tsr"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use only when direct NRD is available and the Q5D matrix passes quality/stability gates."}, + }, + { + {"rank", 3}, + {"mode", "engine_dlss"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use when DLSS is available/requested; fall back to TAA/TSR when DLSS is unavailable."}, + }, + { + {"rank", 4}, + {"mode", "nrd_dlss"}, + {"denoiser_backend", "nrd"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Use only when NRD and DLSS are both available and Q5D evidence beats/ties lower-risk modes."}, + }, + { + {"rank", 5}, + {"mode", "dlss_rr"}, + {"denoiser_backend", "engine"}, + {"temporal_upscaler", "dlss"}, + {"dlss_ray_reconstruction", true}, + {"condition", "Opt-in only; requires DLSS RR availability plus valid depth, motion, disocclusion, ray-direction, hit-distance, and reflected-albedo guides."}, + }, + { + {"rank", 6}, + {"mode", "reference_no_temporal"}, + {"denoiser_backend", "off"}, + {"temporal_upscaler", "off"}, + {"dlss_ray_reconstruction", false}, + {"condition", "Reference/diagnostic accumulation only; never a realtime fallback."}, + }, + }}, + {"promotion_rule", "Do not change defaults until the candidate wins or ties equal-time quality/stability, has valid guide/profile diagnostics, and records a rollback CLI setting."}, + {"failure_policy", "Backend-specific failures must be visible in profile JSON, validation logs, and debug guide exports; tone mapping, auto exposure, final-output clamps, or TAA history must not hide them."}, + }; j["dlss_sdk_configured"] = n.dlssSdkConfigured; j["dlss_available"] = n.dlssAvailable; j["dlss_unavailable_reason"] = n.dlssUnavailableReason; @@ -1136,10 +1390,154 @@ void to_json(nlohmann::json& j, const ProfileReport::NvidiaIntegrationReport& n) j["dlss_ray_reconstruction_unavailable_reason"] = n.dlssRayReconstructionUnavailableReason; j["requested_dlss_ray_reconstruction"] = n.requestedDlssRayReconstruction; j["effective_dlss_ray_reconstruction"] = n.effectiveDlssRayReconstruction; + auto taggedResource = [](const char* role, const char* debugView, const ProfileReport::NvidiaIntegrationReport::StreamlineTagReport& tags) { + return nlohmann::json{ + {"role", role}, + {"debug_view", debugView}, + {"expected", true}, + {"tagged", tags.tagged > 0}, + {"failed", tags.failed}, + {"invalid_layout", tags.invalidLayout}, + {"invalid_format", tags.invalidFormat}, + {"invalid_extent", tags.invalidExtent}, + {"runtime_rejected", tags.runtimeRejected}, + {"evaluation_result_source", "feature_evaluation_summary"}, + }; + }; + j["dlss_guide_contract"] = { + {"schema_version", 1}, + {"guide_pass_ready", n.dlssAvailable || n.streamlineDlss.supported}, + {"debug_views", { + "dlss-depth", + "dlss-motion-vectors", + "dlss-input-color", + "dlss-output-color", + }}, + {"depth_convention", "hardware-depth for DLSS upscale; linear view depth for DLSS Ray Reconstruction"}, + {"motion_convention", "previous-minus-current pixels; Streamline tags and NGX eval use MV scale 1,1"}, + {"jitter_convention", "NGX receives projection jitter as -camera.jitter.xy; Streamline frame constants use the same projection-space sign"}, + {"roughness_clamp", "not used by DLSS upscale"}, + {"tagged_resources", { + taggedResource("scaling-input-color", "dlss-input-color", n.streamlineDlssTags), + taggedResource("scaling-output-color", "dlss-output-color", n.streamlineDlssTags), + taggedResource("depth", "dlss-depth", n.streamlineDlssTags), + taggedResource("motion-vectors", "dlss-motion-vectors", n.streamlineDlssTags), + }}, + {"streamline_tags", n.streamlineDlssTags}, + {"streamline_evaluation", n.streamlineDlssEvaluation}, + }; + j["dlss_ray_reconstruction_guide_contract"] = { + {"schema_version", 1}, + {"mode", n.dlssRayReconstructionGuideMode}, + {"guide_pass_ready", n.dlssRayReconstructionGuidePassReady}, + {"guide_images_allocated", n.dlssRayReconstructionGuideImagesAllocated}, + {"guide_image_count", n.dlssRayReconstructionGuideImageCount}, + {"psr_guide_buffer_allocated", n.dlssRayReconstructionPsrGuideBufferAllocated}, + {"psr_history_signatures_allocated", n.dlssRayReconstructionPsrHistorySignaturesAllocated}, + {"psr_history_signature_bytes_per_pixel", 4}, + {"psr_history_signature_policy", "active replacement pixels invalidate disocclusion when reprojected previous signature differs"}, + {"uses_psr_guides", n.dlssRayReconstructionUsesPsrGuides}, + {"replacement_source", "primary-surface-replacement"}, + {"guide_images", { + "depth", + "motion", + "diffuse_albedo", + "specular_albedo", + "normal", + "roughness", + "diffuse_hit_distance", + "specular_hit_distance", + "reflected_albedo", + "disocclusion_mask", + "diffuse_ray_direction", + "specular_ray_direction", + "diffuse_ray_direction_hit_distance", + "specular_ray_direction_hit_distance", + }}, + {"debug_views", { + "dlss-depth", + "dlss-motion-vectors", + "dlss-input-color", + "dlss-output-color", + "dlss-rr-diffuse-albedo", + "dlss-rr-specular-albedo", + "dlss-rr-normals", + "dlss-rr-roughness", + "dlss-rr-diffuse-hit-distance", + "dlss-rr-specular-hit-distance", + "dlss-rr-reflected-albedo", + "dlss-rr-disocclusion-mask", + "dlss-rr-diffuse-ray-direction", + "dlss-rr-specular-ray-direction", + "dlss-rr-diffuse-ray-direction-hit-distance", + "dlss-rr-specular-ray-direction-hit-distance", + }}, + {"guide_invariants", { + {"depth", "linear view depth, finite, nonnegative"}, + {"motion", "previous-minus-current pixels, matching Streamline/NGX MV scale 1,1"}, + {"jitter", "projection jitter sign documented as -camera.jitter.xy for NGX"}, + {"normals", "normalized world-space vectors"}, + {"roughness", "clamped to [0.001, 1]"}, + {"hit_distance", "finite, nonnegative, paired with the matching ray-direction guide"}, + {"disocclusion", "1 only for history reset, invalid/reprojected history, alpha/adaptive-fill breaks, or PSR signature changes"}, + {"reflected_albedo", "specular/reflection albedo, with PSR reflected paths using replacement diffuse albedo"}, + }}, + {"guide_consistency_validator", { + {"schema_version", 1}, + {"camera_motion", true}, + {"previous_world_position", true}, + {"material_id", true}, + {"instance_id", true}, + {"adaptive_fill_mask", true}, + {"psr_history_signature", n.dlssRayReconstructionPsrHistorySignaturesAllocated}, + }}, + {"tagged_resources", { + taggedResource("scaling-input-color", "dlss-input-color", n.streamlineDlssRayReconstructionTags), + taggedResource("scaling-output-color", "dlss-output-color", n.streamlineDlssRayReconstructionTags), + taggedResource("depth", "dlss-depth", n.streamlineDlssRayReconstructionTags), + taggedResource("motion-vectors", "dlss-motion-vectors", n.streamlineDlssRayReconstructionTags), + taggedResource("albedo", "dlss-rr-diffuse-albedo", n.streamlineDlssRayReconstructionTags), + taggedResource("specular-albedo", "dlss-rr-specular-albedo", n.streamlineDlssRayReconstructionTags), + taggedResource("normals", "dlss-rr-normals", n.streamlineDlssRayReconstructionTags), + taggedResource("roughness", "dlss-rr-roughness", n.streamlineDlssRayReconstructionTags), + taggedResource("diffuse-hit-distance", "dlss-rr-diffuse-hit-distance", n.streamlineDlssRayReconstructionTags), + taggedResource("specular-hit-distance", "dlss-rr-specular-hit-distance", n.streamlineDlssRayReconstructionTags), + taggedResource("reflected-albedo", "dlss-rr-reflected-albedo", n.streamlineDlssRayReconstructionTags), + taggedResource("disocclusion-mask", "dlss-rr-disocclusion-mask", n.streamlineDlssRayReconstructionTags), + taggedResource("diffuse-ray-direction", "dlss-rr-diffuse-ray-direction", n.streamlineDlssRayReconstructionTags), + taggedResource("specular-ray-direction", "dlss-rr-specular-ray-direction", n.streamlineDlssRayReconstructionTags), + taggedResource("diffuse-ray-direction-hit-distance", "dlss-rr-diffuse-ray-direction-hit-distance", n.streamlineDlssRayReconstructionTags), + taggedResource("specular-ray-direction-hit-distance", "dlss-rr-specular-ray-direction-hit-distance", n.streamlineDlssRayReconstructionTags), + }}, + {"streamline_tags", n.streamlineDlssRayReconstructionTags}, + {"streamline_evaluation", n.streamlineDlssRayReconstructionEvaluation}, + {"ngx_evaluation", n.ngxDlssRayReconstructionEvaluation}, + {"psr_replaced_channels", { + "depth", + "motion", + "normal", + "roughness", + "diffuse_albedo", + "specular_f0", + "reflected_albedo", + "hit_distance", + "ray_direction", + }}, + }; j["dlss_frame_generation_available"] = n.dlssFrameGenerationAvailable; j["dlss_frame_generation_unavailable_reason"] = n.dlssFrameGenerationUnavailableReason; j["requested_dlss_frame_generation"] = n.requestedDlssFrameGeneration; j["effective_dlss_frame_generation"] = n.effectiveDlssFrameGeneration; + j["dlss_exposure_contract"] = { + {"schema_version", 1}, + {"manual_exposure", n.dlssManualExposure}, + {"auto_exposure_enabled", n.dlssAutoExposureEnabled}, + {"exposure_buffer_available", n.dlssExposureBufferAvailable}, + {"exposure_buffer_passed_to_sdk", n.dlssExposureBufferPassedToSdk}, + {"pre_exposure", n.dlssPreExposure}, + {"exposure_scale", n.dlssExposureScale}, + {"policy", "DLSS/NGX currently receives explicit pre-exposure and exposure-scale constants; the renderer exposure buffer is reported but not passed to the SDK."}, + }; j["dlss_sharpening_strength"] = n.dlssSharpeningStrength; j["requested_temporal_upscaler"] = n.requestedTemporalUpscaler; j["effective_temporal_upscaler"] = n.effectiveTemporalUpscaler; @@ -1268,6 +1666,7 @@ void to_json(nlohmann::json& j, const RendererSettings& s) { j["sun_intensity"] = s.sunIntensity; j["sun_elevation"] = s.sunElevation; j["sun_azimuth"] = s.sunAzimuth; + j["sky_intensity"] = s.skyIntensity; j["gamma"] = s.gamma; j["contrast"] = s.contrast; j["saturation"] = s.saturation; @@ -1451,6 +1850,10 @@ GpuFrameTimings percentileGpuTimings( result.skipDenoiserCopyMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::skipDenoiserCopyMs, percentile); result.taaMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::taaMs, percentile); result.taaHistoryCopyMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::taaHistoryCopyMs, percentile); + result.dlssGuidesMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::dlssGuidesMs, percentile); + result.dlssMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::dlssMs, percentile); + result.dlssRayReconstructionGuidesMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::dlssRayReconstructionGuidesMs, percentile); + result.dlssRayReconstructionMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::dlssRayReconstructionMs, percentile); result.autoExposureMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::autoExposureMs, percentile); result.autoExposureHistogramClearMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::autoExposureHistogramClearMs, percentile); result.autoExposureHistogramMs = percentileGpuTiming(values, warmupFrames, &GpuFrameTimings::autoExposureHistogramMs, percentile); @@ -1518,6 +1921,10 @@ void assignPerPassGpuMs(ProfileReport::PerPassGpuMs& out, const GpuFrameTimings& out.skipDenoiserCopy = timings.skipDenoiserCopyMs; out.taa = timings.taaMs; out.taaHistoryCopy = timings.taaHistoryCopyMs; + out.dlssGuides = timings.dlssGuidesMs; + out.dlss = timings.dlssMs; + out.dlssRayReconstructionGuides = timings.dlssRayReconstructionGuidesMs; + out.dlssRayReconstruction = timings.dlssRayReconstructionMs; out.autoExposureHistogramClear = timings.autoExposureHistogramClearMs; out.autoExposureHistogram = timings.autoExposureHistogramMs; out.autoExposureReduce = timings.autoExposureReduceMs; @@ -1587,6 +1994,10 @@ GpuFrameTimings averageGpuTimings(const std::vector& values, ui result.skipDenoiserCopyMs += values[i].skipDenoiserCopyMs; result.taaMs += values[i].taaMs; result.taaHistoryCopyMs += values[i].taaHistoryCopyMs; + result.dlssGuidesMs += values[i].dlssGuidesMs; + result.dlssMs += values[i].dlssMs; + result.dlssRayReconstructionGuidesMs += values[i].dlssRayReconstructionGuidesMs; + result.dlssRayReconstructionMs += values[i].dlssRayReconstructionMs; result.autoExposureMs += values[i].autoExposureMs; result.autoExposureHistogramClearMs += values[i].autoExposureHistogramClearMs; result.autoExposureHistogramMs += values[i].autoExposureHistogramMs; @@ -1646,6 +2057,10 @@ GpuFrameTimings averageGpuTimings(const std::vector& values, ui result.skipDenoiserCopyMs *= invCount; result.taaMs *= invCount; result.taaHistoryCopyMs *= invCount; + result.dlssGuidesMs *= invCount; + result.dlssMs *= invCount; + result.dlssRayReconstructionGuidesMs *= invCount; + result.dlssRayReconstructionMs *= invCount; result.autoExposureMs *= invCount; result.autoExposureHistogramClearMs *= invCount; result.autoExposureHistogramMs *= invCount; @@ -1747,6 +2162,542 @@ std::string sequenceFrameFileName(uint32_t frameIndex) { return stream.str(); } +const char* temporalResidencyName(TemporalSystem::TemporalResidency residency) { + switch (residency) { + case TemporalSystem::TemporalResidency::Persistent: return "persistent"; + case TemporalSystem::TemporalResidency::Evictable: return "evictable"; + case TemporalSystem::TemporalResidency::HalfResolution: return "half_resolution"; + case TemporalSystem::TemporalResidency::DynamicResolution: return "dynamic_resolution"; + } + return "unknown"; +} + +nlohmann::json temporalSystemDiagnosticsJson(const TemporalSystem* temporalSystem) { + if (temporalSystem == nullptr) { + return nlohmann::json{ + {"schema_version", 1}, + {"available", false}, + {"slots", nlohmann::json::array()}, + }; + } + + std::vector> slots; + slots.reserve(temporalSystem->historySlots().size()); + for (const auto& [name, slot] : temporalSystem->historySlots()) { + slots.emplace_back(name, &slot); + } + std::sort(slots.begin(), slots.end(), [](const auto& a, const auto& b) { + return a.first < b.first; + }); + + uint32_t validSlotCount = 0; + uint32_t residentSlotCount = 0; + nlohmann::json slotJson = nlohmann::json::array(); + for (const auto& [name, slot] : slots) { + if (slot->valid) { + ++validSlotCount; + } + if (slot->resident) { + ++residentSlotCount; + } + slotJson.push_back({ + {"name", name}, + {"format", static_cast(slot->format)}, + {"extent", {{"width", slot->extent.width}, {"height", slot->extent.height}}}, + {"valid", slot->valid}, + {"resident", slot->resident}, + {"last_written_frame", slot->lastWrittenFrame}, + {"estimated_bytes", slot->estimatedBytes}, + {"residency", temporalResidencyName(slot->residency)}, + {"memory_budget_weight", slot->memoryBudgetWeight}, + }); + } + + return nlohmann::json{ + {"schema_version", 1}, + {"available", true}, + {"frame_index", temporalSystem->frameIndex()}, + {"camera_cut", temporalSystem->isCameraCut()}, + {"last_reset_reason", accumulationResetReasonName(temporalSystem->lastResetReason())}, + {"total_history_memory_bytes", temporalSystem->totalHistoryMemoryBytes()}, + {"slot_count", slots.size()}, + {"valid_slot_count", validSlotCount}, + {"resident_slot_count", residentSlotCount}, + {"slots", std::move(slotJson)}, + }; +} + +nlohmann::json validateRestirReferenceMatrixRows( + const nlohmann::json& rows, + const std::vector& requiredScenes, + const std::vector& requiredModes, + bool qualityRows) { + std::set requiredKeys; + for (const std::string& scene : requiredScenes) { + for (const std::string& mode : requiredModes) { + requiredKeys.insert(scene + "|" + mode); + } + } + + std::set foundKeys; + nlohmann::json failedRows = nlohmann::json::array(); + if (rows.is_array()) { + for (const nlohmann::json& row : rows) { + if (!row.is_object()) { + continue; + } + const std::string scene = row.value("scene", std::string{}); + const std::string mode = row.value("mode", std::string{}); + const std::string key = scene + "|" + mode; + if (requiredKeys.find(key) == requiredKeys.end()) { + continue; + } + foundKeys.insert(key); + const bool renderPassed = row.value("render_exit_code", -1) == 0; + const bool validationPassed = + row.contains("validation_enabled") && + row.value("validation_enabled", false) && + row.contains("validation_error_count") && + row.value("validation_error_count", -1) == 0; + const bool qualityPassed = !qualityRows || + (row.value("compare_exit_code", -1) == 0 && + row.value("status", std::string{}) == "pass"); + if (!renderPassed || !validationPassed || !qualityPassed) { + failedRows.push_back({ + {"scene", scene}, + {"mode", mode}, + {"render_exit_code", row.value("render_exit_code", -1)}, + {"compare_exit_code", row.contains("compare_exit_code") ? row["compare_exit_code"] : nlohmann::json(nullptr)}, + {"validation_enabled", row.contains("validation_enabled") ? row["validation_enabled"] : nlohmann::json(nullptr)}, + {"validation_error_count", row.contains("validation_error_count") ? row["validation_error_count"] : nlohmann::json(nullptr)}, + {"status", row.contains("status") ? row["status"] : nlohmann::json(nullptr)}, + }); + } + } + } + + nlohmann::json missingRows = nlohmann::json::array(); + for (const std::string& key : requiredKeys) { + if (foundKeys.find(key) == foundKeys.end()) { + const size_t separator = key.find('|'); + missingRows.push_back({ + {"scene", key.substr(0, separator)}, + {"mode", separator == std::string::npos ? std::string{} : key.substr(separator + 1u)}, + }); + } + } + + const bool passed = rows.is_array() && missingRows.empty() && failedRows.empty(); + return nlohmann::json{ + {"row_count", rows.is_array() ? rows.size() : 0u}, + {"required_row_count", requiredKeys.size()}, + {"matched_row_count", foundKeys.size()}, + {"missing_rows", std::move(missingRows)}, + {"failed_rows", std::move(failedRows)}, + {"passed", passed}, + }; +} + +nlohmann::json restirReferenceMatrixArtifactAt( + const std::filesystem::path& root, + const std::vector& requiredScenes, + const std::vector& requiredModes, + const char* label) { + auto readJson = [](const std::filesystem::path& path, nlohmann::json& out, std::string& error) { + try { + std::ifstream file(path); + if (!file.is_open()) { + error = "not readable"; + return false; + } + file >> out; + return true; + } catch (const std::exception& e) { + error = e.what(); + return false; + } + }; + + const std::filesystem::path perfPath = root / "perf" / "summary.json"; + const std::filesystem::path qualityPath = root / "quality_1spp" / "summary.json"; + const std::filesystem::path combinedPath = root / "merged" / "combined_summary.json"; + const std::filesystem::path aggregatePath = root / "merged" / "aggregate_summary.json"; + + nlohmann::json perfRows; + nlohmann::json qualityRows; + nlohmann::json combinedRows; + nlohmann::json aggregateRows; + std::string perfError; + std::string qualityError; + std::string combinedError; + std::string aggregateError; + const bool perfLoaded = readJson(perfPath, perfRows, perfError); + const bool qualityLoaded = readJson(qualityPath, qualityRows, qualityError); + const bool combinedLoaded = readJson(combinedPath, combinedRows, combinedError); + const bool aggregateLoaded = readJson(aggregatePath, aggregateRows, aggregateError); + + nlohmann::json perfValidation = validateRestirReferenceMatrixRows( + perfRows, + requiredScenes, + requiredModes, + false); + nlohmann::json qualityValidation = validateRestirReferenceMatrixRows( + qualityRows, + requiredScenes, + requiredModes, + true); + const bool artifactPresent = perfLoaded && qualityLoaded && combinedLoaded && aggregateLoaded; + const bool passed = + artifactPresent && + perfValidation.value("passed", false) && + qualityValidation.value("passed", false); + + return nlohmann::json{ + {"label", label}, + {"artifact_root", root.string()}, + {"artifact_present", artifactPresent}, + {"paths", { + {"perf_summary", perfPath.string()}, + {"quality_summary", qualityPath.string()}, + {"combined_summary", combinedPath.string()}, + {"aggregate_summary", aggregatePath.string()}, + }}, + {"load_errors", { + {"perf_summary", perfLoaded ? nlohmann::json(nullptr) : nlohmann::json(perfError)}, + {"quality_summary", qualityLoaded ? nlohmann::json(nullptr) : nlohmann::json(qualityError)}, + {"combined_summary", combinedLoaded ? nlohmann::json(nullptr) : nlohmann::json(combinedError)}, + {"aggregate_summary", aggregateLoaded ? nlohmann::json(nullptr) : nlohmann::json(aggregateError)}, + }}, + {"required_scenes", requiredScenes}, + {"required_modes", requiredModes}, + {"perf_validation", std::move(perfValidation)}, + {"quality_validation", std::move(qualityValidation)}, + {"aggregate_row_count", aggregateRows.is_array() ? aggregateRows.size() : 0u}, + {"combined_row_count", combinedRows.is_array() ? combinedRows.size() : 0u}, + {"passed", passed}, + }; +} + +nlohmann::json restirReferenceMatrixArtifactJson() { + const std::vector requiredScenes{ + "cornell", + "closeup_cornell", + "material_grid", + "sponza_lightweight", + "san_miguel", + "bistro_interior", + "bistro_exterior", + "sponza_heavy", + }; + const std::vector requiredModes{ + "di_off_gi_off", + "di_on_gi_off", + "di_off_gi_on", + "di_on_gi_on", + }; + const std::vector smokeScenes{"cornell"}; + + nlohmann::json full = restirReferenceMatrixArtifactAt( + std::filesystem::path("out") / "restir_reference_matrix", + requiredScenes, + requiredModes, + "full"); + nlohmann::json smoke = restirReferenceMatrixArtifactAt( + std::filesystem::path("out") / "restir_reference_matrix_smoke", + smokeScenes, + requiredModes, + "cornell_smoke"); + const bool fullPassed = full.value("passed", false); + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "restir_reference_matrix_artifact"}, + {"full", std::move(full)}, + {"smoke", std::move(smoke)}, + {"full_coverage_required", true}, + {"passed", fullPassed}, + }; +} + +nlohmann::json manualBarrierEscapeReportJson() { + const ManualBarrierEscapeDiagnosticsSnapshot snapshot = manualBarrierEscapeDiagnosticsSnapshot(); + nlohmann::json failures = nlohmann::json::array(); + nlohmann::json checks = nlohmann::json::array(); + auto addCheck = [&](const char* name, bool passed, const char* message, nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (!passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + + uint64_t aggregateBarrierCount = 0; + nlohmann::json aggregates = nlohmann::json::array(); + for (const ManualBarrierEscapeAggregate& aggregate : snapshot.aggregates) { + aggregateBarrierCount += aggregate.count; + aggregates.push_back({ + {"source", aggregate.source}, + {"label", aggregate.label}, + {"resource_kind", aggregate.resourceKind}, + {"src_stage", static_cast(aggregate.srcStage)}, + {"src_access", static_cast(aggregate.srcAccess)}, + {"dst_stage", static_cast(aggregate.dstStage)}, + {"dst_access", static_cast(aggregate.dstAccess)}, + {"old_layout", static_cast(aggregate.oldLayout)}, + {"new_layout", static_cast(aggregate.newLayout)}, + {"count", aggregate.count}, + }); + } + + nlohmann::json recentEvents = nlohmann::json::array(); + for (const ManualBarrierEscapeEvent& event : snapshot.recentEvents) { + recentEvents.push_back({ + {"sequence", event.sequence}, + {"source", event.source}, + {"label", event.label}, + {"resource_kind", event.resourceKind}, + {"resource_handle", event.resourceHandle}, + {"src_stage", static_cast(event.srcStage)}, + {"src_access", static_cast(event.srcAccess)}, + {"dst_stage", static_cast(event.dstStage)}, + {"dst_access", static_cast(event.dstAccess)}, + {"old_layout", static_cast(event.oldLayout)}, + {"new_layout", static_cast(event.newLayout)}, + }); + } + + addCheck( + "manual_barrier_escape_diagnostics_enabled", + snapshot.enabled, + "Manual barrier escape diagnostics must be enabled for diagnostic/profile runs."); + addCheck( + "manual_barrier_escape_aggregate_consistent", + aggregateBarrierCount == snapshot.barrierCount, + "Manual barrier escape aggregate counts must match the recorded event count.", + { + {"aggregate_barrier_count", aggregateBarrierCount}, + {"barrier_count", snapshot.barrierCount}, + }); + + return nlohmann::json{ + {"schema_version", 1}, + {"contract", "manual_barrier_escape_report"}, + {"scope", "manual_vkCmdPipelineBarrier2_calls_outside_rendergraph"}, + {"rendergraph_barriers_excluded", true}, + {"enabled", snapshot.enabled}, + {"dependency_call_count", snapshot.dependencyCallCount}, + {"barrier_count", snapshot.barrierCount}, + {"aggregate_count", snapshot.aggregates.size()}, + {"dropped_recent_event_count", snapshot.droppedRecentEventCount}, + {"aggregates", std::move(aggregates)}, + {"recent_events", std::move(recentEvents)}, + {"checks", std::move(checks)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", failures.empty()}, + }; +} + +nlohmann::json renderGraphArtifactValidationJson(const std::optional& path) { + nlohmann::json checks = nlohmann::json::array(); + nlohmann::json failures = nlohmann::json::array(); + auto addCheck = [&](const char* name, bool passed, const char* message, nlohmann::json evidence = nlohmann::json::object()) { + checks.push_back({ + {"name", name}, + {"passed", passed}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + if (!passed) { + failures.push_back({ + {"code", name}, + {"message", message}, + }); + } + }; + + if (!path.has_value()) { + addCheck( + "rendergraph_artifact_not_requested", + true, + "No RenderGraph artifact was requested for this profile; runtime graph validation is deferred."); + return nlohmann::json{ + {"schema_version", 1}, + {"artifact_requested", false}, + {"artifact_path", nullptr}, + {"artifact_present", false}, + {"validated", false}, + {"resource_ownership_passed", nullptr}, + {"resource_lifetime_passed", nullptr}, + {"resource_lifetime_violation_count", nullptr}, + {"resource_lifetime_warning_count", nullptr}, + {"checks", std::move(checks)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", true}, + }; + } + + std::error_code existsError; + const bool artifactPresent = std::filesystem::exists(*path, existsError); + addCheck( + "rendergraph_artifact_present", + artifactPresent && !existsError, + "Requested RenderGraph JSON artifact must exist before profile JSON is written.", + { + {"path", path->string()}, + {"exists_error", existsError ? existsError.message() : std::string{}}, + }); + if (!artifactPresent || existsError) { + return nlohmann::json{ + {"schema_version", 1}, + {"artifact_requested", true}, + {"artifact_path", path->string()}, + {"artifact_present", false}, + {"validated", false}, + {"resource_ownership_passed", nullptr}, + {"resource_lifetime_passed", nullptr}, + {"resource_lifetime_violation_count", nullptr}, + {"resource_lifetime_warning_count", nullptr}, + {"checks", std::move(checks)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", false}, + }; + } + + nlohmann::json renderGraph = nlohmann::json::object(); + try { + std::ifstream file(*path); + if (!file.is_open()) { + addCheck( + "rendergraph_artifact_readable", + false, + "Requested RenderGraph JSON artifact must be readable.", + {{"path", path->string()}}); + } else { + file >> renderGraph; + addCheck( + "rendergraph_artifact_readable", + true, + "Requested RenderGraph JSON artifact is readable.", + {{"path", path->string()}}); + } + } catch (const std::exception& e) { + addCheck( + "rendergraph_artifact_parseable", + false, + "Requested RenderGraph JSON artifact must parse as JSON.", + { + {"path", path->string()}, + {"error", e.what()}, + }); + } + + const bool ownershipPresent = + renderGraph.is_object() && + renderGraph.contains("resource_ownership_validation") && + renderGraph["resource_ownership_validation"].is_object(); + const bool lifetimePresent = + renderGraph.is_object() && + renderGraph.contains("resource_lifetime_validation") && + renderGraph["resource_lifetime_validation"].is_object(); + const bool ownershipPassed = ownershipPresent && + renderGraph["resource_ownership_validation"].value("passed", false); + const bool lifetimePassed = lifetimePresent && + renderGraph["resource_lifetime_validation"].value("passed", false); + const uint32_t lifetimeViolationCount = lifetimePresent + ? renderGraph["resource_lifetime_validation"].value("violation_count", 0u) + : 0u; + const uint32_t lifetimeWarningCount = lifetimePresent + ? renderGraph["resource_lifetime_validation"].value("warning_count", 0u) + : 0u; + uint32_t passCount = 0; + uint32_t timingMappingFieldCount = 0; + uint32_t timingMappedCount = 0; + uint32_t timingUnmappedCount = 0; + nlohmann::json timingUnmappedPasses = nlohmann::json::array(); + if (renderGraph.is_object() && renderGraph.contains("passes") && renderGraph["passes"].is_array()) { + for (const auto& pass : renderGraph["passes"]) { + ++passCount; + const bool hasTimingMappingFields = + pass.is_object() && + pass.contains("gpu_ms_mapped") && + pass.contains("timing_source") && + pass.contains("profile_timing_key"); + if (hasTimingMappingFields) { + ++timingMappingFieldCount; + if (pass.value("gpu_ms_mapped", false)) { + ++timingMappedCount; + } else { + ++timingUnmappedCount; + timingUnmappedPasses.push_back(pass.value("name", "")); + } + } + } + } + + addCheck( + "resource_ownership_validation_present", + ownershipPresent, + "RenderGraph artifact must include resource ownership validation.", + ownershipPresent ? renderGraph["resource_ownership_validation"] : nlohmann::json(nullptr)); + addCheck( + "resource_ownership_validation_passed", + ownershipPassed, + "RenderGraph resource ownership validation must pass.", + ownershipPresent ? renderGraph["resource_ownership_validation"] : nlohmann::json(nullptr)); + addCheck( + "resource_lifetime_validation_present", + lifetimePresent, + "RenderGraph artifact must include resource lifetime and aliasing validation.", + lifetimePresent ? renderGraph["resource_lifetime_validation"] : nlohmann::json(nullptr)); + addCheck( + "resource_lifetime_validation_passed", + lifetimePassed, + "RenderGraph resource lifetime and aliasing validation must pass.", + lifetimePresent ? renderGraph["resource_lifetime_validation"] : nlohmann::json(nullptr)); + addCheck( + "pass_timing_mapping_fields_present", + passCount > 0 && timingMappingFieldCount == passCount, + "Every RenderGraph pass row must include timing mapping provenance fields.", + { + {"pass_count", passCount}, + {"timing_mapping_field_count", timingMappingFieldCount}, + {"timing_mapped_count", timingMappedCount}, + {"timing_unmapped_count", timingUnmappedCount}, + {"timing_unmapped_passes", timingUnmappedPasses}, + }); + + const bool validated = ownershipPresent && lifetimePresent; + const bool passed = failures.empty(); + return nlohmann::json{ + {"schema_version", 1}, + {"artifact_requested", true}, + {"artifact_path", path->string()}, + {"artifact_present", true}, + {"validated", validated}, + {"resource_ownership_passed", ownershipPassed}, + {"resource_lifetime_passed", lifetimePassed}, + {"resource_lifetime_violation_count", lifetimeViolationCount}, + {"resource_lifetime_warning_count", lifetimeWarningCount}, + {"pass_timing_mapping_fields_present", passCount > 0 && timingMappingFieldCount == passCount}, + {"pass_timing_mapped_count", timingMappedCount}, + {"pass_timing_unmapped_count", timingUnmappedCount}, + {"pass_timing_unmapped_passes", std::move(timingUnmappedPasses)}, + {"checks", std::move(checks)}, + {"failure_count", failures.size()}, + {"failures", std::move(failures)}, + {"passed", passed}, + }; +} + } // namespace HeadlessDiagnostics::HeadlessDiagnostics(const HeadlessDiagnosticsConfig& config) @@ -1783,6 +2734,8 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.profiledFrames = config_.totalFrames > config_.warmupFrames ? config_.totalFrames - config_.warmupFrames : 0; profileReport_.rayTracingDiagnosticCountersEnabled = config_.rayTracingDiagnosticCounters; + profileReport_.lastAccumulationResetReason = + accumulationResetReasonName(renderer->lastAccumulationResetReason()); const auto& cpuTimings = app.cpuFrameTimings(); const auto& gpuTimingsVec = app.gpuFrameTimings(); @@ -1826,12 +2779,14 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.opacityMicromap.disabledReason = ommInfo.disabledReason; const auto& serInfo = context->serInfo(); profileReport_.shaderExecutionReordering.supported = serInfo.supported; - profileReport_.shaderExecutionReordering.enabled = renderer->settings().shaderExecutionReorderingEnabled && serInfo.supported; profileReport_.shaderExecutionReordering.extensionSupported = serInfo.extensionSupported; profileReport_.shaderExecutionReordering.invocationReorderFeature = serInfo.invocationReorderFeature; profileReport_.shaderExecutionReordering.dedicatedSerPipeline = - profileReport_.shaderExecutionReordering.enabled && + renderer->settings().shaderExecutionReorderingEnabled && + serInfo.supported && renderer->settings().wavefrontTraceEnabled; + profileReport_.shaderExecutionReordering.enabled = + profileReport_.shaderExecutionReordering.dedicatedSerPipeline; profileReport_.shaderExecutionReordering.pipelineCreateFlagRequired = false; profileReport_.shaderExecutionReordering.maxInvocationReorderDepthReported = serInfo.maxInvocationReorderDepthReported; profileReport_.shaderExecutionReordering.maxRayTracingInvocationReorderDepth = serInfo.maxRayTracingInvocationReorderDepth; @@ -1842,6 +2797,12 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.shaderExecutionReordering.observedImprovementPercent = 0.0f; profileReport_.shaderExecutionReordering.reorderingHint = serReorderingHintName(serInfo.reorderingHint); profileReport_.shaderExecutionReordering.disabledReason = serInfo.disabledReason; + if (renderer->settings().shaderExecutionReorderingEnabled && + serInfo.supported && + !renderer->settings().wavefrontTraceEnabled) { + profileReport_.shaderExecutionReordering.disabledReason = + "the active generic path has no SER reorder instruction"; + } const auto& motionBlurInfo = context->rayTracingMotionBlurInfo(); profileReport_.rayTracingMotionBlur.supported = motionBlurInfo.supported; profileReport_.rayTracingMotionBlur.enabled = renderer->settings().motionBlurEnabled && motionBlurInfo.supported; @@ -1895,6 +2856,11 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.pipelineStatistics.rayInvocations = stats.rayInvocations; profileReport_.pipelineStatistics.triangleHits = stats.triangleHits; profileReport_.pipelineStatistics.aabbHits = stats.aabbHits; + profileReport_.pipelineStatistics.supported = stats.valid; + profileReport_.pipelineStatistics.valid = stats.valid; + profileReport_.pipelineStatistics.unavailableReason = stats.valid + ? std::string{} + : "Vulkan exposes no portable ray-invocation or ray-hit pipeline-statistics queries; use --rt-diagnostic-counters or vendor profiling tools."; const auto rtCounters = renderer->rayTracingDiagnosticCounters(); profileReport_.rayTracingDiagnosticCounters.rawSlots = rtCounters.rawSlots; @@ -2671,6 +3637,7 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.memory.gpuSceneLightBytes = static_cast(sceneMemory.lightBytes); profileReport_.memory.gpuSceneParameterBytes = static_cast(sceneMemory.parameterBytes); profileReport_.memory.temporalHistoryBytes = static_cast(renderer->temporalHistoryMemory()); + profileReport_.temporalSystemDiagnostics = temporalSystemDiagnosticsJson(renderer->temporalSystem()); profileReport_.memory.restirReservoirBytes = static_cast(renderer->restirReservoirMemory()); const auto reservoirBreakdown = renderer->restirReservoirMemoryBreakdown(); profileReport_.memory.restirDiCurrentBytes = static_cast(reservoirBreakdown.diCurrentBytes); @@ -2825,7 +3792,11 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { appendCompletedUploaderSchedulerReport(profileReport_); appendCompletedMainThreadApplySchedulerReport(profileReport_); appendCompletedTopologySchedulerReport(profileReport_); - profileReport_.validationErrorCount = 0; + profileReport_.validationEnabled = context->validationEnabled(); + const uint64_t validationErrors = context->validationErrorCount(); + profileReport_.validationErrorCount = validationErrors > UINT32_MAX + ? UINT32_MAX + : static_cast(validationErrors); profileReport_.settings = renderer->settings(); if (profileReport_.settings.lightingReuseMode == LightingReuseMode::ExperimentalRestirPT || @@ -2857,6 +3828,12 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.nvidiaIntegrations.nrdRequestable = nvidiaStatus.nrdRequestable; profileReport_.nvidiaIntegrations.nrdAvailable = nvidiaStatus.nrdAvailable; profileReport_.nvidiaIntegrations.nrdUnavailableReason = nvidiaStatus.nrdUnavailableReason; + profileReport_.nvidiaIntegrations.nrdDirectRuntimeResourcesReady = nvidiaStatus.nrdDirectRuntimeResourcesReady; + profileReport_.nvidiaIntegrations.nrdHistoryConfidenceInputsAllocated = nvidiaStatus.nrdHistoryConfidenceInputsAllocated; + profileReport_.nvidiaIntegrations.nrdHistoryConfidenceAvailable = nvidiaStatus.nrdHistoryConfidenceAvailable; + profileReport_.nvidiaIntegrations.nrdValidationOutputAllocated = nvidiaStatus.nrdValidationOutputAllocated; + profileReport_.nvidiaIntegrations.nrdValidationOutputEnabled = nvidiaStatus.nrdValidationOutputEnabled; + profileReport_.nvidiaIntegrations.nrdGuideContractReason = nvidiaStatus.nrdGuideContractReason; profileReport_.nvidiaIntegrations.requestedDenoiserBackend = denoiserBackendName(profileReport_.settings.denoiserBackend); profileReport_.nvidiaIntegrations.effectiveDenoiserBackend = denoiserBackendName(renderer->effectiveDenoiserBackend()); if (profileReport_.settings.denoiserBackend != DenoiserBackend::Nrd) { @@ -2879,11 +3856,24 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.nvidiaIntegrations.dlssRayReconstructionUnavailableReason = nvidiaStatus.dlssRayReconstructionUnavailableReason; profileReport_.nvidiaIntegrations.requestedDlssRayReconstruction = profileReport_.settings.dlssRayReconstructionEnabled; profileReport_.nvidiaIntegrations.effectiveDlssRayReconstruction = renderer->dlssRayReconstructionActive(); + profileReport_.nvidiaIntegrations.dlssRayReconstructionGuidePassReady = nvidiaStatus.dlssRayReconstructionGuidePassReady; + profileReport_.nvidiaIntegrations.dlssRayReconstructionGuideImagesAllocated = nvidiaStatus.dlssRayReconstructionGuideImagesAllocated; + profileReport_.nvidiaIntegrations.dlssRayReconstructionPsrGuideBufferAllocated = nvidiaStatus.dlssRayReconstructionPsrGuideBufferAllocated; + profileReport_.nvidiaIntegrations.dlssRayReconstructionPsrHistorySignaturesAllocated = nvidiaStatus.dlssRayReconstructionPsrHistorySignaturesAllocated; + profileReport_.nvidiaIntegrations.dlssRayReconstructionUsesPsrGuides = nvidiaStatus.dlssRayReconstructionUsesPsrGuides; + profileReport_.nvidiaIntegrations.dlssRayReconstructionGuideImageCount = nvidiaStatus.dlssRayReconstructionGuideImageCount; + profileReport_.nvidiaIntegrations.dlssRayReconstructionGuideMode = nvidiaStatus.dlssRayReconstructionGuideMode; profileReport_.nvidiaIntegrations.dlssFrameGenerationAvailable = nvidiaStatus.dlssFrameGenerationAvailable; profileReport_.nvidiaIntegrations.dlssFrameGenerationUnavailableReason = nvidiaStatus.dlssFrameGenerationUnavailableReason; profileReport_.nvidiaIntegrations.requestedDlssFrameGeneration = profileReport_.settings.dlssFrameGenerationEnabled; profileReport_.nvidiaIntegrations.effectiveDlssFrameGeneration = profileReport_.settings.dlssFrameGenerationEnabled && nvidiaStatus.dlssFrameGenerationAvailable; + profileReport_.nvidiaIntegrations.dlssAutoExposureEnabled = nvidiaStatus.dlssAutoExposureEnabled; + profileReport_.nvidiaIntegrations.dlssExposureBufferAvailable = nvidiaStatus.dlssExposureBufferAvailable; + profileReport_.nvidiaIntegrations.dlssExposureBufferPassedToSdk = nvidiaStatus.dlssExposureBufferPassedToSdk; + profileReport_.nvidiaIntegrations.dlssManualExposure = nvidiaStatus.dlssManualExposure; + profileReport_.nvidiaIntegrations.dlssPreExposure = nvidiaStatus.dlssPreExposure; + profileReport_.nvidiaIntegrations.dlssExposureScale = nvidiaStatus.dlssExposureScale; profileReport_.nvidiaIntegrations.dlssSharpeningStrength = profileReport_.settings.dlssSharpeningStrength; profileReport_.nvidiaIntegrations.requestedTemporalUpscaler = temporalUpscalerName(profileReport_.settings.temporalUpscaler); profileReport_.nvidiaIntegrations.effectiveTemporalUpscaler = temporalUpscalerName(renderer->effectiveTemporalUpscaler()); @@ -3022,6 +4012,10 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { {"Adaptive Sampling Diagnostics", profileReport_.perPassGpuMs.adaptiveSamplingDiagnostics}, {"Adaptive Sampling Fill", profileReport_.perPassGpuMs.adaptiveSamplingFill}, {"TAA/TSR", profileReport_.perPassGpuMs.taa}, + {"DLSS Guides", profileReport_.perPassGpuMs.dlssGuides}, + {"DLSS", profileReport_.perPassGpuMs.dlss}, + {"DLSS RR Guides", profileReport_.perPassGpuMs.dlssRayReconstructionGuides}, + {"DLSS RR", profileReport_.perPassGpuMs.dlssRayReconstruction}, {"ToneMap", profileReport_.perPassGpuMs.toneMap}, {"Fullscreen/Present", profileReport_.perPassGpuMs.fullscreen + profileReport_.perPassGpuMs.editorPresentation}, {"Dynamic BLAS Update", profileReport_.perPassGpuMs.dynamicBlasUpdate}, @@ -3066,6 +4060,10 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.perPassGpuMs.adaptiveSamplingDiagnostics + profileReport_.perPassGpuMs.adaptiveSamplingFill + profileReport_.perPassGpuMs.taa + + profileReport_.perPassGpuMs.dlssGuides + + profileReport_.perPassGpuMs.dlss + + profileReport_.perPassGpuMs.dlssRayReconstructionGuides + + profileReport_.perPassGpuMs.dlssRayReconstruction + profileReport_.perPassGpuMs.historyCopy + profileReport_.perPassGpuMs.taaHistoryCopy; if (totalGpuMs > 0.0f && temporalPostMs >= 2.0f && temporalPostMs / totalGpuMs >= 0.2f) { @@ -3188,9 +4186,16 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { : "Vulkan object names are unavailable; resource attribution may be weaker."); pushReadiness( "validation_clean", - profileReport_.validationErrorCount == 0u, - profileReport_.validationErrorCount == 0u ? "No validation errors were recorded." : "Validation errors were recorded; fix them before trusting performance.", - {{"validation_error_count", profileReport_.validationErrorCount}}); + profileReport_.validationEnabled && profileReport_.validationErrorCount == 0u, + !profileReport_.validationEnabled + ? "Vulkan validation was not enabled for this build/run; use a validation-enabled build before trusting this check." + : (profileReport_.validationErrorCount == 0u + ? "No validation errors were recorded." + : "Validation errors were recorded; fix them before trusting performance."), + { + {"validation_enabled", profileReport_.validationEnabled}, + {"validation_error_count", profileReport_.validationErrorCount}, + }); pushReadiness( "streaming_idle", streamingUploads == 0u, @@ -3417,6 +4422,7 @@ ProfileReport HeadlessDiagnostics::run(Application& app) { profileReport_.restirGiCounters.assign(giCounters, giCounters + 64); } profileReport_.restirDiHistoryValid = renderer->restirDiHistoryValid(); + profileReport_.restirGiHistoryValid = renderer->restirGiHistoryValid(); return profileReport_; } @@ -3433,6 +4439,7 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co j["frame_count"] = profileReport_.frameCount; j["warmup_frames"] = profileReport_.warmupFrames; j["profiled_frames"] = profileReport_.profiledFrames; + j["last_accumulation_reset_reason"] = profileReport_.lastAccumulationResetReason; j["cpu_frame_ms"] = profileReport_.cpuFrameMs; j["gpu_frame_ms"] = profileReport_.gpuFrameMs; j["per_pass_gpu_ms"] = profileReport_.perPassGpuMs; @@ -3512,11 +4519,22 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co }; alphaAnyHitTopMaterials = profileReport_.alphaAnyHitTopMaterials; } + const nlohmann::json pipelineRayCount = profileReport_.pipelineStatistics.valid + ? nlohmann::json(profileReport_.pipelineStatistics.rayInvocations) + : nlohmann::json(nullptr); + const nlohmann::json pipelineHitCount = profileReport_.pipelineStatistics.valid + ? nlohmann::json(hitCount) + : nlohmann::json(nullptr); + const nlohmann::json pipelineMissCount = profileReport_.pipelineStatistics.valid + ? nlohmann::json(profileReport_.pipelineStatistics.rayInvocations > hitCount + ? profileReport_.pipelineStatistics.rayInvocations - hitCount + : 0) + : nlohmann::json(nullptr); j["gpu_debug_counters"] = { - {"ray_count", profileReport_.pipelineStatistics.rayInvocations}, + {"ray_count", pipelineRayCount}, {"shadow_ray_count", nullptr}, - {"hit_count", hitCount}, - {"miss_count", profileReport_.pipelineStatistics.rayInvocations > hitCount ? profileReport_.pipelineStatistics.rayInvocations - hitCount : 0}, + {"hit_count", pipelineHitCount}, + {"miss_count", pipelineMissCount}, {"ray_tracing_instrumentation_enabled", profileReport_.rayTracingDiagnosticCountersEnabled}, {"ray_tracing_totals", rayTracingCounterTotals}, {"ray_tracing_per_rendered_frame", rayTracingCounterPerRenderedFrame}, @@ -3531,13 +4549,14 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"denoiser_history_accepted_count", nullptr}, {"denoiser_history_rejected_count", nullptr}, {"notes", nlohmann::json::array({ - "ray_count/hit_count/miss_count come from Vulkan pipeline statistics when available", + "ray_count/hit_count/miss_count are null because Vulkan has no portable ray-tracing pipeline-statistics query bits; use shader instrumentation or vendor tools", "ray tracing diagnostic totals accumulate over all rendered frames, including warmup, when --rt-diagnostic-counters is enabled", "ray tracing diagnostic fields are null when shader instrumentation is disabled", "remaining counters require shader atomic instrumentation and are intentionally null until instrumented" })}, }; j["memory"] = profileReport_.memory; + j["temporal_system"] = profileReport_.temporalSystemDiagnostics; j["texture_cache_diagnostics"] = profileReport_.textureDiagnostics; j["restir_di_counters"] = profileReport_.restirDiCounters; j["restir_gi_counters"] = profileReport_.restirGiCounters; @@ -3614,6 +4633,15 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"count", 1}, }); } + const uint64_t diContractInvalidSourcePdf = 0ull; + const uint64_t diContractInvalidTargetPdf = 0ull; + const uint64_t diContractOrParityViolation = diCounter(63); + const uint64_t diContractViolationCount = diContractOrParityViolation; + const bool diContractChecked = newDiRequested && + !profileReport_.restirDiCounters.empty() && + (initialPassActive || temporalPassActive || spatialPassActive || finalPassActive); + const bool diContractPassed = !newDiRequested || + (diContractChecked && diContractViolationCount == 0ull); const uint64_t diPixels = static_cast(profileReport_.resolution.renderWidth) * static_cast(profileReport_.resolution.renderHeight); const uint64_t diReservoirStride = diPixels > 0ull && profileReport_.memory.restirDiInitialBytes > 0ull @@ -3629,6 +4657,52 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"layout_version", 3}, {"mode", restirDiModeName(profileReport_.settings.restirDiMode)}, {"layout", restirDiReservoirLayoutName(profileReport_.settings.restirDiReservoirLayout)}, + {"reservoir_contract", { + {"schema_version", 1}, + {"selected_sample_identity_stored", true}, + {"selected_target_stored", true}, + {"selected_source_pdf_stored", true}, + {"weight_sum_stored", true}, + {"sample_count_m_stored", true}, + {"visibility_state_stored", true}, + {"age_stored", true}, + {"confidence_stored", true}, + {"rejection_reason_stored", true}, + {"debug_views", nlohmann::json::array({ + "restir-di-selected-light", + "restir-di-target", + "restir-di-source-pdf", + "restir-di-weight-sum", + "restir-di-m", + "restir-di-visibility", + "restir-di-age", + "restir-di-confidence", + "restir-di-rejection-reason", + })}, + }}, + {"reservoir_contract_validation", { + {"schema_version", 1}, + {"active", newDiRequested}, + {"checked", diContractChecked}, + {"passed", diContractPassed}, + {"counter_scope", "last_completed_frame"}, + {"invalid_source_pdf_count", diContractInvalidSourcePdf}, + {"invalid_target_pdf_count", diContractInvalidTargetPdf}, + {"non_finite_count", 0}, + {"contract_or_parity_violation_count", diContractOrParityViolation}, + {"violation_count", diContractViolationCount}, + {"evidence", { + {"initial_invalid_pdf_count", diCounter(10)}, + {"temporal_pdf_rejected_count", diCounter(25)}, + {"initial_invalid_target_count", diCounter(3)}, + {"temporal_target_rejected_count", diCounter(24)}, + {"spatial_target_rejected_count", diCounter(36)}, + {"initial_non_finite_count", diCounter(4)}, + {"temporal_non_finite_count", diCounter(13)}, + {"final_non_finite_count", diCounter(57)}, + {"stored_reservoir_contract_or_parity_invalid_count", diContractOrParityViolation}, + }}, + }}, {"counter_scope", "last_completed_frame"}, {"counter_value_bits", 32}, {"aggregate_encoding", "uint32-low-high-with-carry"}, @@ -3641,9 +4715,22 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co : "none"}, {"light_identity", { {"scheme", "persistent-id-hash32-generation-cached-index"}, - {"reorder_fallback", "history-invalidation"}, + {"reorder_fallback", "cached-index-then-stable-identity-remap"}, {"distribution_version_policy", "source-mass-preserved-current-pdf-recomputed"}, }}, + {"light_history_mapping", { + {"schema_version", 1}, + {"cached_index_fast_path", true}, + {"stable_identity_remap_on_cached_mismatch", true}, + {"remap_lookup_scope", "current-frame-scene-light-records"}, + {"stable_identity_fields", {"identity_hash", "identity_generation", "light_kind"}}, + {"changed_generation_policy", "reject"}, + {"unmapped_deleted_policy", "reject"}, + {"debug_view", "restir-di-light-map-status"}, + {"temporal_changed_or_unmapped_rejection_count", diCounter(23)}, + {"spatial_changed_or_unmapped_rejection_count", diCounter(42)}, + {"final_changed_or_unmapped_rejection_count", diCounter(59)}, + }}, {"direct_light_ownership", { {"emissive_and_analytic_lights", initialPassActive ? "restir_di" : "classic_nee"}, {"sun", initialPassActive && profileReport_.settings.restirDiIncludeSun @@ -3668,7 +4755,8 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"temporal", temporalPassActive}, {"spatial", spatialPassActive}, {"final", finalPassActive}, - {"history_copy", profileReport_.restirDiHistoryValid}, + {"history_copy", profileReport_.perPassGpuMs.restirDiHistoryCopy > 0.0f}, + {"history_valid", profileReport_.restirDiHistoryValid}, }}, {"initial_valid_count", diCounter(1)}, {"initial_invalid_surface_count", diCounter(2)}, @@ -3806,8 +4894,12 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co giCounter(38) + giCounter(39) + giCounter(40) + giCounter(41); const uint64_t giActiveTileCount = giCounter(46); const uint64_t giActivePixelCount = giCounter(47); - const uint64_t giTileColumns = (static_cast(profileReport_.resolution.renderWidth) + 15ull) / 16ull; - const uint64_t giTileRows = (static_cast(profileReport_.resolution.renderHeight) + 15ull) / 16ull; + const uint64_t giTileColumns = + (static_cast(profileReport_.resolution.renderWidth) + passes::RestirGIPass::kActiveTileSize - 1ull) / + passes::RestirGIPass::kActiveTileSize; + const uint64_t giTileRows = + (static_cast(profileReport_.resolution.renderHeight) + passes::RestirGIPass::kActiveTileSize - 1ull) / + passes::RestirGIPass::kActiveTileSize; const uint64_t giTileCount = giTileColumns * giTileRows; const uint64_t giReusePixels = (profileReport_.settings.restirGiHalfResolution && giPixels > 0ull) @@ -3851,15 +4943,86 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"count", 1}, }); } + const uint64_t giContractInvalidSourcePdf = giCounter(48); + const uint64_t giContractInvalidTargetPdf = giCounter(49); + const uint64_t giSourcePdfParityMismatch = giCounter(50); + const uint64_t giTargetPdfParityMismatch = giCounter(51); + const uint64_t giContractNonFinite = giCounter(45); + const uint64_t giContractViolationCount = + giContractInvalidSourcePdf + + giContractInvalidTargetPdf + + giSourcePdfParityMismatch + + giTargetPdfParityMismatch + + giContractNonFinite; + const bool giContractChecked = giProductionPassesActive && !profileReport_.restirGiCounters.empty(); + const bool giContractPassed = !giProductionPassesActive || + (giContractChecked && giContractViolationCount == 0ull); j["restir_gi"] = { {"schema_version", 2}, {"mode", restirGiModeName(profileReport_.settings.restirGiMode)}, {"layout", profileReport_.restirGiLayout}, + {"reservoir_contract", { + {"schema_version", 1}, + {"production_contract_active", giProductionPassesActive}, + {"selected_target_stored", giProductionPassesActive}, + {"selected_source_pdf_stored", giProductionPassesActive}, + {"weight_sum_stored", true}, + {"sample_count_m_stored", true}, + {"visibility_state_stored", true}, + {"age_stored", true}, + {"confidence_stored", giProductionPassesActive}, + {"path_class_stored", giProductionPassesActive}, + {"legacy_limitation", giProductionPassesActive + ? "none" + : "legacy-cache GI stores target PDF/weight/M/age/visibility but not separate selected source PDF or path-class confidence"}, + {"debug_views", nlohmann::json::array({ + "restir-gi-target", + "restir-gi-source-pdf", + "restir-gi-weight-sum", + "restir-gi-m", + "restir-gi-visibility", + "restir-gi-age", + "restir-gi-confidence", + "restir-gi-path-class", + })}, + }}, + {"reservoir_contract_validation", { + {"schema_version", 1}, + {"active", giProductionPassesActive}, + {"checked", giContractChecked}, + {"passed", giContractPassed}, + {"counter_scope", "last_completed_frame"}, + {"invalid_source_pdf_count", giContractInvalidSourcePdf}, + {"invalid_target_pdf_count", giContractInvalidTargetPdf}, + {"source_pdf_parity_mismatch_count", giSourcePdfParityMismatch}, + {"target_pdf_parity_mismatch_count", giTargetPdfParityMismatch}, + {"non_finite_count", giContractNonFinite}, + {"violation_count", giContractViolationCount}, + {"evidence", { + {"temporal_reject_target_count", giCounter(8)}, + {"spatial_reject_target_count", giCounter(15)}, + {"version_reject_count", giCounter(44)}, + {"non_finite_reject_count", giCounter(45)}, + {"contract_invalid_source_pdf_count", giContractInvalidSourcePdf}, + {"contract_invalid_target_pdf_count", giContractInvalidTargetPdf}, + {"source_pdf_parity_mismatch_count", giSourcePdfParityMismatch}, + {"target_pdf_parity_mismatch_count", giTargetPdfParityMismatch}, + }}, + }}, + {"bias_correction", { + {"schema_version", 1}, + {"temporal_reuse_mass_policy", "visibility-confidence-scales-weight-and-effective-M"}, + {"spatial_reuse_mass_policy", "visibility-confidence-scales-weight-and-effective-M"}, + {"visibility_confidence_shortcut", "production-only-unknown-safe-candidates-use-reduced-effective-proposal-mass"}, + {"reference_validation_visibility", "conservative-visibility-required-before-reuse"}, + {"asymmetric_weight_sum_vs_m_damping", false}, + }}, {"effective_production_reuse", giProductionPassesActive}, {"wavefront_current_frame_fallback", profileReport_.settings.wavefrontFinalOutputEnabled}, {"counter_scope", "last_completed_frame"}, {"counter_value_bits", 32}, - {"history_valid", giTemporalPreviousValid > 0ull || giTemporalAccepted > 0ull}, + {"history_valid", profileReport_.restirGiHistoryValid}, + {"history_counter_evidence_available", !profileReport_.restirGiCounters.empty()}, {"half_resolution", profileReport_.settings.restirGiHalfResolution}, {"render_extent", { {"width", profileReport_.resolution.renderWidth}, @@ -3924,6 +5087,10 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"visibility_unknown_count", giCounter(43)}, {"version_reject_count", giCounter(44)}, {"non_finite_reject_count", giCounter(45)}, + {"contract_invalid_source_pdf_count", giContractInvalidSourcePdf}, + {"contract_invalid_target_pdf_count", giContractInvalidTargetPdf}, + {"source_pdf_parity_mismatch_count", giSourcePdfParityMismatch}, + {"target_pdf_parity_mismatch_count", giTargetPdfParityMismatch}, {"half_res_grid_score", giGridOverallScore}, {"grid_score", giGridOverallScore}, {"grid", { @@ -3989,13 +5156,28 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co view == RendererDebugView::AdaptiveFilledImage || view == RendererDebugView::AdaptiveDisocclusionMask; }; + auto activeDiCounter = [this](size_t index) -> uint64_t { + return index < profileReport_.restirDiCounters.size() + ? static_cast(profileReport_.restirDiCounters[index]) + : 0ull; + }; + const bool restirDiTemporalActive = + profileReport_.perPassGpuMs.restirDiTemporal > 0.0f || + activeDiCounter(16) > 0ull; + const bool restirDiSpatialActive = + profileReport_.perPassGpuMs.restirDiSpatial > 0.0f || + activeDiCounter(33) > 0ull; + const bool restirDiFinalActive = + profileReport_.perPassGpuMs.restirDiFinal > 0.0f || + activeDiCounter(48) > 0ull || + profileReport_.restirDiHistoryValid; j["active_passes"] = { {"path_trace", profileReport_.settings.pathTracingEnabled}, {"pathtrace_kernel_native2b", profileReport_.effectivePathTraceKernelMode == PathTraceKernelMode::Native2B}, {"pathtrace_terminal_hit_group", profileReport_.native2BTerminalPayloadActive}, - {"restir_di_temporal", profileReport_.perPassGpuMs.restirDiTemporal > 0.0f}, - {"restir_di_spatial", profileReport_.perPassGpuMs.restirDiSpatial > 0.0f}, - {"restir_di_final", profileReport_.perPassGpuMs.restirDiFinal > 0.0f}, + {"restir_di_temporal", restirDiTemporalActive}, + {"restir_di_spatial", restirDiSpatialActive}, + {"restir_di_final", restirDiFinalActive}, {"regir", profileReport_.settings.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR}, {"regir_spatial_reuse", profileReport_.perPassGpuMs.regirSpatialReuse > 0.0f || (profileReport_.settings.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR && @@ -4030,6 +5212,20 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"taa", profileReport_.perPassGpuMs.taa > 0.0f}, {"history_copy", profileReport_.perPassGpuMs.historyCopy > 0.0f}, {"taa_history_copy", profileReport_.perPassGpuMs.taaHistoryCopy > 0.0f}, + {"dlss_guides", + profileReport_.perPassGpuMs.dlssGuides > 0.0f || + (profileReport_.nvidiaIntegrations.effectiveTemporalUpscaler == "dlss" && + !profileReport_.nvidiaIntegrations.effectiveDlssRayReconstruction)}, + {"dlss", + profileReport_.perPassGpuMs.dlss > 0.0f || + (profileReport_.nvidiaIntegrations.effectiveTemporalUpscaler == "dlss" && + !profileReport_.nvidiaIntegrations.effectiveDlssRayReconstruction)}, + {"dlss_rr_guides", + profileReport_.perPassGpuMs.dlssRayReconstructionGuides > 0.0f || + profileReport_.nvidiaIntegrations.effectiveDlssRayReconstruction}, + {"dlss_rr", + profileReport_.perPassGpuMs.dlssRayReconstruction > 0.0f || + profileReport_.nvidiaIntegrations.effectiveDlssRayReconstruction}, }; j["nvidia_integrations"] = profileReport_.nvidiaIntegrations; j["scene_update_routes"] = profileReport_.sceneUpdateRoutes; @@ -4050,6 +5246,7 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co {"tickets", profileReport_.topologyRebuildTickets}, }}, }; + j["validation_enabled"] = profileReport_.validationEnabled; j["validation_error_count"] = profileReport_.validationErrorCount; j["warnings"] = profileReport_.warnings; j["optimization_hints"] = profileReport_.optimizationHints; @@ -4058,6 +5255,115 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co j["ray_tracing_shader_map"] = profileReport_.rayTracingShaderMap; j["acceleration_structure_diagnostics"] = profileReport_.accelerationStructureDiagnostics; j["barrier_sync_diagnostics"] = profileReport_.barrierSyncDiagnostics; + const nlohmann::json temporalRuntimeValidation = rendererTemporalRuntimeValidationJson( + profileReport_.settings, + profileReport_.lastAccumulationResetReason, + profileReport_.frameCount, + profileReport_.profiledFrames, + profileReport_.memory.temporalHistoryBytes, + profileReport_.restirDiHistoryValid, + profileReport_.restirGiHistoryValid, + profileReport_.regirGrid.temporalHistoryValid, + profileReport_.temporalSystemDiagnostics); + const nlohmann::json activePassRuntimeValidation = rendererActivePassRuntimeValidationJson( + profileReport_.settings, + j["active_passes"], + nlohmann::json(profileReport_.perPassGpuMs), + j["restir_di"], + j["restir_gi"], + temporalRuntimeValidation, + nlohmann::json(profileReport_.nvidiaIntegrations)); + passes::RegirPass::PromotionDiagnostics regirPromotionDiagnostics{}; + regirPromotionDiagnostics.profiledFrames = profileReport_.profiledFrames; + regirPromotionDiagnostics.gpuFrameAvgMs = profileReport_.gpuFrameMs.avg; + regirPromotionDiagnostics.regirBuildMs = profileReport_.perPassGpuMs.regirBuild; + regirPromotionDiagnostics.regirSpatialReuseMs = profileReport_.perPassGpuMs.regirSpatialReuse; + regirPromotionDiagnostics.regirTemporalReuseMs = profileReport_.perPassGpuMs.regirTemporalReuse; + regirPromotionDiagnostics.feedbackAvailable = profileReport_.regirGrid.feedbackAvailable; + regirPromotionDiagnostics.activeCellCount = profileReport_.regirGrid.activeCellCount; + regirPromotionDiagnostics.hashCollisionCount = profileReport_.regirGrid.hashCollisionCount; + regirPromotionDiagnostics.hashSaturationCount = profileReport_.regirGrid.hashSaturationCount; + regirPromotionDiagnostics.hashCellCapacity = profileReport_.regirGrid.hashCellCapacity; + regirPromotionDiagnostics.totalCellCount = profileReport_.regirGrid.totalCellCount; + regirPromotionDiagnostics.denseReservoirBytes = profileReport_.regirGrid.denseReservoirBytes; + regirPromotionDiagnostics.effectiveReservoirBytes = profileReport_.regirGrid.effectiveReservoirBytes; + regirPromotionDiagnostics.backingBytes = profileReport_.regirGrid.backingBytes; + regirPromotionDiagnostics.environmentBankSize = profileReport_.regirGrid.environmentBankSize; + regirPromotionDiagnostics.sunBankSize = profileReport_.regirGrid.sunBankSize; + regirPromotionDiagnostics.validEnvironmentReservoirs = profileReport_.regirGrid.validEnvironmentReservoirs; + regirPromotionDiagnostics.validSunReservoirs = profileReport_.regirGrid.validSunReservoirs; + regirPromotionDiagnostics.environmentBankBytes = profileReport_.regirGrid.environmentBankBytes; + regirPromotionDiagnostics.environmentEffective = profileReport_.regirGrid.environmentEffective; + regirPromotionDiagnostics.sunEffective = profileReport_.regirGrid.sunEffective; + regirPromotionDiagnostics.temporalHistoryValid = profileReport_.regirGrid.temporalHistoryValid; + const std::vector exportableDebugViews = exportableRendererDebugViewNames(); + nlohmann::json rendererContracts = { + {"pass_contracts", rendererPassContractsJson(profileReport_.settings)}, + {"contract_validation", rendererPassContractValidationJson(profileReport_.settings)}, + {"pass_owner_registry_validation", rendererPassOwnerRegistryValidationJson(profileReport_.settings)}, + {"profile_timing_coverage", rendererPassTimingCoverageJson( + profileReport_.settings, + nlohmann::json(profileReport_.perPassGpuMs))}, + {"debug_output_coverage", rendererDebugOutputCoverageJson( + profileReport_.settings, + exportableDebugViews)}, + {"debug_view_registry_validation", rendererDebugViewRegistryValidationJson( + profileReport_.settings, + exportableDebugViews)}, + {"temporal_contract", rendererTemporalContractJson()}, + {"temporal_runtime_validation", temporalRuntimeValidation}, + {"application_bridge_contract", rendererApplicationBridgeContractJson()}, + {"application_bridge_runtime_validation", rendererApplicationBridgeRuntimeValidationJson( + profileReport_.settings, + profileReport_.temporalSystemDiagnostics, + nlohmann::json(profileReport_.rayTracingGeometry), + nlohmann::json(profileReport_.sceneLights), + nlohmann::json(profileReport_.rayTracingDiagnosticCounters), + profileReport_.accelerationStructureDiagnostics, + j["restir_di"], + nlohmann::json(profileReport_.nvidiaIntegrations))}, + {"active_pass_runtime_validation", activePassRuntimeValidation}, + {"denoiser_upscaler_runtime_validation", rendererDenoiserUpscalerRuntimeValidationJson( + profileReport_.settings, + nlohmann::json(profileReport_.nvidiaIntegrations), + temporalRuntimeValidation, + activePassRuntimeValidation)}, + {"performance_budget_validation", rendererPerformanceBudgetValidationJson( + profileReport_.settings, + profileReport_.profiledFrames, + j["resolution"], + j["gpu_frame_ms"], + nlohmann::json(profileReport_.perPassGpuMs))}, + {"regir_promotion_gate", rendererRegirPromotionGateJson( + profileReport_.settings, + regirPromotionDiagnostics)}, + {"restir_reservoir_runtime_validation", rendererRestirReservoirRuntimeValidationJson( + profileReport_.settings, + j["restir_di"], + j["restir_gi"])}, + {"restir_reference_matrix_artifact", restirReferenceMatrixArtifactJson()}, + {"rendergraph_artifact_validation", renderGraphArtifactValidationJson(config_.dumpRenderGraphPath)}, + {"manual_barrier_escape_report", manualBarrierEscapeReportJson()}, + {"diagnostic_runtime_validation", rendererDiagnosticRuntimeValidationJson( + profileReport_.settings, + profileReport_.diagnosticReadiness, + profileReport_.nsightAnalysisPlan, + profileReport_.rayTracingShaderMap, + profileReport_.accelerationStructureDiagnostics, + profileReport_.barrierSyncDiagnostics)}, + {"supported_mode_matrix", rendererSupportedModeMatrixJson()}, + {"current_mode_support", rendererCurrentModeSupportJson(profileReport_.settings)}, + {"architecture_documentation", rendererArchitectureDocumentationJson()}, + {"review_checklist", rendererReviewChecklistJson()}, + }; + rendererContracts["plan_phase_quality_lock"] = + rendererPlanPhaseQualityLockJson(rendererContracts); + rendererContracts["quality_gate_validation"] = + rendererQualityGateValidationJson( + rendererContracts, + profileReport_.validationEnabled, + profileReport_.validationErrorCount); + j["renderer_contracts"] = std::move(rendererContracts); j["settings"] = profileReport_.settings; j["settings"]["restir_gi_active_tile_mask_enabled"] = profileReport_.effectiveRestirGiActiveTileMaskEnabled; @@ -4098,63 +5404,53 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co ? LightingReuseMode::LegacyRestirDiGiPlusReGIR : LightingReuseMode::LegacyRestirDiGi; j["settings"]["lighting_reuse_mode_effective"] = lightingReuseModeName(effectiveLightingReuseMode); - const bool regirRequested = - profileReport_.settings.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR; + const bool regirRequested = passes::RegirPass::isRequested(profileReport_.settings); const bool regirSpatialReuseEffective = - regirRequested && - profileReport_.settings.regirGridMode != RegirGridMode::Hash && - profileReport_.settings.regirSpatialReuse; + passes::RegirPass::spatialReuseEffective(profileReport_.settings, regirRequested); const bool regirTemporalReuseEffective = - regirRequested && - profileReport_.settings.regirGridMode != RegirGridMode::Hash && - profileReport_.settings.regirTemporalReuse; - const bool regirHashGrid = - regirRequested && profileReport_.settings.regirGridMode == RegirGridMode::Hash; + passes::RegirPass::temporalReuseEffective(profileReport_.settings, regirRequested); + const bool regirHashGrid = passes::RegirPass::hashGridActive(profileReport_.settings, regirRequested); const bool regirHashSaturated = - regirHashGrid && profileReport_.regirGrid.hashSaturationCount > 0u; + passes::RegirPass::hashGridSaturated(regirHashGrid, profileReport_.regirGrid.hashSaturationCount); const bool regirHashReuseFallback = - regirHashGrid && - (profileReport_.settings.regirSpatialReuse || profileReport_.settings.regirTemporalReuse); + passes::RegirPass::hashReuseFallback(profileReport_.settings, regirHashGrid); const bool regirUnsupportedAdvancedRequested = - regirHashSaturated || regirHashReuseFallback; - const uint64_t regirCellCount = - static_cast(std::max(profileReport_.settings.regirGridDimensions.x, 1u)) * - static_cast(std::max(profileReport_.settings.regirGridDimensions.y, 1u)) * - static_cast(std::max(profileReport_.settings.regirGridDimensions.z, 1u)); + passes::RegirPass::unsupportedAdvancedRequested( + profileReport_.settings, + regirHashGrid, + profileReport_.regirGrid.hashSaturationCount); + const uint64_t regirCellCount = profileReport_.regirGrid.totalCellCount > 0ull + ? profileReport_.regirGrid.totalCellCount + : passes::RegirPass::gridCellCount(profileReport_.settings); const bool regirActiveGrid = - regirRequested && profileReport_.settings.regirGridMode == RegirGridMode::Active; - const uint64_t regirDenseMemoryBytes = profileReport_.regirGrid.denseReservoirBytes > 0u - ? profileReport_.regirGrid.denseReservoirBytes - : regirCellCount * - static_cast(std::max(profileReport_.settings.regirReservoirsPerCell, 1u)) * - 32ull; + passes::RegirPass::activeGridMode(profileReport_.settings, regirRequested); + const uint64_t regirDenseMemoryBytes = passes::RegirPass::denseReservoirBytes( + profileReport_.settings, + profileReport_.regirGrid.denseReservoirBytes, + 64ull); const bool regirSparseGrid = regirActiveGrid || regirHashGrid; - const uint64_t regirActiveCells = regirSparseGrid - ? (profileReport_.regirGrid.feedbackAvailable - ? std::min(profileReport_.regirGrid.activeCellCount, regirCellCount) - : 0ull) - : (regirRequested ? regirCellCount : 0ull); - const uint64_t regirEffectiveMemoryBytes = regirSparseGrid - ? (profileReport_.regirGrid.feedbackAvailable - ? profileReport_.regirGrid.effectiveReservoirBytes - : 0ull) - : (regirRequested - ? std::max(profileReport_.regirGrid.effectiveReservoirBytes, regirDenseMemoryBytes) - : 0ull); + const uint64_t regirActiveCells = passes::RegirPass::effectiveActiveCellCount( + regirSparseGrid, + profileReport_.regirGrid.feedbackAvailable, + profileReport_.regirGrid.activeCellCount, + regirCellCount, + regirRequested); + const uint64_t regirEffectiveMemoryBytes = passes::RegirPass::effectiveReservoirBytes( + regirSparseGrid, + profileReport_.regirGrid.feedbackAvailable, + profileReport_.regirGrid.effectiveReservoirBytes, + regirDenseMemoryBytes, + regirRequested); j["settings"]["regir_grid_mode_effective"] = !regirRequested ? "off" : regirGridModeName(regirHashGrid ? RegirGridMode::Hash : (regirActiveGrid ? RegirGridMode::Active : RegirGridMode::Dense)); const uint32_t effectiveFiniteQueryFramePeriod = - profileReport_.settings.regirQueryMode == RegirQueryMode::Deterministic - ? 1u - : (profileReport_.settings.regirFiniteQueryFramePeriod > 0u - ? profileReport_.settings.regirFiniteQueryFramePeriod - : (regirHashGrid ? 256u : 8u)); + passes::RegirPass::effectiveFiniteQueryFramePeriod(profileReport_.settings, regirHashGrid); j["settings"]["regir_finite_query_probability_effective"] = !regirRequested ? 0.0 - : 1.0 / static_cast(effectiveFiniteQueryFramePeriod); + : passes::RegirPass::finiteQueryProbability(regirRequested, effectiveFiniteQueryFramePeriod); j["settings"]["regir_finite_query_schedule"] = "frame-coherent"; j["settings"]["regir_finite_query_frame_period_override"] = profileReport_.settings.regirFiniteQueryFramePeriod; @@ -4171,6 +5467,25 @@ void HeadlessDiagnostics::writeProfileJson(const std::filesystem::path& path) co ? profileReport_.regirGrid.backingBytes : 0ull; j["settings"]["regir_effective_memory_bytes"] = regirEffectiveMemoryBytes; + j["settings"]["regir_reservoir_contract"] = { + {"schema_version", 1}, + {"finite_reservoir_bytes", 64}, + {"selected_light_index_stored", true}, + {"selected_light_kind_stored", true}, + {"selected_light_identity_stored", true}, + {"selected_light_generation_stored", true}, + {"cached_index_remap_policy", "identity-match-fast-path-scan-fallback"}, + {"candidate_count_stored", true}, + {"sample_count_m_stored", true}, + {"sample_position_stored", true}, + {"selected_source_weight_stored", true}, + {"source_weight_sum_stored", true}, + {"selected_source_pdf_stored", true}, + {"average_source_weight_stored", true}, + {"selected_target_stored", false}, + {"selected_target_policy", "query-time-material-and-visibility-dependent"}, + {"effective_query_pdf_debug_view", "regir-effective-pdf"}, + }; j["settings"]["regir_environment_effective"] = profileReport_.regirGrid.environmentEffective; j["settings"]["regir_environment_bank_size"] = profileReport_.regirGrid.environmentBankSize; j["settings"]["regir_infinite_lights_effective"] = @@ -4288,9 +5603,29 @@ void HeadlessDiagnostics::exportDebugViews(Application& app, const std::filesyst if (std::find(exported.begin(), exported.end(), "metallic") == exported.end()) { missing.push_back("metallic"); } + const std::vector contractViews = reservoirContractDebugViews(exportSettings); + const std::vector dlssGuideViews = dlssGuideContractDebugViews(exportSettings); + std::vector missingContractViews; + for (const std::string& requiredView : contractViews) { + if (std::find(exported.begin(), exported.end(), requiredView) == exported.end()) { + missingContractViews.push_back(requiredView); + if (std::find(missing.begin(), missing.end(), requiredView) == missing.end()) { + missing.push_back(requiredView); + } + } + } + std::vector missingDlssGuideViews; + for (const std::string& requiredView : dlssGuideViews) { + if (std::find(exported.begin(), exported.end(), requiredView) == exported.end()) { + missingDlssGuideViews.push_back(requiredView); + if (std::find(missing.begin(), missing.end(), requiredView) == missing.end()) { + missing.push_back(requiredView); + } + } + } exporter.writeExportManifest(dir, exported, displayExtent.width, displayExtent.height); - if (!missing.empty() || !skipped.empty()) { + { auto manifestPath = dir / "export_manifest.json"; if (std::filesystem::exists(manifestPath)) { std::ifstream in(manifestPath); @@ -4302,6 +5637,21 @@ void HeadlessDiagnostics::exportDebugViews(Application& app, const std::filesyst if (!skipped.empty()) { manifest["skipped_debug_views"] = skipped; } + manifest["reservoir_contract_validation"] = { + {"schema_version", 1}, + {"required_debug_views", contractViews}, + {"missing_debug_views", missingContractViews}, + {"passed", missingContractViews.empty()}, + }; + manifest["dlss_guide_contract_validation"] = { + {"schema_version", 1}, + {"required_debug_views", dlssGuideViews}, + {"missing_debug_views", missingDlssGuideViews}, + {"passed", missingDlssGuideViews.empty()}, + {"depth_convention", exportSettings.dlssRayReconstructionEnabled ? "linear-view-depth-for-rr" : "hardware-depth-for-dlss"}, + {"motion_convention", "previous-minus-current pixels; NGX eval MV scale 1,1"}, + {"jitter_convention", "NGX receives projection jitter as -camera.jitter.xy"}, + }; std::ofstream out(manifestPath); out << manifest.dump(2); } @@ -4532,6 +5882,8 @@ ValidationSuiteSummary HeadlessDiagnostics::runValidationSuite() { false, false, false, + false, + false, true, ApplicationMode::Headless, sceneConfig.headlessWidth, @@ -4581,6 +5933,7 @@ ValidationSuiteSummary HeadlessDiagnostics::runValidationSuite() { } result.gpuMsTotal = profile.gpuFrameMs.avg; + result.validationEnabled = profile.validationEnabled; result.validationErrors = profile.validationErrorCount; result.framesRendered = sceneConfig.totalFrames; result.wavefrontValidationEnabled = profile.wavefrontValidation.enabled; @@ -4590,7 +5943,7 @@ ValidationSuiteSummary HeadlessDiagnostics::runValidationSuite() { result.wavefrontCheckedShadowRays = profile.wavefrontValidation.checkedShadowRays; result.wavefrontDirectLightingMismatches = profile.wavefrontValidation.directLightingMismatchCount; result.wavefrontProbeGpuMs = profile.wavefrontValidation.wavefrontProbeGpuMs; - result.status = result.validationErrors == 0 && + result.status = result.validationEnabled && result.validationErrors == 0 && (!result.wavefrontValidationEnabled || result.wavefrontValidationPassed) ? "pass" : "fail"; @@ -4630,6 +5983,7 @@ ValidationSuiteSummary HeadlessDiagnostics::runValidationSuite() { sj["name"] = s.name; sj["status"] = s.status; sj["gpu_ms_total"] = s.gpuMsTotal; + sj["validation_enabled"] = s.validationEnabled; sj["validation_errors"] = s.validationErrors; sj["frames_rendered"] = s.framesRendered; if (s.wavefrontValidationEnabled) { diff --git a/src/rtv/ImageBarrier.cpp b/src/rtv/ImageBarrier.cpp index 018d0a6..20726fd 100644 --- a/src/rtv/ImageBarrier.cpp +++ b/src/rtv/ImageBarrier.cpp @@ -1,5 +1,7 @@ #include "rtv/ImageBarrier.h" +#include "rtv/GpuValidation.h" + namespace rtv::barrier { VkImageSubresourceRange colorRange(uint32_t baseMip, uint32_t mipCount) { @@ -31,6 +33,7 @@ void cmdTransitionImage(VkCommandBuffer commandBuffer, const ImageTransition& tr dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &imageBarrier; + recordManualBarrierEscape("ImageBarrier", "cmdTransitionImage", dependency); vkCmdPipelineBarrier2(commandBuffer, &dependency); } @@ -52,6 +55,7 @@ void cmdBufferBarrier(VkCommandBuffer commandBuffer, const BufferTransition& tra dependency.bufferMemoryBarrierCount = 1; dependency.pBufferMemoryBarriers = &bufferBarrier; + recordManualBarrierEscape("ImageBarrier", "cmdBufferBarrier", dependency); vkCmdPipelineBarrier2(commandBuffer, &dependency); } diff --git a/src/rtv/InspectorPanel.cpp b/src/rtv/InspectorPanel.cpp index 81687d3..faa3ad8 100644 --- a/src/rtv/InspectorPanel.cpp +++ b/src/rtv/InspectorPanel.cpp @@ -654,8 +654,8 @@ bool beginInspectorPropertyRow(const char* label) { if (!ImGui::BeginTable(tableId.c_str(), 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX)) { return false; } - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed, EditorUiMetric::inspectorLabelWidth); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, 0.42f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.58f); ImGui::TableNextRow(ImGuiTableRowFlags_None, EditorUiMetric::inspectorRowHeight); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); @@ -815,7 +815,7 @@ void drawInspectorComponentHeader( const ImU32 detailColor = ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); const ImVec2 titleSize = ImGui::CalcTextSize(title); const float textX = iconMin.x + iconSize + 14.0f; - const bool hasDetail = detail != nullptr && detail[0] != '\0'; + const bool hasDetail = false; const float lineHeight = ImGui::GetTextLineHeight(); const float detailGap = hasDetail ? 2.0f : 0.0f; const float textBlockHeight = hasDetail ? (lineHeight * 2.0f + detailGap) : titleSize.y; @@ -833,6 +833,9 @@ void drawInspectorComponentHeader( } ImGui::Dummy(size); + if (detail != nullptr && detail[0] != '\0' && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("%s", detail); + } if (popupId != nullptr && requests != nullptr) { const ImVec2 after = ImGui::GetCursorScreenPos(); const float actionSize = EditorUiMetric::inspectorComponentActionSize; @@ -920,6 +923,34 @@ void drawInspectorStateCard(EditorGlyphIcon icon, const char* title, const char* ImGui::Dummy(size); } +void drawInspectorEntityHeader(Entity& entity, EditorSelection& selection, EditorRequests& requests) { + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + const float width = std::max(1.0f, ImGui::GetContentRegionAvail().x); + const ImVec2 size(width, 56.0f); + const ImVec2 min = cursor; + const ImVec2 max(cursor.x + size.x, cursor.y + size.y); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRectFilled(min, max, ImGui::GetColorU32(editorCardBgColor()), EditorUiMetric::cardRounding); + dl->AddRect(min, max, ImGui::GetColorU32(editorToolbarBorderColor()), EditorUiMetric::cardRounding); + dl->AddRectFilled(ImVec2(min.x, min.y), ImVec2(min.x + 3.0f, max.y), ImGui::GetColorU32(editorAccentColor()), EditorUiMetric::cardRounding); + + const ImVec2 iconMin(min.x + 12.0f, min.y + 16.0f); + editorDrawIconGlyph(editorGlyphForEntity(entity), iconMin, ImVec2(iconMin.x + 24.0f, iconMin.y + 24.0f), ImGui::GetColorU32(editorIconTint(true))); + + const std::string title = entity.name.empty() ? std::string("Unnamed Entity") : entity.name; + const std::string detail = "ID " + std::to_string(entity.id.index) + ":" + std::to_string(entity.id.generation) + + (entity.layer.empty() ? std::string{} : (" | Layer " + entity.layer)); + dl->AddText(ImVec2(min.x + 48.0f, min.y + 9.0f), ImGui::GetColorU32(ImGuiCol_Text), title.c_str()); + dl->AddText(ImVec2(min.x + 48.0f, min.y + 31.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), detail.c_str()); + + ImGui::Dummy(size); + const ImVec2 after = ImGui::GetCursorScreenPos(); + ImGui::SetCursorScreenPos(ImVec2(max.x - EditorUiMetric::inspectorComponentActionSize - 7.0f, min.y + 8.0f)); + drawEntityActionsMenu(entity, selection, requests); + ImGui::SetCursorScreenPos(after); + ImGui::Spacing(); +} + void drawInspectorLockedBanner() { const ImVec2 cursor = ImGui::GetCursorScreenPos(); const float width = std::max(1.0f, ImGui::GetContentRegionAvail().x); @@ -1006,9 +1037,7 @@ void InspectorPanel::draw(const EditorRuntimeState& state, EditorSelection& sele if (entityLocked) { drawInspectorLockedBanner(); } - ImGui::TextDisabled("Entity"); - ImGui::SameLine(); - drawEntityActionsMenu(*entity, selection, requests); + drawInspectorEntityHeader(*entity, selection, requests); ImGui::BeginDisabled(entityLocked); static EntityId nameEditId{}; diff --git a/src/rtv/PathTracerRenderer.cpp b/src/rtv/PathTracerRenderer.cpp index 98ae32b..aa6500d 100644 --- a/src/rtv/PathTracerRenderer.cpp +++ b/src/rtv/PathTracerRenderer.cpp @@ -4,10 +4,12 @@ #include "rtv/AtmosphereSamplingSystem.h" #include "rtv/BufferUploader.h" #include "rtv/Check.h" +#include "rtv/CommandSystem.h" #include "rtv/ComputePipeline.h" #include "rtv/DescriptorLayoutCache.h" #include "rtv/DescriptorWriter.h" #include "rtv/GraphicsPipeline.h" +#include "rtv/GpuValidation.h" #include "rtv/ImageBarrier.h" #include "rtv/NsightMarkers.h" #include "rtv/NsightPerfMarkers.h" @@ -24,6 +26,16 @@ #include "rtv/TextureLoader.h" #include "rtv/UploadContext.h" #include "rtv/VulkanContext.h" +#include "rtv/passes/CompositePass.h" +#include "rtv/passes/DebugViewPass.h" +#include "rtv/passes/DenoiserPass.h" +#include "rtv/passes/DlssPass.h" +#include "rtv/passes/NrdPass.h" +#include "rtv/passes/PathTracePass.h" +#include "rtv/passes/RegirPass.h" +#include "rtv/passes/RestirDIPass.h" +#include "rtv/passes/RestirGIPass.h" +#include "rtv/passes/TemporalAAPass.h" #include #include @@ -43,6 +55,7 @@ #include #include #include +#include #include #include @@ -77,13 +90,21 @@ constexpr VkDeviceSize kFrameFogParamsOffset = 24576; constexpr VkDeviceSize kFrameMomentParamsOffset = 28672; constexpr VkDeviceSize kFrameRestirDiParamsOffset = 32768; constexpr uint32_t kRendererFramesInFlight = 3; -constexpr uint32_t kRestirGiActiveTileSize = 16; +static_assert(kRendererFramesInFlight == CommandSystem::framesInFlight); +static_assert(kRendererFramesInFlight == kBindlessTextureHeapVersionCount); constexpr VkPipelineStageFlags2 kCrossQueueShaderStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; constexpr uint32_t kRayTracingDiagnosticCounterCount = RayTracingDiagnosticCounters::kRawSlotCount; constexpr uint32_t kRayTracingAlphaMaterialCounterCount = RayTracingDiagnosticCounters::kAlphaMaterialCounterSlots * RayTracingDiagnosticCounters::kAlphaMaterialCounterStride; +ResourceAccess persistentRestirHistoryAccess() { + return ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + }; +} + uint64_t fnv1aAppend(uint64_t hash, const void* data, size_t size) { const auto* bytes = static_cast(data); for (size_t i = 0; i < size; ++i) { @@ -262,6 +283,7 @@ void bufferMemoryBarrier( dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.bufferMemoryBarrierCount = 1; dependency.pBufferMemoryBarriers = &barrier; + recordManualBarrierEscape("PathTracerRenderer", "bufferMemoryBarrier", dependency); vkCmdPipelineBarrier2(commandBuffer, &dependency); } @@ -351,42 +373,15 @@ void addOrUpdateDescriptorBinding( } bool isWavefrontDebugView(RendererDebugView view) { - return view == RendererDebugView::WavefrontQueueOccupancy || - view == RendererDebugView::WavefrontPathDepth || - view == RendererDebugView::WavefrontLiveRays || - view == RendererDebugView::WavefrontTerminatedRays || - view == RendererDebugView::WavefrontMaterialBucket || - view == RendererDebugView::WavefrontRestirDi || - view == RendererDebugView::WavefrontDirectLighting || - view == RendererDebugView::WavefrontRestirGi; + return passes::DebugViewPass::isWavefrontView(view); } bool isRestirDiDebugView(RendererDebugView view) { - return view == RendererDebugView::RestirDiSelectedLight || - view == RendererDebugView::RestirDiTarget || - view == RendererDebugView::RestirDiSourcePdf || - view == RendererDebugView::RestirDiVisibility || - view == RendererDebugView::RestirDiRejectionReason || - view == RendererDebugView::RestirDiTemporalAcceptance || - view == RendererDebugView::RestirDiSpatialAcceptance || - view == RendererDebugView::RestirDiFinalContribution || - view == RendererDebugView::RestirDiReceiverPosition || - view == RendererDebugView::RestirDiReceiverNormal || - view == RendererDebugView::RestirDiLightVersion || - view == RendererDebugView::RestirDiInitialReservoir || - view == RendererDebugView::RestirDiTemporalReservoir || - view == RendererDebugView::RestirDiSpatialReservoir || - view == RendererDebugView::RestirDiFinalReservoir || - view == RendererDebugView::RestirDiWeightSum || - view == RendererDebugView::RestirDiM || - view == RendererDebugView::RestirDiLightClass || - view == RendererDebugView::RestirDiAge || - view == RendererDebugView::RestirDiConfidence || - view == RendererDebugView::RestirDiReferenceDiff; + return passes::RestirDIPass::usesDebugView(view); } bool requiresWavefrontShadowTrace(RendererDebugView view) { - return view == RendererDebugView::WavefrontDirectLighting; + return passes::DebugViewPass::requiresWavefrontShadowTrace(view); } uint32_t stbnHash(uint32_t value) { @@ -762,6 +757,8 @@ std::vector rayTracingBindings() { descriptorBinding(69, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_RAYGEN_BIT_KHR), descriptorBinding(70, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_RAYGEN_BIT_KHR), descriptorBinding(71, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_RAYGEN_BIT_KHR), + descriptorBinding(72, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_RAYGEN_BIT_KHR), + descriptorBinding(73, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_RAYGEN_BIT_KHR), descriptorBinding(kStbnScalarTextureBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, allRt), descriptorBinding(kStbnScalarSamplerBinding, VK_DESCRIPTOR_TYPE_SAMPLER, allRt), }; @@ -895,8 +892,19 @@ struct PathTracerRenderer::NrdRuntime { Image viewZ; Image diffRadianceHitdist; Image specRadianceHitdist; + Image diffHistoryConfidence; + Image specHistoryConfidence; + Image currentLightingLuminance; + Image previousLightingLuminance; + Image rawConfidenceGradient; + Image filteredConfidenceGradient; + Buffer confidenceSourcePixelFallback; + Buffer previousLightRecords; + uint32_t previousLightRecordCount = 0; + bool previousLightHistoryValid = false; Image outDiffRadianceHitdist; Image outSpecRadianceHitdist; + Image validationOutput; Image fallbackImage; bool resourcesReady = false; @@ -955,10 +963,16 @@ Image* nrdImageForResource(PathTracerRenderer::NrdRuntime& runtime, const nrd::R return &runtime.diffRadianceHitdist; case nrd::ResourceType::IN_SPEC_RADIANCE_HITDIST: return &runtime.specRadianceHitdist; + case nrd::ResourceType::IN_DIFF_CONFIDENCE: + return &runtime.diffHistoryConfidence; + case nrd::ResourceType::IN_SPEC_CONFIDENCE: + return &runtime.specHistoryConfidence; case nrd::ResourceType::OUT_DIFF_RADIANCE_HITDIST: return &runtime.outDiffRadianceHitdist; case nrd::ResourceType::OUT_SPEC_RADIANCE_HITDIST: return &runtime.outSpecRadianceHitdist; + case nrd::ResourceType::OUT_VALIDATION: + return &runtime.validationOutput; case nrd::ResourceType::PERMANENT_POOL: if (resource.indexInPool < runtime.permanentPoolImages.size()) { return &runtime.permanentPoolImages[resource.indexInPool]; @@ -1048,6 +1062,9 @@ PathTracerRenderer::PathTracerRenderer( : context_(context), allocator_(allocator), uploader_(uploader), + settings_(normalizeSettingsForDevice( + initialSettings != nullptr ? *initialSettings : RendererSettings{}, + context)), scene_( allocator, uploader, @@ -1055,14 +1072,12 @@ PathTracerRenderer::PathTracerRenderer( assets, std::move(environmentPath), std::move(sceneCachePolicy), - initialSettings != nullptr - ? initialSettings->opacityMicromapSubdivisionLevel - : kDefaultOpacityMicromapSubdivisionLevel, - (initialSettings == nullptr || initialSettings->opacityMicromapsEnabled) && context.supportsOpacityMicromaps(), + settings_.opacityMicromapSubdivisionLevel, + settings_.opacityMicromapsEnabled && context.supportsOpacityMicromaps(), materialTextureMaxDimension, fullBindlessTextureCapacityOrThrow(context.bindlessCapabilities()), - initialSettings != nullptr && initialSettings->compactImportedEmissiveTriangleSampling, - initialSettings != nullptr && initialSettings->opacityMicromapBlendEnabled), + settings_.compactImportedEmissiveTriangleSampling, + settings_.opacityMicromapBlendEnabled), gpuSkinningResourcePlan_(std::move(gpuSkinningResourcePlan)), resourceAliasingEnabled_(resourceAliasingEnabled) { StartupTimingSummary startupTiming; @@ -1073,9 +1088,6 @@ PathTracerRenderer::PathTracerRenderer( if (!supportsFullBindlessTextures(context_.bindlessCapabilities())) { (void)fullBindlessTextureCapacityOrThrow(context_.bindlessCapabilities()); } - if (initialSettings != nullptr) { - settings_ = *initialSettings; - } initializeNsightPerfMarkers( context_.instance(), context_.physicalDevice(), @@ -1149,6 +1161,8 @@ PathTracerRenderer::PathTracerRenderer( {"RTV_GENERIC_SECONDARY_DIRECT_PROB", "0.5"}, {"RTV_GENERIC_DEEP_SECONDARY_DIRECT_PROB", "0.5"}, }; + auto genericBeautyFastNoTexturesDefines = genericBeautyFastDefines; + genericBeautyFastNoTexturesDefines.emplace_back("RTV_MATERIAL_TEXTURES_ENABLED", "0"); const std::vector> regirBeautyFastDefines{ {"RTV_RESTIR_DI_VALIDATION_FULL", "0"}, {"RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT", "0"}, @@ -1220,6 +1234,9 @@ PathTracerRenderer::PathTracerRenderer( const auto gpuSkinningSpv = compileShader(shaderDirectory / "gpu_skinning.comp"); const auto dlssGuidesSpv = compileShader(shaderDirectory / "dlss_guides.comp"); const auto dlssRayReconstructionGuidesSpv = compileShader(shaderDirectory / "dlss_rr_guides.comp"); + const auto dlssGuideVisualizeSpv = compileShader(shaderDirectory / "dlss_guide_visualize.comp"); + const auto nrdConfidenceGradientSpv = compileShader(shaderDirectory / "nrd_confidence_gradient.comp"); + const auto nrdConfidenceFilterSpv = compileShader(shaderDirectory / "nrd_confidence_filter.comp"); const auto nrdPrepareSpv = compileShader(shaderDirectory / "nrd_prepare.comp"); const auto nrdResolveSpv = compileShader(shaderDirectory / "nrd_resolve.comp"); const auto restirSpatialSpv = compileShader(shaderDirectory / "restir_spatial.comp"); @@ -1276,6 +1293,10 @@ PathTracerRenderer::PathTracerRenderer( shaderDirectory / "pathtrace.rgen", ".di_packed", restirDiPackedDefines); const auto raygenBeautyFastSpv = compileShaderVariant( shaderDirectory / "pathtrace.rgen", ".beauty_fast.di_packed", genericBeautyFastDefines); + const auto raygenBeautyFastNoTexturesSpv = compileShaderVariant( + shaderDirectory / "pathtrace.rgen", + ".beauty_fast.no_textures.di_packed", + genericBeautyFastNoTexturesDefines); const auto raygenRegirBeautyFastSpv = compileShaderVariant( shaderDirectory / "pathtrace.rgen", ".regir_beauty_fast.di_packed", regirBeautyFastDefines); const auto raygenRegirStochasticBeautyFastSpv = compileShaderVariant( @@ -1331,6 +1352,8 @@ PathTracerRenderer::PathTracerRenderer( ".native2b", { {"RTV_NATIVE2B_PIPELINE", "1"}, + {"RTV_BEAUTY_OUTPUT_ONLY", "1"}, + {"RTV_REGIR_TRACE_ENABLED", "0"}, {"RTV_MOTION_BLUR_ENABLED", "0"}, {"RTV_RESTIR_DI_VALIDATION_FULL", "0"}, {"RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT", "0"}, @@ -1341,6 +1364,8 @@ PathTracerRenderer::PathTracerRenderer( { {"RTV_NATIVE2B_PIPELINE", "1"}, {"RTV_NATIVE2B_COMPACT_PRIMARY_LIGHTS", "1"}, + {"RTV_BEAUTY_OUTPUT_ONLY", "1"}, + {"RTV_REGIR_TRACE_ENABLED", "0"}, {"RTV_MOTION_BLUR_ENABLED", "0"}, {"RTV_RESTIR_DI_VALIDATION_FULL", "0"}, {"RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT", "0"}, @@ -1384,6 +1409,8 @@ PathTracerRenderer::PathTracerRenderer( { {"RTV_RT_DIAGNOSTIC_COUNTERS", "1"}, {"RTV_NATIVE2B_PIPELINE", "1"}, + {"RTV_BEAUTY_OUTPUT_ONLY", "1"}, + {"RTV_REGIR_TRACE_ENABLED", "0"}, {"RTV_MOTION_BLUR_ENABLED", "0"}, {"RTV_RESTIR_DI_VALIDATION_FULL", "0"}, {"RTV_RESTIR_GI_UNCOMPRESSED_LAYOUT", "0"}, @@ -1429,6 +1456,9 @@ PathTracerRenderer::PathTracerRenderer( gpuSkinningShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(gpuSkinningSpv), "gpu skinning compute"); dlssGuidesShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(dlssGuidesSpv), "dlss guide image compute"); dlssRayReconstructionGuidesShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(dlssRayReconstructionGuidesSpv), "dlss ray reconstruction guide image compute"); + dlssGuideVisualizeShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(dlssGuideVisualizeSpv), "dlss guide visualization compute"); + nrdConfidenceGradientShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(nrdConfidenceGradientSpv), "nrd confidence gradient compute"); + nrdConfidenceFilterShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(nrdConfidenceFilterSpv), "nrd confidence gradient filter compute"); nrdPrepareShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(nrdPrepareSpv), "nrd guide and signal prepare compute"); nrdResolveShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(nrdResolveSpv), "nrd resolve compute"); restirSpatialShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(restirSpatialSpv), "restir spatial compute"); @@ -1472,6 +1502,10 @@ PathTracerRenderer::PathTracerRenderer( fullscreenFragmentShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(fragSpv), "fullscreen fragment"); raygenShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(raygenSpv), "path trace raygen"); raygenBeautyFastShader_ = std::make_unique(context_.device(), ShaderCompiler::readSpirv(raygenBeautyFastSpv), "path trace generic beauty fast raygen"); + raygenBeautyFastNoTexturesShader_ = std::make_unique( + context_.device(), + ShaderCompiler::readSpirv(raygenBeautyFastNoTexturesSpv), + "path trace generic beauty fast no-textures raygen"); raygenRegirBeautyFastShader_ = std::make_unique( context_.device(), ShaderCompiler::readSpirv(raygenRegirBeautyFastSpv), @@ -1526,6 +1560,38 @@ PathTracerRenderer::PathTracerRenderer( startupTiming.mark("shader_module_load_reflect"); shaderReloadDependencies_ = std::move(shaderReloadDependencies); + std::unordered_set watchedShaderFiles; + std::unordered_set expandedShaderSources; + auto watchShaderFile = [&](const std::filesystem::path& path) { + std::error_code ec; + const std::filesystem::path absolutePath = std::filesystem::absolute(path, ec).lexically_normal(); + const std::filesystem::path watchPath = ec ? path.lexically_normal() : absolutePath; + if (!watchedShaderFiles.insert(watchPath.generic_string()).second) { + return; + } + + ShaderReloadWatchFile watch; + watch.path = watchPath; + ec.clear(); + watch.writeTime = std::filesystem::last_write_time(watch.path, ec); + watch.exists = !ec; + shaderReloadWatchFiles_.push_back(std::move(watch)); + }; + for (const ShaderReloadDependency& dependency : shaderReloadDependencies_) { + watchShaderFile(dependency.source); + watchShaderFile(dependency.output); + watchShaderFile(dependency.output.string() + ".options"); + + std::error_code ec; + const std::filesystem::path absoluteSource = + std::filesystem::absolute(dependency.source, ec).lexically_normal(); + const std::string sourceKey = (ec ? dependency.source.lexically_normal() : absoluteSource).generic_string(); + if (expandedShaderSources.insert(sourceKey).second) { + for (const std::filesystem::path& include : shaderCompiler_->dependenciesFor(dependency.source)) { + watchShaderFile(include); + } + } + } layoutCache_ = std::make_unique(context_.device()); pipelineCache_ = sharedPipelineCache( @@ -1563,6 +1629,9 @@ PathTracerRenderer::PathTracerRenderer( gpuSkinningSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({gpuSkinningShader_->reflection()}, 0)); dlssGuidesSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({dlssGuidesShader_->reflection()}, 0)); dlssRayReconstructionGuidesSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({dlssRayReconstructionGuidesShader_->reflection()}, 0)); + dlssGuideVisualizeSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({dlssGuideVisualizeShader_->reflection()}, 0)); + nrdConfidenceGradientSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({nrdConfidenceGradientShader_->reflection()}, 0)); + nrdConfidenceFilterSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({nrdConfidenceFilterShader_->reflection()}, 0)); nrdPrepareSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({nrdPrepareShader_->reflection()}, 0)); nrdResolveSetLayout_ = layoutCache_->createLayout(ShaderReflection::bindingsForSet({nrdResolveShader_->reflection()}, 0)); auto createComputeMaterialTextureLayout = [this]( @@ -1688,6 +1757,24 @@ PathTracerRenderer::PathTracerRenderer( std::vector{dlssRayReconstructionGuidesSetLayout_}, ShaderReflection::mergePushConstants({dlssRayReconstructionGuidesShader_->reflection()}), *pipelineCache_); + dlssGuideVisualizePipeline_ = std::make_unique( + context_.device(), + *dlssGuideVisualizeShader_, + std::vector{dlssGuideVisualizeSetLayout_}, + ShaderReflection::mergePushConstants({dlssGuideVisualizeShader_->reflection()}), + *pipelineCache_); + nrdConfidenceGradientPipeline_ = std::make_unique( + context_.device(), + *nrdConfidenceGradientShader_, + std::vector{nrdConfidenceGradientSetLayout_}, + ShaderReflection::mergePushConstants({nrdConfidenceGradientShader_->reflection()}), + *pipelineCache_); + nrdConfidenceFilterPipeline_ = std::make_unique( + context_.device(), + *nrdConfidenceFilterShader_, + std::vector{nrdConfidenceFilterSetLayout_}, + ShaderReflection::mergePushConstants({nrdConfidenceFilterShader_->reflection()}), + *pipelineCache_); nrdPreparePipeline_ = std::make_unique( context_.device(), *nrdPrepareShader_, @@ -1928,10 +2015,13 @@ PathTracerRenderer::PathTracerRenderer( gpuSkinningRayTracingBindingBuffer_.handle() != VK_NULL_HANDLE && gpuSkinningCurrentVertexBuffer_.handle() != VK_NULL_HANDLE; tracePathTraceRecordPhase("constructor_rt_pipeline_begin"); + ShaderModule& beautyFastRaygen = !scene_.hasMaterialTextures() + ? *raygenBeautyFastNoTexturesShader_ + : *raygenBeautyFastShader_; rayTracingBeautyFastPipeline_ = std::make_unique( context_.device(), context_.rayTracingInfo().rayTracingPipelineProperties, - *raygenBeautyFastShader_, + beautyFastRaygen, *primaryMissShader_, *shadowMissShader_, *closestHitShader_, @@ -1945,7 +2035,8 @@ PathTracerRenderer::PathTracerRenderer( tracePathTraceRecordPhase("constructor_rt_pipeline_end"); startupTiming.mark("rt_pipeline_create"); std::cout << "RT pipeline variants: ReGIR/full generic/Native2B/diagnostic/motion/validation/wavefront pipelines are lazy\n"; - std::cout << "RT pipeline: beauty_fast_sbt=" << rayTracingBeautyFastPipeline_->sbtBytes() << " bytes\n"; + std::cout << "RT pipeline: beauty_fast_sbt=" << rayTracingBeautyFastPipeline_->sbtBytes() + << " bytes material_textures=" << (scene_.hasMaterialTextures() ? "on" : "compiled-out") << '\n'; } frames_.reserve(kRendererFramesInFlight); @@ -2059,6 +2150,7 @@ void PathTracerRenderer::ensureRayTracingVariantPipelines(bool restirDiValidatio const bool fastGenericBeautyEligible = shouldUseGenericBeautyFastPath(restirDiValidationFull, restirGiInitialFull); const bool fastRegirBeautyEligible = settings_.pathTraceKernelMode == PathTraceKernelMode::Generic && + settings_.finalBounceFastPathEnabled && settings_.debugView == RendererDebugView::Beauty && settings_.pathTracingEnabled && !settings_.motionBlurEnabled && @@ -2067,6 +2159,7 @@ void PathTracerRenderer::ensureRayTracingVariantPipelines(bool restirDiValidatio !restirDiValidationFull && !restirGiInitialFull && shouldUseRegir(); + const bool native2BBeautyEligible = shouldUseNative2BPathTraceKernel(); if (fastRegirBeautyEligible) { if (rayTracingRegirBeautyFastPipeline_ == nullptr) { tracePathTraceRecordPhase("ensure_variants_create_regir_beauty_fast_begin"); @@ -2079,7 +2172,7 @@ void PathTracerRenderer::ensureRayTracingVariantPipelines(bool restirDiValidatio tracePathTraceRecordPhase("ensure_variants_create_regir_stochastic_beauty_fast_end"); } } - if (!fastGenericBeautyEligible && !fastRegirBeautyEligible && rayTracingPipeline_ == nullptr) { + if (!fastGenericBeautyEligible && !fastRegirBeautyEligible && !native2BBeautyEligible && rayTracingPipeline_ == nullptr) { tracePathTraceRecordPhase("ensure_variants_create_full_generic_begin"); rayTracingPipeline_ = makeRtPipeline(raygenShader_.get()); tracePathTraceRecordPhase("ensure_variants_create_full_generic_end"); @@ -2706,6 +2799,10 @@ float PathTracerRenderer::stbnScalarSample(int32_t x, int32_t y, uint32_t frameI return static_cast(stbnScalarAtlas_.at(atlasIndex)) * (1.0f / 255.0f); } +uint32_t PathTracerRenderer::sampleFrameIndex() const { + return settings_.fixedSeed.value_or(temporalFrameIndex_); +} + bool PathTracerRenderer::shadersNeedReload() { if (shaderCompiler_ == nullptr) { return false; @@ -2719,8 +2816,12 @@ bool PathTracerRenderer::shadersNeedReload() { shaderReloadCheckPrimed_ = true; lastShaderReloadCheck_ = now; - for (const ShaderReloadDependency& dependency : shaderReloadDependencies_) { - if (shaderCompiler_->needsCompile(dependency.source, dependency.output, dependency.extraDefines)) { + for (const ShaderReloadWatchFile& watch : shaderReloadWatchFiles_) { + std::error_code ec; + const std::filesystem::file_time_type writeTime = std::filesystem::last_write_time(watch.path, ec); + const bool exists = !ec; + if (exists != watch.exists || (exists && writeTime != watch.writeTime)) { + std::cout << "Shader hot reload change detected: " << watch.path.string() << '\n'; return true; } } @@ -2731,6 +2832,7 @@ void PathTracerRenderer::beginFrame(uint32_t frameIndex, VkExtent2D renderExtent const size_t profilerSlot = frameIndex % profilers_.size(); currentFrame_ = frames_.at(frameIndex % frames_.size()).get(); currentProfiler_ = &profilers_.at(profilerSlot); + bindlessTextureHeap_.beginFrame(frameIndex); const int8_t completedMaskMode = restirGiActiveTileMaskProfilerModes_.at(profilerSlot); currentProfiler_->collectCompletedFrame(); releaseRetiredResolutionResources(); @@ -2926,7 +3028,9 @@ void PathTracerRenderer::updateAdaptiveQuality(const GpuFrameTimings& timings) { } } -bool PathTracerRenderer::applySettings(const RendererSettings& settings) { +RendererSettings PathTracerRenderer::normalizeSettingsForDevice( + const RendererSettings& settings, + const VulkanContext& context) { RendererSettings next = settings; next.maxBounces = std::clamp(next.maxBounces, 1u, 16u); next.samplesPerPixel = std::clamp(next.samplesPerPixel, 1u, kMaxSamplesPerPixel); @@ -3013,7 +3117,7 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { next.dofBokehRotation = std::isfinite(next.dofBokehRotation) ? std::remainder(next.dofBokehRotation, twoPi) : 0.0f; - if (!context_.supportsRayTracingMotionBlur()) { + if (!context.supportsRayTracingMotionBlur()) { next.motionBlurEnabled = false; } next.motionBlurShutterOpen = std::clamp( @@ -3061,8 +3165,11 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { next.indirectStrength = std::max(0.0f, next.indirectStrength); next.environmentIntensity = std::max(0.0f, next.environmentIntensity); next.environmentBackgroundIntensity = std::max(0.0f, next.environmentBackgroundIntensity); - next.renderResolutionScale = std::clamp(next.renderResolutionScale, 0.25f, 1.0f); - const float maxMaterialAnisotropy = allocator_.supportsSamplerAnisotropy() ? allocator_.maxSamplerAnisotropy() : 1.0f; + next.renderResolutionScale = std::clamp( + std::isfinite(next.renderResolutionScale) ? next.renderResolutionScale : 1.0f, + 0.25f, + 1.0f); + const float maxMaterialAnisotropy = context.supportsSamplerAnisotropy() ? context.maxSamplerAnisotropy() : 1.0f; next.materialTextureAnisotropy = std::clamp( std::isfinite(next.materialTextureAnisotropy) ? next.materialTextureAnisotropy : 1.0f, 1.0f, @@ -3118,10 +3225,10 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { next.restirGiVisibilityRayBudget = std::max(next.restirGiVisibilityRayBudget, 1u); } next.restirGiEnabled = next.restirGiMode != RestirGiMode::Off; - if (!context_.supportsOpacityMicromaps()) { + if (!context.supportsOpacityMicromaps()) { next.opacityMicromapsEnabled = false; } - if (!context_.supportsSER()) { + if (!context.supportsSER()) { next.shaderExecutionReorderingEnabled = false; } if (isWavefrontDebugView(next.debugView)) { @@ -3179,13 +3286,22 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { std::clamp(next.regirGridDimensions.y, 1u, 128u), std::clamp(next.regirGridDimensions.z, 1u, 128u)); next.regirReservoirsPerCell = std::clamp(next.regirReservoirsPerCell, 1u, 64u); + constexpr uint64_t kMaxRegirReservoirCount = 4ull * 1024ull * 1024ull; + const uint64_t regirCellCount = + static_cast(next.regirGridDimensions.x) * + static_cast(next.regirGridDimensions.y) * + static_cast(next.regirGridDimensions.z); + const uint32_t maxReservoirsPerCell = static_cast(std::max( + 1ull, + kMaxRegirReservoirCount / std::max(regirCellCount, 1ull))); + next.regirReservoirsPerCell = std::min(next.regirReservoirsPerCell, maxReservoirsPerCell); next.regirCandidatesPerReservoir = std::clamp(next.regirCandidatesPerReservoir, 1u, 256u); next.regirGridPadding = std::clamp( std::isfinite(next.regirGridPadding) ? next.regirGridPadding : 0.10f, 0.0f, 0.5f); next.regirCanonicalMix = std::clamp( - std::isfinite(next.regirCanonicalMix) ? next.regirCanonicalMix : 0.25f, + std::isfinite(next.regirCanonicalMix) ? next.regirCanonicalMix : 0.70f, 0.0f, 1.0f); if (static_cast(next.regirQueryMode) > static_cast(RegirQueryMode::Stochastic)) { @@ -3237,6 +3353,26 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { if (static_cast(next.restirDiReservoirLayout) > static_cast(RestirDiReservoirLayout::ValidationFull)) { next.restirDiReservoirLayout = RestirDiReservoirLayout::ProductionPacked; } + if (next.restirDiMode == RestirDiMode::Off) { + next.restirMode = RestirMode::ClassicNee; + next.restirDiReservoirLayout = RestirDiReservoirLayout::Legacy; + } else if (next.restirDiMode == RestirDiMode::Legacy) { + next.restirDiReservoirLayout = RestirDiReservoirLayout::Legacy; + if (next.restirMode == RestirMode::ClassicNee) { + next.restirMode = RestirMode::RestirOnly; + } + } else { + if (next.restirDiMode == RestirDiMode::HybridCompare) { + next.restirMode = RestirMode::HybridCompare; + } else { + next.restirMode = RestirMode::ClassicNee; + } + if (next.restirDiReservoirLayout == RestirDiReservoirLayout::Legacy) { + next.restirDiReservoirLayout = RestirDiReservoirLayout::ProductionPacked; + } + next.restirDiFinalVisibilityEnabled = true; + next.restirDiVisibilityRayBudget = std::max(next.restirDiVisibilityRayBudget, 1u); + } if (static_cast(next.restirGiActiveTileMaskMode) > static_cast(RestirGiActiveTileMaskMode::Auto)) { next.restirGiActiveTileMaskMode = RestirGiActiveTileMaskMode::Off; } @@ -3262,6 +3398,12 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { next.native2BTerminalDirectSampleProbability = std::clamp(next.native2BTerminalDirectSampleProbability, 0.0f, 1.0f); + return next; +} + +bool PathTracerRenderer::applySettings(const RendererSettings& settings) { + RendererSettings next = normalizeSettingsForDevice(settings, context_); + const bool changed = next.renderPreset != settings_.renderPreset || next.pathTracingEnabled != settings_.pathTracingEnabled || @@ -3616,6 +3758,24 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { next.regirGridDimensions != settings_.regirGridDimensions || next.regirReservoirsPerCell != settings_.regirReservoirsPerCell || next.regirEnvironment != settings_.regirEnvironment; + const auto usesAdaptiveSamplingResources = [](const RendererSettings& value) { + const bool adaptiveDebugView = + value.debugView == RendererDebugView::AdaptiveDensityMap || + value.debugView == RendererDebugView::AdaptiveSampleCount || + value.debugView == RendererDebugView::AdaptiveUnsampledPixels || + value.debugView == RendererDebugView::AdaptiveFilledImage || + value.debugView == RendererDebugView::AdaptiveDisocclusionMask; + return value.adaptiveSamplingMode != AdaptiveSamplingMode::Disabled || adaptiveDebugView; + }; + const auto usesDlssGuideResources = [](const RendererSettings& value) { + return value.temporalUpscaler == TemporalUpscaler::Dlss || + value.dlssRayReconstructionEnabled; + }; + const bool optionalResolutionResourcesChanged = + usesAdaptiveSamplingResources(next) != usesAdaptiveSamplingResources(settings_) || + usesDlssGuideResources(next) != usesDlssGuideResources(settings_) || + next.dlssRayReconstructionEnabled != settings_.dlssRayReconstructionEnabled || + next.denoiserBackend != settings_.denoiserBackend; const bool regirActiveConfigChanged = regirResourceConfigChanged || next.regirGridMode != settings_.regirGridMode; @@ -3646,7 +3806,8 @@ bool PathTracerRenderer::applySettings(const RendererSettings& settings) { regirHashTablesValid_ = false; regirHashRotationPending_ = false; } - if (regirResourceConfigChanged && renderExtent_.width > 0u && renderExtent_.height > 0u) { + if ((regirResourceConfigChanged || optionalResolutionResourcesChanged) && + renderExtent_.width > 0u && renderExtent_.height > 0u) { retireResolutionResources(); createResolutionResources(renderExtent_, displayExtent_); } @@ -3802,6 +3963,44 @@ PathTracerRenderer::NvidiaIntegrationStatus PathTracerRenderer::nvidiaIntegratio status.ngxDlssRayReconstructionEvaluation = ngxDlssRayReconstructionEvaluation_; status.streamlineNvPerfEvaluation = streamlineNvPerfEvaluation_; status.streamlineReflexMarkers = streamlineReflexMarkers_; + const std::array dlssRayReconstructionGuideImages{ + &dlssDepthImage_, + &dlssMotionVectorImage_, + &dlssDiffuseAlbedoImage_, + &dlssSpecularAlbedoImage_, + &dlssNormalImage_, + &dlssRoughnessImage_, + &dlssDiffuseHitDistanceImage_, + &dlssSpecularHitDistanceImage_, + &dlssReflectedAlbedoImage_, + &dlssDisocclusionMaskImage_, + &dlssDiffuseRayDirectionImage_, + &dlssSpecularRayDirectionImage_, + &dlssDiffuseRayDirectionHitDistanceImage_, + &dlssSpecularRayDirectionHitDistanceImage_, + }; + for (const Image* image : dlssRayReconstructionGuideImages) { + if (image != nullptr && image->handle() != VK_NULL_HANDLE) { + ++status.dlssRayReconstructionGuideImageCount; + } + } + status.dlssRayReconstructionGuideImagesAllocated = + status.dlssRayReconstructionGuideImageCount == dlssRayReconstructionGuideImages.size(); + status.dlssRayReconstructionPsrGuideBufferAllocated = psrGuideBuffer_.handle() != VK_NULL_HANDLE; + status.dlssRayReconstructionPsrHistorySignaturesAllocated = + psrGuideSignatureBuffer_.handle() != VK_NULL_HANDLE && + previousPsrGuideSignatureBuffer_.handle() != VK_NULL_HANDLE; + status.dlssRayReconstructionGuidePassReady = + dlssRayReconstructionGuidesPipeline_ != nullptr && + dlssRayReconstructionGuidesSetLayout_ != VK_NULL_HANDLE && + status.dlssRayReconstructionGuideImagesAllocated; + status.dlssRayReconstructionUsesPsrGuides = + status.dlssRayReconstructionGuidePassReady && + status.dlssRayReconstructionPsrGuideBufferAllocated && + status.dlssRayReconstructionPsrHistorySignaturesAllocated; + status.dlssRayReconstructionGuideMode = settings_.dlssRayReconstructionEnabled + ? "requested-inactive" + : "disabled"; if (!nsightPerfDiagnosticsCache_.has_value()) { nsightPerfDiagnosticsCache_ = collectNsightPerfDiagnostics(context_); } @@ -3814,6 +4013,33 @@ PathTracerRenderer::NvidiaIntegrationStatus PathTracerRenderer::nvidiaIntegratio const bool nrdStorageSupported = context_.supportsNrdShaderStorageFeatures(); #if defined(RTV_NRD_RUNTIME_ENABLED) status.nrdRequestable = nrdStorageSupported && !nrdCreationFailed_; + const bool nrdResourcesReady = nrdRuntime_ && nrdRuntime_->resourcesReady; + status.nrdDirectRuntimeResourcesReady = nrdResourcesReady; + status.nrdHistoryConfidenceInputsAllocated = + nrdResourcesReady && + nrdRuntime_->diffHistoryConfidence.handle() != VK_NULL_HANDLE && + nrdRuntime_->specHistoryConfidence.handle() != VK_NULL_HANDLE && + nrdRuntime_->rawConfidenceGradient.handle() != VK_NULL_HANDLE && + nrdRuntime_->filteredConfidenceGradient.handle() != VK_NULL_HANDLE && + nrdRuntime_->confidenceSourcePixelFallback.handle() != VK_NULL_HANDLE && + nrdRuntime_->previousLightRecords.handle() != VK_NULL_HANDLE; + status.nrdHistoryConfidenceAvailable = + status.nrdHistoryConfidenceInputsAllocated && + nrdAvailable_ && + !nrdCreationFailed_; + status.nrdValidationOutputAllocated = + nrdResourcesReady && nrdRuntime_->validationOutput.handle() != VK_NULL_HANDLE; + status.nrdValidationOutputEnabled = + status.nrdValidationOutputAllocated && + settings_.debugView == RendererDebugView::NrdValidation && + settings_.denoiserBackend == DenoiserBackend::Nrd && + nrdAvailable_ && + !nrdCreationFailed_; + status.nrdGuideContractReason = + "Direct NRD uses motion, normal/roughness, viewZ, and diffuse/specular radiance-hitdist guides; " + "diffuse/specular history confidence inputs are generated from sparse, filtered temporal gradients; " + "when ReSTIR DI is active, the finalized selected light is resolved through current/previous stable-light " + "identity tables and re-evaluated at both receiver states, with cached luminance as the fallback."; #endif status.nrdAvailable = nrdAvailable_ && !nrdCreationFailed_; status.nrdUnavailableReason = status.nrdAvailable @@ -3885,11 +4111,36 @@ PathTracerRenderer::NvidiaIntegrationStatus PathTracerRenderer::nvidiaIntegratio status.dlssRayReconstructionUnavailableReason = status.dlssUnavailableReason; status.dlssFrameGenerationUnavailableReason = status.dlssUnavailableReason; #endif + status.dlssAutoExposureEnabled = settings_.autoExposureEnabled; + status.dlssExposureBufferAvailable = exposureBuffer_.handle() != VK_NULL_HANDLE; + status.dlssExposureBufferPassedToSdk = false; + status.dlssManualExposure = settings_.usePhysicalCamera + ? 2.0f * std::exp2(14.0f - physicalCamera_.ev100()) + : settings_.exposure; + status.dlssPreExposure = 1.0f; + status.dlssExposureScale = 1.0f; + const bool dlssRayReconstructionRunReady = + !settings_.wavefrontFinalOutputEnabled && + settings_.pathTracingEnabled && + settings_.taaEnabled && + !shouldBypassTemporalUpscalerForDebugView() && + settings_.dlssRayReconstructionEnabled && + (status.dlssRayReconstructionAvailable || status.streamlineDlssRayReconstruction.supported) && + status.dlssRayReconstructionGuidePassReady && + taaImage_.handle() != VK_NULL_HANDLE && + depthNormalBuffer_.handle() != VK_NULL_HANDLE && + velocityBuffer_.handle() != VK_NULL_HANDLE && + previousWorldPositionBuffer_.handle() != VK_NULL_HANDLE && + previousPsrGuideSignatureBuffer_.handle() != VK_NULL_HANDLE && + pathDataBuffer_.handle() != VK_NULL_HANDLE; + status.dlssRayReconstructionGuideMode = settings_.dlssRayReconstructionEnabled + ? (dlssRayReconstructionRunReady ? "active" : "requested-inactive") + : "disabled"; return status; } DenoiserBackend PathTracerRenderer::effectiveDenoiserBackend() const { - if (settings_.debugView != RendererDebugView::Beauty) { + if (!passes::NrdPass::supportsDebugView(settings_.debugView)) { return DenoiserBackend::Engine; } if (settings_.denoiserBackend == DenoiserBackend::Nrd && nvidiaIntegrationStatus().nrdAvailable) { @@ -3900,7 +4151,7 @@ DenoiserBackend PathTracerRenderer::effectiveDenoiserBackend() const { TemporalUpscaler PathTracerRenderer::effectiveTemporalUpscaler() const { const auto status = nvidiaIntegrationStatus(); - if (settings_.temporalUpscaler == TemporalUpscaler::Dlss && + if (passes::DlssPass::isUpscaleActive(settings_) && (status.dlssAvailable || status.streamlineDlss.supported)) { return TemporalUpscaler::Dlss; } @@ -4065,8 +4316,29 @@ void PathTracerRenderer::createNrdResolutionResources() { createImage(runtime.viewZ, VK_FORMAT_R32_SFLOAT, "nrd view z"); createImage(runtime.diffRadianceHitdist, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd input diffuse radiance hitdist"); createImage(runtime.specRadianceHitdist, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd input specular radiance hitdist"); + createImage(runtime.diffHistoryConfidence, VK_FORMAT_R16_SFLOAT, "nrd diffuse history confidence"); + createImage(runtime.specHistoryConfidence, VK_FORMAT_R16_SFLOAT, "nrd specular history confidence"); + createImage(runtime.currentLightingLuminance, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd current lighting luminance"); + createImage(runtime.previousLightingLuminance, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd previous lighting luminance"); + createImage(runtime.rawConfidenceGradient, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd raw confidence gradient"); + createImage(runtime.filteredConfidenceGradient, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd filtered confidence gradient"); + runtime.confidenceSourcePixelFallback.create(allocator_, BufferDesc{ + .size = static_cast(renderExtent_.width) * renderExtent_.height * sizeof(uint32_t), + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "nrd confidence source pixel fallback", + }); + runtime.previousLightRecords.create(allocator_, BufferDesc{ + .size = passes::RestirDIPass::previousLightRecordBufferByteSize(), + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "nrd previous light records", + }); + runtime.previousLightRecordCount = 0u; + runtime.previousLightHistoryValid = false; createImage(runtime.outDiffRadianceHitdist, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd output diffuse radiance hitdist"); createImage(runtime.outSpecRadianceHitdist, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd output specular radiance hitdist"); + createImage(runtime.validationOutput, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd validation output"); createImage(runtime.fallbackImage, VK_FORMAT_R16G16B16A16_SFLOAT, "nrd fallback image"); auto createPoolImages = [this](const nrd::TextureDesc* descs, uint32_t count, const char* prefix) { @@ -4320,6 +4592,7 @@ void PathTracerRenderer::recordStreamlineEvaluationCommandStateBoundary(VkComman dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1u; dependency.pMemoryBarriers = &memoryBarrier; + recordManualBarrierEscape("PathTracerRenderer", "streamline_command_state_boundary", dependency); vkCmdPipelineBarrier2(commandBuffer, &dependency); validationLog_.recordPass( @@ -4528,6 +4801,7 @@ void PathTracerRenderer::setCameraFovY(float fovY) { return; } camera_.fovY = clampedFov; + cameraChangedThisFrame_ = true; resetAccumulation(AccumulationResetReason::CameraMoved); } @@ -4573,6 +4847,7 @@ void PathTracerRenderer::setCameraProjection( camera_.fovY = clampedFov; camera_.projectionControls = projectionControls; camera_.clipControls = clipControls; + cameraChangedThisFrame_ = true; resetAccumulation(AccumulationResetReason::CameraMoved); } @@ -4606,6 +4881,15 @@ void PathTracerRenderer::resetAccumulation(AccumulationResetReason reason) { } } +void PathTracerRenderer::resetAccumulationPreserveTemporalHistory(AccumulationResetReason reason) { + lastResetReason_ = reason; + ++pickSceneVersion_; + pendingPick_ = {}; + validationLog_.recordAccumulationInvalidation(accumulationResetReasonName(reason), temporalFrameIndex_); + frameCount_ = 0; + stillFrameCount_ = 0; +} + void PathTracerRenderer::applyStreamingResetMasks( const std::vector& temporalEntityUuids, const std::vector& restirEntityUuids, @@ -4763,12 +5047,35 @@ bool PathTracerRenderer::updateMaterials(const SceneAsset& scene, const AssetMan return updated; } +bool PathTracerRenderer::patchStreamedMaterialTexture( + const SceneAsset& scene, + TextureAssetHandle texture, + const Image& image) { + uint32_t slot = UINT32_MAX; + VkDescriptorImageInfo descriptor{}; + if (!scene_.streamedMaterialTextureDescriptor(scene, texture, image, slot, descriptor)) { + return false; + } + bindlessTextureHeap_.patch(slot, descriptor); + ++materialVersionCounter_; + ++lightVersionCounter_; + resetAccumulation(AccumulationResetReason::MaterialChanged); + return true; +} + bool PathTracerRenderer::updateSceneLights(const SceneAsset& scene, bool rebuildLightBvh) { const uint64_t retireFrame = temporalFrameIndex_ + static_cast(std::max(frames_.size(), 1)) + 1u; + const std::vector previousRecords = scene_.lightRecordsCpu(); const bool updated = scene_.updateSceneLights(uploader_, scene, retireFrame, false, rebuildLightBvh); if (updated) { ++lightVersionCounter_; - resetAccumulation(AccumulationResetReason::LightingChanged); + const bool stableIdentitySetPreserved = + passes::RestirDIPass::preservesStableLightHistory(previousRecords, scene_.lightRecordsCpu()); + if (stableIdentitySetPreserved) { + resetAccumulationPreserveTemporalHistory(AccumulationResetReason::LightingChanged); + } else { + resetAccumulation(AccumulationResetReason::LightingChanged); + } } return updated; } @@ -5703,6 +6010,9 @@ void PathTracerRenderer::retireResolutionResources() { retireBuffer(entityIdBuffer_); retireBuffer(entityIdReadbackBuffer_); retireBuffer(pathDataBuffer_); + retireBuffer(psrGuideBuffer_); + retireBuffer(psrGuideSignatureBuffer_); + retireBuffer(previousPsrGuideSignatureBuffer_); retireBuffer(adaptiveSamplingDensityBuffer_); retireBuffer(adaptiveSamplingSampleCountBuffer_); retireBuffer(adaptiveSamplingStatsBuffer_); @@ -5775,6 +6085,9 @@ void PathTracerRenderer::retireResolutionResources() { retireBuffer(restirDiTemporalReservoirBuffer_); retireBuffer(restirDiSpatialReservoirBuffer_); retireBuffer(restirDiFinalReservoirBuffer_); + retireBuffer(restirDiTemporalSourcePixelBuffer_); + retireBuffer(restirDiSpatialSourcePixelBuffer_); + retireBuffer(restirDiFinalSourcePixelBuffer_); retireBuffer(previousRestirDiReservoirBuffer_); retireBuffer(previousRestirDiReceiverBuffer_); retireBuffer(restirDiCountersBuffer_); @@ -5792,6 +6105,7 @@ void PathTracerRenderer::retireResolutionResources() { retireImage(nrdRuntime_->outDiffRadianceHitdist); retireImage(nrdRuntime_->outSpecRadianceHitdist); retireImage(nrdRuntime_->fallbackImage); + retireBuffer(nrdRuntime_->confidenceSourcePixelFallback); for (Image& image : nrdRuntime_->permanentPoolImages) { retireImage(image); } @@ -5822,25 +6136,30 @@ void PathTracerRenderer::releaseRetiredResolutionResources() { } void PathTracerRenderer::createRestirDiResources(VkDeviceSize pixelCount) { + static_assert(sizeof(RestirDiReceiverGpu) == passes::RestirDIPass::kReceiverFullByteSize); + static_assert(sizeof(RestirDiReceiverPackedGpu) == passes::RestirDIPass::kReceiverPackedByteSize); + static_assert(sizeof(RestirDiReservoirGpu) == passes::RestirDIPass::kReservoirFullByteSize); + static_assert(sizeof(RestirDiReservoirPackedGpu) == passes::RestirDIPass::kReservoirPackedByteSize); + if (!shouldRunRestirDiEstimator() || pixelCount == 0 || restirDiReceiverBuffer_.handle() != VK_NULL_HANDLE) { return; } - const VkDeviceSize receiverStride = settings_.restirDiReservoirLayout == RestirDiReservoirLayout::ValidationFull - ? sizeof(RestirDiReceiverGpu) - : sizeof(RestirDiReceiverPackedGpu); + const auto sizes = passes::RestirDIPass::resourceByteSizes( + pixelCount, + settings_.restirDiReservoirLayout, + shouldAliasRestirDiFinal(), + settings_.restirHistoryCopyMode, + kRendererFramesInFlight); restirDiReceiverBuffer_.create(allocator_, BufferDesc{ - .size = pixelCount * receiverStride, - .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .size = sizes.receiverBytes, + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "restir di receiver", }); - const VkDeviceSize reservoirStride = settings_.restirDiReservoirLayout == RestirDiReservoirLayout::ValidationFull - ? sizeof(RestirDiReservoirGpu) - : sizeof(RestirDiReservoirPackedGpu); const BufferDesc reservoirDesc{ - .size = pixelCount * reservoirStride, - .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .size = sizes.reservoirBytes, + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "restir di reservoir", }; @@ -5852,9 +6171,23 @@ void PathTracerRenderer::createRestirDiResources(VkDeviceSize pixelCount) { createReservoir(restirDiInitialReservoirBuffer_, "restir di initial reservoir"); createReservoir(restirDiTemporalReservoirBuffer_, "restir di temporal reservoir"); createReservoir(restirDiSpatialReservoirBuffer_, "restir di spatial reservoir"); - if (!shouldAliasRestirDiFinal() || settings_.restirHistoryCopyMode == RestirHistoryCopyMode::PingPong) { + if (sizes.finalReservoirBytes > 0u) { createReservoir(restirDiFinalReservoirBuffer_, "restir di final reservoir"); } + const BufferDesc sourcePixelDesc{ + .size = sizes.sourcePixelBytes, + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "restir di source pixel", + }; + auto createSourcePixel = [&](Buffer& buffer, const char* debugName) { + BufferDesc desc = sourcePixelDesc; + desc.debugName = debugName; + buffer.create(allocator_, desc); + }; + createSourcePixel(restirDiTemporalSourcePixelBuffer_, "restir di temporal source pixel"); + createSourcePixel(restirDiSpatialSourcePixelBuffer_, "restir di spatial source pixel"); + createSourcePixel(restirDiFinalSourcePixelBuffer_, "restir di final source pixel"); previousRestirDiReservoirBuffer_.create(allocator_, BufferDesc{ .size = reservoirDesc.size, .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, @@ -5862,19 +6195,19 @@ void PathTracerRenderer::createRestirDiResources(VkDeviceSize pixelCount) { .debugName = "restir di previous reservoir", }); previousRestirDiReceiverBuffer_.create(allocator_, BufferDesc{ - .size = pixelCount * receiverStride, + .size = sizes.receiverBytes, .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "restir di previous receiver", }); restirDiCountersBuffer_.create(allocator_, BufferDesc{ - .size = sizeof(uint32_t) * 64 * kRendererFramesInFlight, + .size = sizes.counterBytes, .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "restir di counters", }); restirDiCountersReadbackBuffer_.create(allocator_, BufferDesc{ - .size = sizeof(uint32_t) * 64 * kRendererFramesInFlight, + .size = sizes.counterBytes, .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::Readback, .persistentMapped = true, @@ -5886,20 +6219,63 @@ void PathTracerRenderer::createRestirDiResources(VkDeviceSize pixelCount) { void PathTracerRenderer::reconcileRestirDiResources() { const VkDeviceSize pixelCount = static_cast(renderExtent_.width) * renderExtent_.height; if (shouldRunRestirDiEstimator()) { - const VkDeviceSize reservoirStride = settings_.restirDiReservoirLayout == RestirDiReservoirLayout::ValidationFull - ? sizeof(RestirDiReservoirGpu) - : sizeof(RestirDiReservoirPackedGpu); - const VkDeviceSize receiverStride = settings_.restirDiReservoirLayout == RestirDiReservoirLayout::ValidationFull - ? sizeof(RestirDiReceiverGpu) - : sizeof(RestirDiReceiverPackedGpu); - const bool finalAllocationMismatch = shouldAliasRestirDiFinal() + const auto sizes = passes::RestirDIPass::resourceByteSizes( + pixelCount, + settings_.restirDiReservoirLayout, + shouldAliasRestirDiFinal(), + settings_.restirHistoryCopyMode, + kRendererFramesInFlight); + const bool reservoirAllocationMismatch = + restirDiInitialReservoirBuffer_.handle() == VK_NULL_HANDLE || + restirDiInitialReservoirBuffer_.size() != sizes.reservoirBytes || + restirDiTemporalReservoirBuffer_.handle() == VK_NULL_HANDLE || + restirDiTemporalReservoirBuffer_.size() != sizes.reservoirBytes || + restirDiSpatialReservoirBuffer_.handle() == VK_NULL_HANDLE || + restirDiSpatialReservoirBuffer_.size() != sizes.reservoirBytes; + const bool receiverAllocationMismatch = + restirDiReceiverBuffer_.handle() == VK_NULL_HANDLE || + restirDiReceiverBuffer_.size() != sizes.receiverBytes; + const bool finalAllocationMismatch = sizes.finalReservoirBytes == 0u ? restirDiFinalReservoirBuffer_.handle() != VK_NULL_HANDLE : (restirDiFinalReservoirBuffer_.handle() == VK_NULL_HANDLE || - restirDiFinalReservoirBuffer_.size() != pixelCount * reservoirStride); - if (restirDiInitialReservoirBuffer_.handle() != VK_NULL_HANDLE && - (restirDiInitialReservoirBuffer_.size() != pixelCount * reservoirStride || - restirDiReceiverBuffer_.size() != pixelCount * receiverStride || - finalAllocationMismatch)) { + restirDiFinalReservoirBuffer_.size() != sizes.finalReservoirBytes); + const bool sourcePixelAllocationMismatch = + restirDiTemporalSourcePixelBuffer_.handle() == VK_NULL_HANDLE || + restirDiTemporalSourcePixelBuffer_.size() != sizes.sourcePixelBytes || + restirDiSpatialSourcePixelBuffer_.handle() == VK_NULL_HANDLE || + restirDiSpatialSourcePixelBuffer_.size() != sizes.sourcePixelBytes || + restirDiFinalSourcePixelBuffer_.handle() == VK_NULL_HANDLE || + restirDiFinalSourcePixelBuffer_.size() != sizes.sourcePixelBytes; + const bool historyAllocationMismatch = + previousRestirDiReservoirBuffer_.handle() == VK_NULL_HANDLE || + previousRestirDiReservoirBuffer_.size() != sizes.reservoirBytes || + previousRestirDiReceiverBuffer_.handle() == VK_NULL_HANDLE || + previousRestirDiReceiverBuffer_.size() != sizes.receiverBytes; + const bool counterAllocationMismatch = + restirDiCountersBuffer_.handle() == VK_NULL_HANDLE || + restirDiCountersBuffer_.size() != sizes.counterBytes || + restirDiCountersReadbackBuffer_.handle() == VK_NULL_HANDLE || + restirDiCountersReadbackBuffer_.size() != sizes.counterBytes; + const bool hasAnyRestirDiResource = + restirDiReceiverBuffer_.handle() != VK_NULL_HANDLE || + restirDiInitialReservoirBuffer_.handle() != VK_NULL_HANDLE || + restirDiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE || + restirDiSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE || + restirDiFinalReservoirBuffer_.handle() != VK_NULL_HANDLE || + restirDiTemporalSourcePixelBuffer_.handle() != VK_NULL_HANDLE || + restirDiSpatialSourcePixelBuffer_.handle() != VK_NULL_HANDLE || + restirDiFinalSourcePixelBuffer_.handle() != VK_NULL_HANDLE || + previousRestirDiReservoirBuffer_.handle() != VK_NULL_HANDLE || + previousRestirDiReceiverBuffer_.handle() != VK_NULL_HANDLE || + restirDiCountersBuffer_.handle() != VK_NULL_HANDLE || + restirDiCountersReadbackBuffer_.handle() != VK_NULL_HANDLE; + if (hasAnyRestirDiResource && + (reservoirAllocationMismatch || + receiverAllocationMismatch || + finalAllocationMismatch || + sourcePixelAllocationMismatch || + historyAllocationMismatch || + counterAllocationMismatch)) { RetiredResolutionResources retired{}; retired.releaseFrame = temporalFrameIndex_ + static_cast(std::max(frames_.size(), 1)) + 1u; auto retire = [&](Buffer& buffer) { @@ -5910,6 +6286,9 @@ void PathTracerRenderer::reconcileRestirDiResources() { retire(restirDiTemporalReservoirBuffer_); retire(restirDiSpatialReservoirBuffer_); retire(restirDiFinalReservoirBuffer_); + retire(restirDiTemporalSourcePixelBuffer_); + retire(restirDiSpatialSourcePixelBuffer_); + retire(restirDiFinalSourcePixelBuffer_); retire(previousRestirDiReservoirBuffer_); retire(previousRestirDiReceiverBuffer_); retire(restirDiCountersBuffer_); @@ -5935,6 +6314,9 @@ void PathTracerRenderer::reconcileRestirDiResources() { retire(restirDiTemporalReservoirBuffer_); retire(restirDiSpatialReservoirBuffer_); retire(restirDiFinalReservoirBuffer_); + retire(restirDiTemporalSourcePixelBuffer_); + retire(restirDiSpatialSourcePixelBuffer_); + retire(restirDiFinalSourcePixelBuffer_); retire(previousRestirDiReservoirBuffer_); retire(previousRestirDiReceiverBuffer_); retire(restirDiCountersBuffer_); @@ -5947,6 +6329,17 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx renderExtent_ = renderExtent; displayExtent_ = displayExtent; const VkDeviceSize pixelCount = static_cast(renderExtent.width) * renderExtent.height; + const bool allocateAdaptiveSamplingResources = + settings_.adaptiveSamplingMode != AdaptiveSamplingMode::Disabled || isAdaptiveSamplingDebugView(); + const bool allocateDlssGuideResources = + settings_.temporalUpscaler == TemporalUpscaler::Dlss || settings_.dlssRayReconstructionEnabled; + const bool allocateDlssRayReconstructionGuides = settings_.dlssRayReconstructionEnabled; + const uint32_t adaptiveWidth = allocateAdaptiveSamplingResources ? renderExtent.width : 1u; + const uint32_t adaptiveHeight = allocateAdaptiveSamplingResources ? renderExtent.height : 1u; + const uint32_t dlssGuideWidth = allocateDlssGuideResources ? renderExtent.width : 1u; + const uint32_t dlssGuideHeight = allocateDlssGuideResources ? renderExtent.height : 1u; + const uint32_t dlssRayReconstructionWidth = allocateDlssRayReconstructionGuides ? renderExtent.width : 1u; + const uint32_t dlssRayReconstructionHeight = allocateDlssRayReconstructionGuides ? renderExtent.height : 1u; const uint32_t wavefrontQueueCapacity = settings_.wavefrontQueuesEnabled ? wavefrontQueueCapacityFor(pixelCount) : 0u; wavefrontRayQueueCapacity_ = wavefrontQueueCapacity; wavefrontCompactedRayQueueCapacity_ = settings_.wavefrontCompactEnabled ? wavefrontQueueCapacity : 0u; @@ -6094,22 +6487,22 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx .debugName = "path tracer moment debug resolved", }); adaptiveSamplingDebugImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = adaptiveWidth, + .height = adaptiveHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .debugName = "path tracer adaptive sampling debug", }); adaptiveSamplingFilledMaskImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = adaptiveWidth, + .height = adaptiveHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .debugName = "path tracer adaptive sampling filled mask", }); adaptiveSamplingFilledImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = adaptiveWidth, + .height = adaptiveHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, .debugName = "path tracer adaptive sampling filled hdr", @@ -6129,104 +6522,106 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx .debugName = "path tracer taa history", }); dlssDepthImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssGuideWidth, + .height = dlssGuideHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss depth guide", }); dlssMotionVectorImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssGuideWidth, + .height = dlssGuideHeight, .format = VK_FORMAT_R16G16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss motion vector guide", }); dlssDiffuseAlbedoImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr diffuse albedo guide", }); dlssSpecularAlbedoImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr specular albedo guide", }); dlssNormalImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr normal guide", }); dlssRoughnessImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr roughness guide", }); dlssDiffuseHitDistanceImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr diffuse hit distance guide", }); dlssSpecularHitDistanceImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr specular hit distance guide", }); dlssReflectedAlbedoImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr reflected albedo guide", }); dlssDisocclusionMaskImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr disocclusion mask guide", }); dlssDiffuseRayDirectionImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr diffuse ray direction guide", }); dlssSpecularRayDirectionImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R16G16B16A16_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr specular ray direction guide", }); dlssDiffuseRayDirectionHitDistanceImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr diffuse ray direction hit distance guide", }); dlssSpecularRayDirectionHitDistanceImage_.create(allocator_, ImageDesc{ - .width = renderExtent.width, - .height = renderExtent.height, + .width = dlssRayReconstructionWidth, + .height = dlssRayReconstructionHeight, .format = VK_FORMAT_R32_SFLOAT, .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, .debugName = "dlss rr specular ray direction hit distance guide", }); - createNrdResolutionResources(); + if (nrdRequested()) { + createNrdResolutionResources(); + } presentationImage_.create(allocator_, ImageDesc{ .width = displayExtent.width, .height = displayExtent.height, @@ -6317,14 +6712,32 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx .memory = BufferMemory::GpuOnly, .debugName = "path data channels", }); + psrGuideBuffer_.create(allocator_, BufferDesc{ + .size = pixelCount * sizeof(PsrGuideGpu), + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "primary surface replacement guides", + }); + psrGuideSignatureBuffer_.create(allocator_, BufferDesc{ + .size = pixelCount * sizeof(uint32_t), + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "primary surface replacement signatures", + }); + previousPsrGuideSignatureBuffer_.create(allocator_, BufferDesc{ + .size = pixelCount * sizeof(uint32_t), + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "previous primary surface replacement signatures", + }); adaptiveSamplingDensityBuffer_.create(allocator_, BufferDesc{ - .size = pixelCount * sizeof(float), + .size = allocateAdaptiveSamplingResources ? pixelCount * sizeof(float) : sizeof(float), .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "adaptive sampling density", }); adaptiveSamplingSampleCountBuffer_.create(allocator_, BufferDesc{ - .size = pixelCount * sizeof(uint32_t), + .size = allocateAdaptiveSamplingResources ? pixelCount * sizeof(uint32_t) : sizeof(uint32_t), .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "adaptive sampling sample count", @@ -6685,18 +7098,16 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx .debugName = "wavefront restir gi candidate reservoir", }); } - const uint32_t restirGiWidth = effectiveRestirGiHalfResolution() - ? (renderExtent_.width + 1u) / 2u - : renderExtent_.width; - const uint32_t restirGiHeight = effectiveRestirGiHalfResolution() - ? (renderExtent_.height + 1u) / 2u - : renderExtent_.height; const VkDeviceSize restirGiProductionStride = shouldUseRestirGiReferenceValidation() ? sizeof(RestirGiReservoirValidationGpu) : sizeof(RestirGiReservoirProductionPackedGpu); - const VkDeviceSize restirGiProductionBytes = - static_cast(restirGiWidth) * restirGiHeight * restirGiProductionStride; - const VkDeviceSize restirGiProductionFullBytes = pixelCount * restirGiProductionStride; + const VkDeviceSize restirGiProductionBytes = passes::RestirGIPass::productionReservoirByteSize( + renderExtent_.width, + renderExtent_.height, + effectiveRestirGiHalfResolution(), + restirGiProductionStride); + const VkDeviceSize restirGiProductionFullBytes = + passes::RestirGIPass::pixelByteSize(pixelCount, restirGiProductionStride); restirGiTemporalReservoirBuffer_.create(allocator_, BufferDesc{ .size = restirGiProductionBytes, .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, @@ -6723,15 +7134,14 @@ void PathTracerRenderer::createResolutionResources(VkExtent2D renderExtent, VkEx .debugName = "restir gi production upsampled reservoir", }); } - const uint32_t restirGiTileColumns = (renderExtent_.width + kRestirGiActiveTileSize - 1u) / kRestirGiActiveTileSize; - const uint32_t restirGiTileRows = (renderExtent_.height + kRestirGiActiveTileSize - 1u) / kRestirGiActiveTileSize; restirGiActiveTileMaskBuffer_.create(allocator_, BufferDesc{ - .size = std::max(1u, static_cast(restirGiTileColumns) * restirGiTileRows) * sizeof(uint32_t), + .size = passes::RestirGIPass::activeTileMaskByteSize(renderExtent_.width, renderExtent_.height), .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, .memory = BufferMemory::GpuOnly, .debugName = "restir gi active tile mask", }); - const VkDeviceSize restirGiReceiverBytes = pixelCount * sizeof(RestirGiReceiverGpu); + const VkDeviceSize restirGiReceiverBytes = + passes::RestirGIPass::pixelByteSize(pixelCount, sizeof(RestirGiReceiverGpu)); restirGiReceiverBuffer_.create(allocator_, BufferDesc{ .size = restirGiReceiverBytes, .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, @@ -6927,8 +7337,9 @@ void PathTracerRenderer::updateCamera() { static_cast(raygenRestirMode), temporalHistoryAvailable ? 1.0f : 0.0f, settings_.sunAzimuth); + const uint32_t stochasticFrameIndex = sampleFrameIndex(); camera_.frameCount = frameCount_; - camera_.temporalFrameIndex = settings_.fixedSeed.has_value() ? *settings_.fixedSeed : temporalFrameIndex_; + camera_.temporalFrameIndex = stochasticFrameIndex; const bool cameraMovingForTemporal = cameraChangedThisFrame_; const bool suppressJitterForDebugView = settings_.debugView == RendererDebugView::MotionVectors; @@ -6937,16 +7348,13 @@ void PathTracerRenderer::updateCamera() { } else { stillFrameCount_ = std::min(stillFrameCount_ + 1u, 60u); } - const bool dlssTemporalRequested = settings_.temporalUpscaler == TemporalUpscaler::Dlss || - settings_.dlssRayReconstructionEnabled; - const bool preserveMovingJitterForDlss = dlssTemporalRequested && - settings_.pathTracingEnabled && - settings_.taaEnabled && - !shouldBypassTemporalUpscalerForDebugView(); - const bool restoreFullJitter = preserveMovingJitterForDlss || stillFrameCount_ >= 2u; - const float effectiveJitterScale = ((cameraMovingForTemporal && !preserveMovingJitterForDlss) || suppressJitterForDebugView) - ? 0.0f - : (restoreFullJitter ? 1.0f : 0.0f); + const bool runTaaTemporalResolve = shouldRunTaa(); + const bool runDlssTemporalResolve = shouldRunDlss(); + const bool runDlssRayReconstruction = shouldRunDlssRayReconstruction(); + const bool temporalResolveActive = + runTaaTemporalResolve || runDlssTemporalResolve || runDlssRayReconstruction; + const bool dlssTemporalRequested = runDlssTemporalResolve || runDlssRayReconstruction; + const float effectiveJitterScale = suppressJitterForDebugView ? 0.0f : 1.0f; camera_.effectiveJitterScale = effectiveJitterScale; camera_.cameraMoving = cameraMovingForTemporal ? 1u : 0u; @@ -6968,27 +7376,28 @@ void PathTracerRenderer::updateCamera() { projection[1][1] *= -1.0f; const glm::mat4 nonJitteredProjection = projection; const bool jitterEnabled = settings_.pathTracingEnabled && - settings_.taaEnabled && + temporalResolveActive && settings_.cameraJitterEnabled && effectiveJitterScale > 0.0f && renderExtent_.width > 0 && renderExtent_.height > 0; - const uint32_t jitterBase = settings_.fixedSeed.has_value() ? *settings_.fixedSeed : temporalFrameIndex_; + const uint32_t jitterBase = stochasticFrameIndex; const uint32_t jitterIndex = settings_.fixedSeed.has_value() ? jitterBase : (jitterBase + 1u); const glm::vec2 currentJitter = jitterEnabled ? glm::vec2(halton(jitterIndex, 2u) - 0.5f, halton(jitterIndex, 3u) - 0.5f) * effectiveJitterScale : glm::vec2(0.0f); projection[2][0] -= currentJitter.x * 2.0f / static_cast(std::max(renderExtent_.width, 1u)); projection[2][1] -= currentJitter.y * 2.0f / static_cast(std::max(renderExtent_.height, 1u)); + const glm::mat4 nonJitteredViewProj = nonJitteredProjection * view; const glm::mat4 viewProj = projection * view; camera_.jitter = glm::vec4(currentJitter, previousJitter_); Buffer& frameUniforms = currentFrame_->uniformRing(); frameUniforms.write(&camera_, sizeof(camera_), kFrameCameraUniformOffset); frameUniforms.flush(sizeof(camera_), kFrameCameraUniformOffset); - prevCamera_.viewProj = viewProj; - prevCamera_.invViewProj = glm::inverse(viewProj); - prevCamera_.prevViewProj = previousViewProj_; + prevCamera_.viewProj = nonJitteredViewProj; + prevCamera_.invViewProj = glm::inverse(nonJitteredViewProj); + prevCamera_.prevViewProj = previousNonJitteredViewProj_; prevCamera_.currentPos = camera_.pos; prevCamera_.prevPos = previousCameraPos_; prevCamera_.jitter = glm::vec4(currentJitter, previousJitter_); @@ -6997,7 +7406,7 @@ void PathTracerRenderer::updateCamera() { const bool denoiserDebugView = !isNonDenoiserDebugView(); const bool allowDenoiserForDebugView = denoiserDebugView; - const bool stablePreview = shouldRunTaa() || shouldRunDlss(); + const bool stablePreview = temporalResolveActive; const bool allowDenoiserWhileMoving = settings_.denoiseWhileMoving || stablePreview || !cameraMovingForTemporal; denoiserParams_.enabled = settings_.pathTracingEnabled && settings_.denoiserEnabled && allowDenoiserForDebugView && allowDenoiserWhileMoving && !adaptiveSkipDenoiser_ ? 1u : 0u; denoiserParams_.strength = settings_.denoiserStrength; @@ -7017,7 +7426,7 @@ void PathTracerRenderer::updateCamera() { frameUniforms.write(&denoiserParams_, sizeof(denoiserParams_), kFrameDenoiserParamsOffset); frameUniforms.flush(sizeof(denoiserParams_), kFrameDenoiserParamsOffset); - taaParams_.enabled = (shouldRunTaa() || shouldRunDlss() || shouldRunDlssRayReconstruction()) ? 1u : 0u; + taaParams_.enabled = temporalResolveActive ? 1u : 0u; taaParams_.frameCount = temporalFrameIndex_; taaParams_.width = displayExtent_.width; taaParams_.height = displayExtent_.height; @@ -7034,6 +7443,10 @@ void PathTracerRenderer::updateCamera() { taaParams_.renderHeight = renderExtent_.height; taaParams_.motionFeedback = settings_.taaMotionFeedback; taaParams_.reactiveFeedback = settings_.taaReactiveFeedback; + taaParams_.inputPixelOffsetX = currentJitter.x; + taaParams_.inputPixelOffsetY = currentJitter.y; + taaParams_.clampingFactor = 1.3f; + taaParams_.maxRadiance = 200.0f; frameUniforms.write(&taaParams_, sizeof(taaParams_), kFrameTaaParamsOffset); frameUniforms.flush(sizeof(taaParams_), kFrameTaaParamsOffset); if (temporalFrameIndex_ == 1u || temporalFrameIndex_ % 120u == 0u) { @@ -7046,58 +7459,36 @@ void PathTracerRenderer::updateCamera() { " taaFeedback=" + std::to_string(taaParams_.feedback)); } - restirSpatialParams_.width = renderExtent_.width; - restirSpatialParams_.height = renderExtent_.height; - restirSpatialParams_.frameCount = temporalFrameIndex_; - const uint32_t restirReuseEnabled = (shouldRunRestirSpatial() || shouldUseRestirGiReservoirs()) ? 1u : 0u; - restirSpatialParams_.enabled = restirReuseEnabled | - ((shouldUseRestirGiReservoirs() && settings_.restirGiFinalStabilizationEnabled) ? 2u : 0u); - restirSpatialParams_.giSpatialRounds = settings_.restirGiSpatialRounds; - restirSpatialParams_.giHalfResolution = effectiveRestirGiHalfResolution() ? 1u : 0u; - restirSpatialParams_.giTemporalMaxAge = settings_.restirGiTemporalMaxAge; - restirSpatialParams_.giVisibilityRayBudget = settings_.restirGiVisibilityRayBudget; - restirSpatialParams_.giSpatialRadius = settings_.restirGiSpatialRadius; - restirSpatialParams_.giDepthThresholdScale = settings_.restirGiDepthThresholdScale; - restirSpatialParams_.giSpatialCompatibilityThreshold = settings_.restirGiSpatialCompatibilityThreshold; - restirSpatialParams_.rawOutputIsCurrentSample = shouldRunDlssRayReconstruction() ? 1.0f : 0.0f; - restirSpatialParams_.cameraPosition = camera_.pos; + restirSpatialParams_ = passes::RestirGIPass::makeSpatialParams( + settings_, + passes::RestirGIPass::SpatialParameterInputs{ + .width = renderExtent_.width, + .height = renderExtent_.height, + .frameIndex = stochasticFrameIndex, + .legacySpatialReuseRuns = shouldRunRestirSpatial(), + .giReservoirsRun = shouldUseRestirGiReservoirs(), + .giHalfResolution = effectiveRestirGiHalfResolution(), + .rawOutputIsCurrentSample = shouldRunDlssRayReconstruction(), + }, + camera_.pos); frameUniforms.write(&restirSpatialParams_, sizeof(restirSpatialParams_), kFrameRestirSpatialParamsOffset); frameUniforms.flush(sizeof(restirSpatialParams_), kFrameRestirSpatialParamsOffset); - // Populate and upload ReSTIR DI params for the new pipeline passes - if (shouldRunRestirDiEstimator()) { - restirDiParams_.width = renderExtent_.width; - restirDiParams_.height = renderExtent_.height; - restirDiParams_.frameIndex = temporalFrameIndex_; - restirDiParams_.enabled = 1u; - restirDiParams_.temporalMaxAge = settings_.restirDiTemporalMaxAge; - restirDiParams_.spatialRounds = settings_.restirDiSpatialRounds; - restirDiParams_.spatialMaxM = settings_.restirDiMaxM; - restirDiParams_.spatialRadius = settings_.restirDiSpatialRadius; - restirDiParams_.normalThreshold = 0.85f; - restirDiParams_.depthThreshold = 0.05f; - restirDiParams_.temporalLuminanceLimitFactor = settings_.restirDiProductionStabilizationEnabled ? 8.0f : 0.0f; - restirDiParams_.confidenceDecay = 0.96f; - restirDiParams_.lumClampNeighborAvgFactor = 6.0f; - restirDiParams_.lumClampNeighborMaxFactor = 3.0f; - restirDiParams_.fireflyClamp = settings_.fireflyClamp; - restirDiParams_.productionClampLuminance = settings_.restirDiProductionStabilizationEnabled - ? settings_.restirDiClampLuminance : 0.0f; - restirDiParams_.visibilityPolicy = settings_.restirDiFinalVisibilityEnabled ? 1u : 0u; - restirDiParams_.mode = settings_.restirMode == RestirMode::HybridCompare || - settings_.restirDiMode == RestirDiMode::HybridCompare - ? static_cast(RestirDiMode::HybridCompare) - : static_cast(settings_.restirDiMode); - restirDiParams_.spatialResultValid = settings_.restirDiSpatialEnabled ? 1u : 0u; - restirDiParams_.visibilityRayBudget = settings_.restirDiVisibilityRayBudget; - restirDiParams_.historyValid = restirDiHistoryValid_ ? 1u : 0u; - restirDiParams_.materialVisibilityFlags = - (rayTracingScene_ != nullptr && rayTracingScene_->opacityMicromapStats().active) ? 1u : 0u; - restirDiParams_.counterEnabled = shouldCollectRestirCounters() ? 1u : 0u; - restirDiParams_.rawOutputIsCurrentSample = shouldRunDlssRayReconstruction() ? 1u : 0u; - } else { - restirDiParams_ = RestirDiParams{}; - } + restirDiParams_ = passes::RestirDIPass::makeParams( + settings_, + passes::RestirDIPass::ParameterInputs{ + .width = renderExtent_.width, + .height = renderExtent_.height, + .frameIndex = stochasticFrameIndex, + .lightVersion = lightVersionCounter_, + .environmentVersion = environmentVersionCounter_, + .estimatorRuns = shouldRunRestirDiEstimator(), + .temporalStageRuns = shouldRunRestirDiTemporal(), + .historyValid = restirDiHistoryValid_, + .materialVisibilityEnabled = rayTracingScene_ != nullptr && rayTracingScene_->opacityMicromapStats().active, + .counterEnabled = shouldCollectRestirCounters(), + .rawOutputIsCurrentSample = shouldRunDlssRayReconstruction(), + }); frameUniforms.write(&restirDiParams_, sizeof(restirDiParams_), kFrameRestirDiParamsOffset); frameUniforms.flush(sizeof(restirDiParams_), kFrameRestirDiParamsOffset); @@ -7116,7 +7507,7 @@ void PathTracerRenderer::updateCamera() { updateRegirParamsBuffer(); if (streamlineFrameActive_) { - const glm::mat4 currentNonJitteredViewProj = nonJitteredProjection * view; + const glm::mat4 currentNonJitteredViewProj = nonJitteredViewProj; const glm::mat4 previousNonJitteredViewProj = (temporalFrameIndex_ > 1u && !temporalCameraCut) ? (nrdViewToClip_ * nrdWorldToView_) : currentNonJitteredViewProj; @@ -7154,6 +7545,7 @@ void PathTracerRenderer::updateCamera() { } previousViewProj_ = viewProj; + previousNonJitteredViewProj_ = nonJitteredViewProj; nrdViewToClipPrev_ = nrdViewToClip_; nrdWorldToViewPrev_ = nrdWorldToView_; nrdViewToClip_ = nonJitteredProjection; @@ -7172,27 +7564,22 @@ void PathTracerRenderer::updateRegirParamsBuffer() { std::max(settings_.regirGridDimensions.z, 1u), }; const uint32_t reservoirsPerCell = std::max(settings_.regirReservoirsPerCell, 1u); - const VkDeviceSize cellCount = - static_cast(dims.x) * - static_cast(dims.y) * - static_cast(dims.z); - const bool wantsRegir = settings_.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR; - const bool wantsActiveGrid = wantsRegir && settings_.regirGridMode == RegirGridMode::Active; - const bool wantsHashGrid = wantsRegir && settings_.regirGridMode == RegirGridMode::Hash; - const VkDeviceSize storageCellCount = wantsHashGrid - ? static_cast(regirHashCellCapacity()) - : cellCount; + const bool wantsRegir = passes::RegirPass::isRequested(settings_); + const bool wantsActiveGrid = passes::RegirPass::activeGridMode(settings_, wantsRegir); + const bool wantsHashGrid = passes::RegirPass::hashGridActive(settings_, wantsRegir); const VkDeviceSize targetReservoirBytes = wantsRegir - ? storageCellCount * static_cast(reservoirsPerCell) * sizeof(ReGIRReservoirGpu) + ? passes::RegirPass::reservoirStorageByteSize(settings_, sizeof(ReGIRReservoirGpu)) : sizeof(ReGIRReservoirGpu); const VkDeviceSize targetEnvironmentReservoirBytes = wantsRegir && settings_.regirEnvironment - ? sizeof(ReGIREnvironmentReservoirGpu) * kRegirInfiniteLightBankSize + ? passes::RegirPass::environmentReservoirByteSize( + kRegirInfiniteLightBankSize, + sizeof(ReGIREnvironmentReservoirGpu)) : sizeof(ReGIREnvironmentReservoirGpu); const VkDeviceSize targetActiveCellBytes = wantsActiveGrid - ? (cellCount + 4ull) * sizeof(uint32_t) + ? passes::RegirPass::activeGridFeedbackByteSize(settings_) : sizeof(uint32_t) * 5u; const VkDeviceSize targetHashCellBytes = wantsHashGrid - ? (static_cast(regirHashCellCapacity()) + 4ull) * sizeof(uint32_t) + ? passes::RegirPass::hashGridFeedbackByteSize(regirHashCellCapacity()) : sizeof(uint32_t) * 5u; if (regirReservoirBuffer_.handle() == VK_NULL_HANDLE || regirReservoirBuffer_.size() != targetReservoirBytes) { @@ -7287,9 +7674,6 @@ void PathTracerRenderer::updateRegirParamsBuffer() { }); regirTemporalHistoryValid_ = false; } - const uint32_t regirTemporalHistoryFrames = settings_.regirTemporalReuse - ? std::max(settings_.regirTemporalHistory, 1u) - : 0u; const glm::uvec4 currentRegirVersion{ lightVersionCounter_, materialVersionCounter_, @@ -7303,32 +7687,21 @@ void PathTracerRenderer::updateRegirParamsBuffer() { regirHashRotationPending_ = false; regirTemporalHistoryVersion_ = currentRegirVersion; } - const ReGIRParams params{ - .gridDimensionsReservoirs = glm::uvec4(dims, reservoirsPerCell), - .controls = glm::uvec4( - (shouldUseRegir() ? 1u : 0u) | - (static_cast(settings_.regirGridMode) << 1u) | - (settings_.regirEnvironment ? 8u : 0u) | - (settings_.regirVisibilityReuse ? 16u : 0u), - std::max(settings_.regirCandidatesPerReservoir, 1u), - settings_.fixedSeed.has_value() ? *settings_.fixedSeed : temporalFrameIndex_, - regirTemporalHistoryFrames), - .gridPadding = glm::vec4( - std::max(settings_.regirGridPadding, 0.0f), - settings_.regirTemporalReuse ? 1.0f : 0.0f, - regirTemporalHistoryValid_ ? 1.0f : 0.0f, - static_cast(std::max(settings_.regirTemporalMaxM, 1u))), - .queryControls = glm::vec4( - std::clamp(settings_.regirCanonicalMix, 0.0f, 1.0f), - settings_.regirQueryMode == RegirQueryMode::Stochastic ? 1.0f : 0.0f, - settings_.regirSpatialReuse ? static_cast(std::clamp(settings_.regirSpatialRounds, 1u, 8u)) : 1.0f, - settings_.regirSpatialReuse ? 1.0f : 0.0f), - .environmentControls = glm::uvec4( - environmentVersionCounter_, - kRegirInfiniteLightBankSize, - kRegirEnvironmentBankSize, - kRegirSunBankSize), - }; + const ReGIRParams params = passes::RegirPass::makeParams( + settings_, + passes::RegirPass::ParameterInputs{ + .gridDimX = dims.x, + .gridDimY = dims.y, + .gridDimZ = dims.z, + .reservoirsPerCell = reservoirsPerCell, + .sampleFrameIndex = sampleFrameIndex(), + .environmentVersion = environmentVersionCounter_, + .infiniteLightBankSize = kRegirInfiniteLightBankSize, + .environmentBankSize = kRegirEnvironmentBankSize, + .sunBankSize = kRegirSunBankSize, + .regirRuns = shouldUseRegir(), + .temporalHistoryValid = regirTemporalHistoryValid_, + }); regirParamsBuffer_.write(¶ms, sizeof(params)); regirParamsBuffer_.flush(sizeof(params)); } @@ -7468,6 +7841,13 @@ RayTracingSceneBuildOptions PathTracerRenderer::makeRayTracingSceneBuildOptions( ommStats.totalTriangleCount >= 1'000'000u) { rayTracingBuildOptions.opacityMicromapsEnabled = false; } + if (rayTracingBuildOptions.opacityMicromapsEnabled && + settings_.restirGiEnabled && + settings_.restirGiMode == RestirGiMode::Production) { + // Production GI's full raygen currently exercises a driver/OMM traversal fault on + // split alpha scenes; keep the split software-alpha geometry path for GI stability. + rayTracingBuildOptions.opacityMicromapsEnabled = false; + } if (gpuSkinningResourcePlan_.initialComputeDispatchSubmitted && gpuSkinningCurrentVertexBuffer_.handle() != VK_NULL_HANDLE) { rayTracingBuildOptions.gpuSkinnedVertexBuffer = &gpuSkinningCurrentVertexBuffer_; @@ -7803,6 +8183,7 @@ void PathTracerRenderer::updateGpuSkinningOutputReadbackValidation() { void PathTracerRenderer::recordPostTraceCompute(VkCommandBuffer commandBuffer, bool deferHistoryCopy) { const bool runDlssRayReconstruction = shouldRunDlssRayReconstruction(); + bool ranDlss = false; if (shouldRunAdaptiveSamplingFill()) { recordAdaptiveSamplingFill(commandBuffer); } @@ -7832,6 +8213,7 @@ void PathTracerRenderer::recordPostTraceCompute(VkCommandBuffer commandBuffer, b if (!runDlssRayReconstruction) { if (shouldRunDlss()) { recordDlss(commandBuffer); + ranDlss = true; } else if (shouldRunTaa()) { recordTaa(commandBuffer, deferHistoryCopy); } @@ -7839,7 +8221,11 @@ void PathTracerRenderer::recordPostTraceCompute(VkCommandBuffer commandBuffer, b if (settings_.autoExposureEnabled) { recordAutoExposure(commandBuffer); } - recordToneMap(commandBuffer); + if (isDlssGuideDebugView() && (runDlssRayReconstruction || ranDlss)) { + recordDlssGuideVisualization(commandBuffer); + } else { + recordToneMap(commandBuffer); + } recordSelectionOutline(commandBuffer); rotateRealtimeHistoryResources(); } @@ -7954,6 +8340,9 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { } const RenderGraphResourceId velocity = graph.createBuffer(bufferResource(velocityBuffer_, "screen velocity")); const RenderGraphResourceId pathData = graph.createBuffer(bufferResource(pathDataBuffer_, "path data")); + const RenderGraphResourceId psrGuides = graph.createBuffer(bufferResource(psrGuideBuffer_, "primary surface replacement guides")); + const RenderGraphResourceId psrGuideSignatures = graph.createBuffer(bufferResource(psrGuideSignatureBuffer_, "primary surface replacement signatures")); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(bufferResource(previousPsrGuideSignatureBuffer_, "previous primary surface replacement signatures")); const RenderGraphResourceId historyLength = graph.createTexture(imageResource(historyLengthImage_, "adaptive history length")); const RenderGraphResourceId adaptiveSamplingDebug = graph.createTexture(imageResource(adaptiveSamplingDebugImage_, "adaptive sampling debug")); const RenderGraphResourceId adaptiveSamplingFilledMask = graph.createTexture(imageResource(adaptiveSamplingFilledMaskImage_, "adaptive sampling filled mask")); @@ -7990,6 +8379,11 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { const RenderGraphResourceId regirSpatialReservoir = graph.createBuffer(bufferResource(regirSpatialReservoirBuffer_, "ReGIR spatial reservoirs")); const RenderGraphResourceId regirTemporalReservoir = graph.createBuffer(bufferResource(regirTemporalReservoirBuffer_, "ReGIR temporal reservoirs")); const RenderGraphResourceId regirPreviousReservoir = graph.createBuffer(bufferResource(regirPreviousReservoirBuffer_, "ReGIR previous reservoirs")); + graph.resources()[regirPreviousReservoir.index].hasInitialAccess = true; + graph.resources()[regirPreviousReservoir.index].initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .access = VK_ACCESS_2_TRANSFER_WRITE_BIT, + }; const RenderGraphResourceId regirActiveCells = graph.createBuffer(bufferResource(regirActiveCellBuffer_, "ReGIR active cells")); const RenderGraphResourceId regirActiveCellsReadback = graph.createBuffer(bufferResource(regirActiveCellReadbackBuffer_, "ReGIR active cells readback")); const RenderGraphResourceId regirHashCurrentCells = graph.createBuffer(bufferResource(regirHashCurrentCellBuffer_, "ReGIR current hash cells")); @@ -7997,6 +8391,8 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { const RenderGraphResourceId restirReservoir = graph.createBuffer(bufferResource(restirReservoirBuffer_, "restir reservoir")); RenderGraphResourceId wavefrontRestirReservoir{}; const RenderGraphResourceId previousRestirReservoir = graph.createBuffer(bufferResource(previousRestirReservoirBuffer_, "previous restir reservoir")); + graph.resources()[previousRestirReservoir.index].hasInitialAccess = true; + graph.resources()[previousRestirReservoir.index].initialAccess = persistentRestirHistoryAccess(); const bool resetRestirHistory = settings_.restirMode != RestirMode::ClassicNee && ((temporalSystem_ != nullptr && temporalSystem_->isCameraCut()) || @@ -8008,6 +8404,9 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { restirDiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && restirDiSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE && restirDiFinalOutputBuffer().handle() != VK_NULL_HANDLE && + restirDiTemporalSourcePixelBuffer_.handle() != VK_NULL_HANDLE && + restirDiSpatialSourcePixelBuffer_.handle() != VK_NULL_HANDLE && + restirDiFinalSourcePixelBuffer_.handle() != VK_NULL_HANDLE && restirDiPreviousHistoryReservoirBuffer().handle() != VK_NULL_HANDLE && restirDiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE && restirDiCountersBuffer_.handle() != VK_NULL_HANDLE && @@ -8114,6 +8513,9 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { RenderGraphResourceId restirDiTemporal{}; RenderGraphResourceId restirDiSpatial{}; RenderGraphResourceId restirDiFinal{}; + RenderGraphResourceId restirDiTemporalSourcePixel{}; + RenderGraphResourceId restirDiSpatialSourcePixel{}; + RenderGraphResourceId restirDiFinalSourcePixel{}; RenderGraphResourceId restirDiPrevious{}; RenderGraphResourceId restirDiCounters{}; RenderGraphResourceId restirDiCountersReadback{}; @@ -8126,6 +8528,9 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { restirDiFinal = shouldAliasRestirDiFinal() ? restirDiTemporal : graph.createBuffer(bufferResource(restirDiCurrentHistoryReservoirBuffer(), "restir di final reservoir")); + restirDiTemporalSourcePixel = graph.createBuffer(bufferResource(restirDiTemporalSourcePixelBuffer_, "restir di temporal source pixel")); + restirDiSpatialSourcePixel = graph.createBuffer(bufferResource(restirDiSpatialSourcePixelBuffer_, "restir di spatial source pixel")); + restirDiFinalSourcePixel = graph.createBuffer(bufferResource(restirDiFinalSourcePixelBuffer_, "restir di final source pixel")); restirDiPrevious = graph.createBuffer(bufferResource(restirDiPreviousHistoryReservoirBuffer(), "restir di previous reservoir")); restirDiCounters = graph.createBuffer(bufferResource(restirDiCountersBuffer_, "restir di counters")); restirDiCountersReadback = graph.createBuffer(bufferResource(restirDiCountersReadbackBuffer_, "restir di counters readback")); @@ -8136,10 +8541,12 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { if (resetRestirHistory) { graph.addPass("restir_history_clear") .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer) + .addStorageWrite(previousPsrGuideSignatures, PipelineDomain::Transfer) .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { validationLog_.recordPass("restir history clear"); currentProfiler_->write(cmd, GpuProfiler::RestirHistoryClearStart, VK_PIPELINE_STAGE_2_TRANSFER_BIT); vkCmdFillBuffer(cmd, previousRestirReservoirBuffer_.handle(), 0, previousRestirReservoirBuffer_.size(), 0u); + vkCmdFillBuffer(cmd, previousPsrGuideSignatureBuffer_.handle(), 0, previousPsrGuideSignatureBuffer_.size(), 0u); currentProfiler_->write(cmd, GpuProfiler::RestirHistoryClearEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); }); } @@ -8154,12 +8561,31 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { restirGiClearPass.setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { validationLog_.recordPass("restir gi reservoir clear"); currentProfiler_->write(cmd, GpuProfiler::RestirGiClearStart, VK_PIPELINE_STAGE_2_TRANSFER_BIT); - vkCmdFillBuffer(cmd, previousRestirGiReservoirBuffer_.handle(), 0, previousRestirGiReservoirBuffer_.size(), 0u); - if (previousRestirGiProductionReservoirBuffer_.handle() != VK_NULL_HANDLE) { - vkCmdFillBuffer(cmd, previousRestirGiProductionReservoirBuffer_.handle(), 0, previousRestirGiProductionReservoirBuffer_.size(), 0u); + struct FillRange { + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceSize size = 0; + }; + std::array uniqueFills{}; + size_t uniqueFillCount = 0; + auto addFill = [&](const Buffer& buffer) { + if (buffer.handle() == VK_NULL_HANDLE || buffer.size() == 0) { + return; + } + for (size_t i = 0; i < uniqueFillCount; ++i) { + if (uniqueFills[i].buffer == buffer.handle()) { + uniqueFills[i].size = std::max(uniqueFills[i].size, buffer.size()); + return; + } + } + uniqueFills[uniqueFillCount++] = FillRange{buffer.handle(), buffer.size()}; + }; + addFill(previousRestirGiReservoirBuffer_); + addFill(previousRestirGiProductionReservoirBuffer_); + addFill(restirGiSpatialReservoirBuffer_); + addFill(previousRestirGiReceiverBuffer_); + for (size_t i = 0; i < uniqueFillCount; ++i) { + vkCmdFillBuffer(cmd, uniqueFills[i].buffer, 0, uniqueFills[i].size, 0u); } - vkCmdFillBuffer(cmd, restirGiSpatialReservoirBuffer_.handle(), 0, restirGiSpatialReservoirBuffer_.size(), 0u); - vkCmdFillBuffer(cmd, previousRestirGiReceiverBuffer_.handle(), 0, previousRestirGiReceiverBuffer_.size(), 0u); currentProfiler_->write(cmd, GpuProfiler::RestirGiClearEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); }); } @@ -8409,6 +8835,8 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { .addStorageWrite(worldPosition, traceDomain) .addStorageWrite(velocity, traceDomain) .addStorageWrite(pathData, traceDomain) + .addStorageWrite(psrGuides, traceDomain) + .addStorageWrite(psrGuideSignatures, traceDomain) .addStorageWrite(rayTracingDiagnosticCounters, traceDomain) .addStorageWrite(rayTracingAlphaMaterialCounters, traceDomain) .addStorageRead(previousRestirReservoir, traceDomain) @@ -8480,6 +8908,9 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { if (useNewRestirDi) { RenderGraphResourceId finalInput = restirDiInitial; RenderGraphResourceId temporalDebugInput = restirDiInitial; + RenderGraphResourceId sourceInput = restirDiTemporalSourcePixel; + const bool temporalSourceProduced = shouldRunRestirDiTemporal(); + bool sourceInputProduced = false; if (shouldRunRestirDiTemporal()) { graph.addPass("restir_di_temporal") .addStorageRead(restirDiInitial, PipelineDomain::Compute) @@ -8491,24 +8922,34 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { .addUniformBuffer(sceneMeshParams, PipelineDomain::Compute) .addStorageReadWrite(restirDiCounters, PipelineDomain::Compute) .addStorageWrite(restirDiTemporal, PipelineDomain::Compute) + .addStorageWrite(restirDiTemporalSourcePixel, PipelineDomain::Compute) .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { recordRestirDiTemporalPass(cmd); }); finalInput = restirDiTemporal; temporalDebugInput = restirDiTemporal; + sourceInput = restirDiTemporalSourcePixel; + sourceInputProduced = true; } if (shouldRunRestirDiSpatial()) { - graph.addPass("restir_di_spatial") + RenderGraphPass& spatialPass = graph.addPass("restir_di_spatial"); + spatialPass .addStorageRead(finalInput, PipelineDomain::Compute) .addStorageRead(restirDiReceiver, PipelineDomain::Compute) .addStorageRead(sceneLightRecords, PipelineDomain::Compute) .addUniformBuffer(sceneMeshParams, PipelineDomain::Compute) .addStorageReadWrite(restirDiCounters, PipelineDomain::Compute) .addStorageWrite(restirDiSpatial, PipelineDomain::Compute) - .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { - recordRestirDiSpatialPass(cmd); - }); + .addStorageWrite(restirDiSpatialSourcePixel, PipelineDomain::Compute); + if (temporalSourceProduced) { + spatialPass.addStorageRead(sourceInput, PipelineDomain::Compute); + } + spatialPass.setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { + recordRestirDiSpatialPass(cmd); + }); finalInput = restirDiSpatial; + sourceInput = restirDiSpatialSourcePixel; + sourceInputProduced = true; } if (shouldRunRestirDiFinal()) { RenderGraphPass& finalPass = graph.addPass("restir_di_final"); @@ -8522,7 +8963,11 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { .addStorageReadWrite(raw, PipelineDomain::Compute) .addStorageReadWrite(pathData, PipelineDomain::Compute) .addStorageReadWrite(accumulation, PipelineDomain::Compute) - .addStorageReadWrite(variance, PipelineDomain::Compute); + .addStorageReadWrite(variance, PipelineDomain::Compute) + .addStorageWrite(restirDiFinalSourcePixel, PipelineDomain::Compute); + if (sourceInputProduced) { + finalPass.addStorageRead(sourceInput, PipelineDomain::Compute); + } if (shouldAliasRestirDiFinal()) { finalPass.addStorageReadWrite(restirDiFinal, PipelineDomain::Compute); } else { @@ -8662,7 +9107,10 @@ void PathTracerRenderer::recordPathTraceGraph(VkCommandBuffer commandBuffer) { const RenderGraphResourceId finalCurrentReservoir = restirGiTemporalReservoir; const RenderGraphResourceId finalSpatialReservoir = shouldRunRestirGiUpsample() ? restirGiUpsampledReservoir - : (shouldRunRestirGiSpatialStage() ? restirGiProductionReservoir : restirGiTemporalReservoir); + : passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiProductionReservoir, + restirGiTemporalReservoir); RenderGraphPass& restirGiFinalProdPass = graph.addPass("restir_gi_final_production"); restirGiFinalProdPass .addStorageReadWrite(raw, PipelineDomain::Compute) @@ -9084,7 +9532,7 @@ void PathTracerRenderer::recordWavefrontQueueClearPass(VkCommandBuffer commandBu .shadowCapacity = wavefrontShadowQueueCapacity_, .pixelCapacity = wavefrontPixelStateCapacity_, .maxPathDepth = wavefrontMaxPathDepth(), - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .validationValue = kWavefrontQueueClearValidationValue, .flags = 0u, }; @@ -9163,7 +9611,7 @@ void PathTracerRenderer::recordWavefrontPrimaryGeneratePass(VkCommandBuffer comm const WavefrontPrimaryGeneratePush push{ .width = renderExtent_.width, .height = renderExtent_.height, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .flags = 0u, .cameraCut = cameraCut ? 1u : 0u, }; @@ -9311,7 +9759,7 @@ void PathTracerRenderer::recordWavefrontShadePass( const WavefrontShadePush push{ .width = renderExtent_.width, .height = renderExtent_.height, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .maxDepth = wavefrontMaxPathDepth(), .flags = flags, }; @@ -9489,7 +9937,7 @@ void PathTracerRenderer::recordWavefrontCompactPass(VkCommandBuffer commandBuffe .pixelCapacity = wavefrontPixelStateCapacity_, .mode = 0u, .maxPathDepth = wavefrontMaxPathDepth(), - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .flags = 0u, }; @@ -9644,7 +10092,7 @@ void PathTracerRenderer::recordWavefrontSortPass(VkCommandBuffer commandBuffer) .pixelCapacity = wavefrontPixelStateCapacity_, .mode = 0u, .bucketCount = kWavefrontSortBucketCount, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .flags = 0u, }; auto dispatchMode = [&](uint32_t mode, uint32_t groupCount) { @@ -10026,40 +10474,45 @@ void PathTracerRenderer::recordRestirGiTemporalPass(VkCommandBuffer commandBuffe currentProfiler_->write(commandBuffer, GpuProfiler::RestirGiTemporalStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); DescriptorSet set = currentFrame_->descriptors().allocate(restirGiTemporalSetLayout_); - DescriptorWriter() + DescriptorWriter writer; + writer .writeBuffer(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, initialReservoir.descriptorInfo()) .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiTemporalReservoirBuffer_.descriptorInfo()) .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiPreviousProductionHistoryBuffer().descriptorInfo()) .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCurrentReceiverBuffer().descriptorInfo()) .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiPreviousReceiverBuffer().descriptorInfo()) .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) + passes::RestirGIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirGIPass::counterSlotByteSize())) .writeBuffer(6, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()) - .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(33, rayTracingScene_->tlas()) - .update(context_.device(), set); + .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()); + passes::RestirGIPass::writeSceneDescriptors(writer, passes::RestirGIPass::SceneDescriptorBindings{ + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); + writer.update(context_.device(), set); const RestirGiTemporalPush push{ .width = renderExtent_.width, .height = renderExtent_.height, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .temporalMaxAge = settings_.restirGiTemporalMaxAge, .depthThresholdScale = settings_.restirGiDepthThresholdScale, .visibilityRayBudget = settings_.restirGiVisibilityRayBudget, .giHalfResolution = effectiveRestirGiHalfResolution() ? 1u : 0u, .enabled = (shouldUseNewRestirGi() ? 1u : 0u) | (shouldCollectRestirCounters() ? 4u : 0u) | - (!shouldRunRestirGiSpatialStage() ? 8u : 0u) | + (passes::RestirGIPass::usesTemporalReservoirAsProductionHistorySource( + shouldRunRestirGiSpatialStage()) ? 8u : 0u) | (shouldUseRestirGiActiveTileMask() ? 16u : 0u), .cameraPosition = glm::vec4(glm::vec3(camera_.pos), 1.0f), .reprojectionMatrix = glm::mat4(1.0f), @@ -10084,30 +10537,34 @@ void PathTracerRenderer::recordRestirGiSpatialProdPass(VkCommandBuffer commandBu currentProfiler_->write(commandBuffer, GpuProfiler::RestirGiSpatialStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); DescriptorSet set = currentFrame_->descriptors().allocate(restirGiSpatialProdSetLayout_); - DescriptorWriter() + DescriptorWriter writer; + writer .writeBuffer(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiTemporalReservoirBuffer_.descriptorInfo()) .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCurrentProductionHistoryBuffer().descriptorInfo()) .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCurrentReceiverBuffer().descriptorInfo()) .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) - .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(33, rayTracingScene_->tlas()) - .update(context_.device(), set); + passes::RestirGIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirGIPass::counterSlotByteSize())) + .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()); + passes::RestirGIPass::writeSceneDescriptors(writer, passes::RestirGIPass::SceneDescriptorBindings{ + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); + writer.update(context_.device(), set); const RestirGiSpatialProdPush push{ .width = renderExtent_.width, .height = renderExtent_.height, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .enabled = (shouldUseNewRestirGi() ? 1u : 0u) | (shouldCollectRestirCounters() ? 4u : 0u) | (shouldUseRestirGiActiveTileMask() ? 8u : 0u), @@ -10151,7 +10608,8 @@ void PathTracerRenderer::recordRestirGiFinalProdPass(VkCommandBuffer commandBuff : restirGiTemporalReservoirBuffer_); DescriptorSet set = currentFrame_->descriptors().allocate(restirGiFinalProdSetLayout_); - DescriptorWriter() + DescriptorWriter writer; + writer .writeImage(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, rawImage_.storageDescriptor()) .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, currentInput.descriptorInfo()) .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, spatialInput.descriptorInfo()) @@ -10160,20 +10618,23 @@ void PathTracerRenderer::recordRestirGiFinalProdPass(VkCommandBuffer commandBuff .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()) .writeBuffer(6, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, velocityBuffer_.descriptorInfo()) .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) - .writeBuffer(8, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(33, rayTracingScene_->tlas()) - .update(context_.device(), set); + passes::RestirGIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirGIPass::counterSlotByteSize())) + .writeBuffer(8, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()); + passes::RestirGIPass::writeSceneDescriptors(writer, passes::RestirGIPass::SceneDescriptorBindings{ + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); + writer.update(context_.device(), set); const uint32_t enabledFlags = (shouldUseNewRestirGi() ? 1u : 0u) | @@ -10183,7 +10644,7 @@ void PathTracerRenderer::recordRestirGiFinalProdPass(VkCommandBuffer commandBuff const RestirGiFinalProdPush push{ .width = renderExtent_.width, .height = renderExtent_.height, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .enabled = enabledFlags, .debugView = static_cast(settings_.debugView), .fireflyClamp = settings_.fireflyClamp, @@ -10223,8 +10684,8 @@ void PathTracerRenderer::recordRestirGiUpsamplePass(VkCommandBuffer commandBuffe .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCurrentReceiverBuffer().descriptorInfo()) .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiTemporalReservoirBuffer_.descriptorInfo()) .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) + passes::RestirGIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirGIPass::counterSlotByteSize())) .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiActiveTileMaskBuffer_.descriptorInfo()) .update(context_.device(), set); @@ -10233,7 +10694,7 @@ void PathTracerRenderer::recordRestirGiUpsamplePass(VkCommandBuffer commandBuffe .fullHeight = renderExtent_.height, .halfWidth = (renderExtent_.width + 1u) / 2u, .halfHeight = (renderExtent_.height + 1u) / 2u, - .frameIndex = temporalFrameIndex_, + .frameIndex = sampleFrameIndex(), .depthThresholdScale = settings_.restirGiDepthThresholdScale, .normalThreshold = 1.0f - settings_.restirGiSpatialCompatibilityThreshold, .flags = 1u | (shouldCollectRestirCounters() ? 2u : 0u) | @@ -10273,6 +10734,7 @@ void PathTracerRenderer::recordRestirDiTemporal(VkCommandBuffer commandBuffer) { const auto previousReceiverRes = graph.createBuffer(bufferResource(restirDiPreviousReceiverBuffer(), "restir di previous receiver")); const auto velocityRes = graph.createBuffer(bufferResource(velocityBuffer_, "velocity")); const auto temporalRes = graph.createBuffer(bufferResource(restirDiTemporalReservoirBuffer_, "restir di temporal")); + const auto temporalSourceRes = graph.createBuffer(bufferResource(restirDiTemporalSourcePixelBuffer_, "restir di temporal source pixel")); const auto countersRes = graph.createBuffer(bufferResource(restirDiCountersBuffer_, "restir di counters")); graph.addPass("restir_di_temporal") @@ -10282,6 +10744,7 @@ void PathTracerRenderer::recordRestirDiTemporal(VkCommandBuffer commandBuffer) { .addStorageRead(previousReceiverRes, PipelineDomain::Compute) .addStorageRead(velocityRes, PipelineDomain::Compute) .addStorageWrite(temporalRes, PipelineDomain::Compute) + .addStorageWrite(temporalSourceRes, PipelineDomain::Compute) .addStorageReadWrite(countersRes, PipelineDomain::Compute) .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { recordRestirDiTemporalPass(cmd); @@ -10308,22 +10771,25 @@ void PathTracerRenderer::recordRestirDiTemporalPass(VkCommandBuffer commandBuffe .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, velocityBuffer_.descriptorInfo()) .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiTemporalReservoirBuffer_.descriptorInfo()) .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) + passes::RestirDIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirDIPass::counterSlotByteSize())) .writeBuffer(6, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameRestirDiParamsOffset, sizeof(RestirDiParams))) .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiPreviousReceiverBuffer().descriptorInfo()) - .writeBuffer(12, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightRecords().descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(10, rayTracingScene_->tlas()); + .writeBuffer(24, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiTemporalSourcePixelBuffer_.descriptorInfo()); + passes::RestirDIPass::writeSceneDescriptors(writer, passes::RestirDIPass::SceneDescriptorBindings{ + .lightRecords = scene_.lightRecords().descriptorInfo(), + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); writeStbnDescriptors(writer); writer.update(context_.device(), set); @@ -10349,19 +10815,29 @@ void PathTracerRenderer::recordRestirDiSpatial(VkCommandBuffer commandBuffer) { .debugName = name, }; }; - const auto temporalRes = graph.createBuffer(bufferResource(restirDiTemporalReservoirBuffer_, "restir di temporal in")); + const Buffer& spatialInputBuffer = shouldRunRestirDiTemporal() + ? restirDiTemporalReservoirBuffer_ + : restirDiInitialReservoirBuffer_; + const auto temporalRes = graph.createBuffer(bufferResource(spatialInputBuffer, "restir di spatial input")); + const auto temporalSourceRes = graph.createBuffer(bufferResource(restirDiTemporalSourcePixelBuffer_, "restir di temporal source pixel")); const auto receiverRes = graph.createBuffer(bufferResource(restirDiCurrentReceiverBuffer(), "restir di receiver")); const auto spatialRes = graph.createBuffer(bufferResource(restirDiSpatialReservoirBuffer_, "restir di spatial")); + const auto spatialSourceRes = graph.createBuffer(bufferResource(restirDiSpatialSourcePixelBuffer_, "restir di spatial source pixel")); const auto countersRes = graph.createBuffer(bufferResource(restirDiCountersBuffer_, "restir di counters")); - graph.addPass("restir_di_spatial") + RenderGraphPass& spatialPass = graph.addPass("restir_di_spatial"); + spatialPass .addStorageRead(temporalRes, PipelineDomain::Compute) .addStorageRead(receiverRes, PipelineDomain::Compute) .addStorageWrite(spatialRes, PipelineDomain::Compute) - .addStorageReadWrite(countersRes, PipelineDomain::Compute) - .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { - recordRestirDiSpatialPass(cmd); - }); + .addStorageWrite(spatialSourceRes, PipelineDomain::Compute) + .addStorageReadWrite(countersRes, PipelineDomain::Compute); + if (shouldRunRestirDiTemporal()) { + spatialPass.addStorageRead(temporalSourceRes, PipelineDomain::Compute); + } + spatialPass.setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { + recordRestirDiSpatialPass(cmd); + }); graph.compile(); graph.execute(commandBuffer, temporalFrameIndex_); } @@ -10385,21 +10861,25 @@ void PathTracerRenderer::recordRestirDiSpatialPass(VkCommandBuffer commandBuffer .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiCurrentReceiverBuffer().descriptorInfo()) .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiSpatialReservoirBuffer_.descriptorInfo()) .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) + passes::RestirDIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirDIPass::counterSlotByteSize())) .writeBuffer(4, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameRestirDiParamsOffset, sizeof(RestirDiParams))) - .writeBuffer(12, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightRecords().descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(10, rayTracingScene_->tlas()); + .writeBuffer(24, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiTemporalSourcePixelBuffer_.descriptorInfo()) + .writeBuffer(25, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiSpatialSourcePixelBuffer_.descriptorInfo()); + passes::RestirDIPass::writeSceneDescriptors(writer, passes::RestirDIPass::SceneDescriptorBindings{ + .lightRecords = scene_.lightRecords().descriptorInfo(), + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); writeStbnDescriptors(writer); writer.update(context_.device(), set); @@ -10428,6 +10908,9 @@ void PathTracerRenderer::recordRestirDiFinal(VkCommandBuffer commandBuffer) { const Buffer& finalInputBuffer = shouldRunRestirDiSpatial() ? restirDiSpatialReservoirBuffer_ : (shouldRunRestirDiTemporal() ? restirDiTemporalReservoirBuffer_ : restirDiInitialReservoirBuffer_); + const Buffer& finalSourceInputBuffer = shouldRunRestirDiSpatial() + ? restirDiSpatialSourcePixelBuffer_ + : restirDiTemporalSourcePixelBuffer_; const Buffer& temporalDebugBuffer = shouldRunRestirDiTemporal() ? restirDiTemporalReservoirBuffer_ : restirDiInitialReservoirBuffer_; @@ -10435,6 +10918,8 @@ void PathTracerRenderer::recordRestirDiFinal(VkCommandBuffer commandBuffer) { const auto receiverRes = graph.createBuffer(bufferResource(restirDiCurrentReceiverBuffer(), "restir di receiver")); const auto initialRes = graph.createBuffer(bufferResource(restirDiInitialReservoirBuffer_, "restir di initial")); const auto temporalRes = graph.createBuffer(bufferResource(temporalDebugBuffer, "restir di temporal debug")); + const auto sourceInputRes = graph.createBuffer(bufferResource(finalSourceInputBuffer, "restir di selected source pixel")); + const auto finalSourceRes = graph.createBuffer(bufferResource(restirDiFinalSourcePixelBuffer_, "restir di final source pixel")); const Buffer& finalOutputBuffer = restirDiFinalOutputBuffer(); const auto finalRes = shouldAliasRestirDiFinal() ? temporalRes @@ -10466,13 +10951,15 @@ void PathTracerRenderer::recordRestirDiFinal(VkCommandBuffer commandBuffer) { RenderGraphPass& finalPass = graph.addPass("restir_di_final"); finalPass .addStorageRead(spatialRes, PipelineDomain::Compute) + .addStorageRead(sourceInputRes, PipelineDomain::Compute) .addStorageRead(receiverRes, PipelineDomain::Compute) .addStorageRead(initialRes, PipelineDomain::Compute) .addStorageReadWrite(countersRes, PipelineDomain::Compute) .addStorageReadWrite(rawTex, PipelineDomain::Compute) .addStorageReadWrite(pathDataRes, PipelineDomain::Compute) .addStorageReadWrite(accumulationRes, PipelineDomain::Compute) - .addStorageReadWrite(varianceRes, PipelineDomain::Compute); + .addStorageReadWrite(varianceRes, PipelineDomain::Compute) + .addStorageWrite(finalSourceRes, PipelineDomain::Compute); if (shouldAliasRestirDiFinal()) { finalPass.addStorageReadWrite(finalRes, PipelineDomain::Compute); } else { @@ -10501,6 +10988,9 @@ void PathTracerRenderer::recordRestirDiFinalPass(VkCommandBuffer commandBuffer) const Buffer& finalInputBuffer = shouldRunRestirDiSpatial() ? restirDiSpatialReservoirBuffer_ : (shouldRunRestirDiTemporal() ? restirDiTemporalReservoirBuffer_ : restirDiInitialReservoirBuffer_); + const Buffer& finalSourceInputBuffer = shouldRunRestirDiSpatial() + ? restirDiSpatialSourcePixelBuffer_ + : restirDiTemporalSourcePixelBuffer_; const Buffer& temporalDebugBuffer = shouldRunRestirDiTemporal() ? restirDiTemporalReservoirBuffer_ : restirDiInitialReservoirBuffer_; @@ -10511,26 +11001,30 @@ void PathTracerRenderer::recordRestirDiFinalPass(VkCommandBuffer commandBuffer) .writeImage(3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, rawImage_.storageDescriptor()) .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiCurrentHistoryReservoirBuffer().descriptorInfo()) .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiCountersBuffer_.descriptorInfo( - (temporalFrameIndex_ % kRendererFramesInFlight) * sizeof(uint32_t) * 64u, - sizeof(uint32_t) * 64u)) + passes::RestirDIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight), + passes::RestirDIPass::counterSlotByteSize())) .writeBuffer(6, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameRestirDiParamsOffset, sizeof(RestirDiParams))) .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()) .writeBuffer(8, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, accumulationBuffer_.descriptorInfo()) .writeBuffer(9, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameDebugParamsOffset, sizeof(RendererDebugParams))) .writeBuffer(11, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, temporalDebugBuffer.descriptorInfo()) - .writeBuffer(12, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightRecords().descriptorInfo()) - .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) .writeBuffer(14, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, varianceBuffer_.descriptorInfo()) - .writeBuffer(15, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.materials().descriptorInfo()) - .writeBuffer(16, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.rtTriangleMaterialIds().descriptorInfo()) - .writeBuffer(17, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.instanceRecords().descriptorInfo()) - .writeBuffer(18, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.meshRecords().descriptorInfo()) - .writeBuffer(19, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localVertices().descriptorInfo()) - .writeBuffer(20, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.localIndices().descriptorInfo()) - .writeBuffer(21, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo()) - .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo()) - .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo()) - .writeAccelerationStructure(10, rayTracingScene_->tlas()); + .writeBuffer(24, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, finalSourceInputBuffer.descriptorInfo()) + .writeBuffer(25, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirDiFinalSourcePixelBuffer_.descriptorInfo()); + passes::RestirDIPass::writeSceneDescriptors(writer, passes::RestirDIPass::SceneDescriptorBindings{ + .lightRecords = scene_.lightRecords().descriptorInfo(), + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .materials = scene_.materials().descriptorInfo(), + .rtTriangleMaterialIds = scene_.rtTriangleMaterialIds().descriptorInfo(), + .instanceRecords = scene_.instanceRecords().descriptorInfo(), + .meshRecords = scene_.meshRecords().descriptorInfo(), + .localVertices = scene_.localVertices().descriptorInfo(), + .localIndices = scene_.localIndices().descriptorInfo(), + .geometryTriangleOffsets = rayTracingScene_->geometryTriangleOffsetsBuffer().descriptorInfo(), + .meshGeometryRanges = rayTracingScene_->meshGeometryRangesBuffer().descriptorInfo(), + .tlasGeometryRanges = rayTracingScene_->tlasGeometryRangesBuffer().descriptorInfo(), + .tlas = rayTracingScene_->tlas(), + }); writer.update(context_.device(), set); pipeline->bind(commandBuffer); @@ -10655,8 +11149,8 @@ void PathTracerRenderer::recordRestirDiCountersReadback(VkCommandBuffer commandB restirDiCountersReadbackBuffer_.handle() == VK_NULL_HANDLE) return; currentProfiler_->write(commandBuffer, GpuProfiler::RestirDiCountersReadbackStart, VK_PIPELINE_STAGE_2_COPY_BIT); VkBufferCopy copy{}; - copy.size = sizeof(uint32_t) * 64u; - copy.srcOffset = (temporalFrameIndex_ % kRendererFramesInFlight) * copy.size; + copy.size = passes::RestirDIPass::counterSlotByteSize(); + copy.srcOffset = passes::RestirDIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight); copy.dstOffset = copy.srcOffset; vkCmdCopyBuffer(commandBuffer, restirDiCountersBuffer_.handle(), restirDiCountersReadbackBuffer_.handle(), 1, ©); currentProfiler_->write(commandBuffer, GpuProfiler::RestirDiCountersReadbackEnd, VK_PIPELINE_STAGE_2_COPY_BIT); @@ -10668,15 +11162,15 @@ void PathTracerRenderer::recordRestirGiCountersReadback(VkCommandBuffer commandB restirGiCountersReadbackBuffer_.handle() == VK_NULL_HANDLE) return; currentProfiler_->write(commandBuffer, GpuProfiler::RestirGiCountersReadbackStart, VK_PIPELINE_STAGE_2_COPY_BIT); VkBufferCopy copy{}; - copy.size = sizeof(uint32_t) * 64u; - copy.srcOffset = (temporalFrameIndex_ % kRendererFramesInFlight) * copy.size; + copy.size = passes::RestirGIPass::counterSlotByteSize(); + copy.srcOffset = passes::RestirGIPass::counterSlotByteOffset(temporalFrameIndex_, kRendererFramesInFlight); copy.dstOffset = copy.srcOffset; vkCmdCopyBuffer(commandBuffer, restirGiCountersBuffer_.handle(), restirGiCountersReadbackBuffer_.handle(), 1, ©); currentProfiler_->write(commandBuffer, GpuProfiler::RestirGiCountersReadbackEnd, VK_PIPELINE_STAGE_2_COPY_BIT); } void PathTracerRenderer::recordHeightFog(VkCommandBuffer commandBuffer) { - if (settings_.wavefrontFinalOutputEnabled) { + if (!settings_.pathTracingEnabled || !settings_.heightFogEnabled || settings_.wavefrontFinalOutputEnabled) { return; } if (fogPipeline_ == nullptr || fogSetLayout_ == VK_NULL_HANDLE || rawImage_.handle() == VK_NULL_HANDLE || depthNormalBuffer_.handle() == VK_NULL_HANDLE) { @@ -10813,6 +11307,9 @@ void PathTracerRenderer::recordRenderGraphPlan() { const RenderGraphResourceId previousWorldPosition = graph.createBuffer(bufferResource(previousWorldPositionBuffer_, "previous world position")); const RenderGraphResourceId velocity = graph.createBuffer(bufferResource(velocityBuffer_, "screen velocity")); const RenderGraphResourceId pathData = graph.createBuffer(bufferResource(pathDataBuffer_, "path data")); + const RenderGraphResourceId psrGuides = graph.createBuffer(bufferResource(psrGuideBuffer_, "primary surface replacement guides")); + const RenderGraphResourceId psrGuideSignatures = graph.createBuffer(bufferResource(psrGuideSignatureBuffer_, "primary surface replacement signatures")); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(bufferResource(previousPsrGuideSignatureBuffer_, "previous primary surface replacement signatures")); const RenderGraphResourceId adaptiveSamplingDensity = graph.createBuffer(bufferResource(adaptiveSamplingDensityBuffer_, "adaptive sampling density")); const RenderGraphResourceId adaptiveSamplingSampleCount = graph.createBuffer(bufferResource(adaptiveSamplingSampleCountBuffer_, "adaptive sampling sample count")); const RenderGraphResourceId adaptiveSamplingStats = graph.createBuffer(bufferResource(adaptiveSamplingStatsBuffer_, "adaptive sampling stats")); @@ -10839,8 +11336,17 @@ void PathTracerRenderer::recordRenderGraphPlan() { RenderGraphResourceId nrdViewZ{}; RenderGraphResourceId nrdDiffIn{}; RenderGraphResourceId nrdSpecIn{}; + RenderGraphResourceId nrdDiffConfidence{}; + RenderGraphResourceId nrdSpecConfidence{}; + RenderGraphResourceId nrdCurrentLightingLuminance{}; + RenderGraphResourceId nrdPreviousLightingLuminance{}; + RenderGraphResourceId nrdRawConfidenceGradient{}; + RenderGraphResourceId nrdFilteredConfidenceGradient{}; + RenderGraphResourceId nrdConfidenceSourcePixelFallback{}; + RenderGraphResourceId nrdPreviousLightRecords{}; RenderGraphResourceId nrdDiffOut{}; RenderGraphResourceId nrdSpecOut{}; + RenderGraphResourceId nrdValidation{}; std::vector nrdPoolResources; if (shouldRunNrdDenoiser() && nrdRuntime_) { #if defined(RTV_NRD_RUNTIME_ENABLED) @@ -10849,8 +11355,17 @@ void PathTracerRenderer::recordRenderGraphPlan() { nrdViewZ = graph.createTexture(imageResource(nrdRuntime_->viewZ, "nrd view z")); nrdDiffIn = graph.createTexture(imageResource(nrdRuntime_->diffRadianceHitdist, "nrd diffuse input")); nrdSpecIn = graph.createTexture(imageResource(nrdRuntime_->specRadianceHitdist, "nrd specular input")); + nrdDiffConfidence = graph.createTexture(imageResource(nrdRuntime_->diffHistoryConfidence, "nrd diffuse history confidence")); + nrdSpecConfidence = graph.createTexture(imageResource(nrdRuntime_->specHistoryConfidence, "nrd specular history confidence")); + nrdCurrentLightingLuminance = graph.createTexture(imageResource(nrdRuntime_->currentLightingLuminance, "nrd current lighting luminance")); + nrdPreviousLightingLuminance = graph.createTexture(imageResource(nrdRuntime_->previousLightingLuminance, "nrd previous lighting luminance")); + nrdRawConfidenceGradient = graph.createTexture(imageResource(nrdRuntime_->rawConfidenceGradient, "nrd raw confidence gradient")); + nrdFilteredConfidenceGradient = graph.createTexture(imageResource(nrdRuntime_->filteredConfidenceGradient, "nrd filtered confidence gradient")); + nrdConfidenceSourcePixelFallback = graph.createBuffer(bufferResource(nrdRuntime_->confidenceSourcePixelFallback, "nrd confidence source pixel fallback")); + nrdPreviousLightRecords = graph.createBuffer(bufferResource(nrdRuntime_->previousLightRecords, "nrd previous light records")); nrdDiffOut = graph.createTexture(imageResource(nrdRuntime_->outDiffRadianceHitdist, "nrd diffuse output")); nrdSpecOut = graph.createTexture(imageResource(nrdRuntime_->outSpecRadianceHitdist, "nrd specular output")); + nrdValidation = graph.createTexture(imageResource(nrdRuntime_->validationOutput, "nrd validation output")); nrdPoolResources.reserve(nrdRuntime_->permanentPoolImages.size() + nrdRuntime_->transientPoolImages.size()); for (Image& image : nrdRuntime_->permanentPoolImages) { nrdPoolResources.push_back(graph.createTexture(imageResource(image, "nrd permanent pool"))); @@ -10863,6 +11378,8 @@ void PathTracerRenderer::recordRenderGraphPlan() { const RenderGraphResourceId restirReservoir = graph.createBuffer(bufferResource(restirReservoirBuffer_, "restir reservoir")); RenderGraphResourceId wavefrontRestirReservoir{}; const RenderGraphResourceId previousRestirReservoir = graph.createBuffer(bufferResource(previousRestirReservoirBuffer_, "previous restir reservoir")); + graph.resources()[previousRestirReservoir.index].hasInitialAccess = true; + graph.resources()[previousRestirReservoir.index].initialAccess = persistentRestirHistoryAccess(); const RenderGraphResourceId restirSpatialReservoir = graph.createBuffer(bufferResource(restirSpatialReservoirBuffer_, "restir spatial reservoir")); const RenderGraphResourceId sceneLightRecords = graph.createBuffer(bufferResource(scene_.lightRecords(), "scene light records")); const RenderGraphResourceId sceneLightBvh = graph.createBuffer(bufferResource(scene_.lightBvhNodes(), "scene light bvh")); @@ -10873,6 +11390,11 @@ void PathTracerRenderer::recordRenderGraphPlan() { const RenderGraphResourceId regirSpatialReservoir = graph.createBuffer(bufferResource(regirSpatialReservoirBuffer_, "ReGIR spatial reservoirs")); const RenderGraphResourceId regirTemporalReservoir = graph.createBuffer(bufferResource(regirTemporalReservoirBuffer_, "ReGIR temporal reservoirs")); const RenderGraphResourceId regirPreviousReservoir = graph.createBuffer(bufferResource(regirPreviousReservoirBuffer_, "ReGIR previous reservoirs")); + graph.resources()[regirPreviousReservoir.index].hasInitialAccess = true; + graph.resources()[regirPreviousReservoir.index].initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .access = VK_ACCESS_2_TRANSFER_WRITE_BIT, + }; const RenderGraphResourceId regirActiveCells = graph.createBuffer(bufferResource(regirActiveCellBuffer_, "ReGIR active cells")); const RenderGraphResourceId regirActiveCellsReadback = graph.createBuffer(bufferResource(regirActiveCellReadbackBuffer_, "ReGIR active cells readback")); const RenderGraphResourceId regirHashCurrentCells = graph.createBuffer(bufferResource(regirHashCurrentCellBuffer_, "ReGIR current hash cells")); @@ -10890,6 +11412,9 @@ void PathTracerRenderer::recordRenderGraphPlan() { restirDiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && restirDiSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE && restirDiFinalOutputBuffer().handle() != VK_NULL_HANDLE && + restirDiTemporalSourcePixelBuffer_.handle() != VK_NULL_HANDLE && + restirDiSpatialSourcePixelBuffer_.handle() != VK_NULL_HANDLE && + restirDiFinalSourcePixelBuffer_.handle() != VK_NULL_HANDLE && restirDiPreviousHistoryReservoirBuffer().handle() != VK_NULL_HANDLE && restirDiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE && restirDiCountersBuffer_.handle() != VK_NULL_HANDLE && @@ -10906,6 +11431,9 @@ void PathTracerRenderer::recordRenderGraphPlan() { RenderGraphResourceId restirDiTemporal{}; RenderGraphResourceId restirDiSpatial{}; RenderGraphResourceId restirDiFinal{}; + RenderGraphResourceId restirDiTemporalSourcePixel{}; + RenderGraphResourceId restirDiSpatialSourcePixel{}; + RenderGraphResourceId restirDiFinalSourcePixel{}; RenderGraphResourceId restirDiPrevious{}; RenderGraphResourceId restirDiCounters{}; RenderGraphResourceId restirDiCountersReadback{}; @@ -10918,6 +11446,9 @@ void PathTracerRenderer::recordRenderGraphPlan() { restirDiFinal = shouldAliasRestirDiFinal() ? restirDiTemporal : graph.createBuffer(bufferResource(restirDiCurrentHistoryReservoirBuffer(), "restir di final reservoir")); + restirDiTemporalSourcePixel = graph.createBuffer(bufferResource(restirDiTemporalSourcePixelBuffer_, "restir di temporal source pixel")); + restirDiSpatialSourcePixel = graph.createBuffer(bufferResource(restirDiSpatialSourcePixelBuffer_, "restir di spatial source pixel")); + restirDiFinalSourcePixel = graph.createBuffer(bufferResource(restirDiFinalSourcePixelBuffer_, "restir di final source pixel")); restirDiPrevious = graph.createBuffer(bufferResource(restirDiPreviousHistoryReservoirBuffer(), "restir di previous reservoir")); restirDiCounters = graph.createBuffer(bufferResource(restirDiCountersBuffer_, "restir di counters")); restirDiCountersReadback = graph.createBuffer(bufferResource(restirDiCountersReadbackBuffer_, "restir di counters readback")); @@ -11185,6 +11716,8 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageWrite(worldPosition, traceDomain) .addStorageWrite(velocity, traceDomain) .addStorageWrite(pathData, traceDomain) + .addStorageWrite(psrGuides, traceDomain) + .addStorageWrite(psrGuideSignatures, traceDomain) .addStorageWrite(rayTracingDiagnosticCounters, traceDomain) .addStorageWrite(rayTracingAlphaMaterialCounters, traceDomain) .addStorageRead(previousRestirReservoir, traceDomain) @@ -11241,6 +11774,7 @@ void PathTracerRenderer::recordRenderGraphPlan() { if (useNewRestirDi) { RenderGraphResourceId finalInput = restirDiInitial; RenderGraphResourceId temporalDebugInput = restirDiInitial; + RenderGraphResourceId sourceInput = restirDiTemporalSourcePixel; if (settings_.restirDiTemporalEnabled) { graph.addPass("restir_di_temporal") .addStorageRead(restirDiInitial, PipelineDomain::Compute) @@ -11249,28 +11783,35 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageRead(restirDiPreviousReceiver, PipelineDomain::Compute) .addStorageRead(velocity, PipelineDomain::Compute) .addStorageReadWrite(restirDiCounters, PipelineDomain::Compute) - .addStorageWrite(restirDiTemporal, PipelineDomain::Compute); + .addStorageWrite(restirDiTemporal, PipelineDomain::Compute) + .addStorageWrite(restirDiTemporalSourcePixel, PipelineDomain::Compute); finalInput = restirDiTemporal; temporalDebugInput = restirDiTemporal; + sourceInput = restirDiTemporalSourcePixel; } if (settings_.restirDiSpatialEnabled) { graph.addPass("restir_di_spatial") .addStorageRead(finalInput, PipelineDomain::Compute) + .addStorageRead(sourceInput, PipelineDomain::Compute) .addStorageRead(restirDiReceiver, PipelineDomain::Compute) .addStorageReadWrite(restirDiCounters, PipelineDomain::Compute) - .addStorageWrite(restirDiSpatial, PipelineDomain::Compute); + .addStorageWrite(restirDiSpatial, PipelineDomain::Compute) + .addStorageWrite(restirDiSpatialSourcePixel, PipelineDomain::Compute); finalInput = restirDiSpatial; + sourceInput = restirDiSpatialSourcePixel; } RenderGraphPass& finalPass = graph.addPass("restir_di_final"); finalPass .addStorageRead(finalInput, PipelineDomain::Compute) + .addStorageRead(sourceInput, PipelineDomain::Compute) .addStorageRead(restirDiInitial, PipelineDomain::Compute) .addStorageRead(restirDiReceiver, PipelineDomain::Compute) .addStorageReadWrite(restirDiCounters, PipelineDomain::Compute) .addStorageReadWrite(raw, PipelineDomain::Compute) .addStorageReadWrite(pathData, PipelineDomain::Compute) .addStorageReadWrite(accumulation, PipelineDomain::Compute) - .addStorageReadWrite(variance, PipelineDomain::Compute); + .addStorageReadWrite(variance, PipelineDomain::Compute) + .addStorageWrite(restirDiFinalSourcePixel, PipelineDomain::Compute); if (shouldAliasRestirDiFinal()) { finalPass.addStorageReadWrite(restirDiFinal, PipelineDomain::Compute); } else { @@ -11366,7 +11907,10 @@ void PathTracerRenderer::recordRenderGraphPlan() { const RenderGraphResourceId finalCurrentReservoir = restirGiTemporalReservoir; const RenderGraphResourceId finalSpatialReservoir = shouldRunRestirGiUpsample() ? restirGiUpsampledReservoir - : (shouldRunRestirGiSpatialStage() ? restirGiProductionReservoir : restirGiTemporalReservoir); + : passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiProductionReservoir, + restirGiTemporalReservoir); RenderGraphPass& restirGiFinalProdPass = graph.addPass("restir_gi_final_production"); restirGiFinalProdPass .addStorageReadWrite(raw, PipelineDomain::Compute) @@ -11548,15 +12092,41 @@ void PathTracerRenderer::recordRenderGraphPlan() { const RenderGraphResourceId adaptiveDenoiserInput = shouldRunAdaptiveSamplingFill() ? adaptiveSamplingFilled : raw; RenderGraphResourceId toneInput = adaptiveDenoiserInput; if (shouldRunNrdDenoiser()) { + RenderGraphPass& nrdConfidenceGradient = graph.addPass("nrd_confidence_gradient"); + nrdConfidenceGradient + .addStorageRead(depthNormal, PipelineDomain::Compute) + .addStorageRead(pathData, PipelineDomain::Compute) + .addStorageRead(useNewRestirDi ? restirDiFinalSourcePixel : nrdConfidenceSourcePixelFallback, PipelineDomain::Compute) + .addStorageRead(nrdPreviousLightRecords, PipelineDomain::Compute) + .addStorageRead(sceneLightRecords, PipelineDomain::Compute) + .addStorageRead(sceneMeshParams, PipelineDomain::Compute) + .addStorageRead(nrdPreviousLightingLuminance, PipelineDomain::Compute) + .addStorageRead(adaptiveSamplingFilledMask, PipelineDomain::Compute) + .addStorageWrite(nrdCurrentLightingLuminance, PipelineDomain::Compute) + .addStorageWrite(nrdRawConfidenceGradient, PipelineDomain::Compute); + if (useNewRestirDi) { + nrdConfidenceGradient + .addStorageRead(restirDiReceiver, PipelineDomain::Compute) + .addStorageRead(restirDiPreviousReceiver, PipelineDomain::Compute) + .addStorageRead(restirDiFinal, PipelineDomain::Compute); + } + graph.addPass("nrd_confidence_filter") + .addStorageRead(nrdRawConfidenceGradient, PipelineDomain::Compute) + .addStorageRead(depthNormal, PipelineDomain::Compute) + .addStorageWrite(nrdFilteredConfidenceGradient, PipelineDomain::Compute); RenderGraphPass& nrdPrepare = graph.addPass("nrd_prepare") .addStorageRead(depthNormal, PipelineDomain::Compute) .addStorageRead(velocity, PipelineDomain::Compute) .addStorageRead(pathData, PipelineDomain::Compute) + .addStorageRead(psrGuides, PipelineDomain::Compute) + .addStorageRead(nrdFilteredConfidenceGradient, PipelineDomain::Compute) .addStorageWrite(nrdMotion, PipelineDomain::Compute) .addStorageWrite(nrdNormalRoughness, PipelineDomain::Compute) .addStorageWrite(nrdViewZ, PipelineDomain::Compute) .addStorageWrite(nrdDiffIn, PipelineDomain::Compute) - .addStorageWrite(nrdSpecIn, PipelineDomain::Compute); + .addStorageWrite(nrdSpecIn, PipelineDomain::Compute) + .addStorageWrite(nrdDiffConfidence, PipelineDomain::Compute) + .addStorageWrite(nrdSpecConfidence, PipelineDomain::Compute); if (shouldRunAdaptiveSamplingPrepass()) { nrdPrepare.addStorageRead(adaptiveSamplingFilledMask, PipelineDomain::Compute); } @@ -11566,8 +12136,11 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageReadWrite(nrdViewZ, PipelineDomain::Compute) .addStorageReadWrite(nrdDiffIn, PipelineDomain::Compute) .addStorageReadWrite(nrdSpecIn, PipelineDomain::Compute) + .addStorageReadWrite(nrdDiffConfidence, PipelineDomain::Compute) + .addStorageReadWrite(nrdSpecConfidence, PipelineDomain::Compute) .addStorageReadWrite(nrdDiffOut, PipelineDomain::Compute) - .addStorageReadWrite(nrdSpecOut, PipelineDomain::Compute); + .addStorageReadWrite(nrdSpecOut, PipelineDomain::Compute) + .addStorageReadWrite(nrdValidation, PipelineDomain::Compute); for (RenderGraphResourceId resource : nrdPoolResources) { nrdDispatch.addStorageReadWrite(resource, PipelineDomain::Compute); } @@ -11575,14 +12148,23 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageRead(adaptiveDenoiserInput, PipelineDomain::Compute) .addStorageRead(nrdDiffOut, PipelineDomain::Compute) .addStorageRead(nrdSpecOut, PipelineDomain::Compute) + .addStorageRead(nrdDiffConfidence, PipelineDomain::Compute) + .addStorageRead(nrdSpecConfidence, PipelineDomain::Compute) + .addStorageRead(nrdRawConfidenceGradient, PipelineDomain::Compute) + .addStorageRead(nrdFilteredConfidenceGradient, PipelineDomain::Compute) + .addStorageRead(nrdValidation, PipelineDomain::Compute) .addStorageRead(pathData, PipelineDomain::Compute) .addStorageRead(depthNormal, PipelineDomain::Compute) .addStorageWrite(denoised, PipelineDomain::Compute); RenderGraphPass& historyCopyPass = graph.addPass("history_copy") .addStorageRead(denoised, PipelineDomain::Transfer) .addStorageWrite(history, PipelineDomain::Transfer) + .addStorageRead(nrdCurrentLightingLuminance, PipelineDomain::Transfer) + .addStorageWrite(nrdPreviousLightingLuminance, PipelineDomain::Transfer) .addStorageRead(worldPosition, PipelineDomain::Transfer) .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) + .addStorageRead(psrGuideSignatures, PipelineDomain::Transfer) + .addStorageWrite(previousPsrGuideSignatures, PipelineDomain::Transfer) .addStorageRead(restirReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); if (useRestirGiReservoirs) { @@ -11630,29 +12212,9 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageWrite(diffuseResolved, PipelineDomain::Compute) .addStorageWrite(specularResolved, PipelineDomain::Compute); RenderGraphPass& historyCopyPass = graph.addPass("history_copy") - .addStorageRead(denoised, PipelineDomain::Transfer) - .addStorageWrite(history, PipelineDomain::Transfer) - .addStorageRead(diffuseResolved, PipelineDomain::Transfer) - .addStorageWrite(diffuseHistory, PipelineDomain::Transfer) - .addStorageRead(specularResolved, PipelineDomain::Transfer) - .addStorageWrite(specularHistory, PipelineDomain::Transfer) - .addStorageRead(worldPosition, PipelineDomain::Transfer) - .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) - .addStorageRead(directDiffuseResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(directDiffuseMoments, PipelineDomain::Transfer) - .addStorageRead(directSpecularResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(directSpecularMoments, PipelineDomain::Transfer) - .addStorageRead(indirectDiffuseResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(indirectDiffuseMoments, PipelineDomain::Transfer) - .addStorageRead(indirectSpecularResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(indirectSpecularMoments, PipelineDomain::Transfer) - .addStorageRead(historyLengthResolved, PipelineDomain::Transfer) - .addStorageWrite(historyLength, PipelineDomain::Transfer) - .addStorageRead(momentDebugResolved, PipelineDomain::Transfer) - .addStorageWrite(momentDebug, PipelineDomain::Transfer) .addStorageRead(restirReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); - if (useRestirGiReservoirs) { + if (useRestirGiReservoirs && shouldUseRestirGiLegacyCache()) { historyCopyPass .addStorageRead(restirGiSpatialReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirGiReservoir, PipelineDomain::Transfer); @@ -11664,6 +12226,8 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageWrite(denoised, PipelineDomain::Transfer) .addStorageRead(worldPosition, PipelineDomain::Transfer) .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) + .addStorageRead(psrGuideSignatures, PipelineDomain::Transfer) + .addStorageWrite(previousPsrGuideSignatures, PipelineDomain::Transfer) .addStorageRead(restirReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); if (useRestirGiReservoirs) { @@ -11698,9 +12262,6 @@ void PathTracerRenderer::recordRenderGraphPlan() { .addStorageRead(depthNormal, PipelineDomain::Compute) .addStorageRead(pathData, PipelineDomain::Compute) .addStorageWrite(taa, PipelineDomain::Compute); - graph.addPass("taa_history_copy") - .addStorageRead(taa, PipelineDomain::Transfer) - .addStorageWrite(taaHistory, PipelineDomain::Transfer); toneInput = taa; } @@ -11841,6 +12402,8 @@ void PathTracerRenderer::writeRayTracingDescriptors( .writeBuffer(39, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, previousRestirReservoirBuffer_.descriptorInfo()) .writeBuffer(40, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightBvhNodes().descriptorInfo()) .writeBuffer(42, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()) + .writeBuffer(72, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, psrGuideBuffer_.descriptorInfo()) + .writeBuffer(73, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, psrGuideSignatureBuffer_.descriptorInfo()) .writeBuffer(43, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiCurrentBinding.descriptorInfo()) .writeBuffer(44, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, previousRestirGiBinding.descriptorInfo()) .writeBuffer(45, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, restirGiSpatialBinding.descriptorInfo()) @@ -11881,7 +12444,12 @@ void PathTracerRenderer::writeRayTracingDescriptors( .writeBuffer(49, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, wavefrontRayQueue.descriptorInfo()) .writeBuffer(50, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, wavefrontHitQueueBuffer_.descriptorInfo()); } - writer.update(context_.device(), set); + writer.update(context_.device(), set, { + .owner = "PathTracerRenderer", + .pass = "ray_tracing_shared", + .setName = "raytracing_set_0", + .setIndex = 0, + }); } void PathTracerRenderer::recordHardwarePathTrace(VkCommandBuffer commandBuffer) { @@ -11891,6 +12459,11 @@ void PathTracerRenderer::recordHardwarePathTrace(VkCommandBuffer commandBuffer) tracePathTraceRecordPhase("hardware_path_trace_ensure_variants_begin"); ensureRayTracingVariantPipelines(restirDiValidationFull, restirGiInitialFull); tracePathTraceRecordPhase("hardware_path_trace_ensure_variants_end"); + if (native2BSettingsEligible()) { + tracePathTraceRecordPhase("hardware_path_trace_ensure_native2b_begin"); + ensureNative2BPathTracePipelines(); + tracePathTraceRecordPhase("hardware_path_trace_ensure_native2b_end"); + } RayTracingPipeline* basePipeline = rayTracingPipeline_.get(); RayTracingPipeline* motionPipeline = rayTracingMotionPipeline_.get(); if (restirDiValidationFull && restirGiInitialFull) { @@ -11916,12 +12489,23 @@ void PathTracerRenderer::recordHardwarePathTrace(VkCommandBuffer commandBuffer) } motionPipeline = nullptr; } - if (basePipeline == nullptr || rayTracingScene_ == nullptr) { - throw std::runtime_error("Hardware ray tracing backend is active but RT pipeline/scene is not initialized"); - } - if (rayTracingDiagnosticCountersEnabled_ && - rayTracingDiagnosticPipeline_ == nullptr && - !settings_.motionBlurEnabled && + if (shouldUseNative2BPathTraceKernel()) { + if (rayTracingDiagnosticCountersEnabled_ && rayTracingNative2BDiagnosticPipeline_ != nullptr) { + basePipeline = rayTracingNative2BDiagnosticPipeline_.get(); + } else if (shouldSkipImportedEmissiveDirectSampling() && + rayTracingNative2BCompactPrimaryLightsPipeline_ != nullptr) { + basePipeline = rayTracingNative2BCompactPrimaryLightsPipeline_.get(); + } else { + basePipeline = rayTracingNative2BPipeline_.get(); + } + motionPipeline = nullptr; + } + if (basePipeline == nullptr || rayTracingScene_ == nullptr) { + throw std::runtime_error("Hardware ray tracing backend is active but RT pipeline/scene is not initialized"); + } + if (rayTracingDiagnosticCountersEnabled_ && + rayTracingDiagnosticPipeline_ == nullptr && + !settings_.motionBlurEnabled && !restirDiValidationFull && !restirGiInitialFull) { const bool ommActive = rayTracingScene_->opacityMicromapStats().active; @@ -11957,22 +12541,6 @@ void PathTracerRenderer::recordHardwarePathTrace(VkCommandBuffer commandBuffer) pipeline = rayTracingDiagnosticPipeline_.get(); } } - if (native2BSettingsEligible()) { - tracePathTraceRecordPhase("hardware_path_trace_ensure_native2b_begin"); - ensureNative2BPathTracePipelines(); - tracePathTraceRecordPhase("hardware_path_trace_ensure_native2b_end"); - } - if (shouldUseNative2BPathTraceKernel()) { - if (rayTracingDiagnosticCountersEnabled_ && rayTracingNative2BDiagnosticPipeline_ != nullptr) { - pipeline = rayTracingNative2BDiagnosticPipeline_.get(); - } else if (shouldSkipImportedEmissiveDirectSampling() && - rayTracingNative2BCompactPrimaryLightsPipeline_ != nullptr) { - pipeline = rayTracingNative2BCompactPrimaryLightsPipeline_.get(); - } else { - pipeline = rayTracingNative2BPipeline_.get(); - } - } - tracePathTraceRecordPhase("hardware_path_trace_allocate_descriptors_begin"); DescriptorSet set = currentFrame_->descriptors().allocate(rayTracingSetLayout_); DescriptorSet atmosphereSet = currentFrame_->descriptors().allocate(atmosphereSetLayout_); @@ -12332,6 +12900,33 @@ void PathTracerRenderer::recordDenoiser(VkCommandBuffer commandBuffer) { const RenderGraphResourceId indirectSpecularMoments = graph.createTexture(imageResource(indirectSpecularMomentsImage_, "indirect specular moments")); const RenderGraphResourceId historyLength = graph.createTexture(imageResource(historyLengthImage_, "history length")); const RenderGraphResourceId momentDebug = graph.createTexture(imageResource(momentDebugImage_, "moment debug")); + auto trackedImageAccess = [](const Image& image) { + ResourceAccess access{}; + access.layout = image.layout(); + switch (image.layout()) { + case VK_IMAGE_LAYOUT_UNDEFINED: + access.stage = VK_PIPELINE_STAGE_2_NONE; + access.access = VK_ACCESS_2_NONE; + break; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + access.stage = kCrossQueueShaderStage; + access.access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT; + break; + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + access.stage = VK_PIPELINE_STAGE_2_COPY_BIT; + access.access = VK_ACCESS_2_TRANSFER_READ_BIT; + break; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + access.stage = VK_PIPELINE_STAGE_2_COPY_BIT; + access.access = VK_ACCESS_2_TRANSFER_WRITE_BIT; + break; + default: + access.stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + access.access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + break; + } + return access; + }; graph.resources()[raw.index].hasInitialAccess = true; graph.resources()[raw.index].initialAccess = ResourceAccess{ .stage = pathTraceShaderStage(), @@ -12339,41 +12934,17 @@ void PathTracerRenderer::recordDenoiser(VkCommandBuffer commandBuffer) { .layout = VK_IMAGE_LAYOUT_GENERAL, }; graph.resources()[history.index].hasInitialAccess = true; - graph.resources()[history.index].initialAccess = ResourceAccess{ - .stage = historyImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_COPY_BIT, - .access = historyImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : VK_ACCESS_2_TRANSFER_WRITE_BIT, - .layout = historyImage_.layout(), - }; + graph.resources()[history.index].initialAccess = trackedImageAccess(historyImage_); graph.resources()[diffuseHistory.index].hasInitialAccess = true; - graph.resources()[diffuseHistory.index].initialAccess = ResourceAccess{ - .stage = diffuseHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .access = diffuseHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : (VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT), - .layout = diffuseHistoryImage_.layout(), - }; + graph.resources()[diffuseHistory.index].initialAccess = trackedImageAccess(diffuseHistoryImage_); graph.resources()[specularHistory.index].hasInitialAccess = true; - graph.resources()[specularHistory.index].initialAccess = ResourceAccess{ - .stage = specularHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .access = specularHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : (VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT), - .layout = specularHistoryImage_.layout(), - }; + graph.resources()[specularHistory.index].initialAccess = trackedImageAccess(specularHistoryImage_); graph.resources()[diffuseResolved.index].hasInitialAccess = true; - graph.resources()[diffuseResolved.index].initialAccess = ResourceAccess{ - .stage = diffuseResolvedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_TRANSFER_BIT, - .access = diffuseResolvedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : VK_ACCESS_2_TRANSFER_READ_BIT, - .layout = diffuseResolvedImage_.layout(), - }; + graph.resources()[diffuseResolved.index].initialAccess = trackedImageAccess(diffuseResolvedImage_); graph.resources()[specularResolved.index].hasInitialAccess = true; - graph.resources()[specularResolved.index].initialAccess = ResourceAccess{ - .stage = specularResolvedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_TRANSFER_BIT, - .access = specularResolvedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : VK_ACCESS_2_TRANSFER_READ_BIT, - .layout = specularResolvedImage_.layout(), - }; + graph.resources()[specularResolved.index].initialAccess = trackedImageAccess(specularResolvedImage_); graph.resources()[denoised.index].hasInitialAccess = true; - graph.resources()[denoised.index].initialAccess = ResourceAccess{ - .stage = denoisedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : kCrossQueueShaderStage, - .access = denoisedImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, - .layout = denoisedImage_.layout(), - }; + graph.resources()[denoised.index].initialAccess = trackedImageAccess(denoisedImage_); graph.resources()[variance.index].hasInitialAccess = true; graph.resources()[variance.index].initialAccess = ResourceAccess{ .stage = pathTraceShaderStage(), @@ -12401,11 +12972,7 @@ void PathTracerRenderer::recordDenoiser(VkCommandBuffer commandBuffer) { }; auto setMomentInitialAccess = [&](RenderGraphResourceId id, const Image& image) { graph.resources()[id.index].hasInitialAccess = true; - graph.resources()[id.index].initialAccess = ResourceAccess{ - .stage = image.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .access = image.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : (VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT), - .layout = image.layout(), - }; + graph.resources()[id.index].initialAccess = trackedImageAccess(image); }; setMomentInitialAccess(directDiffuseMoments, directDiffuseMomentsImage_); setMomentInitialAccess(directSpecularMoments, directSpecularMomentsImage_); @@ -12491,6 +13058,8 @@ bool PathTracerRenderer::recordNrdDenoiser(VkCommandBuffer commandBuffer) { skipDenoiserPass(commandBuffer); return false; } + recordNrdConfidenceGradientPass(commandBuffer); + recordNrdConfidenceFilterPass(commandBuffer); recordNrdPreparePass(commandBuffer); if (!recordNrdDispatches(commandBuffer)) { nrdAvailable_ = false; @@ -12507,6 +13076,161 @@ bool PathTracerRenderer::recordNrdDenoiser(VkCommandBuffer commandBuffer) { #endif } +void PathTracerRenderer::recordNrdConfidenceGradientPass(VkCommandBuffer commandBuffer) { +#if defined(RTV_NRD_RUNTIME_ENABLED) + if (!nrdRuntime_ || !nrdRuntime_->resourcesReady) { + return; + } + validationLog_.recordPass("nrd confidence gradient"); + + bufferMemoryBarrier( + commandBuffer, + pathTraceShaderStage(), + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + depthNormalBuffer_.handle(), + depthNormalBuffer_.size()); + bufferMemoryBarrier( + commandBuffer, + pathTraceShaderStage(), + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + pathDataBuffer_.handle(), + pathDataBuffer_.size()); + NrdRuntime& runtime = *nrdRuntime_; + Buffer* sourcePixelBuffer = shouldRunRestirDiFinal() && + restirDiFinalSourcePixelBuffer_.handle() != VK_NULL_HANDLE + ? &restirDiFinalSourcePixelBuffer_ + : &runtime.confidenceSourcePixelFallback; + if (sourcePixelBuffer->handle() == VK_NULL_HANDLE) { + return; + } + if (sourcePixelBuffer == &runtime.confidenceSourcePixelFallback) { + vkCmdFillBuffer(commandBuffer, sourcePixelBuffer->handle(), 0, sourcePixelBuffer->size(), 0xffffffffu); + bufferMemoryBarrier( + commandBuffer, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + sourcePixelBuffer->handle(), + sourcePixelBuffer->size()); + } else { + bufferMemoryBarrier( + commandBuffer, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + sourcePixelBuffer->handle(), + sourcePixelBuffer->size()); + } + if (runtime.previousLightRecords.handle() != VK_NULL_HANDLE) { + bufferMemoryBarrier( + commandBuffer, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + runtime.previousLightRecords.handle(), + runtime.previousLightRecords.size()); + } + nrdTransitionImage(commandBuffer, runtime.previousLightingLuminance, true); + nrdTransitionImage(commandBuffer, adaptiveSamplingFilledMaskImage_, true); + nrdTransitionImage(commandBuffer, runtime.currentLightingLuminance, true); + nrdTransitionImage(commandBuffer, runtime.rawConfidenceGradient, true); + + const bool replayResourcesAvailable = passes::RestirDIPass::canRunConfidenceReplay( + passes::RestirDIPass::ConfidenceReplayResources{ + .diFinalRuns = shouldRunRestirDiFinal(), + .currentReceiverReady = restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .previousReceiverReady = restirDiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE, + .finalReservoirReady = restirDiFinalOutputBuffer().handle() != VK_NULL_HANDLE, + .previousLightRecordsReady = runtime.previousLightRecords.handle() != VK_NULL_HANDLE, + .previousLightHistoryValid = runtime.previousLightHistoryValid, + }); + const Buffer& currentReceiverBuffer = replayResourcesAvailable + ? restirDiCurrentReceiverBuffer() : depthNormalBuffer_; + const Buffer& previousReceiverBuffer = replayResourcesAvailable + ? restirDiPreviousReceiverBuffer() : depthNormalBuffer_; + const Buffer& finalReservoirBuffer = replayResourcesAvailable + ? restirDiFinalOutputBuffer() : depthNormalBuffer_; + + DescriptorSet set = currentFrame_->descriptors().allocate(nrdConfidenceGradientSetLayout_); + DescriptorWriter() + .writeBuffer(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, depthNormalBuffer_.descriptorInfo()) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, pathDataBuffer_.descriptorInfo()) + .writeImage(2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.previousLightingLuminance.storageDescriptor()) + .writeImage(3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, adaptiveSamplingFilledMaskImage_.storageDescriptor()) + .writeImage(4, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.currentLightingLuminance.storageDescriptor()) + .writeImage(5, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.rawConfidenceGradient.storageDescriptor()) + .writeBuffer(6, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameTaaParamsOffset, sizeof(TaaParams))) + .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, sourcePixelBuffer->descriptorInfo()) + .writeBuffer(8, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, currentReceiverBuffer.descriptorInfo()) + .writeBuffer(9, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, previousReceiverBuffer.descriptorInfo()) + .writeBuffer(10, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, finalReservoirBuffer.descriptorInfo()) + .writeBuffer(11, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, runtime.previousLightRecords.descriptorInfo()) + .writeBuffer(12, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightRecords().descriptorInfo()) + .writeBuffer(13, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) + .update(context_.device(), set); + + nrdConfidenceGradientPipeline_->bind(commandBuffer); + const VkDescriptorSet descriptorSet = set.handle(); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, nrdConfidenceGradientPipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); + struct ConfidenceReplayParams { + uint32_t restirAvailable; + uint32_t currentLightCount; + uint32_t previousLightCount; + uint32_t previousLightHistoryValid; + } replayParams{ + replayResourcesAvailable ? 1u : 0u, + passes::RestirDIPass::clampedLightRecordCount(scene_.lightRecordsCpu().size()), + std::min(runtime.previousLightRecordCount, passes::RestirDIPass::kPreviousLightRecordCapacity), + runtime.previousLightHistoryValid ? 1u : 0u, + }; + vkCmdPushConstants( + commandBuffer, + nrdConfidenceGradientPipeline_->layout(), + VK_SHADER_STAGE_COMPUTE_BIT, + 0u, + sizeof(replayParams), + &replayParams); + nrdConfidenceGradientPipeline_->dispatch(commandBuffer, renderExtent_.width, renderExtent_.height); +#else + (void)commandBuffer; +#endif +} + +void PathTracerRenderer::recordNrdConfidenceFilterPass(VkCommandBuffer commandBuffer) { +#if defined(RTV_NRD_RUNTIME_ENABLED) + if (!nrdRuntime_ || !nrdRuntime_->resourcesReady) { + return; + } + validationLog_.recordPass("nrd confidence filter"); + + NrdRuntime& runtime = *nrdRuntime_; + nrdTransitionImage(commandBuffer, runtime.rawConfidenceGradient, true); + nrdTransitionImage(commandBuffer, runtime.filteredConfidenceGradient, true); + + DescriptorSet set = currentFrame_->descriptors().allocate(nrdConfidenceFilterSetLayout_); + DescriptorWriter() + .writeImage(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.rawConfidenceGradient.storageDescriptor()) + .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, depthNormalBuffer_.descriptorInfo()) + .writeImage(2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.filteredConfidenceGradient.storageDescriptor()) + .writeBuffer(3, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameTaaParamsOffset, sizeof(TaaParams))) + .update(context_.device(), set); + + nrdConfidenceFilterPipeline_->bind(commandBuffer); + const VkDescriptorSet descriptorSet = set.handle(); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, nrdConfidenceFilterPipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); + nrdConfidenceFilterPipeline_->dispatch(commandBuffer, renderExtent_.width, renderExtent_.height); +#else + (void)commandBuffer; +#endif +} + void PathTracerRenderer::recordNrdPreparePass(VkCommandBuffer commandBuffer) { #if defined(RTV_NRD_RUNTIME_ENABLED) if (!nrdRuntime_ || !nrdRuntime_->resourcesReady) { @@ -12538,6 +13262,14 @@ void PathTracerRenderer::recordNrdPreparePass(VkCommandBuffer commandBuffer) { VK_ACCESS_2_SHADER_STORAGE_READ_BIT, pathDataBuffer_.handle(), pathDataBuffer_.size()); + bufferMemoryBarrier( + commandBuffer, + pathTraceShaderStage(), + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + psrGuideBuffer_.handle(), + psrGuideBuffer_.size()); NrdRuntime& runtime = *nrdRuntime_; nrdTransitionImage(commandBuffer, runtime.motionVectors, true); @@ -12545,6 +13277,9 @@ void PathTracerRenderer::recordNrdPreparePass(VkCommandBuffer commandBuffer) { nrdTransitionImage(commandBuffer, runtime.viewZ, true); nrdTransitionImage(commandBuffer, runtime.diffRadianceHitdist, true); nrdTransitionImage(commandBuffer, runtime.specRadianceHitdist, true); + nrdTransitionImage(commandBuffer, runtime.diffHistoryConfidence, true); + nrdTransitionImage(commandBuffer, runtime.specHistoryConfidence, true); + nrdTransitionImage(commandBuffer, runtime.filteredConfidenceGradient, true); nrdTransitionImage(commandBuffer, adaptiveSamplingFilledMaskImage_, true); DescriptorSet set = currentFrame_->descriptors().allocate(nrdPrepareSetLayout_); @@ -12560,6 +13295,10 @@ void PathTracerRenderer::recordNrdPreparePass(VkCommandBuffer commandBuffer) { .writeBuffer(8, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameTaaParamsOffset, sizeof(TaaParams))) .writeBuffer(9, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameCameraUniformOffset, sizeof(CameraUniform))) .writeImage(10, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, adaptiveSamplingFilledMaskImage_.storageDescriptor()) + .writeImage(11, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.diffHistoryConfidence.storageDescriptor()) + .writeImage(12, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.specHistoryConfidence.storageDescriptor()) + .writeImage(13, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.filteredConfidenceGradient.storageDescriptor()) + .writeBuffer(14, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, psrGuideBuffer_.descriptorInfo()) .update(context_.device(), set); nrdPreparePipeline_->bind(commandBuffer); @@ -12608,8 +13347,11 @@ bool PathTracerRenderer::recordNrdDispatches(VkCommandBuffer commandBuffer) { ? nrd::AccumulationMode::CLEAR_AND_RESTART : nrd::AccumulationMode::CONTINUE; common.isMotionVectorInWorldSpace = false; - common.isHistoryConfidenceAvailable = false; + common.isHistoryConfidenceAvailable = + runtime.diffHistoryConfidence.handle() != VK_NULL_HANDLE && + runtime.specHistoryConfidence.handle() != VK_NULL_HANDLE; common.isDisocclusionThresholdMixAvailable = false; + common.enableValidation = settings_.debugView == RendererDebugView::NrdValidation; nrd::Result result = nrd::SetCommonSettings(*runtime.instance, common); if (result != nrd::Result::SUCCESS) { @@ -12748,6 +13490,49 @@ void PathTracerRenderer::recordNrdResolvePass(VkCommandBuffer commandBuffer) { } validationLog_.recordPass("nrd resolve output"); NrdRuntime& runtime = *nrdRuntime_; + if (settings_.debugView == RendererDebugView::NrdValidation) { + barrier::cmdTransitionImage(commandBuffer, barrier::ImageTransition{ + .image = runtime.validationOutput.handle(), + .oldLayout = runtime.validationOutput.layout(), + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .range = runtime.validationOutput.fullRange(), + .srcStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .srcAccess = VK_ACCESS_2_MEMORY_WRITE_BIT, + .dstStage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccess = VK_ACCESS_2_TRANSFER_READ_BIT, + }); + runtime.validationOutput.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + barrier::cmdTransitionImage(commandBuffer, barrier::ImageTransition{ + .image = denoisedImage_.handle(), + .oldLayout = denoisedImage_.layout(), + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .range = denoisedImage_.fullRange(), + .srcStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .srcAccess = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, + .dstStage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccess = VK_ACCESS_2_TRANSFER_WRITE_BIT, + }); + denoisedImage_.setLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + VkImageCopy copy{}; + copy.srcSubresource = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0u, + .baseArrayLayer = 0u, + .layerCount = 1u, + }; + copy.dstSubresource = copy.srcSubresource; + copy.extent = {renderExtent_.width, renderExtent_.height, 1u}; + vkCmdCopyImage( + commandBuffer, + runtime.validationOutput.handle(), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + denoisedImage_.handle(), + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1u, + ©); + return; + } const Image& inputImage = adaptiveDenoiserInputImage(); barrier::cmdTransitionImage(commandBuffer, barrier::ImageTransition{ .image = inputImage.handle(), @@ -12762,6 +13547,10 @@ void PathTracerRenderer::recordNrdResolvePass(VkCommandBuffer commandBuffer) { inputImage.setLayout(VK_IMAGE_LAYOUT_GENERAL); nrdTransitionImage(commandBuffer, runtime.outDiffRadianceHitdist, true); nrdTransitionImage(commandBuffer, runtime.outSpecRadianceHitdist, true); + nrdTransitionImage(commandBuffer, runtime.diffHistoryConfidence, true); + nrdTransitionImage(commandBuffer, runtime.specHistoryConfidence, true); + nrdTransitionImage(commandBuffer, runtime.rawConfidenceGradient, true); + nrdTransitionImage(commandBuffer, runtime.filteredConfidenceGradient, true); nrdTransitionImage(commandBuffer, denoisedImage_, true); DescriptorSet set = currentFrame_->descriptors().allocate(nrdResolveSetLayout_); @@ -12774,11 +13563,26 @@ void PathTracerRenderer::recordNrdResolvePass(VkCommandBuffer commandBuffer) { .writeImage(5, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, denoisedImage_.storageDescriptor()) .writeBuffer(6, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameTaaParamsOffset, sizeof(TaaParams))) .writeBuffer(7, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameCameraUniformOffset, sizeof(CameraUniform))) + .writeImage(8, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.diffHistoryConfidence.storageDescriptor()) + .writeImage(9, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.specHistoryConfidence.storageDescriptor()) + .writeImage(10, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.rawConfidenceGradient.storageDescriptor()) + .writeImage(11, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, runtime.filteredConfidenceGradient.storageDescriptor()) .update(context_.device(), set); + struct NrdResolvePush { + uint32_t debugView = 0; + }; + const NrdResolvePush push{.debugView = static_cast(settings_.debugView)}; nrdResolvePipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, nrdResolvePipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); + vkCmdPushConstants( + commandBuffer, + nrdResolvePipeline_->layout(), + VK_SHADER_STAGE_COMPUTE_BIT, + 0u, + sizeof(push), + &push); nrdResolvePipeline_->dispatch(commandBuffer, renderExtent_.width, renderExtent_.height); denoisedImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); #else @@ -13142,6 +13946,7 @@ void PathTracerRenderer::recordAdaptiveSamplingDiagnosticsPass(VkCommandBuffer c .bufferMemoryBarrierCount = static_cast(clearBarriers.size()), .pBufferMemoryBarriers = clearBarriers.data(), }; + recordManualBarrierEscape("PathTracerRenderer", "adaptive_sampling_stats_clear", dependencyInfo); vkCmdPipelineBarrier2(commandBuffer, &dependencyInfo); DescriptorSet densitySet = currentFrame_->descriptors().allocate(adaptiveSamplingSetLayout_); @@ -13205,6 +14010,7 @@ void PathTracerRenderer::recordAdaptiveSamplingDiagnosticsPass(VkCommandBuffer c .imageMemoryBarrierCount = 1u, .pImageMemoryBarriers = &debugBarrier, }; + recordManualBarrierEscape("PathTracerRenderer", "adaptive_sampling_density_ready", dependencyInfo); vkCmdPipelineBarrier2(commandBuffer, &dependencyInfo); DescriptorSet discretizeSet = currentFrame_->descriptors().allocate(adaptiveSamplingDiscretizeSetLayout_); @@ -13244,6 +14050,7 @@ void PathTracerRenderer::recordAdaptiveSamplingDiagnosticsPass(VkCommandBuffer c .bufferMemoryBarrierCount = 1u, .pBufferMemoryBarriers = &statsCopyBarrier, }; + recordManualBarrierEscape("PathTracerRenderer", "adaptive_sampling_stats_readback", dependencyInfo); vkCmdPipelineBarrier2(commandBuffer, &dependencyInfo); VkBufferCopy copy{}; @@ -13348,21 +14155,19 @@ void PathTracerRenderer::recordRegirBuildPass(VkCommandBuffer commandBuffer) { return; } currentProfiler_->write(commandBuffer, GpuProfiler::RegirBuildStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); - const VkDeviceSize reservoirCount64 = - static_cast(regirStorageCellCapacity()) * - static_cast(std::max(settings_.regirReservoirsPerCell, 1u)); - const uint32_t reservoirCount = static_cast(std::min(reservoirCount64, std::numeric_limits::max())); + const uint32_t reservoirCount = passes::RegirPass::buildReservoirDispatchCount(settings_); DescriptorSet set = currentFrame_->descriptors().allocate(regirBuildSetLayout_); - DescriptorWriter() - .writeBuffer(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightRecords().descriptorInfo()) - .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.lightBvhNodes().descriptorInfo()) - .writeBuffer(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.meshParamsBuffer().descriptorInfo()) - .writeBuffer(3, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, regirParamsBuffer_.descriptorInfo()) - .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirReservoirBuffer_.descriptorInfo()) - .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - (shouldUseRegirHashGrid() ? regirHashCurrentCellBuffer_ : regirActiveCellBuffer_).descriptorInfo()) - .update(context_.device(), set); + DescriptorWriter writer; + passes::RegirPass::writeBuildDescriptors(writer, passes::RegirPass::BuildDescriptorBindings{ + .lightRecords = scene_.lightRecords().descriptorInfo(), + .lightBvhNodes = scene_.lightBvhNodes().descriptorInfo(), + .meshParams = scene_.meshParamsBuffer().descriptorInfo(), + .params = regirParamsBuffer_.descriptorInfo(), + .reservoirs = regirReservoirBuffer_.descriptorInfo(), + .gridCells = (shouldUseRegirHashGrid() ? regirHashCurrentCellBuffer_ : regirActiveCellBuffer_).descriptorInfo(), + }); + writer.update(context_.device(), set); regirBuildPipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); @@ -13390,18 +14195,19 @@ void PathTracerRenderer::recordRegirEnvironmentBuildPass(VkCommandBuffer command skyCdfCols.handle(), skyCdfCols.size()); DescriptorSet set = currentFrame_->descriptors().allocate(regirEnvironmentBuildSetLayout_); - DescriptorWriter() - .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, regirParamsBuffer_.descriptorInfo()) - .writeBuffer(1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, scene_.envParamsBuffer().descriptorInfo()) - .writeImage(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, scene_.environmentImage().sampledDescriptor(VK_NULL_HANDLE)) - .writeImage(3, VK_DESCRIPTOR_TYPE_SAMPLER, VkDescriptorImageInfo{.sampler = scene_.environmentSampler()}) - .writeBuffer(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.envRows().descriptorInfo()) - .writeBuffer(5, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, scene_.envCols().descriptorInfo()) - .writeBuffer(6, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, skyCdfCols.descriptorInfo()) - .writeBuffer(7, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirEnvironmentReservoirBuffer_.descriptorInfo()) - .writeBuffer(8, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - currentFrame_->uniformRing().descriptorInfo(kFrameCameraUniformOffset, sizeof(CameraUniform))) - .update(context_.device(), set); + DescriptorWriter writer; + passes::RegirPass::writeEnvironmentDescriptors(writer, passes::RegirPass::EnvironmentDescriptorBindings{ + .params = regirParamsBuffer_.descriptorInfo(), + .envParams = scene_.envParamsBuffer().descriptorInfo(), + .environmentImage = scene_.environmentImage().sampledDescriptor(VK_NULL_HANDLE), + .environmentSampler = VkDescriptorImageInfo{.sampler = scene_.environmentSampler()}, + .envRows = scene_.envRows().descriptorInfo(), + .envCols = scene_.envCols().descriptorInfo(), + .skyCdfCols = skyCdfCols.descriptorInfo(), + .environmentReservoirs = regirEnvironmentReservoirBuffer_.descriptorInfo(), + .cameraUniform = currentFrame_->uniformRing().descriptorInfo(kFrameCameraUniformOffset, sizeof(CameraUniform)), + }); + writer.update(context_.device(), set); regirEnvironmentBuildPipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); @@ -13475,24 +14281,16 @@ void PathTracerRenderer::recordRegirSpatialReusePass(VkCommandBuffer commandBuff return; } currentProfiler_->write(commandBuffer, GpuProfiler::RegirSpatialReuseStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); - const glm::uvec3 dims{ - std::max(settings_.regirGridDimensions.x, 1u), - std::max(settings_.regirGridDimensions.y, 1u), - std::max(settings_.regirGridDimensions.z, 1u), - }; - const VkDeviceSize reservoirCount64 = - static_cast(dims.x) * - static_cast(dims.y) * - static_cast(dims.z) * - static_cast(std::max(settings_.regirReservoirsPerCell, 1u)); - const uint32_t reservoirCount = static_cast(std::min(reservoirCount64, std::numeric_limits::max())); + const uint32_t reservoirCount = passes::RegirPass::reuseReservoirDispatchCount(settings_); DescriptorSet set = currentFrame_->descriptors().allocate(regirSpatialReuseSetLayout_); - DescriptorWriter() - .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, regirParamsBuffer_.descriptorInfo()) - .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirReservoirBuffer_.descriptorInfo()) - .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirSpatialReservoirBuffer_.descriptorInfo()) - .update(context_.device(), set); + DescriptorWriter writer; + passes::RegirPass::writeSpatialReuseDescriptors(writer, passes::RegirPass::SpatialReuseDescriptorBindings{ + .params = regirParamsBuffer_.descriptorInfo(), + .inputReservoirs = regirReservoirBuffer_.descriptorInfo(), + .outputReservoirs = regirSpatialReservoirBuffer_.descriptorInfo(), + }); + writer.update(context_.device(), set); regirSpatialReusePipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); @@ -13507,26 +14305,18 @@ void PathTracerRenderer::recordRegirTemporalReusePass(VkCommandBuffer commandBuf return; } currentProfiler_->write(commandBuffer, GpuProfiler::RegirTemporalReuseStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); - const glm::uvec3 dims{ - std::max(settings_.regirGridDimensions.x, 1u), - std::max(settings_.regirGridDimensions.y, 1u), - std::max(settings_.regirGridDimensions.z, 1u), - }; - const VkDeviceSize reservoirCount64 = - static_cast(dims.x) * - static_cast(dims.y) * - static_cast(dims.z) * - static_cast(std::max(settings_.regirReservoirsPerCell, 1u)); - const uint32_t reservoirCount = static_cast(std::min(reservoirCount64, std::numeric_limits::max())); + const uint32_t reservoirCount = passes::RegirPass::reuseReservoirDispatchCount(settings_); const Buffer& currentReservoirs = shouldUseRegirSpatialReuse() ? regirSpatialReservoirBuffer_ : regirReservoirBuffer_; DescriptorSet set = currentFrame_->descriptors().allocate(regirTemporalReuseSetLayout_); - DescriptorWriter() - .writeBuffer(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, regirParamsBuffer_.descriptorInfo()) - .writeBuffer(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, currentReservoirs.descriptorInfo()) - .writeBuffer(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirPreviousReservoirBuffer_.descriptorInfo()) - .writeBuffer(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, regirTemporalReservoirBuffer_.descriptorInfo()) - .update(context_.device(), set); + DescriptorWriter writer; + passes::RegirPass::writeTemporalReuseDescriptors(writer, passes::RegirPass::TemporalReuseDescriptorBindings{ + .params = regirParamsBuffer_.descriptorInfo(), + .currentReservoirs = currentReservoirs.descriptorInfo(), + .previousReservoirs = regirPreviousReservoirBuffer_.descriptorInfo(), + .outputReservoirs = regirTemporalReservoirBuffer_.descriptorInfo(), + }); + writer.update(context_.device(), set); regirTemporalReusePipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); @@ -13554,6 +14344,10 @@ void PathTracerRenderer::recordRegirTemporalHistoryCopyPass(VkCommandBuffer comm } void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { + const bool copyLegacyRestirDi = + settings_.restirDiMode == RestirDiMode::Legacy && + settings_.restirMode != RestirMode::ClassicNee; + RenderGraph graph(&allocator_, resourceAliasingEnabled_); auto copySourceInitialAccess = [](const Image& image) { ResourceAccess access{}; @@ -13631,7 +14425,7 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { .imageRange = diffuseResolvedImage_.fullRange(), .external = true, .hasInitialAccess = true, - .initialAccess = copySourceInitialAccess(denoisedImage_), + .initialAccess = copySourceInitialAccess(diffuseResolvedImage_), .debugName = "current diffuse history hdr", }); const RenderGraphResourceId specularResolved = graph.createTexture(RenderGraphResource{ @@ -13756,6 +14550,32 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { }, .debugName = "previous world position", }); + const RenderGraphResourceId psrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = psrGuideSignatureBuffer_.size(), + .buffer = psrGuideSignatureBuffer_.handle(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "primary surface replacement signatures", + }); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = previousPsrGuideSignatureBuffer_.size(), + .buffer = previousPsrGuideSignatureBuffer_.handle(), + .external = true, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "previous primary surface replacement signatures", + }); const RenderGraphResourceId restirReservoir = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, .lifetime = RenderGraphResource::Lifetime::Persistent, @@ -13775,11 +14595,10 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { .size = previousRestirReservoirBuffer_.size(), .buffer = previousRestirReservoirBuffer_.handle(), .external = true, + .hasInitialAccess = true, + .initialAccess = persistentRestirHistoryAccess(), .hasFinalAccess = true, - .finalAccess = ResourceAccess{ - .stage = pathTraceShaderStage(), - .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, - }, + .finalAccess = persistentRestirHistoryAccess(), .debugName = "previous restir reservoir", }); const bool useRestirGiReservoirs = shouldUseRestirGiReservoirs(); @@ -13847,9 +14666,10 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { }, .debugName = "restir gi production reservoir", }); - restirGiProductionHistorySource = shouldRunRestirGiSpatialStage() - ? restirGiProductionReservoir - : restirGiTemporalReservoir; + restirGiProductionHistorySource = passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiProductionReservoir, + restirGiTemporalReservoir); previousRestirGiProductionReservoir = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, .lifetime = RenderGraphResource::Lifetime::Persistent, @@ -13891,39 +14711,27 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { .debugName = "previous restir gi receiver", }); } - RenderGraphPass& historyCopyPass = graph.addPass("history_copy") - .addStorageRead(denoised, PipelineDomain::Transfer) - .addStorageWrite(history, PipelineDomain::Transfer) - .addStorageRead(diffuseResolved, PipelineDomain::Transfer) - .addStorageWrite(diffuseHistory, PipelineDomain::Transfer) - .addStorageRead(specularResolved, PipelineDomain::Transfer) - .addStorageWrite(specularHistory, PipelineDomain::Transfer) - .addStorageRead(directDiffuseResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(directDiffuseMoments, PipelineDomain::Transfer) - .addStorageRead(directSpecularResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(directSpecularMoments, PipelineDomain::Transfer) - .addStorageRead(indirectDiffuseResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(indirectDiffuseMoments, PipelineDomain::Transfer) - .addStorageRead(indirectSpecularResolvedMoments, PipelineDomain::Transfer) - .addStorageWrite(indirectSpecularMoments, PipelineDomain::Transfer) - .addStorageRead(historyLengthResolved, PipelineDomain::Transfer) - .addStorageWrite(historyLength, PipelineDomain::Transfer) - .addStorageRead(momentDebugResolved, PipelineDomain::Transfer) - .addStorageWrite(momentDebug, PipelineDomain::Transfer) - .addStorageRead(worldPosition, PipelineDomain::Transfer) - .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) - .addStorageRead(restirReservoir, PipelineDomain::Transfer) - .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); - if (useRestirGiReservoirs) { + RenderGraphPass& historyCopyPass = graph.addPass("history_copy"); + if (copyLegacyRestirDi) { historyCopyPass - .addStorageRead(restirGiSpatialReservoir, PipelineDomain::Transfer) - .addStorageWrite(previousRestirGiReservoir, PipelineDomain::Transfer) - .addStorageRead(restirGiReceiver, PipelineDomain::Transfer) - .addStorageWrite(previousRestirGiReceiver, PipelineDomain::Transfer); - if (restirGiProductionHistorySource.valid() && previousRestirGiProductionReservoir.valid()) { + .addStorageRead(restirReservoir, PipelineDomain::Transfer) + .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); + } + if (useRestirGiReservoirs) { + if (shouldUseRestirGiLegacyCache()) { historyCopyPass - .addStorageRead(restirGiProductionHistorySource, PipelineDomain::Transfer) - .addStorageWrite(previousRestirGiProductionReservoir, PipelineDomain::Transfer); + .addStorageRead(restirGiSpatialReservoir, PipelineDomain::Transfer) + .addStorageWrite(previousRestirGiReservoir, PipelineDomain::Transfer); + } + if (!shouldUseRestirHistoryPingPong()) { + historyCopyPass + .addStorageRead(restirGiReceiver, PipelineDomain::Transfer) + .addStorageWrite(previousRestirGiReceiver, PipelineDomain::Transfer); + if (restirGiProductionHistorySource.valid() && previousRestirGiProductionReservoir.valid()) { + historyCopyPass + .addStorageRead(restirGiProductionHistorySource, PipelineDomain::Transfer) + .addStorageWrite(previousRestirGiProductionReservoir, PipelineDomain::Transfer); + } } } historyCopyPass.setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { @@ -13933,20 +14741,22 @@ void PathTracerRenderer::copyHistoryResources(VkCommandBuffer commandBuffer) { graph.execute(commandBuffer, temporalFrameIndex_); denoisedImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); historyImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + diffuseResolvedImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + specularResolvedImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); diffuseHistoryImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); specularHistoryImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - directDiffuseMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - directSpecularMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - indirectDiffuseMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - indirectSpecularMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - historyLengthImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - momentDebugImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); directDiffuseResolvedMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); directSpecularResolvedMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); indirectDiffuseResolvedMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); indirectSpecularResolvedMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + directDiffuseMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + directSpecularMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + indirectDiffuseMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + indirectSpecularMomentsImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); historyLengthResolvedImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + historyLengthImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); momentDebugResolvedImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + momentDebugImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); } void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) { @@ -13995,6 +14805,80 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) }, .debugName = "history hdr", }); + RenderGraphResourceId nrdCurrentLightingLuminance{}; + RenderGraphResourceId nrdPreviousLightingLuminance{}; + RenderGraphResourceId nrdCurrentLightRecords{}; + RenderGraphResourceId nrdPreviousLightRecords{}; + if (nrdRuntime_ != nullptr && nrdRuntime_->resourcesReady) { + nrdCurrentLightingLuminance = graph.createTexture(RenderGraphResource{ + .type = RenderGraphResource::Type::Texture, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .format = nrdRuntime_->currentLightingLuminance.format(), + .extent = nrdRuntime_->currentLightingLuminance.extent(), + .image = nrdRuntime_->currentLightingLuminance.handle(), + .imageRange = nrdRuntime_->currentLightingLuminance.fullRange(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + .layout = VK_IMAGE_LAYOUT_GENERAL, + }, + .debugName = "nrd current lighting luminance", + }); + nrdPreviousLightingLuminance = graph.createTexture(RenderGraphResource{ + .type = RenderGraphResource::Type::Texture, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .format = nrdRuntime_->previousLightingLuminance.format(), + .extent = nrdRuntime_->previousLightingLuminance.extent(), + .image = nrdRuntime_->previousLightingLuminance.handle(), + .imageRange = nrdRuntime_->previousLightingLuminance.fullRange(), + .external = true, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + .layout = VK_IMAGE_LAYOUT_GENERAL, + }, + .debugName = "nrd previous lighting luminance", + }); + nrdCurrentLightRecords = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = scene_.lightRecords().size(), + .buffer = scene_.lightRecords().handle(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "scene light records", + }); + nrdPreviousLightRecords = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = nrdRuntime_->previousLightRecords.size(), + .buffer = nrdRuntime_->previousLightRecords.handle(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "nrd previous light records", + }); + } const RenderGraphResourceId worldPosition = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, .lifetime = RenderGraphResource::Lifetime::Persistent, @@ -14021,6 +14905,32 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) }, .debugName = "previous world position", }); + const RenderGraphResourceId psrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = psrGuideSignatureBuffer_.size(), + .buffer = psrGuideSignatureBuffer_.handle(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = pathTraceShaderStage(), + .access = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + }, + .debugName = "primary surface replacement signatures", + }); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = previousPsrGuideSignatureBuffer_.size(), + .buffer = previousPsrGuideSignatureBuffer_.handle(), + .external = true, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = pathTraceShaderStage(), + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "previous primary surface replacement signatures", + }); const RenderGraphResourceId restirReservoir = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, .lifetime = RenderGraphResource::Lifetime::Persistent, @@ -14040,11 +14950,10 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) .size = previousRestirReservoirBuffer_.size(), .buffer = previousRestirReservoirBuffer_.handle(), .external = true, + .hasInitialAccess = true, + .initialAccess = persistentRestirHistoryAccess(), .hasFinalAccess = true, - .finalAccess = ResourceAccess{ - .stage = pathTraceShaderStage(), - .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, - }, + .finalAccess = persistentRestirHistoryAccess(), .debugName = "previous restir reservoir", }); const bool useRestirGiReservoirs = shouldUseRestirGiReservoirs(); @@ -14125,9 +15034,10 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) }, .debugName = "previous restir gi production reservoir", }); - restirGiProductionHistorySource = shouldRunRestirGiSpatialStage() - ? restirGiProductionReservoir - : restirGiTemporalReservoir; + restirGiProductionHistorySource = passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiProductionReservoir, + restirGiTemporalReservoir); } restirGiReceiver = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, @@ -14162,8 +15072,20 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) .addStorageWrite(history, PipelineDomain::Transfer) .addStorageRead(worldPosition, PipelineDomain::Transfer) .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) + .addStorageRead(psrGuideSignatures, PipelineDomain::Transfer) + .addStorageWrite(previousPsrGuideSignatures, PipelineDomain::Transfer) .addStorageRead(restirReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); + if (nrdCurrentLightingLuminance.valid() && nrdPreviousLightingLuminance.valid()) { + historyCopyPass + .addStorageRead(nrdCurrentLightingLuminance, PipelineDomain::Transfer) + .addStorageWrite(nrdPreviousLightingLuminance, PipelineDomain::Transfer); + } + if (nrdCurrentLightRecords.valid() && nrdPreviousLightRecords.valid()) { + historyCopyPass + .addStorageRead(nrdCurrentLightRecords, PipelineDomain::Transfer) + .addStorageWrite(nrdPreviousLightRecords, PipelineDomain::Transfer); + } if (useRestirGiReservoirs) { historyCopyPass .addStorageRead(restirGiSpatialReservoir, PipelineDomain::Transfer) @@ -14183,6 +15105,10 @@ void PathTracerRenderer::copyNrdHistoryResources(VkCommandBuffer commandBuffer) graph.execute(commandBuffer, temporalFrameIndex_); denoisedImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); historyImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + if (nrdRuntime_ != nullptr && nrdRuntime_->resourcesReady) { + nrdRuntime_->currentLightingLuminance.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + nrdRuntime_->previousLightingLuminance.setLayout(VK_IMAGE_LAYOUT_GENERAL); + } } void PathTracerRenderer::copyNrdHistoryResourcesPass(VkCommandBuffer commandBuffer) { @@ -14206,10 +15132,102 @@ void PathTracerRenderer::copyNrdHistoryResourcesPass(VkCommandBuffer commandBuff 1, &imageCopy); + if (nrdRuntime_ != nullptr && + nrdRuntime_->currentLightingLuminance.handle() != VK_NULL_HANDLE && + nrdRuntime_->previousLightingLuminance.handle() != VK_NULL_HANDLE) { + Image& currentLighting = nrdRuntime_->currentLightingLuminance; + Image& previousLighting = nrdRuntime_->previousLightingLuminance; + barrier::cmdTransitionImage(commandBuffer, barrier::ImageTransition{ + .image = currentLighting.handle(), + .oldLayout = currentLighting.layout(), + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .range = currentLighting.fullRange(), + .srcStage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .srcAccess = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + .dstStage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccess = VK_ACCESS_2_TRANSFER_READ_BIT, + }); + currentLighting.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + barrier::cmdTransitionImage(commandBuffer, barrier::ImageTransition{ + .image = previousLighting.handle(), + .oldLayout = previousLighting.layout(), + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .range = previousLighting.fullRange(), + .srcStage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .srcAccess = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + .dstStage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .dstAccess = VK_ACCESS_2_TRANSFER_WRITE_BIT, + }); + previousLighting.setLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + VkImageCopy nrdLightingCopy{}; + nrdLightingCopy.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + nrdLightingCopy.srcSubresource.layerCount = 1; + nrdLightingCopy.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + nrdLightingCopy.dstSubresource.layerCount = 1; + nrdLightingCopy.extent = currentLighting.extent(); + vkCmdCopyImage( + commandBuffer, + currentLighting.handle(), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + previousLighting.handle(), + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1u, + &nrdLightingCopy); + currentLighting.setLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + previousLighting.setLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + } + + if (nrdRuntime_ != nullptr && + nrdRuntime_->previousLightRecords.handle() != VK_NULL_HANDLE && + scene_.lightRecords().handle() != VK_NULL_HANDLE) { + const uint32_t lightCount = passes::RestirDIPass::clampedLightRecordCount(scene_.lightRecordsCpu().size()); + const VkDeviceSize copyBytes = passes::RestirDIPass::previousLightRecordCopyBytes( + lightCount, + scene_.lightRecords().size(), + nrdRuntime_->previousLightRecords.size()); + if (copyBytes > 0u) { + bufferMemoryBarrier( + commandBuffer, + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_READ_BIT, + scene_.lightRecords().handle(), + copyBytes); + bufferMemoryBarrier( + commandBuffer, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT, + nrdRuntime_->previousLightRecords.handle(), + copyBytes); + VkBufferCopy lightCopy{}; + lightCopy.size = copyBytes; + vkCmdCopyBuffer( + commandBuffer, + scene_.lightRecords().handle(), + nrdRuntime_->previousLightRecords.handle(), + 1u, + &lightCopy); + } + nrdRuntime_->previousLightRecordCount = lightCount; + nrdRuntime_->previousLightHistoryValid = passes::RestirDIPass::previousLightHistoryValid(copyBytes); + } + VkBufferCopy worldCopy{}; worldCopy.size = worldPositionBuffer_.size(); vkCmdCopyBuffer(commandBuffer, worldPositionBuffer_.handle(), previousWorldPositionBuffer_.handle(), 1, &worldCopy); + VkBufferCopy psrSignatureCopy{}; + psrSignatureCopy.size = psrGuideSignatureBuffer_.size(); + vkCmdCopyBuffer( + commandBuffer, + psrGuideSignatureBuffer_.handle(), + previousPsrGuideSignatureBuffer_.handle(), + 1, + &psrSignatureCopy); + VkBufferCopy restirCopy{}; restirCopy.size = restirReservoirBuffer_.size(); vkCmdCopyBuffer(commandBuffer, restirReservoirBuffer_.handle(), previousRestirReservoirBuffer_.handle(), 1, &restirCopy); @@ -14254,9 +15272,12 @@ void PathTracerRenderer::copyHistoryResourcesPass(VkCommandBuffer commandBuffer) validationLog_.recordPass("history rotate"); currentProfiler_->write(commandBuffer, GpuProfiler::HistoryCopyStart, VK_PIPELINE_STAGE_2_COPY_BIT); engineHistoryRotationPending_ = true; - VkBufferCopy restirCopy{}; - restirCopy.size = restirReservoirBuffer_.size(); - vkCmdCopyBuffer(commandBuffer, restirReservoirBuffer_.handle(), previousRestirReservoirBuffer_.handle(), 1, &restirCopy); + if (settings_.restirDiMode == RestirDiMode::Legacy && + settings_.restirMode != RestirMode::ClassicNee) { + VkBufferCopy restirCopy{}; + restirCopy.size = restirReservoirBuffer_.size(); + vkCmdCopyBuffer(commandBuffer, restirReservoirBuffer_.handle(), previousRestirReservoirBuffer_.handle(), 1, &restirCopy); + } if (shouldUseRestirGiReservoirs()) { if (shouldUseRestirGiLegacyCache()) { VkBufferCopy restirGiCopy{}; @@ -14296,32 +15317,11 @@ void PathTracerRenderer::copyHistoryResourcesPass(VkCommandBuffer commandBuffer) } bool PathTracerRenderer::shouldRunDenoiser() const { - if (settings_.wavefrontFinalOutputEnabled) { - return false; - } - const bool engineDebugView = denoiserParams_.debugView != 0u; - if (effectiveDenoiserBackend() != DenoiserBackend::Engine && !engineDebugView) { - return false; - } - if (denoiserParams_.enabled != 0u) { - return true; - } - if (denoiserParams_.debugView >= 1u && denoiserParams_.debugView <= 4u) { - return true; - } - if (denoiserParams_.debugView == static_cast(RendererDebugView::TemporalReactiveMask) || - denoiserParams_.debugView == static_cast(RendererDebugView::TemporalHistoryWeight)) { - return true; - } - if (denoiserParams_.debugView >= static_cast(RendererDebugView::PathDirectDiffuse) && - denoiserParams_.debugView <= static_cast(RendererDebugView::DenoiserSpecularHistoryWeight)) { - return true; - } - if (denoiserParams_.debugView >= static_cast(RendererDebugView::DenoiserDirectDiffuseVariance) && - denoiserParams_.debugView <= static_cast(RendererDebugView::DenoiserSpecularRawVariance)) { - return true; - } - return false; + return passes::DenoiserPass::requestsRun( + settings_, + effectiveDenoiserBackend(), + denoiserParams_.enabled, + denoiserParams_.debugView); } bool PathTracerRenderer::nrdRequested() const { @@ -14333,7 +15333,7 @@ bool PathTracerRenderer::shouldRunNrdDenoiser() const { if (settings_.wavefrontFinalOutputEnabled) { return false; } - if (settings_.debugView != RendererDebugView::Beauty) { + if (!passes::NrdPass::supportsDebugView(settings_.debugView)) { return false; } if (effectiveDenoiserBackend() != DenoiserBackend::Nrd) { @@ -14345,8 +15345,12 @@ bool PathTracerRenderer::shouldRunNrdDenoiser() const { return nrdRuntime_ != nullptr && nrdAvailable_ && nrdRuntime_->resourcesReady && + nrdConfidenceGradientPipeline_ != nullptr && + nrdConfidenceFilterPipeline_ != nullptr && nrdPreparePipeline_ != nullptr && nrdResolvePipeline_ != nullptr && + nrdConfidenceGradientSetLayout_ != VK_NULL_HANDLE && + nrdConfidenceFilterSetLayout_ != VK_NULL_HANDLE && nrdPrepareSetLayout_ != VK_NULL_HANDLE && nrdResolveSetLayout_ != VK_NULL_HANDLE; #else @@ -14408,149 +15412,171 @@ bool PathTracerRenderer::shouldRunAdaptiveSamplingFill() const { bool PathTracerRenderer::shouldUseRegir() const { const VkDeviceSize requiredBytes = - static_cast(regirStorageCellCapacity()) * - static_cast(std::max(settings_.regirReservoirsPerCell, 1u)) * - sizeof(ReGIRReservoirGpu); - return !settings_.wavefrontFinalOutputEnabled && - settings_.lightingReuseMode == LightingReuseMode::LegacyRestirDiGiPlusReGIR && - regirBuildPipeline_ != nullptr && - regirBuildSetLayout_ != VK_NULL_HANDLE && - regirParamsBuffer_.handle() != VK_NULL_HANDLE && - regirReservoirBuffer_.handle() != VK_NULL_HANDLE && - regirReservoirBuffer_.size() >= requiredBytes && - scene_.lightRecords().handle() != VK_NULL_HANDLE && - scene_.lightBvhNodes().handle() != VK_NULL_HANDLE && - scene_.meshParamsBuffer().handle() != VK_NULL_HANDLE; + passes::RegirPass::reservoirStorageByteSize(settings_, sizeof(ReGIRReservoirGpu)); + return passes::RegirPass::canRunBuild( + settings_, + passes::RegirPass::BuildStageResources{ + .pipelineReady = regirBuildPipeline_ != nullptr, + .descriptorLayoutReady = regirBuildSetLayout_ != VK_NULL_HANDLE, + .paramsReady = regirParamsBuffer_.handle() != VK_NULL_HANDLE, + .reservoirReady = regirReservoirBuffer_.handle() != VK_NULL_HANDLE, + .reservoirCapacityReady = regirReservoirBuffer_.size() >= requiredBytes, + .lightRecordsReady = scene_.lightRecords().handle() != VK_NULL_HANDLE, + .lightBvhReady = scene_.lightBvhNodes().handle() != VK_NULL_HANDLE, + .meshParamsReady = scene_.meshParamsBuffer().handle() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldUseRegirEnvironment() const { - const float sunPower = settings_.usePhysicalCamera - ? settings_.sunIlluminanceLux - : settings_.sunIntensity; - const bool sunAvailable = settings_.sunlightEnabled && - settings_.sunDirection.y > 0.0f && sunPower > 0.0f && - glm::dot(settings_.sunColor, glm::vec3(0.2126f, 0.7152f, 0.0722f)) > 0.0f; - return shouldUseRegir() && - settings_.regirEnvironment && - (scene_.envParams().enabled != 0u || sunAvailable) && - regirEnvironmentBuildPipeline_ != nullptr && - regirEnvironmentBuildSetLayout_ != VK_NULL_HANDLE && - regirEnvironmentReservoirBuffer_.handle() != VK_NULL_HANDLE && - regirEnvironmentReservoirBuffer_.size() >= - sizeof(ReGIREnvironmentReservoirGpu) * kRegirInfiniteLightBankSize; + const VkDeviceSize requiredBytes = passes::RegirPass::environmentReservoirByteSize( + kRegirInfiniteLightBankSize, + sizeof(ReGIREnvironmentReservoirGpu)); + return passes::RegirPass::canRunEnvironment( + settings_, + shouldUseRegir(), + scene_.envParams().enabled != 0u, + passes::RegirPass::EnvironmentStageResources{ + .pipelineReady = regirEnvironmentBuildPipeline_ != nullptr, + .descriptorLayoutReady = regirEnvironmentBuildSetLayout_ != VK_NULL_HANDLE, + .reservoirReady = regirEnvironmentReservoirBuffer_.handle() != VK_NULL_HANDLE, + .reservoirCapacityReady = regirEnvironmentReservoirBuffer_.size() >= requiredBytes, + }); } bool PathTracerRenderer::shouldUseRegirActiveGrid() const { - const glm::uvec3 dims{ - std::max(settings_.regirGridDimensions.x, 1u), - std::max(settings_.regirGridDimensions.y, 1u), - std::max(settings_.regirGridDimensions.z, 1u), - }; - const VkDeviceSize requiredBytes = - (static_cast(dims.x) * - static_cast(dims.y) * - static_cast(dims.z) + 4ull) * - sizeof(uint32_t); - return shouldUseRegir() && - settings_.regirGridMode == RegirGridMode::Active && - regirActiveCellBuffer_.handle() != VK_NULL_HANDLE && - regirActiveCellBuffer_.size() >= requiredBytes && - regirActiveCellReadbackBuffer_.handle() != VK_NULL_HANDLE && - regirActiveCellReadbackBuffer_.size() >= sizeof(uint32_t) * 4u; + const VkDeviceSize requiredBytes = passes::RegirPass::activeGridFeedbackByteSize(settings_); + return passes::RegirPass::canUseActiveGrid( + settings_, + shouldUseRegir(), + passes::RegirPass::ActiveGridResources{ + .activeCellBufferReady = regirActiveCellBuffer_.handle() != VK_NULL_HANDLE, + .activeCellCapacityReady = regirActiveCellBuffer_.size() >= requiredBytes, + .readbackBufferReady = regirActiveCellReadbackBuffer_.handle() != VK_NULL_HANDLE, + .readbackCapacityReady = regirActiveCellReadbackBuffer_.size() >= sizeof(uint32_t) * 4u, + }); } uint32_t PathTracerRenderer::regirHashCellCapacity() const { - const uint64_t totalCells = - static_cast(std::max(settings_.regirGridDimensions.x, 1u)) * - static_cast(std::max(settings_.regirGridDimensions.y, 1u)) * - static_cast(std::max(settings_.regirGridDimensions.z, 1u)); - const uint64_t target = totalCells <= 64ull ? totalCells : std::max(64ull, totalCells / 4ull); - uint64_t capacity = 1ull; - while (capacity < target && capacity < (1ull << 31u)) { - capacity <<= 1u; - } - return static_cast(std::max(capacity, 1ull)); + return passes::RegirPass::hashCellCapacity(settings_); } uint32_t PathTracerRenderer::regirStorageCellCapacity() const { - if (settings_.regirGridMode == RegirGridMode::Hash) { - return regirHashCellCapacity(); - } - const uint64_t totalCells = - static_cast(std::max(settings_.regirGridDimensions.x, 1u)) * - static_cast(std::max(settings_.regirGridDimensions.y, 1u)) * - static_cast(std::max(settings_.regirGridDimensions.z, 1u)); - return static_cast(std::min(totalCells, std::numeric_limits::max())); + return passes::RegirPass::storageCellCapacity(settings_); } bool PathTracerRenderer::shouldUseRegirHashGrid() const { - const VkDeviceSize requiredBytes = - (static_cast(regirHashCellCapacity()) + 4ull) * sizeof(uint32_t); - return shouldUseRegir() && - settings_.regirGridMode == RegirGridMode::Hash && - regirHashCurrentCellBuffer_.handle() != VK_NULL_HANDLE && - regirHashCurrentCellBuffer_.size() >= requiredBytes && - regirHashNextCellBuffer_.handle() != VK_NULL_HANDLE && - regirHashNextCellBuffer_.size() >= requiredBytes && - regirActiveCellReadbackBuffer_.handle() != VK_NULL_HANDLE; + const VkDeviceSize requiredBytes = passes::RegirPass::hashGridFeedbackByteSize(regirHashCellCapacity()); + return passes::RegirPass::canUseHashGrid( + settings_, + shouldUseRegir(), + passes::RegirPass::HashGridResources{ + .currentCellBufferReady = regirHashCurrentCellBuffer_.handle() != VK_NULL_HANDLE, + .currentCellCapacityReady = regirHashCurrentCellBuffer_.size() >= requiredBytes, + .nextCellBufferReady = regirHashNextCellBuffer_.handle() != VK_NULL_HANDLE, + .nextCellCapacityReady = regirHashNextCellBuffer_.size() >= requiredBytes, + .readbackBufferReady = regirActiveCellReadbackBuffer_.handle() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldUseRegirSpatialReuse() const { - return shouldUseRegir() && - settings_.regirGridMode != RegirGridMode::Hash && - settings_.regirSpatialReuse && - regirSpatialReusePipeline_ != nullptr && - regirSpatialReuseSetLayout_ != VK_NULL_HANDLE && - regirSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE && - regirSpatialReservoirBuffer_.size() >= regirReservoirBuffer_.size(); + return passes::RegirPass::canRunSpatialReuse( + settings_, + shouldUseRegir(), + passes::RegirPass::SpatialReuseResources{ + .pipelineReady = regirSpatialReusePipeline_ != nullptr, + .descriptorLayoutReady = regirSpatialReuseSetLayout_ != VK_NULL_HANDLE, + .spatialReservoirReady = regirSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE, + .spatialReservoirCapacityReady = regirSpatialReservoirBuffer_.size() >= regirReservoirBuffer_.size(), + }); } bool PathTracerRenderer::shouldUseRegirTemporalReuse() const { - return shouldUseRegir() && - settings_.regirGridMode != RegirGridMode::Hash && - settings_.regirTemporalReuse && - regirTemporalReusePipeline_ != nullptr && - regirTemporalReuseSetLayout_ != VK_NULL_HANDLE && - regirTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - regirPreviousReservoirBuffer_.handle() != VK_NULL_HANDLE && - regirTemporalReservoirBuffer_.size() >= regirReservoirBuffer_.size() && - regirPreviousReservoirBuffer_.size() >= regirReservoirBuffer_.size(); + return passes::RegirPass::canRunTemporalReuse( + settings_, + shouldUseRegir(), + passes::RegirPass::TemporalReuseResources{ + .pipelineReady = regirTemporalReusePipeline_ != nullptr, + .descriptorLayoutReady = regirTemporalReuseSetLayout_ != VK_NULL_HANDLE, + .temporalReservoirReady = regirTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .temporalReservoirCapacityReady = regirTemporalReservoirBuffer_.size() >= regirReservoirBuffer_.size(), + .previousReservoirReady = regirPreviousReservoirBuffer_.handle() != VK_NULL_HANDLE, + .previousReservoirCapacityReady = regirPreviousReservoirBuffer_.size() >= regirReservoirBuffer_.size(), + }); } bool PathTracerRenderer::isNonDenoiserDebugView() const { - const uint32_t view = debugParams_.view; - if (view == 0u) { - return false; - } - if (view <= 4u) { - return false; - } - if (view == static_cast(RendererDebugView::TemporalReactiveMask) || - view == static_cast(RendererDebugView::TemporalHistoryWeight)) { - return false; - } - if (view >= static_cast(RendererDebugView::PathDirectDiffuse) && - view <= static_cast(RendererDebugView::DenoiserSpecularHistoryWeight)) { - return false; - } - if (view >= static_cast(RendererDebugView::DenoiserDirectDiffuseVariance) && - view <= static_cast(RendererDebugView::DenoiserSpecularRawVariance)) { - return false; + return passes::DebugViewPass::isNonDenoiserDebugView(debugParams_.view); +} + +bool PathTracerRenderer::isDlssDebugView() const { + return passes::DlssPass::isUpscaleDebugView(settings_.debugView); +} + +bool PathTracerRenderer::isDlssRayReconstructionDebugView() const { + return passes::DlssPass::isRayReconstructionDebugView(settings_.debugView); +} + +bool PathTracerRenderer::isDlssGuideDebugView() const { + return passes::DlssPass::isGuideDebugView(settings_.debugView); +} + +const Image& PathTracerRenderer::dlssGuideVisualizationSource() const { + const bool rrActive = dlssRayReconstructionActive(); + switch (settings_.debugView) { + case RendererDebugView::DlssInputColor: + return rrActive ? adaptiveDenoiserInputImage() : postDenoiseImage(); + case RendererDebugView::DlssOutputColor: + return taaImage_; + case RendererDebugView::DlssDepth: + return dlssDepthImage_; + case RendererDebugView::DlssMotionVectors: + return dlssMotionVectorImage_; + case RendererDebugView::DlssRrDiffuseAlbedo: + return dlssDiffuseAlbedoImage_; + case RendererDebugView::DlssRrSpecularAlbedo: + return dlssSpecularAlbedoImage_; + case RendererDebugView::DlssRrNormals: + return dlssNormalImage_; + case RendererDebugView::DlssRrRoughness: + return dlssRoughnessImage_; + case RendererDebugView::DlssRrDiffuseHitDistance: + return dlssDiffuseHitDistanceImage_; + case RendererDebugView::DlssRrSpecularHitDistance: + return dlssSpecularHitDistanceImage_; + case RendererDebugView::DlssRrReflectedAlbedo: + return dlssReflectedAlbedoImage_; + case RendererDebugView::DlssRrDisocclusionMask: + return dlssDisocclusionMaskImage_; + case RendererDebugView::DlssRrDiffuseRayDirection: + return dlssDiffuseRayDirectionImage_; + case RendererDebugView::DlssRrSpecularRayDirection: + return dlssSpecularRayDirectionImage_; + case RendererDebugView::DlssRrDiffuseRayDirectionHitDistance: + return dlssDiffuseRayDirectionHitDistanceImage_; + case RendererDebugView::DlssRrSpecularRayDirectionHitDistance: + return dlssSpecularRayDirectionHitDistanceImage_; + default: + return postDenoiseImage(); } - return true; +} + +uint32_t PathTracerRenderer::dlssGuideVisualizationMode() const { + return passes::DlssPass::guideVisualizationMode(settings_.debugView, dlssRayReconstructionActive()); +} + +float PathTracerRenderer::dlssGuideVisualizationScale() const { + return passes::DlssPass::guideVisualizationScale(settings_.debugView, dlssRayReconstructionActive()); } bool PathTracerRenderer::shouldBypassTemporalUpscalerForDebugView() const { - return settings_.debugView != RendererDebugView::Beauty; + return passes::DlssPass::shouldBypassTemporalUpscalerForDebugView(settings_); } bool PathTracerRenderer::shouldRunTaa() const { - return !settings_.wavefrontFinalOutputEnabled && - settings_.pathTracingEnabled && - settings_.taaEnabled && - !shouldBypassTemporalUpscalerForDebugView() && - (effectiveTemporalUpscaler() == TemporalUpscaler::TaaTsr || - effectiveTemporalUpscaler() == TemporalUpscaler::Nis) && + return passes::TemporalAAPass::requestsRun( + settings_, + shouldBypassTemporalUpscalerForDebugView(), + effectiveTemporalUpscaler()) && taaPipeline_ != nullptr && taaSetLayout_ != VK_NULL_HANDLE && taaImage_.handle() != VK_NULL_HANDLE && @@ -14559,22 +15585,19 @@ bool PathTracerRenderer::shouldRunTaa() const { } bool PathTracerRenderer::dlssRequested() const { - return settings_.temporalUpscaler == TemporalUpscaler::Dlss || - settings_.dlssRayReconstructionEnabled || - settings_.dlssFrameGenerationEnabled; + return passes::DlssPass::isRequested(settings_); } bool PathTracerRenderer::streamlineRequested() const { return dlssRequested() || settings_.streamlineReflexEnabled || settings_.streamlineNvPerfEnabled; } - -bool PathTracerRenderer::shouldRunDlss() const { - return !settings_.wavefrontFinalOutputEnabled && - settings_.pathTracingEnabled && - settings_.taaEnabled && - !shouldBypassTemporalUpscalerForDebugView() && - !shouldRunDlssRayReconstruction() && - effectiveTemporalUpscaler() == TemporalUpscaler::Dlss && + +bool PathTracerRenderer::shouldRunDlss() const { + return passes::DlssPass::requestsUpscaleRun( + settings_, + shouldBypassTemporalUpscalerForDebugView(), + shouldRunDlssRayReconstruction(), + effectiveTemporalUpscaler()) && dlssGuidesPipeline_ != nullptr && dlssGuidesSetLayout_ != VK_NULL_HANDLE && taaImage_.handle() != VK_NULL_HANDLE && @@ -14586,12 +15609,10 @@ bool PathTracerRenderer::shouldRunDlss() const { bool PathTracerRenderer::shouldRunDlssRayReconstruction() const { const auto status = nvidiaIntegrationStatus(); - return !settings_.wavefrontFinalOutputEnabled && - settings_.pathTracingEnabled && - settings_.taaEnabled && - !shouldBypassTemporalUpscalerForDebugView() && - settings_.dlssRayReconstructionEnabled && - (status.dlssRayReconstructionAvailable || status.streamlineDlssRayReconstruction.supported) && + return passes::DlssPass::requestsRayReconstructionRun( + settings_, + shouldBypassTemporalUpscalerForDebugView(), + status.dlssRayReconstructionAvailable || status.streamlineDlssRayReconstruction.supported) && dlssRayReconstructionGuidesPipeline_ != nullptr && dlssRayReconstructionGuidesSetLayout_ != VK_NULL_HANDLE && taaImage_.handle() != VK_NULL_HANDLE && @@ -14612,58 +15633,45 @@ bool PathTracerRenderer::shouldRunDlssRayReconstruction() const { depthNormalBuffer_.handle() != VK_NULL_HANDLE && velocityBuffer_.handle() != VK_NULL_HANDLE && previousWorldPositionBuffer_.handle() != VK_NULL_HANDLE && + previousPsrGuideSignatureBuffer_.handle() != VK_NULL_HANDLE && pathDataBuffer_.handle() != VK_NULL_HANDLE; } bool PathTracerRenderer::shouldUseGenericBeautyFastPath(bool restirDiValidationFull, bool restirGiInitialFull) const { return rayTracingBeautyFastPipeline_ != nullptr && - settings_.pathTraceKernelMode == PathTraceKernelMode::Generic && - settings_.debugView == RendererDebugView::Beauty && - settings_.pathTracingEnabled && - !settings_.motionBlurEnabled && - !settings_.wavefrontFinalOutputEnabled && - !rayTracingDiagnosticCountersEnabled_ && - !restirDiValidationFull && - !restirGiInitialFull && - !shouldUseRegir(); + passes::PathTracePass::requestsGenericBeautyFastPath( + settings_, + effectivePathTraceKernelMode(), + restirDiValidationFull, + restirGiInitialFull, + rayTracingDiagnosticCountersEnabled_, + shouldUseRegir()); } bool PathTracerRenderer::shouldUseRegirBeautyFastPath(bool restirDiValidationFull, bool restirGiInitialFull) const { return rayTracingRegirBeautyFastPipeline_ != nullptr && rayTracingRegirStochasticBeautyFastPipeline_ != nullptr && - settings_.pathTraceKernelMode == PathTraceKernelMode::Generic && - settings_.debugView == RendererDebugView::Beauty && - settings_.pathTracingEnabled && - !settings_.motionBlurEnabled && - !settings_.wavefrontFinalOutputEnabled && - !rayTracingDiagnosticCountersEnabled_ && - !restirDiValidationFull && - !restirGiInitialFull && - shouldUseRegir(); + passes::PathTracePass::requestsRegirBeautyFastPath( + settings_, + effectivePathTraceKernelMode(), + restirDiValidationFull, + restirGiInitialFull, + rayTracingDiagnosticCountersEnabled_, + shouldUseRegir()); } bool PathTracerRenderer::shouldTraceRegirFiniteLightsThisFrame() const { - if (settings_.regirQueryMode == RegirQueryMode::Deterministic) { - return true; - } - const uint32_t framePeriod = settings_.regirFiniteQueryFramePeriod > 0u - ? settings_.regirFiniteQueryFramePeriod - : (shouldUseRegirHashGrid() ? 256u : 8u); - return temporalFrameIndex_ % framePeriod == framePeriod - 1u; + return passes::RegirPass::shouldTraceFiniteLightsThisFrame( + settings_, + temporalFrameIndex_, + shouldUseRegirHashGrid()); } bool PathTracerRenderer::native2BSettingsEligible() const { - return settings_.pathTraceKernelMode == PathTraceKernelMode::Native2B && - settings_.pathTracingEnabled && - settings_.maxBounces == 2u && - settings_.samplesPerPixel == 1u && - effectiveLimitSamplesPerPixel() && - settings_.restirGiMode == RestirGiMode::Off && - !settings_.restirGiEnabled && - !settings_.homogeneousVolumeEnabled && - !settings_.motionBlurEnabled && - settings_.debugView == RendererDebugView::Beauty && - !settings_.wavefrontFinalOutputEnabled; + return passes::PathTracePass::native2BSettingsEligible( + settings_, + effectiveLimitSamplesPerPixel(), + shouldUseRegir()); } bool PathTracerRenderer::shouldUseNative2BPathTraceKernel() const { @@ -14682,15 +15690,13 @@ bool PathTracerRenderer::native2BCompactPrimaryLightsActive() const { } const char* PathTracerRenderer::pathTraceKernelFallbackReason() const { + if (const char* reason = passes::PathTracePass::native2BSettingsFallbackReason( + settings_, + effectiveLimitSamplesPerPixel(), + shouldUseRegir())) { + return reason; + } if (settings_.pathTraceKernelMode != PathTraceKernelMode::Native2B) return nullptr; - if (!settings_.pathTracingEnabled) return "path_tracing_disabled"; - if (settings_.maxBounces != 2u) return "requires_exactly_two_bounces"; - if (settings_.samplesPerPixel != 1u || !effectiveLimitSamplesPerPixel()) return "requires_effective_one_spp"; - if (settings_.restirGiMode != RestirGiMode::Off || settings_.restirGiEnabled) return "restir_gi_enabled"; - if (settings_.homogeneousVolumeEnabled) return "volume_enabled"; - if (settings_.motionBlurEnabled) return "motion_blur_enabled"; - if (settings_.debugView != RendererDebugView::Beauty) return "debug_view_active"; - if (settings_.wavefrontFinalOutputEnabled) return "wavefront_final_output_enabled"; if (rayTracingNative2BPipeline_ == nullptr && native2BPipelineBuildJob_.has_value() && native2BPipelineBuildJob_->variant == Native2BPipelineVariant::Base) { @@ -14710,10 +15716,7 @@ PathTraceKernelMode PathTracerRenderer::effectivePathTraceKernelMode() const { } bool PathTracerRenderer::shouldRunRestirSpatial() const { - return !settings_.wavefrontFinalOutputEnabled && - !adaptiveSkipRestirSpatial_ && - settings_.restirDiMode == RestirDiMode::Legacy && - settings_.restirMode != RestirMode::ClassicNee && + return passes::RestirDIPass::requestsLegacySpatialStage(settings_, adaptiveSkipRestirSpatial_) && restirSpatialPipeline_ != nullptr && restirSpatialSetLayout_ != VK_NULL_HANDLE && restirReservoirBuffer_.handle() != VK_NULL_HANDLE && @@ -14722,32 +15725,11 @@ bool PathTracerRenderer::shouldRunRestirSpatial() const { } bool PathTracerRenderer::shouldUseRestirGiReservoirs() const { - // Wavefront final output owns its multi-bounce radiance and does not yet - // produce the megakernel receiver/fallback ABI required by production GI. - if (settings_.wavefrontFinalOutputEnabled) { - return false; - } - return settings_.restirGiMode != RestirGiMode::Off || - settings_.debugView == RendererDebugView::RestirGiValidity || - settings_.debugView == RendererDebugView::RestirGiAge || - settings_.debugView == RendererDebugView::RestirGiInitial || - settings_.debugView == RendererDebugView::RestirGiTemporal || - settings_.debugView == RendererDebugView::RestirGiSpatial || - settings_.debugView == RendererDebugView::RestirGiFinal || - settings_.debugView == RendererDebugView::RestirGiNormal || - settings_.debugView == RendererDebugView::RestirGiHitDistance || - settings_.debugView == RendererDebugView::RestirGiGrid || - settings_.debugView == RendererDebugView::RestirGiPathClass || - settings_.debugView == RendererDebugView::WavefrontRestirGi; + return passes::RestirGIPass::requestsReservoirs(settings_); } bool PathTracerRenderer::shouldRunRestirGiFinal() const { - return !settings_.wavefrontFinalOutputEnabled && - (shouldUseRestirGiLegacyCache() || - settings_.debugView == RendererDebugView::RestirGiSpatial || - settings_.debugView == RendererDebugView::RestirGiFinal || - settings_.debugView == RendererDebugView::RestirGiGrid || - settings_.debugView == RendererDebugView::RestirGiPathClass); + return passes::RestirGIPass::requestsLegacyFinal(settings_); } bool PathTracerRenderer::shouldRunRestirGiTemporal() const { @@ -14755,74 +15737,76 @@ bool PathTracerRenderer::shouldRunRestirGiTemporal() const { ? restirGiTemporalFullPipeline_.get() : restirGiTemporalPipeline_.get(); const Buffer& initialReservoir = restirGiReservoirBuffer_; - return !settings_.wavefrontFinalOutputEnabled && - shouldUseNewRestirGi() && - temporalPipeline != nullptr && - restirGiTemporalSetLayout_ != VK_NULL_HANDLE && - initialReservoir.handle() != VK_NULL_HANDLE && - restirGiPreviousProductionHistoryBuffer().handle() != VK_NULL_HANDLE && - restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - restirGiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE && - rayTracingScene_ != nullptr && - rayTracingScene_->tlas() != VK_NULL_HANDLE; + return passes::RestirGIPass::canRunProductionTemporal( + settings_, + passes::RestirGIPass::TemporalStageResources{ + .pipelineReady = temporalPipeline != nullptr, + .descriptorLayoutReady = restirGiTemporalSetLayout_ != VK_NULL_HANDLE, + .initialReservoirReady = initialReservoir.handle() != VK_NULL_HANDLE, + .previousProductionHistoryReady = restirGiPreviousProductionHistoryBuffer().handle() != VK_NULL_HANDLE, + .temporalReservoirReady = restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .previousReceiverReady = restirGiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE, + .tlasReady = rayTracingScene_ != nullptr && rayTracingScene_->tlas() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirGiSpatialProd() const { - return !settings_.wavefrontFinalOutputEnabled && - shouldUseNewRestirGi() && - shouldRunRestirGiSpatialStage() && - restirGiSpatialProdPipeline_ != nullptr && - restirGiSpatialProdSetLayout_ != VK_NULL_HANDLE && - restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE && - restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - rayTracingScene_ != nullptr && - rayTracingScene_->tlas() != VK_NULL_HANDLE; + return passes::RestirGIPass::canRunProductionSpatial( + settings_, + shouldUseRestirGiReferenceValidation(), + passes::RestirGIPass::SpatialStageResources{ + .pipelineReady = restirGiSpatialProdPipeline_ != nullptr, + .descriptorLayoutReady = restirGiSpatialProdSetLayout_ != VK_NULL_HANDLE, + .temporalReservoirReady = restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentProductionHistoryReady = restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .tlasReady = rayTracingScene_ != nullptr && rayTracingScene_->tlas() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirGiFinalProd() const { - return !settings_.wavefrontFinalOutputEnabled && - shouldUseNewRestirGi() && - restirGiFinalProdPipeline_ != nullptr && - restirGiFinalProdSetLayout_ != VK_NULL_HANDLE && - (shouldRunRestirGiUpsample() - ? restirGiUpsampledReservoirBuffer_.handle() != VK_NULL_HANDLE - : (restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE)) && - restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - rawImage_.handle() != VK_NULL_HANDLE && - pathDataBuffer_.handle() != VK_NULL_HANDLE && - accumulationBuffer_.handle() != VK_NULL_HANDLE; + return passes::RestirGIPass::canRunProductionFinal( + settings_, + passes::RestirGIPass::FinalStageResources{ + .pipelineReady = restirGiFinalProdPipeline_ != nullptr, + .descriptorLayoutReady = restirGiFinalProdSetLayout_ != VK_NULL_HANDLE, + .upsampleStageRuns = shouldRunRestirGiUpsample(), + .upsampledReservoirReady = restirGiUpsampledReservoirBuffer_.handle() != VK_NULL_HANDLE, + .temporalReservoirReady = restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentProductionHistoryReady = restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .rawImageReady = rawImage_.handle() != VK_NULL_HANDLE, + .pathDataReady = pathDataBuffer_.handle() != VK_NULL_HANDLE, + .accumulationReady = accumulationBuffer_.handle() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirGiUpsample() const { - return !settings_.wavefrontFinalOutputEnabled && - shouldUseNewRestirGi() && - shouldRunRestirGiSpatialStage() && - effectiveRestirGiHalfResolution() && - restirGiUpsamplePipeline_ != nullptr && - restirGiUpsampleSetLayout_ != VK_NULL_HANDLE && - restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE && - restirGiUpsampledReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE; + return passes::RestirGIPass::canRunUpsample( + settings_, + shouldUseRestirGiReferenceValidation(), + effectiveRestirGiHalfResolution(), + passes::RestirGIPass::UpsampleStageResources{ + .pipelineReady = restirGiUpsamplePipeline_ != nullptr, + .descriptorLayoutReady = restirGiUpsampleSetLayout_ != VK_NULL_HANDLE, + .temporalReservoirReady = restirGiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentProductionHistoryReady = restirGiCurrentProductionHistoryBuffer().handle() != VK_NULL_HANDLE, + .upsampledReservoirReady = restirGiUpsampledReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirGiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirGiSpatialStage() const { - return settings_.restirGiSpatialRounds > 0u || - settings_.debugView == RendererDebugView::RestirGiSpatial || - settings_.debugView == RendererDebugView::RestirGiGrid || - settings_.debugView == RendererDebugView::RestirGiPathClass || - shouldUseRestirGiReferenceValidation(); + return passes::RestirGIPass::requestsSpatialStage(settings_, shouldUseRestirGiReferenceValidation()); } bool PathTracerRenderer::shouldUseRestirGiActiveTileMask() const { - const bool modeEnabled = settings_.restirGiActiveTileMaskMode == RestirGiActiveTileMaskMode::On || - (settings_.restirGiActiveTileMaskMode == RestirGiActiveTileMaskMode::Auto && restirGiActiveTileMaskAutoEnabled_); - return modeEnabled && - shouldUseNewRestirGi() && - !shouldUseRestirGiReferenceValidation() && + return passes::RestirGIPass::requestsActiveTileMask( + settings_, + restirGiActiveTileMaskAutoEnabled_, + shouldUseRestirGiReferenceValidation()) && + passes::RestirGIPass::isNewReservoirMode(settings_) && restirGiActiveTileMaskBuffer_.handle() != VK_NULL_HANDLE; } @@ -14836,38 +15820,21 @@ bool PathTracerRenderer::shouldCollectRestirCounters() const { break; } return rayTracingDiagnosticCountersEnabled_ || - isRestirDiDebugView(settings_.debugView) || - settings_.debugView == RendererDebugView::RestirGiValidity || - settings_.debugView == RendererDebugView::RestirGiAge || - settings_.debugView == RendererDebugView::RestirGiInitial || - settings_.debugView == RendererDebugView::RestirGiTemporal || - settings_.debugView == RendererDebugView::RestirGiSpatial || - settings_.debugView == RendererDebugView::RestirGiFinal || - settings_.debugView == RendererDebugView::RestirGiNormal || - settings_.debugView == RendererDebugView::RestirGiHitDistance || - settings_.debugView == RendererDebugView::RestirGiGrid || - settings_.debugView == RendererDebugView::RestirGiPathClass || - settings_.debugView == RendererDebugView::WavefrontRestirGi || - settings_.restirDiMode == RestirDiMode::ReferenceValidation || - settings_.restirDiMode == RestirDiMode::HybridCompare || - settings_.restirGiMode == RestirGiMode::ReferenceValidation; + passes::RestirDIPass::requestsCounterCollectionInAutoMode(settings_) || + passes::RestirGIPass::requestsCounterCollectionInAutoMode(settings_); } const Buffer& PathTracerRenderer::restirGiProductionHistorySourceBuffer() const { - return shouldRunRestirGiSpatialStage() - ? restirGiCurrentProductionHistoryBuffer() - : restirGiTemporalReservoirBuffer_; + return passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiCurrentProductionHistoryBuffer(), + restirGiTemporalReservoirBuffer_); } bool PathTracerRenderer::shouldUseRestirHistoryPingPong() const { - if (settings_.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong || - settings_.wavefrontFinalOutputEnabled) { - return false; - } - const bool useDiHistory = shouldRunRestirDiEstimator(); const bool useGiHistory = shouldUseNewRestirGi() && shouldRunRestirGiSpatialStage(); - if (!useDiHistory && !useGiHistory) { + if (!passes::RestirDIPass::requestsSharedHistoryPingPong(settings_, useDiHistory, useGiHistory)) { return false; } @@ -14887,23 +15854,22 @@ bool PathTracerRenderer::shouldUseRestirHistoryPingPong() const { } RestirHistoryCopyMode PathTracerRenderer::effectiveRestirHistoryCopyMode() const { - return shouldUseRestirHistoryPingPong() - ? RestirHistoryCopyMode::PingPong - : RestirHistoryCopyMode::Copy; + return passes::RestirDIPass::effectiveHistoryCopyMode(shouldUseRestirHistoryPingPong()); } const char* PathTracerRenderer::restirHistoryCopyFallbackReason() const { - if (settings_.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong || - shouldUseRestirHistoryPingPong()) { - return nullptr; - } - if (settings_.wavefrontFinalOutputEnabled) { - return "wavefront final output is enabled"; - } + const bool pingPongReady = shouldUseRestirHistoryPingPong(); const bool useDiHistory = shouldRunRestirDiEstimator(); const bool useGiHistory = shouldUseNewRestirGi() && shouldRunRestirGiSpatialStage(); - if (!useDiHistory && !useGiHistory) { - return "no production ReSTIR history path is active"; + if (const char* policyReason = passes::RestirDIPass::sharedHistoryPingPongFallbackReason( + settings_, + pingPongReady, + useDiHistory, + useGiHistory)) { + return policyReason; + } + if (settings_.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong || pingPongReady) { + return nullptr; } if (useDiHistory && (restirDiFinalReservoirBuffer_.handle() == VK_NULL_HANDLE || @@ -14927,162 +15893,187 @@ bool PathTracerRenderer::effectiveRestirGiActiveTileMaskEnabled() const { } const Buffer& PathTracerRenderer::restirDiCurrentReceiverBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return restirDiReceiverBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? restirDiReceiverBuffer_ : previousRestirDiReceiverBuffer_; + return passes::RestirDIPass::selectCurrentHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirDiReceiverBuffer_, + previousRestirDiReceiverBuffer_); } const Buffer& PathTracerRenderer::restirDiPreviousReceiverBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return previousRestirDiReceiverBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? previousRestirDiReceiverBuffer_ : restirDiReceiverBuffer_; + return passes::RestirDIPass::selectPreviousHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirDiReceiverBuffer_, + previousRestirDiReceiverBuffer_); } const Buffer& PathTracerRenderer::restirDiCurrentHistoryReservoirBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return restirDiFinalOutputBuffer(); - return (temporalFrameIndex_ & 1u) == 0u ? restirDiFinalReservoirBuffer_ : previousRestirDiReservoirBuffer_; + if (!shouldUseRestirHistoryPingPong()) { + return restirDiFinalOutputBuffer(); + } + return passes::RestirDIPass::selectCurrentHistoryResource( + true, + temporalFrameIndex_, + restirDiFinalReservoirBuffer_, + previousRestirDiReservoirBuffer_); } const Buffer& PathTracerRenderer::restirDiPreviousHistoryReservoirBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return previousRestirDiReservoirBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? previousRestirDiReservoirBuffer_ : restirDiFinalReservoirBuffer_; + return passes::RestirDIPass::selectPreviousHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirDiFinalReservoirBuffer_, + previousRestirDiReservoirBuffer_); } const Buffer& PathTracerRenderer::restirGiCurrentReceiverBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return restirGiReceiverBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? restirGiReceiverBuffer_ : previousRestirGiReceiverBuffer_; + return passes::RestirGIPass::selectCurrentHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirGiReceiverBuffer_, + previousRestirGiReceiverBuffer_); } const Buffer& PathTracerRenderer::restirGiPreviousReceiverBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return previousRestirGiReceiverBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? previousRestirGiReceiverBuffer_ : restirGiReceiverBuffer_; + return passes::RestirGIPass::selectPreviousHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirGiReceiverBuffer_, + previousRestirGiReceiverBuffer_); } const Buffer& PathTracerRenderer::restirGiCurrentProductionHistoryBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return restirGiProductionReservoirBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? restirGiProductionReservoirBuffer_ : previousRestirGiProductionReservoirBuffer_; + return passes::RestirGIPass::selectCurrentHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirGiProductionReservoirBuffer_, + previousRestirGiProductionReservoirBuffer_); } const Buffer& PathTracerRenderer::restirGiPreviousProductionHistoryBuffer() const { - if (!shouldUseRestirHistoryPingPong()) return previousRestirGiProductionReservoirBuffer_; - return (temporalFrameIndex_ & 1u) == 0u ? previousRestirGiProductionReservoirBuffer_ : restirGiProductionReservoirBuffer_; + return passes::RestirGIPass::selectPreviousHistoryResource( + shouldUseRestirHistoryPingPong(), + temporalFrameIndex_, + restirGiProductionReservoirBuffer_, + previousRestirGiProductionReservoirBuffer_); } bool PathTracerRenderer::shouldUseRestirGiLegacyCache() const { - return settings_.restirGiMode == RestirGiMode::LegacyCache; + return passes::RestirGIPass::isLegacyCacheMode(settings_); } bool PathTracerRenderer::shouldUseRestirGiProduction() const { - return settings_.restirGiMode == RestirGiMode::Production; + return passes::RestirGIPass::isProductionMode(settings_); } bool PathTracerRenderer::shouldUseRestirGiReferenceValidation() const { - return settings_.restirGiMode == RestirGiMode::ReferenceValidation; + return passes::RestirGIPass::isReferenceValidationMode(settings_); } bool PathTracerRenderer::shouldUseNewRestirGi() const { - return shouldUseRestirGiProduction() || shouldUseRestirGiReferenceValidation(); + return passes::RestirGIPass::isNewReservoirMode(settings_); } bool PathTracerRenderer::usesRestirGiUncompressedInitialReservoir() const { - return restirGiUncompressedLayout_ || - settings_.restirGiReservoirLayout == RestirGiReservoirLayout::ValidationFull; + return passes::RestirGIPass::usesUncompressedInitialReservoir(settings_, restirGiUncompressedLayout_); } bool PathTracerRenderer::shouldUseNewRestirDi() const { - const bool diagnosticView = isRestirDiDebugView(settings_.debugView); - const bool productionMode = settings_.restirDiMode == RestirDiMode::Production || - settings_.restirDiMode == RestirDiMode::ReferenceValidation || - settings_.restirDiMode == RestirDiMode::HybridCompare; const bool unsupportedRayQueryVisibility = rayTracingScene_ != nullptr && rayTracingScene_->motionBlurActive(); - return (productionMode || diagnosticView) && - effectiveSamplesPerPixel() == 1u && - !settings_.homogeneousVolumeEnabled && - !unsupportedRayQueryVisibility; + return passes::RestirDIPass::supportsNewEstimatorMode( + settings_, + effectiveSamplesPerPixel(), + unsupportedRayQueryVisibility); } bool PathTracerRenderer::shouldSkipImportedEmissiveDirectSampling() const { - if (!settings_.compactImportedEmissiveTriangleSampling) { - return false; - } - + const auto& lightRecords = scene_.lightRecordsCpu(); bool hasImportedEmissive = false; + bool hasNonCompactEmissive = false; float maxImportedPower = 0.0f; - for (const GpuLightRecord& record : scene_.lightRecordsCpu()) { + for (const GpuLightRecord& record : lightRecords) { if (record.metadata.x > 1u) { - return false; + hasNonCompactEmissive = true; + break; } hasImportedEmissive = true; maxImportedPower = std::max(maxImportedPower, record.data0.x); } - return hasImportedEmissive && - scene_.lightRecordsCpu().size() <= 16'384u && - maxImportedPower <= 1.0f; + return passes::RestirDIPass::canSkipCompactImportedEmissiveDirectSampling( + settings_, + hasImportedEmissive, + hasNonCompactEmissive, + lightRecords.size(), + maxImportedPower); } bool PathTracerRenderer::shouldRunRestirDiEstimator() const { - const bool diagnosticOrValidation = isRestirDiDebugView(settings_.debugView) || - settings_.restirDiMode == RestirDiMode::ReferenceValidation || - settings_.restirDiMode == RestirDiMode::HybridCompare; const bool hasUsefulLightCandidate = !scene_.lightRecordsCpu().empty() && !shouldSkipImportedEmissiveDirectSampling(); return shouldUseNewRestirDi() && - (diagnosticOrValidation || hasUsefulLightCandidate); + passes::RestirDIPass::requestsEstimatorWork(settings_, hasUsefulLightCandidate); } bool PathTracerRenderer::shouldRunRestirDiTemporal() const { - return shouldRunRestirDiEstimator() && - settings_.restirDiTemporalEnabled && - restirDiTemporalPipeline_ != nullptr && - restirDiTemporalSetLayout_ != VK_NULL_HANDLE && - restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - restirDiInitialReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirDiPreviousHistoryReservoirBuffer().handle() != VK_NULL_HANDLE && - restirDiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE && - rayTracingScene_ != nullptr && - rayTracingScene_->tlas() != VK_NULL_HANDLE; + return passes::RestirDIPass::canRunTemporalStage( + settings_, + shouldRunRestirDiEstimator(), + passes::RestirDIPass::TemporalStageResources{ + .pipelineReady = restirDiTemporalPipeline_ != nullptr, + .descriptorLayoutReady = restirDiTemporalSetLayout_ != VK_NULL_HANDLE, + .currentReceiverReady = restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .initialReservoirReady = restirDiInitialReservoirBuffer_.handle() != VK_NULL_HANDLE, + .previousReservoirReady = restirDiPreviousHistoryReservoirBuffer().handle() != VK_NULL_HANDLE, + .previousReceiverReady = restirDiPreviousReceiverBuffer().handle() != VK_NULL_HANDLE, + .tlasReady = rayTracingScene_ != nullptr && rayTracingScene_->tlas() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirDiSpatial() const { - return shouldRunRestirDiEstimator() && - settings_.restirDiSpatialEnabled && - restirDiSpatialPipeline_ != nullptr && - restirDiSpatialSetLayout_ != VK_NULL_HANDLE && - restirDiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - rayTracingScene_ != nullptr && - rayTracingScene_->tlas() != VK_NULL_HANDLE; + return passes::RestirDIPass::canRunSpatialStage( + settings_, + shouldRunRestirDiEstimator(), + passes::RestirDIPass::SpatialStageResources{ + .pipelineReady = restirDiSpatialPipeline_ != nullptr, + .descriptorLayoutReady = restirDiSpatialSetLayout_ != VK_NULL_HANDLE, + .temporalReservoirReady = restirDiTemporalReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .tlasReady = rayTracingScene_ != nullptr && rayTracingScene_->tlas() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldRunRestirDiFinal() const { - return shouldRunRestirDiEstimator() && - restirDiFinalPipeline_ != nullptr && - restirDiFinalSetLayout_ != VK_NULL_HANDLE && - restirDiSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE && - restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE && - rawImage_.handle() != VK_NULL_HANDLE && - pathDataBuffer_.handle() != VK_NULL_HANDLE && - accumulationBuffer_.handle() != VK_NULL_HANDLE && - rayTracingScene_ != nullptr && - rayTracingScene_->tlas() != VK_NULL_HANDLE; + return passes::RestirDIPass::canRunFinalStage( + shouldRunRestirDiEstimator(), + passes::RestirDIPass::FinalStageResources{ + .pipelineReady = restirDiFinalPipeline_ != nullptr, + .descriptorLayoutReady = restirDiFinalSetLayout_ != VK_NULL_HANDLE, + .spatialReservoirReady = restirDiSpatialReservoirBuffer_.handle() != VK_NULL_HANDLE, + .currentReceiverReady = restirDiCurrentReceiverBuffer().handle() != VK_NULL_HANDLE, + .rawImageReady = rawImage_.handle() != VK_NULL_HANDLE, + .pathDataReady = pathDataBuffer_.handle() != VK_NULL_HANDLE, + .accumulationReady = accumulationBuffer_.handle() != VK_NULL_HANDLE, + .tlasReady = rayTracingScene_ != nullptr && rayTracingScene_->tlas() != VK_NULL_HANDLE, + }); } bool PathTracerRenderer::shouldAliasRestirDiFinal() const { // Temporal is no longer sampled by reuse once spatial has completed. The // final shader snapshots its same-pixel debug value before writing output. - return resourceAliasingEnabled_ && - settings_.restirHistoryCopyMode != RestirHistoryCopyMode::PingPong && - settings_.restirDiTemporalEnabled && - settings_.restirDiSpatialEnabled; + return passes::RestirDIPass::canAliasFinalReservoir(settings_, resourceAliasingEnabled_); } const Buffer& PathTracerRenderer::restirDiFinalOutputBuffer() const { - if (shouldUseRestirHistoryPingPong()) { - return (temporalFrameIndex_ & 1u) == 0u ? restirDiFinalReservoirBuffer_ : previousRestirDiReservoirBuffer_; - } - return shouldAliasRestirDiFinal() - ? restirDiTemporalReservoirBuffer_ - : restirDiFinalReservoirBuffer_; + return passes::RestirDIPass::selectFinalOutputResource( + shouldUseRestirHistoryPingPong(), + shouldAliasRestirDiFinal(), + temporalFrameIndex_, + restirDiFinalReservoirBuffer_, + previousRestirDiReservoirBuffer_, + restirDiTemporalReservoirBuffer_); } bool PathTracerRenderer::shouldUseWavefrontFinalOutput() const { @@ -15104,9 +16095,9 @@ bool PathTracerRenderer::shouldUseWavefrontFinalOutput() const { } bool PathTracerRenderer::shouldRunWavefrontDebugWrite() const { - const bool needsRestirReservoir = settings_.debugView == RendererDebugView::WavefrontRestirDi; - const bool needsRestirGiReservoir = settings_.debugView == RendererDebugView::WavefrontRestirGi; - return isWavefrontDebugView(settings_.debugView) && + const bool needsRestirReservoir = passes::DebugViewPass::requiresWavefrontRestirReservoir(settings_.debugView); + const bool needsRestirGiReservoir = passes::DebugViewPass::requiresWavefrontRestirGiReservoir(settings_.debugView); + return passes::DebugViewPass::isWavefrontView(settings_.debugView) && wavefrontDebugWritePipeline_ != nullptr && wavefrontDebugWriteSetLayout_ != VK_NULL_HANDLE && rawImage_.handle() != VK_NULL_HANDLE && @@ -15119,43 +16110,26 @@ bool PathTracerRenderer::shouldRunWavefrontDebugWrite() const { } bool PathTracerRenderer::effectiveLimitSamplesPerPixel() const { - return settings_.limitSamplesPerPixel || memoryPressureTier_ > 0u; + return passes::PathTracePass::effectiveLimitSamplesPerPixel(settings_, memoryPressureTier_); } bool PathTracerRenderer::effectiveRestirGiHalfResolution() const { - return settings_.restirGiHalfResolution || memoryPressureTier_ > 0u; + return passes::RestirGIPass::effectiveHalfResolution(settings_, memoryPressureTier_); } uint32_t PathTracerRenderer::wavefrontMaxPathDepth() const { - return std::max(1u, adaptiveEffectiveMaxBounces_); + return passes::PathTracePass::wavefrontMaxPathDepth(adaptiveEffectiveMaxBounces_); } uint32_t PathTracerRenderer::wavefrontQueueCapacityFor(VkDeviceSize pixelCount) const { - if (pixelCount == 0) { - return 1u; - } - const VkDeviceSize multiplier = settings_.wavefrontShadeEnabled - ? static_cast(std::max(1u, wavefrontMaxPathDepth())) - : 1u; - const VkDeviceSize capacity = pixelCount > std::numeric_limits::max() / multiplier - ? static_cast(std::numeric_limits::max()) - : pixelCount * multiplier; - return capacity > static_cast(std::numeric_limits::max()) - ? std::numeric_limits::max() - : static_cast(capacity); + return passes::PathTracePass::wavefrontQueueCapacityFor( + static_cast(pixelCount), + settings_.wavefrontShadeEnabled, + wavefrontMaxPathDepth()); } uint32_t PathTracerRenderer::effectiveDenoiserMaxHistoryLength() const { - if (memoryPressureTier_ >= 3u) { - return std::min(settings_.denoiserMaxHistoryLength, 16u); - } - if (memoryPressureTier_ >= 2u) { - return std::min(settings_.denoiserMaxHistoryLength, 24u); - } - if (memoryPressureTier_ >= 1u) { - return std::min(settings_.denoiserMaxHistoryLength, 32u); - } - return settings_.denoiserMaxHistoryLength; + return passes::DenoiserPass::effectiveMaxHistoryLength(settings_, memoryPressureTier_); } VkDeviceSize PathTracerRenderer::restirGiReservoirStride() const { @@ -15177,15 +16151,28 @@ const Image& PathTracerRenderer::postDenoiseImage() const { } const Image& PathTracerRenderer::hdrPostProcessImage() const { - if (settings_.debugView == RendererDebugView::AdaptiveFilledImage && + const bool adaptiveFilledReady = adaptiveSamplingFilledImage_.handle() != VK_NULL_HANDLE && - shouldRunAdaptiveSamplingFill()) { + shouldRunAdaptiveSamplingFill(); + const bool adaptiveDebugReady = + isAdaptiveSamplingDebugView() && + adaptiveSamplingDebugImage_.handle() != VK_NULL_HANDLE; + const bool temporalOutputReady = shouldRunTaa() || shouldRunDlss() || shouldRunDlssRayReconstruction(); + switch (passes::CompositePass::selectHdrPostProcessSource( + settings_, + adaptiveFilledReady, + adaptiveDebugReady, + temporalOutputReady)) { + case passes::CompositePass::HdrPostProcessSource::AdaptiveFilled: return adaptiveSamplingFilledImage_; - } - if (isAdaptiveSamplingDebugView() && adaptiveSamplingDebugImage_.handle() != VK_NULL_HANDLE) { + case passes::CompositePass::HdrPostProcessSource::AdaptiveDebug: return adaptiveSamplingDebugImage_; + case passes::CompositePass::HdrPostProcessSource::TemporalOutput: + return taaImage_; + case passes::CompositePass::HdrPostProcessSource::PostDenoise: + default: + return postDenoiseImage(); } - return (shouldRunTaa() || shouldRunDlss() || shouldRunDlssRayReconstruction()) ? taaImage_ : postDenoiseImage(); } bool PathTracerRenderer::ensureDlssFeature(VkCommandBuffer commandBuffer) { @@ -15504,6 +16491,7 @@ void PathTracerRenderer::recordDlss(VkCommandBuffer commandBuffer) { void PathTracerRenderer::recordDlssGuidesPass(VkCommandBuffer commandBuffer) { validationLog_.recordPass("dlss guide generation"); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssGuidesStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); dlssDepthImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); dlssMotionVectorImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); @@ -15521,11 +16509,12 @@ void PathTracerRenderer::recordDlssGuidesPass(VkCommandBuffer commandBuffer) { const VkDescriptorSet descriptorSet = set.handle(); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, dlssGuidesPipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); dlssGuidesPipeline_->dispatch(commandBuffer, renderExtent_.width, renderExtent_.height); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssGuidesEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); } void PathTracerRenderer::recordDlssPass(VkCommandBuffer commandBuffer) { validationLog_.recordPass("dlss upscale"); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); const Image& inputImage = postDenoiseImage(); inputImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); dlssDepthImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); @@ -15568,14 +16557,14 @@ void PathTracerRenderer::recordDlssPass(VkCommandBuffer commandBuffer) { streamlineDlssEvaluation_, "dlss")) { taaImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); return; } } if (!ensureDlssFeature(commandBuffer)) { fallbackBlitPostDenoiseToTemporalOutput(commandBuffer); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); return; } @@ -15646,12 +16635,12 @@ void PathTracerRenderer::recordDlssPass(VkCommandBuffer commandBuffer) { validationLog_.recordPass("dlss evaluation failed: " + dlssUnavailableReason_); releaseDlssFeature(); fallbackBlitPostDenoiseToTemporalOutput(commandBuffer); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); return; } #endif taaImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); } void PathTracerRenderer::recordDlssRayReconstruction(VkCommandBuffer commandBuffer) { @@ -15700,8 +16689,10 @@ void PathTracerRenderer::recordDlssRayReconstruction(VkCommandBuffer commandBuff const RenderGraphResourceId depthNormal = graph.createBuffer(bufferResource(depthNormalBuffer_, "depth normal")); const RenderGraphResourceId velocity = graph.createBuffer(bufferResource(velocityBuffer_, "screen velocity")); const RenderGraphResourceId pathData = graph.createBuffer(bufferResource(pathDataBuffer_, "path data channels")); + const RenderGraphResourceId psrGuides = graph.createBuffer(bufferResource(psrGuideBuffer_, "primary surface replacement guides")); const RenderGraphResourceId adaptiveFilledMask = graph.createTexture(imageResource(adaptiveSamplingFilledMaskImage_, "adaptive sampling filled mask")); const RenderGraphResourceId previousWorldPosition = graph.createBuffer(bufferResource(previousWorldPositionBuffer_, "previous world position")); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(bufferResource(previousPsrGuideSignatureBuffer_, "previous primary surface replacement signatures")); graph.resources()[input.index].hasInitialAccess = true; graph.resources()[input.index].initialAccess = ResourceAccess{ @@ -15763,17 +16754,29 @@ void PathTracerRenderer::recordDlssRayReconstruction(VkCommandBuffer commandBuff .stage = pathTraceShaderStage(), .access = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, }; + graph.resources()[psrGuides.index].hasInitialAccess = true; + graph.resources()[psrGuides.index].initialAccess = ResourceAccess{ + .stage = pathTraceShaderStage(), + .access = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + }; graph.resources()[previousWorldPosition.index].hasInitialAccess = true; graph.resources()[previousWorldPosition.index].initialAccess = ResourceAccess{ .stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, }; + graph.resources()[previousPsrGuideSignatures.index].hasInitialAccess = true; + graph.resources()[previousPsrGuideSignatures.index].initialAccess = ResourceAccess{ + .stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + }; RenderGraphPass& guidesPass = graph.addPass("dlss_rr_guides") .addStorageRead(depthNormal, PipelineDomain::Compute) .addStorageRead(velocity, PipelineDomain::Compute) .addStorageRead(pathData, PipelineDomain::Compute) + .addStorageRead(psrGuides, PipelineDomain::Compute) .addStorageRead(previousWorldPosition, PipelineDomain::Compute) + .addStorageRead(previousPsrGuideSignatures, PipelineDomain::Compute) .addStorageWrite(depthGuide, PipelineDomain::Compute) .addStorageWrite(motionGuide, PipelineDomain::Compute) .addStorageWrite(diffuseAlbedo, PipelineDomain::Compute) @@ -15821,6 +16824,7 @@ void PathTracerRenderer::recordDlssRayReconstruction(VkCommandBuffer commandBuff void PathTracerRenderer::recordDlssRayReconstructionGuidesPass(VkCommandBuffer commandBuffer) { validationLog_.recordPass("dlss ray reconstruction guide generation"); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionGuidesStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); dlssDepthImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); dlssMotionVectorImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); dlssDiffuseAlbedoImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); @@ -15861,17 +16865,20 @@ void PathTracerRenderer::recordDlssRayReconstructionGuidesPass(VkCommandBuffer c .writeBuffer(19, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameTaaParamsOffset, sizeof(TaaParams))) .writeBuffer(20, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, currentFrame_->uniformRing().descriptorInfo(kFrameCameraUniformOffset, sizeof(CameraUniform))) .writeImage(21, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, adaptiveSamplingFilledMaskImage_.storageDescriptor()) + .writeBuffer(22, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, psrGuideBuffer_.descriptorInfo()) + .writeBuffer(23, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, previousPsrGuideSignatureBuffer_.descriptorInfo()) .update(context_.device(), set); dlssRayReconstructionGuidesPipeline_->bind(commandBuffer); const VkDescriptorSet descriptorSet = set.handle(); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, dlssRayReconstructionGuidesPipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); dlssRayReconstructionGuidesPipeline_->dispatch(commandBuffer, renderExtent_.width, renderExtent_.height); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionGuidesEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); } void PathTracerRenderer::recordDlssRayReconstructionPass(VkCommandBuffer commandBuffer) { validationLog_.recordPass("dlss ray reconstruction"); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); const Image& inputImage = adaptiveDenoiserInputImage(); inputImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); dlssDepthImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); @@ -15945,7 +16952,7 @@ void PathTracerRenderer::recordDlssRayReconstructionPass(VkCommandBuffer command streamlineDlssRayReconstructionEvaluation_, "dlss ray reconstruction")) { taaImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); return; } } @@ -15957,7 +16964,7 @@ void PathTracerRenderer::recordDlssRayReconstructionPass(VkCommandBuffer command ? "DLSS Ray Reconstruction feature could not be created" : dlssRayReconstructionUnavailableReason_; fallbackBlitPostDenoiseToTemporalOutput(commandBuffer); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); return; } @@ -16055,14 +17062,103 @@ void PathTracerRenderer::recordDlssRayReconstructionPass(VkCommandBuffer command validationLog_.recordPass("dlss ray reconstruction evaluation failed: " + dlssRayReconstructionUnavailableReason_); releaseDlssRayReconstructionFeature(); fallbackBlitPostDenoiseToTemporalOutput(commandBuffer); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionEnd, VK_PIPELINE_STAGE_2_TRANSFER_BIT); return; } ++ngxDlssRayReconstructionEvaluation_.succeeded; ngxDlssRayReconstructionEvaluation_.lastError.clear(); #endif taaImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); - currentProfiler_->write(commandBuffer, GpuProfiler::TaaEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + currentProfiler_->write(commandBuffer, GpuProfiler::DlssRayReconstructionEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); +} + +void PathTracerRenderer::recordDlssGuideVisualization(VkCommandBuffer commandBuffer) { + RenderGraph graph(&allocator_, resourceAliasingEnabled_); + const Image& sourceImage = dlssGuideVisualizationSource(); + const RenderGraphResourceId source = graph.createTexture(RenderGraphResource{ + .type = RenderGraphResource::Type::Texture, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .format = sourceImage.format(), + .extent = sourceImage.extent(), + .image = sourceImage.handle(), + .imageRange = sourceImage.fullRange(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = sourceImage.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : kCrossQueueShaderStage, + .access = sourceImage.layout() == VK_IMAGE_LAYOUT_UNDEFINED + ? VK_ACCESS_2_NONE + : (VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT), + .layout = sourceImage.layout(), + }, + .debugName = "dlss guide debug source", + }); + const RenderGraphResourceId presentation = graph.createTexture(RenderGraphResource{ + .type = RenderGraphResource::Type::Texture, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .format = presentationImage_.format(), + .extent = presentationImage_.extent(), + .image = presentationImage_.handle(), + .imageRange = presentationImage_.fullRange(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = presentationImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : kCrossQueueShaderStage, + .access = presentationImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ACCESS_2_NONE : VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, + .layout = presentationImage_.layout(), + }, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = kCrossQueueShaderStage, + .access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, + .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }, + .debugName = "presentation ldr", + }); + + graph.addPass("dlss_guide_visualize") + .addStorageRead(source, PipelineDomain::Compute) + .addStorageWrite(presentation, PipelineDomain::Compute) + .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { + recordDlssGuideVisualizationPass(cmd); + }); + graph.compile(); + graph.execute(commandBuffer, temporalFrameIndex_); + presentationImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} + +void PathTracerRenderer::recordDlssGuideVisualizationPass(VkCommandBuffer commandBuffer) { + validationLog_.recordPass(std::string("dlss guide visualization view=") + rendererDebugViewName(settings_.debugView)); + currentProfiler_->write(commandBuffer, GpuProfiler::ToneMapStart, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + const Image& sourceImage = dlssGuideVisualizationSource(); + sourceImage.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + presentationImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + + DescriptorSet set = currentFrame_->descriptors().allocate(dlssGuideVisualizeSetLayout_); + DescriptorWriter() + .writeImage(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, sourceImage.sampledDescriptor(VK_NULL_HANDLE)) + .writeImage(1, VK_DESCRIPTOR_TYPE_SAMPLER, VkDescriptorImageInfo{.sampler = fullscreenSampler_}) + .writeImage(2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, presentationImage_.storageDescriptor()) + .update(context_.device(), set); + + dlssGuideVisualizePipeline_->bind(commandBuffer); + const VkDescriptorSet descriptorSet = set.handle(); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, dlssGuideVisualizePipeline_->layout(), 0, 1, &descriptorSet, 0, nullptr); + const DlssGuideVisualizeParams params{ + .mode = dlssGuideVisualizationMode(), + .scale = dlssGuideVisualizationScale(), + .bias = 0.0f, + .reserved0 = 0.0f, + }; + vkCmdPushConstants( + commandBuffer, + dlssGuideVisualizePipeline_->layout(), + VK_SHADER_STAGE_COMPUTE_BIT, + 0, + sizeof(params), + ¶ms); + dlssGuideVisualizePipeline_->dispatch(commandBuffer, displayExtent_.width, displayExtent_.height); + currentProfiler_->write(commandBuffer, GpuProfiler::ToneMapEnd, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); } void PathTracerRenderer::recordTaa(VkCommandBuffer commandBuffer, bool deferHistoryCopy) { @@ -16181,21 +17277,13 @@ void PathTracerRenderer::recordTaa(VkCommandBuffer commandBuffer, bool deferHist .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { recordTaaPass(cmd); }); - if (!deferHistoryCopy) { - graph.addPass("taa_history_copy") - .addStorageRead(output, PipelineDomain::Transfer) - .addStorageWrite(history, PipelineDomain::Transfer) - .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { - recordTaaHistoryCopyPass(cmd); - }); - } graph.compile(); graph.execute(commandBuffer, temporalFrameIndex_); taaImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); if (deferHistoryCopy) { asyncTaaHistoryCopyPending_ = true; } else { - taaHistoryImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + recordTaaHistoryCopyPass(commandBuffer); } } @@ -16229,69 +17317,7 @@ void PathTracerRenderer::recordTaaPass(VkCommandBuffer commandBuffer) { } void PathTracerRenderer::copyTaaHistory(VkCommandBuffer commandBuffer) { - RenderGraph graph(&allocator_, resourceAliasingEnabled_); - const RenderGraphResourceId output = graph.createTexture(RenderGraphResource{ - .type = RenderGraphResource::Type::Texture, - .lifetime = RenderGraphResource::Lifetime::Persistent, - .format = taaImage_.format(), - .extent = taaImage_.extent(), - .image = taaImage_.handle(), - .imageRange = taaImage_.fullRange(), - .external = true, - .hasInitialAccess = true, - .initialAccess = ResourceAccess{ - .stage = taaImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED - ? VK_PIPELINE_STAGE_2_NONE - : kCrossQueueShaderStage, - .access = taaImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED - ? VK_ACCESS_2_NONE - : VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, - .layout = taaImage_.layout(), - }, - .hasFinalAccess = true, - .finalAccess = ResourceAccess{ - .stage = kCrossQueueShaderStage, - .access = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, - .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - }, - .debugName = "taa output hdr", - }); - const RenderGraphResourceId history = graph.createTexture(RenderGraphResource{ - .type = RenderGraphResource::Type::Texture, - .lifetime = RenderGraphResource::Lifetime::Persistent, - .format = taaHistoryImage_.format(), - .extent = taaHistoryImage_.extent(), - .image = taaHistoryImage_.handle(), - .imageRange = taaHistoryImage_.fullRange(), - .external = true, - .hasInitialAccess = true, - .initialAccess = ResourceAccess{ - .stage = taaHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED - ? VK_PIPELINE_STAGE_2_NONE - : VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .access = taaHistoryImage_.layout() == VK_IMAGE_LAYOUT_UNDEFINED - ? VK_ACCESS_2_NONE - : VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, - .layout = taaHistoryImage_.layout(), - }, - .hasFinalAccess = true, - .finalAccess = ResourceAccess{ - .stage = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, - .layout = VK_IMAGE_LAYOUT_GENERAL, - }, - .debugName = "taa history hdr", - }); - graph.addPass("taa_history_copy") - .addStorageRead(output, PipelineDomain::Transfer) - .addStorageWrite(history, PipelineDomain::Transfer) - .setExecuteCallback([this](FrameGraphContext&, VkCommandBuffer cmd) { - recordTaaHistoryCopyPass(cmd); - }); - graph.compile(); - graph.execute(commandBuffer, temporalFrameIndex_); - taaImage_.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - taaHistoryImage_.setLayout(VK_IMAGE_LAYOUT_GENERAL); + recordTaaHistoryCopyPass(commandBuffer); } void PathTracerRenderer::recordTaaHistoryCopyPass(VkCommandBuffer commandBuffer) { @@ -16322,6 +17348,7 @@ void PathTracerRenderer::rotateRealtimeHistoryResources() { std::swap(historyLengthResolvedImage_, historyLengthImage_); std::swap(momentDebugResolvedImage_, momentDebugImage_); std::swap(worldPositionBuffer_, previousWorldPositionBuffer_); + std::swap(psrGuideSignatureBuffer_, previousPsrGuideSignatureBuffer_); engineHistoryRotationPending_ = false; } if (taaHistoryRotationPending_) { @@ -16635,6 +17662,32 @@ void PathTracerRenderer::skipDenoiserPass(VkCommandBuffer commandBuffer) { }, .debugName = "previous world position", }); + const RenderGraphResourceId psrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = psrGuideSignatureBuffer_.size(), + .buffer = psrGuideSignatureBuffer_.handle(), + .external = true, + .hasInitialAccess = true, + .initialAccess = ResourceAccess{ + .stage = pathTraceShaderStage(), + .access = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + }, + .debugName = "primary surface replacement signatures", + }); + const RenderGraphResourceId previousPsrGuideSignatures = graph.createBuffer(RenderGraphResource{ + .type = RenderGraphResource::Type::Buffer, + .lifetime = RenderGraphResource::Lifetime::Persistent, + .size = previousPsrGuideSignatureBuffer_.size(), + .buffer = previousPsrGuideSignatureBuffer_.handle(), + .external = true, + .hasFinalAccess = true, + .finalAccess = ResourceAccess{ + .stage = pathTraceShaderStage(), + .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, + }, + .debugName = "previous primary surface replacement signatures", + }); const RenderGraphResourceId restirReservoir = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, .lifetime = RenderGraphResource::Lifetime::Persistent, @@ -16654,11 +17707,10 @@ void PathTracerRenderer::skipDenoiserPass(VkCommandBuffer commandBuffer) { .size = previousRestirReservoirBuffer_.size(), .buffer = previousRestirReservoirBuffer_.handle(), .external = true, + .hasInitialAccess = true, + .initialAccess = persistentRestirHistoryAccess(), .hasFinalAccess = true, - .finalAccess = ResourceAccess{ - .stage = pathTraceShaderStage(), - .access = VK_ACCESS_2_SHADER_STORAGE_READ_BIT, - }, + .finalAccess = persistentRestirHistoryAccess(), .debugName = "previous restir reservoir", }); const bool useRestirGiReservoirs = shouldUseRestirGiReservoirs(); @@ -16739,9 +17791,10 @@ void PathTracerRenderer::skipDenoiserPass(VkCommandBuffer commandBuffer) { }, .debugName = "previous restir gi production reservoir", }); - restirGiProductionHistorySource = shouldRunRestirGiSpatialStage() - ? restirGiProductionReservoir - : restirGiTemporalReservoir; + restirGiProductionHistorySource = passes::RestirGIPass::selectProductionHistorySource( + shouldRunRestirGiSpatialStage(), + restirGiProductionReservoir, + restirGiTemporalReservoir); } restirGiReceiver = graph.createBuffer(RenderGraphResource{ .type = RenderGraphResource::Type::Buffer, @@ -16775,6 +17828,8 @@ void PathTracerRenderer::skipDenoiserPass(VkCommandBuffer commandBuffer) { .addStorageWrite(denoised, PipelineDomain::Transfer) .addStorageRead(worldPosition, PipelineDomain::Transfer) .addStorageWrite(previousWorldPosition, PipelineDomain::Transfer) + .addStorageRead(psrGuideSignatures, PipelineDomain::Transfer) + .addStorageWrite(previousPsrGuideSignatures, PipelineDomain::Transfer) .addStorageRead(restirReservoir, PipelineDomain::Transfer) .addStorageWrite(previousRestirReservoir, PipelineDomain::Transfer); if (useRestirGiReservoirs) { @@ -16823,6 +17878,16 @@ void PathTracerRenderer::skipDenoiserCopyPass(VkCommandBuffer commandBuffer) { VkBufferCopy worldCopy{}; worldCopy.size = worldPositionBuffer_.size(); vkCmdCopyBuffer(commandBuffer, worldPositionBuffer_.handle(), previousWorldPositionBuffer_.handle(), 1, &worldCopy); + + VkBufferCopy psrSignatureCopy{}; + psrSignatureCopy.size = psrGuideSignatureBuffer_.size(); + vkCmdCopyBuffer( + commandBuffer, + psrGuideSignatureBuffer_.handle(), + previousPsrGuideSignatureBuffer_.handle(), + 1, + &psrSignatureCopy); + VkBufferCopy restirCopy{}; restirCopy.size = restirReservoirBuffer_.size(); vkCmdCopyBuffer(commandBuffer, restirReservoirBuffer_.handle(), previousRestirReservoirBuffer_.handle(), 1, &restirCopy); @@ -16998,8 +18063,15 @@ VkDeviceSize PathTracerRenderer::estimatedTextureMemory() const { total += texSize(nrdRuntime_->viewZ); total += texSize(nrdRuntime_->diffRadianceHitdist); total += texSize(nrdRuntime_->specRadianceHitdist); + total += texSize(nrdRuntime_->diffHistoryConfidence); + total += texSize(nrdRuntime_->specHistoryConfidence); + total += texSize(nrdRuntime_->currentLightingLuminance); + total += texSize(nrdRuntime_->previousLightingLuminance); + total += texSize(nrdRuntime_->rawConfidenceGradient); + total += texSize(nrdRuntime_->filteredConfidenceGradient); total += texSize(nrdRuntime_->outDiffRadianceHitdist); total += texSize(nrdRuntime_->outSpecRadianceHitdist); + total += texSize(nrdRuntime_->validationOutput); total += texSize(nrdRuntime_->fallbackImage); for (const Image& image : nrdRuntime_->permanentPoolImages) { total += texSize(image); @@ -17024,6 +18096,9 @@ VkDeviceSize PathTracerRenderer::estimatedBufferMemory() const { total += entityIdBuffer_.size(); total += entityIdReadbackBuffer_.size(); total += pathDataBuffer_.size(); + total += psrGuideBuffer_.size(); + total += psrGuideSignatureBuffer_.size(); + total += previousPsrGuideSignatureBuffer_.size(); total += adaptiveSamplingDensityBuffer_.size(); total += adaptiveSamplingSampleCountBuffer_.size(); total += adaptiveSamplingStatsBuffer_.size(); @@ -17058,6 +18133,8 @@ VkDeviceSize PathTracerRenderer::estimatedBufferMemory() const { for (const Buffer& buffer : nrdRuntime_->constantBuffers) { total += buffer.size(); } + total += nrdRuntime_->confidenceSourcePixelFallback.size(); + total += nrdRuntime_->previousLightRecords.size(); } #endif return total; @@ -17114,6 +18191,9 @@ VkDeviceSize PathTracerRenderer::restirReservoirMemory() const { addUnique(restirDiTemporalReservoirBuffer_); addUnique(restirDiSpatialReservoirBuffer_); addUnique(restirDiFinalReservoirBuffer_); + addUnique(restirDiTemporalSourcePixelBuffer_); + addUnique(restirDiSpatialSourcePixelBuffer_); + addUnique(restirDiFinalSourcePixelBuffer_); addUnique(previousRestirDiReservoirBuffer_); addUnique(previousRestirDiReceiverBuffer_); addUnique(restirDiCountersBuffer_); @@ -17132,6 +18212,9 @@ PathTracerRenderer::RestirReservoirMemoryBreakdown PathTracerRenderer::restirRes previousRestirDiReservoirBuffer_.size() + restirDiReceiverBuffer_.size() + previousRestirDiReceiverBuffer_.size() + + restirDiTemporalSourcePixelBuffer_.size() + + restirDiSpatialSourcePixelBuffer_.size() + + restirDiFinalSourcePixelBuffer_.size() + restirDiCountersBuffer_.size() + restirDiCountersReadbackBuffer_.size(); const VkDeviceSize aliasSavings = shouldAliasRestirDiFinal() ? logicalFinalBytes : 0; diff --git a/src/rtv/RayTracingScene.cpp b/src/rtv/RayTracingScene.cpp index ae00007..caa8eb6 100644 --- a/src/rtv/RayTracingScene.cpp +++ b/src/rtv/RayTracingScene.cpp @@ -3,6 +3,7 @@ #include "rtv/BufferUploader.h" #include "rtv/Check.h" #include "rtv/GpuScene.h" +#include "rtv/GpuValidation.h" #include "rtv/ResourceAllocator.h" #include "rtv/UploadContext.h" #include "rtv/VulkanContext.h" @@ -219,6 +220,7 @@ void appendBlasGeometryRange( std::vector buildBlasGeometryPlans( const GpuScene& scene, + const RayTracingSceneBuildOptions& options, const std::vector& opacityMicromapBuilds, const OpacityMicromapBuildStats& opacityMicromapStats, const std::vector* activeMeshMask, @@ -254,11 +256,14 @@ std::vector buildBlasGeometryPlans( enableMixedSidednessPartitioning && hardwareBackfaceCullingEnabled && !meshHasOmm && - mesh.updateMode == AccelUpdateMode::Static && + !mesh.containsAlphaTestedGeometry && + !mesh.containsBlendedGeometry && + !meshAllowsDynamicBlasUpdate(mesh, options) && mesh.containsSingleSidedGeometry && mesh.containsDoubleSidedGeometry; - const bool preserveAlphaClassRanges = meshHasOmm && - (mesh.containsAlphaTestedGeometry || mesh.containsBlendedGeometry); + // Mixed alpha meshes need one stable primitive-ID domain for any-hit and OMM + // lookups. Splitting them changes gl_PrimitiveID addressing between BLAS ranges. + const bool preserveAlphaClassRanges = meshHasOmm && mesh.containsBlendedGeometry; if (mesh.indexCount >= 3u && !preserveAlphaClassRanges && !preserveSidednessRanges) { @@ -348,7 +353,7 @@ std::vector buildBlasGeometryPlans( enableMixedSidednessPartitioning && hardwareBackfaceCullingEnabled && !meshHasOmm && - mesh.updateMode == AccelUpdateMode::Static && + !meshAllowsDynamicBlasUpdate(mesh, options) && hasSingleSided && hasDoubleSided; if (plan.splitSidedness) { @@ -437,7 +442,12 @@ std::vector prepareOpacityMicromapBuilds( } const RayTracingMeshBuildInput& mesh = rtMeshes[meshBuildIndex]; const uint32_t primitiveCount = mesh.indexCount / 3u; - if ((!mesh.containsAlphaTestedGeometry && !mesh.containsBlendedGeometry) || primitiveCount == 0) { + // Vulkan requires UPDATE builds to use the same geometry pNext contract + // as the original build. Dynamic/GPU-skinned geometry therefore uses + // shader any-hit instead of attaching an OMM that its refit would drop. + if (meshAllowsDynamicBlasUpdate(mesh, options) || + (!mesh.containsAlphaTestedGeometry && !mesh.containsBlendedGeometry) || + primitiveCount == 0) { continue; } @@ -655,12 +665,14 @@ void accelerationBuildBarrier(VkCommandBuffer cmd) { barrier.dstStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR | VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; - barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR; + barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; + recordManualBarrierEscape("RayTracingScene", "acceleration_build", dependency); vkCmdPipelineBarrier2(cmd, &dependency); } @@ -679,6 +691,7 @@ void dynamicBlasInputBarrier(VkCommandBuffer cmd, const Buffer& vertexBuffer) { dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.bufferMemoryBarrierCount = 1; dependency.pBufferMemoryBarriers = &barrier; + recordManualBarrierEscape("RayTracingScene", "dynamic_blas_input", dependency); vkCmdPipelineBarrier2(cmd, &dependency); } @@ -697,6 +710,7 @@ void shaderStorageUploadBarrier(VkCommandBuffer cmd, const Buffer& buffer) { dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.bufferMemoryBarrierCount = 1; dependency.pBufferMemoryBarriers = &barrier; + recordManualBarrierEscape("RayTracingScene", "shader_storage_upload", dependency); vkCmdPipelineBarrier2(cmd, &dependency); } @@ -782,7 +796,8 @@ std::vector buildVkInstances( VkAccelerationStructureMotionInstanceNV makeVkMotionInstance( const RayTracingInstanceBuildInput& instance, - const AccelerationStructure& blas) { + const AccelerationStructure& blas, + uint32_t tlasRecordIndex) { const uint8_t mask = rayTracingInstanceMask(instance); VkAccelerationStructureMotionInstanceNV vkInstance{}; vkInstance.type = VK_ACCELERATION_STRUCTURE_MOTION_INSTANCE_TYPE_MATRIX_MOTION_NV; @@ -790,7 +805,7 @@ VkAccelerationStructureMotionInstanceNV makeVkMotionInstance( VkAccelerationStructureMatrixMotionInstanceNV& matrix = vkInstance.data.matrixMotionInstance; matrix.transformT0 = toVkTransform(instance.previousTransform); matrix.transformT1 = toVkTransform(instance.transform); - matrix.instanceCustomIndex = instance.instanceIndex; + matrix.instanceCustomIndex = tlasRecordIndex; matrix.mask = mask; matrix.instanceShaderBindingTableRecordOffset = 0; matrix.flags = rayTracingInstanceFlags(instance, true); @@ -830,9 +845,14 @@ RayTracingMotionInstanceStats collectMotionInstanceStats( std::vector buildVkMotionInstances( const GpuScene& scene, - const std::vector& blases) { + const std::vector& blases, + const std::vector& blasGeometryPlans, + const std::vector& meshGeometryRanges, + std::vector& tlasGeometryRanges) { std::vector instances; instances.reserve(scene.rayTracingInstances().size()); + tlasGeometryRanges.clear(); + tlasGeometryRanges.reserve(scene.rayTracingInstances().size()); for (const RayTracingInstanceBuildInput& instance : scene.rayTracingInstances()) { if (instance.meshIndex >= blases.size() || blases[instance.meshIndex].handle() == VK_NULL_HANDLE) { continue; @@ -840,7 +860,24 @@ std::vector buildVkMotionInstances( if (rayTracingInstanceMask(instance) == 0u) { continue; } - instances.push_back(makeVkMotionInstance(instance, blases[instance.meshIndex])); + const uint32_t tlasRecordIndex = static_cast(tlasGeometryRanges.size()); + MeshGeometryRangeGpu range{.offset = 0u, .count = 1u}; + if (instance.meshIndex < blasGeometryPlans.size() && + blasGeometryPlans[instance.meshIndex].allGeometryRange.count > 0u) { + range = blasGeometryPlans[instance.meshIndex].allGeometryRange; + } else if (instance.meshIndex < meshGeometryRanges.size()) { + range = meshGeometryRanges[instance.meshIndex]; + } + instances.push_back(makeVkMotionInstance(instance, blases[instance.meshIndex], tlasRecordIndex)); + tlasGeometryRanges.push_back(TlasGeometryRangeGpu{ + .offset = range.offset, + .count = range.count, + .instanceIndex = instance.instanceIndex, + ._pad = 0u, + }); + } + if (tlasGeometryRanges.empty()) { + tlasGeometryRanges.push_back(TlasGeometryRangeGpu{.offset = 0u, .count = 1u, .instanceIndex = 0u, ._pad = 0u}); } return instances; } @@ -861,13 +898,18 @@ void micromapBuildBarrier(VkCommandBuffer cmd) { barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; barrier.srcStageMask = VK_PIPELINE_STAGE_2_MICROMAP_BUILD_BIT_EXT; barrier.srcAccessMask = VK_ACCESS_2_MICROMAP_WRITE_BIT_EXT; - barrier.dstStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; - barrier.dstAccessMask = VK_ACCESS_2_MICROMAP_READ_BIT_EXT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_MICROMAP_BUILD_BIT_EXT | + VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; + barrier.dstAccessMask = VK_ACCESS_2_MICROMAP_READ_BIT_EXT | + VK_ACCESS_2_MICROMAP_WRITE_BIT_EXT | + VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; + recordManualBarrierEscape("RayTracingScene", "micromap_build", dependency); vkCmdPipelineBarrier2(cmd, &dependency); } @@ -973,6 +1015,7 @@ void RayTracingScene::build( std::vector meshGeometryRanges; std::vector blasGeometryPlans = buildBlasGeometryPlans( scene, + options, opacityMicromapBuilds, opacityMicromapStats_, &activeMeshMask, @@ -1219,10 +1262,10 @@ void RayTracingScene::build( micromapBuildInfo.triangleArray.deviceAddress = opacityBuild.triangleArrayBuffer.deviceAddress() + opacityBuild.triangleArrayBufferOffset; micromapBuildInfo.triangleArrayStride = sizeof(VkMicromapTriangleEXT); vkCmdBuildMicromapsEXT(cmd, 1, µmapBuildInfo); - recordedOpacityMicromapBuilds = true; - } - if (recordedOpacityMicromapBuilds) { + // Every build reuses the same scratch address, so serialize it + // before recording the next micromap build. micromapBuildBarrier(cmd); + recordedOpacityMicromapBuilds = true; } } @@ -1374,7 +1417,14 @@ void RayTracingScene::build( blasGeometryPlans, meshGeometryRanges, tlasGeometryRanges); - const std::vector motionInstances = motionBlurActive_ ? buildVkMotionInstances(scene, blases_) : std::vector{}; + const std::vector motionInstances = motionBlurActive_ + ? buildVkMotionInstances( + scene, + blases_, + blasGeometryPlans, + meshGeometryRanges, + tlasGeometryRanges) + : std::vector{}; const uint32_t builtInstanceCount = static_cast(motionBlurActive_ ? motionInstances.size() : instances.size()); if (builtInstanceCount == 0) { throw std::runtime_error("Cannot build ray tracing TLAS: no valid instances"); @@ -1396,9 +1446,6 @@ void RayTracingScene::build( instanceBuffer_.write(motionBlurActive_ ? static_cast(motionInstances.data()) : static_cast(instances.data()), instanceBytes); instanceBuffer_.flush(); - if (motionBlurActive_) { - tlasGeometryRanges.assign(1u, TlasGeometryRangeGpu{.offset = 0u, .count = 1u, .instanceIndex = 0u, ._pad = 0u}); - } tlasGeometryRangesBuffer_.create(allocator, BufferDesc{ .size = sizeof(TlasGeometryRangeGpu) * tlasGeometryRanges.size(), .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, @@ -1518,6 +1565,7 @@ bool RayTracingScene::recordDynamicBlasUpdates( const OpacityMicromapBuildStats noOpacityMicromapStats{}; const std::vector blasGeometryPlans = buildBlasGeometryPlans( scene, + options, noOpacityMicromapBuilds, noOpacityMicromapStats, nullptr, @@ -1645,7 +1693,14 @@ bool RayTracingScene::refitTransforms( emptyGeometryPlans, emptyMeshGeometryRanges, ignoredTlasGeometryRanges); - const std::vector motionInstances = motionBlurActive_ ? buildVkMotionInstances(scene, blases_) : std::vector{}; + const std::vector motionInstances = motionBlurActive_ + ? buildVkMotionInstances( + scene, + blases_, + emptyGeometryPlans, + emptyMeshGeometryRanges, + ignoredTlasGeometryRanges) + : std::vector{}; const uint32_t builtInstanceCount = static_cast(motionBlurActive_ ? motionInstances.size() : instances.size()); if (builtInstanceCount == 0 || builtInstanceCount != instanceCount_) { return false; diff --git a/src/rtv/RenderGraph.cpp b/src/rtv/RenderGraph.cpp index 37eaa87..db6d871 100644 --- a/src/rtv/RenderGraph.cpp +++ b/src/rtv/RenderGraph.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -201,12 +202,29 @@ bool lifetimeOverlaps(const TransientResourceLifetime& a, const TransientResourc return !(a.lastUsePass < b.firstUsePass || b.lastUsePass < a.firstUsePass); } +VkDeviceSize bufferRangeEnd(VkDeviceSize offset, VkDeviceSize size) { + const VkDeviceSize maxValue = std::numeric_limits::max(); + if (size == VK_WHOLE_SIZE || maxValue - offset < size) { + return maxValue; + } + return offset + size; +} + +bool bufferRangesOverlap(const RenderGraphResource& a, const RenderGraphResource& b) { + if (a.buffer == VK_NULL_HANDLE || a.buffer != b.buffer) { + return false; + } + const VkDeviceSize aEnd = bufferRangeEnd(a.bufferOffset, a.size); + const VkDeviceSize bEnd = bufferRangeEnd(b.bufferOffset, b.size); + return a.bufferOffset < bEnd && b.bufferOffset < aEnd; +} + bool resourcesSharePhysicalHandle(const RenderGraphResource& a, const RenderGraphResource& b) { if (a.type != b.type) { return false; } if (a.type == RenderGraphResource::Type::Buffer) { - return a.buffer != VK_NULL_HANDLE && a.buffer == b.buffer; + return bufferRangesOverlap(a, b); } return a.image != VK_NULL_HANDLE && a.image == b.image; } @@ -271,54 +289,92 @@ void traceRenderGraphPass(const char* phase, const std::string& name) { std::cout << "RENDER_GRAPH pass=" << name << " phase=" << phase << '\n' << std::flush; } -void emitBarrier(VkCommandBuffer commandBuffer, const RenderGraphResource& resource, const RenderGraphBarrier& barrier) { - if (resource.type == RenderGraphResource::Type::Texture && resource.image != VK_NULL_HANDLE) { - if (barrier.before.stage == barrier.after.stage && - barrier.before.access == barrier.after.access && - barrier.before.layout == barrier.after.layout) { - return; - } +bool accessIncludesWrite(VkAccessFlags2 access) { + constexpr VkAccessFlags2 writeAccess = + VK_ACCESS_2_SHADER_WRITE_BIT | + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_2_TRANSFER_WRITE_BIT | + VK_ACCESS_2_HOST_WRITE_BIT | + VK_ACCESS_2_MEMORY_WRITE_BIT | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; + return (access & writeAccess) != 0; +} + +bool barrierRequired( + const RenderGraphResource& resource, + const ResourceAccess& before, + const ResourceAccess& after) { + if (resource.type == RenderGraphResource::Type::Texture && before.layout != after.layout) { + return true; + } + return accessIncludesWrite(before.access) || accessIncludesWrite(after.access); +} - VkImageMemoryBarrier2 imageBarrier{}; - imageBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; - imageBarrier.srcStageMask = barrier.before.stage; - imageBarrier.srcAccessMask = barrier.before.access; - imageBarrier.dstStageMask = barrier.after.stage; - imageBarrier.dstAccessMask = barrier.after.access; - imageBarrier.oldLayout = barrier.before.layout; - imageBarrier.newLayout = barrier.after.layout; - imageBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - imageBarrier.image = resource.image; - imageBarrier.subresourceRange = resource.imageRange; +void emitBarrierBatch( + VkCommandBuffer commandBuffer, + const std::vector& resources, + const std::vector& barriers, + const std::vector& barrierIndices) { + if (barrierIndices.empty()) { + return; + } - VkDependencyInfo dependency{}; - dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; - dependency.imageMemoryBarrierCount = 1; - dependency.pImageMemoryBarriers = &imageBarrier; - vkCmdPipelineBarrier2(commandBuffer, &dependency); - } else if (resource.type == RenderGraphResource::Type::Buffer && resource.buffer != VK_NULL_HANDLE) { - if (barrier.before.stage == barrier.after.stage && - barrier.before.access == barrier.after.access) { - return; - } + std::vector imageBarriers; + std::vector bufferBarriers; + imageBarriers.reserve(barrierIndices.size()); + bufferBarriers.reserve(barrierIndices.size()); - VkBufferMemoryBarrier2 bufferBarrier{}; - bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; - bufferBarrier.srcStageMask = barrier.before.stage; - bufferBarrier.srcAccessMask = barrier.before.access; - bufferBarrier.dstStageMask = barrier.after.stage; - bufferBarrier.dstAccessMask = barrier.after.access; - bufferBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - bufferBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - bufferBarrier.buffer = resource.buffer; - bufferBarrier.offset = resource.bufferOffset; - bufferBarrier.size = resource.size == 0 ? VK_WHOLE_SIZE : resource.size; + for (uint32_t barrierIndex : barrierIndices) { + if (barrierIndex >= barriers.size()) { + continue; + } + const RenderGraphBarrier& barrier = barriers[barrierIndex]; + if (barrier.resource.index >= resources.size()) { + continue; + } + const RenderGraphResource& resource = resources[barrier.resource.index]; + if (!barrierRequired(resource, barrier.before, barrier.after)) { + continue; + } + if (resource.type == RenderGraphResource::Type::Texture && resource.image != VK_NULL_HANDLE) { + VkImageMemoryBarrier2 imageBarrier{}; + imageBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + imageBarrier.srcStageMask = barrier.before.stage; + imageBarrier.srcAccessMask = barrier.before.access; + imageBarrier.dstStageMask = barrier.after.stage; + imageBarrier.dstAccessMask = barrier.after.access; + imageBarrier.oldLayout = barrier.before.layout; + imageBarrier.newLayout = barrier.after.layout; + imageBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageBarrier.image = resource.image; + imageBarrier.subresourceRange = resource.imageRange; + imageBarriers.push_back(imageBarrier); + } else if (resource.type == RenderGraphResource::Type::Buffer && resource.buffer != VK_NULL_HANDLE) { + VkBufferMemoryBarrier2 bufferBarrier{}; + bufferBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; + bufferBarrier.srcStageMask = barrier.before.stage; + bufferBarrier.srcAccessMask = barrier.before.access; + bufferBarrier.dstStageMask = barrier.after.stage; + bufferBarrier.dstAccessMask = barrier.after.access; + bufferBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufferBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + bufferBarrier.buffer = resource.buffer; + bufferBarrier.offset = resource.bufferOffset; + bufferBarrier.size = resource.size == 0 ? VK_WHOLE_SIZE : resource.size; + bufferBarriers.push_back(bufferBarrier); + } + } + if (!imageBarriers.empty() || !bufferBarriers.empty()) { VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; - dependency.bufferMemoryBarrierCount = 1; - dependency.pBufferMemoryBarriers = &bufferBarrier; + dependency.imageMemoryBarrierCount = static_cast(imageBarriers.size()); + dependency.pImageMemoryBarriers = imageBarriers.data(); + dependency.bufferMemoryBarrierCount = static_cast(bufferBarriers.size()); + dependency.pBufferMemoryBarriers = bufferBarriers.data(); vkCmdPipelineBarrier2(commandBuffer, &dependency); } } @@ -348,6 +404,8 @@ ResourceAccess resourceAccessFor(ResourceState state, PipelineDomain domain) { return {domain == PipelineDomain::Compute ? VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL}; case ResourceState::UniformBuffer: return {domain == PipelineDomain::Compute ? VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT : VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT, VK_ACCESS_2_UNIFORM_READ_BIT, VK_IMAGE_LAYOUT_UNDEFINED}; + case ResourceState::RayTracingRead: + return {VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_2_SHADER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL}; case ResourceState::RayTracing: return {VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL}; case ResourceState::ComputeShaderRead: @@ -378,7 +436,7 @@ RenderGraphPass& RenderGraphPass::addInputAttachment(RenderGraphResourceId id) { RenderGraphPass& RenderGraphPass::addStorageRead(RenderGraphResourceId id, PipelineDomain domain) { const ResourceState state = domain == PipelineDomain::Transfer ? ResourceState::TransferSource - : (domain == PipelineDomain::RayTracing ? ResourceState::RayTracing : (domain == PipelineDomain::Compute ? ResourceState::ComputeShaderRead : ResourceState::ShaderRead)); + : (domain == PipelineDomain::RayTracing ? ResourceState::RayTracingRead : (domain == PipelineDomain::Compute ? ResourceState::ComputeShaderRead : ResourceState::ShaderRead)); return addUse(id, state, PassAccess::Read, domain); } @@ -481,6 +539,8 @@ bool RenderGraph::removePass(const char* name) { void RenderGraph::compile() { compiledPassOrder_.clear(); compiledBarriers_.clear(); + compiledBarrierBatches_.clear(); + compiledFinalBarrierBatch_.clear(); if (passes_.empty()) { compiled_ = true; return; @@ -515,6 +575,17 @@ void RenderGraph::compile() { std::vector> edges(passes_.size()); std::vector indegree(passes_.size(), 0); std::vector lastWriter(resources_.size(), std::numeric_limits::max()); + std::vector> readersSinceWrite(resources_.size()); + const auto addEdge = [&](uint32_t before, uint32_t after) { + if (before == std::numeric_limits::max() || before == after) { + return; + } + std::vector& outgoing = edges[before]; + if (std::find(outgoing.begin(), outgoing.end(), after) == outgoing.end()) { + outgoing.push_back(after); + ++indegree[after]; + } + }; for (uint32_t passIndex = 0; passIndex < passes_.size(); ++passIndex) { if (live[passIndex] == 0) { @@ -525,12 +596,18 @@ void RenderGraph::compile() { throw std::runtime_error("RenderGraph pass references a missing resource"); } const uint32_t writer = lastWriter[use.resource.index]; - if (writer != std::numeric_limits::max() && writer != passIndex) { - edges[writer].push_back(passIndex); - ++indegree[passIndex]; - } + addEdge(writer, passIndex); if (writesResource(use)) { + for (uint32_t reader : readersSinceWrite[use.resource.index]) { + addEdge(reader, passIndex); + } + readersSinceWrite[use.resource.index].clear(); lastWriter[use.resource.index] = passIndex; + } else if (readsResource(use)) { + std::vector& readers = readersSinceWrite[use.resource.index]; + if (std::find(readers.begin(), readers.end(), passIndex) == readers.end()) { + readers.push_back(passIndex); + } } } } @@ -556,43 +633,60 @@ void RenderGraph::compile() { throw std::runtime_error("RenderGraph compile failed: cycle detected"); } - std::vector previousUse(resources_.size()); + std::vector previousAccess(resources_.size()); std::vector previousUsePass(resources_.size(), 0); + std::vector previousUseQueue(resources_.size(), RenderGraphQueueDomain::Graphics); + std::vector previousUsesAreReads(resources_.size(), 0); std::vector hasPreviousUse(resources_.size(), 0); for (uint32_t resourceIndex = 0; resourceIndex < resources_.size(); ++resourceIndex) { const RenderGraphResource& resource = resources_[resourceIndex]; if (!resource.hasInitialAccess) { continue; } - previousUse[resourceIndex] = RenderGraphResourceUse{ - .resource = RenderGraphResourceId{resourceIndex}, - .state = ResourceState::Undefined, - .access = PassAccess::ReadWrite, - .domain = PipelineDomain::Graphics, - }; + previousAccess[resourceIndex] = resource.initialAccess; previousUsePass[resourceIndex] = std::numeric_limits::max(); + previousUsesAreReads[resourceIndex] = accessIncludesWrite(resource.initialAccess.access) ? 0 : 1; hasPreviousUse[resourceIndex] = 1; } for (uint32_t passIndex : compiledPassOrder_) { for (const RenderGraphResourceUse& use : passes_[passIndex].uses()) { + const ResourceAccess currentAccess = resourceAccessFor(use.state, use.domain); + const bool currentIsReadOnly = readsResource(use) && !writesResource(use); if (hasPreviousUse[use.resource.index] != 0) { - const RenderGraphResourceUse& previous = previousUse[use.resource.index]; - compiledBarriers_.push_back(RenderGraphBarrier{ + RenderGraphBarrier barrier{ .resource = use.resource, .beforePass = previousUsePass[use.resource.index], .afterPass = passIndex, - .before = previousUsePass[use.resource.index] == std::numeric_limits::max() - ? resources_[use.resource.index].initialAccess - : resourceAccessFor(previous.state, previous.domain), - .after = resourceAccessFor(use.state, use.domain), - .beforeQueue = previousUsePass[use.resource.index] == std::numeric_limits::max() - ? RenderGraphQueueDomain::Graphics - : passes_[previousUsePass[use.resource.index]].queueDomain(), + .before = previousAccess[use.resource.index], + .after = currentAccess, + .beforeQueue = previousUseQueue[use.resource.index], .afterQueue = passes_[passIndex].queueDomain(), - }); + }; + const bool samePass = previousUsePass[use.resource.index] == passIndex; + const bool needsBarrier = barrierRequired(resources_[use.resource.index], barrier.before, barrier.after); + if (!samePass && needsBarrier) { + compiledBarriers_.push_back(barrier); + } + + const bool accumulateReaders = !samePass && !needsBarrier && + previousUsesAreReads[use.resource.index] != 0 && currentIsReadOnly; + if (samePass || accumulateReaders) { + previousAccess[use.resource.index].stage |= currentAccess.stage; + previousAccess[use.resource.index].access |= currentAccess.access; + if (samePass && previousAccess[use.resource.index].layout != currentAccess.layout) { + throw std::runtime_error("RenderGraph pass uses one resource with conflicting image layouts"); + } + previousUsePass[use.resource.index] = passIndex; + previousUseQueue[use.resource.index] = passes_[passIndex].queueDomain(); + previousUsesAreReads[use.resource.index] = + previousUsesAreReads[use.resource.index] != 0 && currentIsReadOnly ? 1 : 0; + continue; + } } - previousUse[use.resource.index] = use; + previousAccess[use.resource.index] = currentAccess; previousUsePass[use.resource.index] = passIndex; + previousUseQueue[use.resource.index] = passes_[passIndex].queueDomain(); + previousUsesAreReads[use.resource.index] = currentIsReadOnly ? 1 : 0; hasPreviousUse[use.resource.index] = 1; } } @@ -601,18 +695,28 @@ void RenderGraph::compile() { if (!resource.hasFinalAccess || hasPreviousUse[resourceIndex] == 0) { continue; } - const RenderGraphResourceUse& previous = previousUse[resourceIndex]; - compiledBarriers_.push_back(RenderGraphBarrier{ + RenderGraphBarrier barrier{ .resource = RenderGraphResourceId{resourceIndex}, .beforePass = previousUsePass[resourceIndex], .afterPass = std::numeric_limits::max(), - .before = resourceAccessFor(previous.state, previous.domain), + .before = previousAccess[resourceIndex], .after = resource.finalAccess, - .beforeQueue = previousUsePass[resourceIndex] == std::numeric_limits::max() - ? RenderGraphQueueDomain::Graphics - : passes_[previousUsePass[resourceIndex]].queueDomain(), + .beforeQueue = previousUseQueue[resourceIndex], .afterQueue = RenderGraphQueueDomain::Graphics, - }); + }; + if (barrierRequired(resources_[resourceIndex], barrier.before, barrier.after)) { + compiledBarriers_.push_back(barrier); + } + } + + compiledBarrierBatches_.resize(passes_.size()); + for (uint32_t barrierIndex = 0; barrierIndex < compiledBarriers_.size(); ++barrierIndex) { + const uint32_t afterPass = compiledBarriers_[barrierIndex].afterPass; + if (afterPass == std::numeric_limits::max()) { + compiledFinalBarrierBatch_.push_back(barrierIndex); + } else if (afterPass < compiledBarrierBatches_.size()) { + compiledBarrierBatches_[afterPass].push_back(barrierIndex); + } } compiled_ = true; @@ -688,10 +792,16 @@ void RenderGraph::compile() { } for (uint32_t i = 0; i < resources_.size(); ++i) { + if (!resourceLifetimes_[i].aliasEligible) { + continue; + } if (resourceLifetimes_[i].firstUsePass == UINT32_MAX) { continue; } for (uint32_t j = 0; j < i; ++j) { + if (!resourceLifetimes_[j].aliasEligible) { + continue; + } if (resourceLifetimes_[j].firstUsePass == UINT32_MAX) { continue; } @@ -752,10 +862,8 @@ void RenderGraph::execute(VkCommandBuffer commandBuffer, uint64_t frameIndex) { .graph = this, }; for (uint32_t passIndex : compiledPassOrder_) { - for (const RenderGraphBarrier& barrier : compiledBarriers_) { - if (barrier.afterPass == passIndex && barrier.resource.index < resources_.size()) { - emitBarrier(commandBuffer, resources_[barrier.resource.index], barrier); - } + if (passIndex < compiledBarrierBatches_.size()) { + emitBarrierBatch(commandBuffer, resources_, compiledBarriers_, compiledBarrierBatches_[passIndex]); } const RenderGraphPass::ExecuteCallback& callback = passes_[passIndex].callback(); if (callback) { @@ -769,11 +877,7 @@ void RenderGraph::execute(VkCommandBuffer commandBuffer, uint64_t frameIndex) { traceRenderGraphPass("end", passes_[passIndex].name()); } } - for (const RenderGraphBarrier& barrier : compiledBarriers_) { - if (barrier.afterPass == std::numeric_limits::max() && barrier.resource.index < resources_.size()) { - emitBarrier(commandBuffer, resources_[barrier.resource.index], barrier); - } - } + emitBarrierBatch(commandBuffer, resources_, compiledBarriers_, compiledFinalBarrierBatch_); } void RenderGraph::setAsyncComputeQueue(VkQueue queue, uint32_t familyIndex) { @@ -821,10 +925,8 @@ void RenderGraph::executeAsync(VkCommandBuffer graphicsCommandBuffer, VkCommandB queueDomain == RenderGraphQueueDomain::SameFamilyCompute; VkCommandBuffer targetCmd = computeDomain ? computeCommandBuffer : graphicsCommandBuffer; - for (const RenderGraphBarrier& barrier : compiledBarriers_) { - if (barrier.afterPass == passIndex && barrier.resource.index < resources_.size()) { - emitBarrier(targetCmd, resources_[barrier.resource.index], barrier); - } + if (passIndex < compiledBarrierBatches_.size()) { + emitBarrierBatch(targetCmd, resources_, compiledBarriers_, compiledBarrierBatches_[passIndex]); } const RenderGraphPass::ExecuteCallback& callback = pass.callback(); @@ -840,23 +942,16 @@ void RenderGraph::executeAsync(VkCommandBuffer graphicsCommandBuffer, VkCommandB } } - for (const RenderGraphBarrier& barrier : compiledBarriers_) { - if (barrier.afterPass == std::numeric_limits::max() && barrier.resource.index < resources_.size()) { - emitBarrier(graphicsCommandBuffer, resources_[barrier.resource.index], barrier); - } - } + emitBarrierBatch(graphicsCommandBuffer, resources_, compiledBarriers_, compiledFinalBarrierBatch_); } void RenderGraph::emitCompiledBarriers(VkCommandBuffer commandBuffer) const { if (!compiled_) { throw std::runtime_error("RenderGraph::emitCompiledBarriers called before compile"); } - for (const RenderGraphBarrier& barrier : compiledBarriers_) { - if (barrier.resource.index >= resources_.size()) { - continue; - } - emitBarrier(commandBuffer, resources_[barrier.resource.index], barrier); - } + std::vector allBarrierIndices(compiledBarriers_.size()); + std::iota(allBarrierIndices.begin(), allBarrierIndices.end(), 0u); + emitBarrierBatch(commandBuffer, resources_, compiledBarriers_, allBarrierIndices); } void RenderGraph::reset() { @@ -864,6 +959,8 @@ void RenderGraph::reset() { passes_.clear(); compiledPassOrder_.clear(); compiledBarriers_.clear(); + compiledBarrierBatches_.clear(); + compiledFinalBarrierBatch_.clear(); resourceLifetimes_.clear(); if (transientPool_) { transientPool_->beginFrame(); diff --git a/src/rtv/RenderGraphDump.cpp b/src/rtv/RenderGraphDump.cpp index 688c1ca..5fe3b8f 100644 --- a/src/rtv/RenderGraphDump.cpp +++ b/src/rtv/RenderGraphDump.cpp @@ -6,11 +6,13 @@ #include +#include #include #include #include #include #include +#include namespace rtv { @@ -44,51 +46,84 @@ const char* queueNameForDomain(RenderGraphQueueDomain domain) { return "unknown"; } -float timingForPassName(const GpuFrameTimings& timings, const std::string& name) { - if (name.find("path_trace") == 0) return timings.pathTraceMs; - if (name.find("restir_history_clear") == 0) return timings.restirHistoryClearMs; - if (name.find("restir_gi_clear") == 0) return timings.restirGiClearMs; - if (name.find("restir_gi_temporal") == 0) return timings.restirGiTemporalMs; - if (name.find("restir_spatial_copy") == 0) return timings.restirSpatialCopyMs; - if (name.find("restir_spatial") == 0) return timings.restirSpatialMs; - if (name.find("restir_gi_spatial") == 0) return timings.restirGiSpatialMs; - if (name.find("restir_gi_upsample") == 0) return timings.restirGiUpsampleMs; - if (name.find("restir_gi_final") == 0) return timings.restirGiFinalMs; - if (name.find("restir_gi_counters_readback") == 0) return timings.restirGiCountersReadbackMs; - if (name.find("regir_spatial_reuse") == 0) return timings.regirSpatialReuseMs; - if (name.find("regir_temporal_reuse") == 0) return timings.regirTemporalReuseMs; - if (name.find("regir_build") == 0) return timings.regirBuildMs; - if (name.find("restir_di_temporal") == 0) return timings.restirDiTemporalMs; - if (name.find("restir_di_spatial") == 0) return timings.restirDiSpatialMs; - if (name.find("restir_di_final") == 0) return timings.restirDiFinalMs; - if (name.find("fog") == 0) return timings.fogIntegrateMs; - if (name.find("atmosphere") == 0) return timings.atmosphereMs; - if (name.find("temporal_denoiser") == 0) return timings.denoiserMs; - if (name.find("moment_update") == 0) return timings.momentUpdateMs; - if (name.find("adaptive_sampling_prepare") == 0) return timings.adaptiveSamplingDiagnosticsMs; - if (name.find("adaptive_sampling_diagnostics") == 0) return timings.adaptiveSamplingDiagnosticsMs; - if (name.find("adaptive_sampling_fill") == 0) return timings.adaptiveSamplingFillMs; - if (name.find("skip_denoiser_copy") == 0) return timings.skipDenoiserCopyMs; - if (name.find("history_copy") == 0) return timings.historyCopyMs; - if (name.find("taa_history_copy") == 0) return timings.taaHistoryCopyMs; - if (name.find("taa") == 0) return timings.taaMs; - if (name.find("auto_exposure_histogram_clear") == 0) return timings.autoExposureHistogramClearMs; - if (name.find("auto_exposure_histogram") == 0) return timings.autoExposureHistogramMs; - if (name.find("auto_exposure_reduce") == 0) return timings.autoExposureReduceMs; - if (name.find("tone_map") == 0) return timings.toneMapMs; - if (name.find("selection_outline") == 0) return timings.selectionOutlineMs; - if (name.find("fullscreen") == 0) return timings.fullscreenMs; - if (name.find("editor_presentation") == 0) return timings.editorPresentationMs; - if (name == "wavefront_trace_rt") return timings.wavefrontTraceMs; - if (name == "wavefront_secondary_trace_rt") return timings.wavefrontSecondaryTraceMs; - if (name == "wavefront_sorted_trace_rt") return timings.wavefrontSortedTraceMs; - if (name == "wavefront_shadow_trace_rt") return timings.wavefrontShadowTraceMs; - if (name == "wavefront_shade") return timings.wavefrontShadeMs; - if (name == "wavefront_secondary_shade") return timings.wavefrontSecondaryShadeMs; - if (name == "wavefront_sorted_shade") return timings.wavefrontSortedShadeMs; - if (name == "wavefront_compact") return timings.wavefrontCompactMs; - if (name == "wavefront_sort") return timings.wavefrontSortMs; - return 0.0f; +struct TimingMapping { + float gpuMs = 0.0f; + const char* profileTimingKey = ""; + const char* timingSource = "unmapped"; + const char* note = ""; + bool mapped = false; +}; + +bool startsWith(const std::string& value, const char* prefix) { + return value.rfind(prefix, 0) == 0; +} + +TimingMapping mappedTiming(const char* key, float gpuMs, const char* note = "") { + return { + .gpuMs = gpuMs, + .profileTimingKey = key, + .timingSource = "per_frame_gpu_timings", + .note = note, + .mapped = true, + }; +} + +TimingMapping timingForPassName(const GpuFrameTimings& timings, const std::string& name) { + if (name == "path_trace_rt" || startsWith(name, "path_trace")) { + return mappedTiming("path_trace", timings.pathTraceMs); + } + if (startsWith(name, "restir_history_clear")) return mappedTiming("restir_history_clear", timings.restirHistoryClearMs); + if (startsWith(name, "restir_gi_clear")) return mappedTiming("restir_gi_clear", timings.restirGiClearMs); + if (startsWith(name, "restir_gi_temporal")) return mappedTiming("restir_gi_temporal", timings.restirGiTemporalMs); + if (startsWith(name, "restir_spatial_copy")) return mappedTiming("restir_spatial_copy", timings.restirSpatialCopyMs); + if (startsWith(name, "restir_spatial")) return mappedTiming("restir_spatial", timings.restirSpatialMs); + if (startsWith(name, "restir_gi_spatial")) return mappedTiming("restir_gi_spatial", timings.restirGiSpatialMs); + if (startsWith(name, "restir_gi_upsample")) return mappedTiming("restir_gi_upsample", timings.restirGiUpsampleMs); + if (startsWith(name, "restir_gi_final")) return mappedTiming("restir_gi_final", timings.restirGiFinalMs); + if (startsWith(name, "restir_gi_counters_readback")) return mappedTiming("restir_gi_counters_readback", timings.restirGiCountersReadbackMs); + if (startsWith(name, "regir_spatial_reuse")) return mappedTiming("regir_spatial_reuse", timings.regirSpatialReuseMs); + if (startsWith(name, "regir_temporal_reuse")) return mappedTiming("regir_temporal_reuse", timings.regirTemporalReuseMs); + if (startsWith(name, "regir_build")) return mappedTiming("regir_build", timings.regirBuildMs); + if (startsWith(name, "restir_di_temporal")) return mappedTiming("restir_di_temporal", timings.restirDiTemporalMs); + if (startsWith(name, "restir_di_spatial")) return mappedTiming("restir_di_spatial", timings.restirDiSpatialMs); + if (startsWith(name, "restir_di_final")) return mappedTiming("restir_di_final", timings.restirDiFinalMs); + if (startsWith(name, "fog")) return mappedTiming("fog_integrate", timings.fogIntegrateMs); + if (startsWith(name, "atmosphere")) return mappedTiming("atmosphere", timings.atmosphereMs, "Aggregate atmosphere timing."); + if (startsWith(name, "temporal_denoiser") || startsWith(name, "nrd_reblur")) { + return mappedTiming("denoiser", timings.denoiserMs, "Aggregate denoiser timing."); + } + if (startsWith(name, "moment_update")) return mappedTiming("moment_update", timings.momentUpdateMs); + if (startsWith(name, "adaptive_sampling_prepare")) return mappedTiming("adaptive_sampling_diagnostics", timings.adaptiveSamplingDiagnosticsMs); + if (startsWith(name, "adaptive_sampling_diagnostics")) return mappedTiming("adaptive_sampling_diagnostics", timings.adaptiveSamplingDiagnosticsMs); + if (startsWith(name, "adaptive_sampling_fill")) return mappedTiming("adaptive_sampling_fill", timings.adaptiveSamplingFillMs); + if (startsWith(name, "skip_denoiser_copy")) return mappedTiming("skip_denoiser_copy", timings.skipDenoiserCopyMs); + if (startsWith(name, "history_copy")) return mappedTiming("history_copy", timings.historyCopyMs); + if (startsWith(name, "taa_history_copy")) return mappedTiming("taa_history_copy", timings.taaHistoryCopyMs); + if (name == "taa_resolve" || startsWith(name, "taa")) return mappedTiming("taa", timings.taaMs); + if (name == "dlss_guides") return mappedTiming("dlss_guides", timings.dlssGuidesMs); + if (name == "dlss_upscale") return mappedTiming("dlss", timings.dlssMs); + if (name == "dlss_rr_guides") return mappedTiming("dlss_rr_guides", timings.dlssRayReconstructionGuidesMs); + if (name == "dlss_ray_reconstruction") return mappedTiming("dlss_rr", timings.dlssRayReconstructionMs); + if (startsWith(name, "nrd_prepare") || startsWith(name, "nrd_resolve")) { + return mappedTiming("denoiser", timings.denoiserMs, "NRD prepare/resolve are currently represented by aggregate denoiser timing."); + } + if (startsWith(name, "auto_exposure_histogram_clear")) return mappedTiming("auto_exposure_histogram_clear", timings.autoExposureHistogramClearMs); + if (startsWith(name, "auto_exposure_histogram")) return mappedTiming("auto_exposure_histogram", timings.autoExposureHistogramMs); + if (startsWith(name, "auto_exposure_reduce")) return mappedTiming("auto_exposure_reduce", timings.autoExposureReduceMs); + if (startsWith(name, "tone_map")) return mappedTiming("tone_map", timings.toneMapMs); + if (startsWith(name, "selection_outline")) return mappedTiming("selection_outline", timings.selectionOutlineMs); + if (startsWith(name, "fullscreen")) return mappedTiming("fullscreen", timings.fullscreenMs); + if (startsWith(name, "editor_presentation")) return mappedTiming("editor_presentation", timings.editorPresentationMs); + if (name == "wavefront_trace_rt") return mappedTiming("wavefront_trace", timings.wavefrontTraceMs); + if (name == "wavefront_secondary_trace_rt") return mappedTiming("wavefront_secondary_trace", timings.wavefrontSecondaryTraceMs); + if (name == "wavefront_sorted_trace_rt") return mappedTiming("wavefront_sorted_trace", timings.wavefrontSortedTraceMs); + if (name == "wavefront_shadow_trace_rt") return mappedTiming("wavefront_shadow_trace", timings.wavefrontShadowTraceMs); + if (name == "wavefront_shade") return mappedTiming("wavefront_shade", timings.wavefrontShadeMs); + if (name == "wavefront_secondary_shade") return mappedTiming("wavefront_secondary_shade", timings.wavefrontSecondaryShadeMs); + if (name == "wavefront_sorted_shade") return mappedTiming("wavefront_sorted_shade", timings.wavefrontSortedShadeMs); + if (name == "wavefront_compact") return mappedTiming("wavefront_compact", timings.wavefrontCompactMs); + if (name == "wavefront_sort") return mappedTiming("wavefront_sort", timings.wavefrontSortMs); + return {}; } const char* formatName(VkFormat format) { @@ -150,12 +185,29 @@ bool resourcesAliasCompatible(const RenderGraphResource& a, const RenderGraphRes a.usage == b.usage; } +VkDeviceSize bufferRangeEnd(VkDeviceSize offset, VkDeviceSize size) { + const VkDeviceSize maxValue = std::numeric_limits::max(); + if (size == VK_WHOLE_SIZE || maxValue - offset < size) { + return maxValue; + } + return offset + size; +} + +bool bufferRangesOverlap(const RenderGraphResource& a, const RenderGraphResource& b) { + if (a.buffer == VK_NULL_HANDLE || a.buffer != b.buffer) { + return false; + } + const VkDeviceSize aEnd = bufferRangeEnd(a.bufferOffset, a.size); + const VkDeviceSize bEnd = bufferRangeEnd(b.bufferOffset, b.size); + return a.bufferOffset < bEnd && b.bufferOffset < aEnd; +} + bool resourcesSharePhysicalHandle(const RenderGraphResource& a, const RenderGraphResource& b) { if (a.type != b.type) { return false; } if (a.type == RenderGraphResource::Type::Buffer) { - return a.buffer != VK_NULL_HANDLE && a.buffer == b.buffer; + return bufferRangesOverlap(a, b); } return a.image != VK_NULL_HANDLE && a.image == b.image; } @@ -308,7 +360,14 @@ void dumpRenderGraphJson( } } pj["barriers"] = passBarriers; - pj["gpu_ms"] = timingForPassName(timings, pass.name()); + const TimingMapping timing = timingForPassName(timings, pass.name()); + pj["gpu_ms"] = timing.gpuMs; + pj["gpu_ms_mapped"] = timing.mapped; + pj["timing_source"] = timing.timingSource; + pj["profile_timing_key"] = timing.mapped ? nlohmann::json(timing.profileTimingKey) : nlohmann::json(nullptr); + if (timing.note[0] != '\0') { + pj["timing_note"] = timing.note; + } passesJson.push_back(pj); } @@ -322,6 +381,7 @@ void dumpRenderGraphJson( rj["name"] = res.debugName ? res.debugName : "unnamed"; rj["type"] = resourceTypeName(res.type); rj["lifetime"] = resourceLifetimeName(res.lifetime); + rj["external"] = res.external; if (res.type == RenderGraphResource::Type::Texture) { rj["format"] = static_cast(res.format); rj["extent"] = { {"width", res.extent.width}, {"height", res.extent.height} }; @@ -438,6 +498,397 @@ void dumpRenderGraphJson( } j["barriers"] = barriersJson; + std::vector> resourceReaders(resources.size()); + std::vector> resourceWriters(resources.size()); + nlohmann::json invalidUses = nlohmann::json::array(); + for (uint32_t passIndex : compiledOrder) { + if (passIndex >= passes.size()) { + continue; + } + const auto& pass = passes[passIndex]; + for (const auto& use : pass.uses()) { + if (!use.resource.valid() || use.resource.index >= resources.size()) { + invalidUses.push_back({ + {"pass", pass.name()}, + {"resource_index", use.resource.index}, + {"access", use.access == PassAccess::Read ? "read" : (use.access == PassAccess::Write ? "write" : "read_write")}, + {"domain", pipelineDomainName(use.domain)}, + }); + continue; + } + if (use.access == PassAccess::Read || use.access == PassAccess::ReadWrite) { + resourceReaders[use.resource.index].push_back(pass.name()); + } + if (use.access == PassAccess::Write || use.access == PassAccess::ReadWrite) { + resourceWriters[use.resource.index].push_back(pass.name()); + } + } + } + + nlohmann::json resourceOwnership = nlohmann::json::array(); + nlohmann::json resourcesWithoutWriters = nlohmann::json::array(); + nlohmann::json multiWriterResources = nlohmann::json::array(); + for (uint32_t resourceIndex = 0; resourceIndex < resources.size(); ++resourceIndex) { + const auto& res = resources[resourceIndex]; + const char* resourceName = res.debugName ? res.debugName : "unnamed"; + nlohmann::json ownership = { + {"resource", resourceName}, + {"index", resourceIndex}, + {"lifetime", resourceLifetimeName(res.lifetime)}, + {"external", res.external}, + {"has_initial_access", res.hasInitialAccess}, + {"first_writer", resourceWriters[resourceIndex].empty() ? nlohmann::json(nullptr) : nlohmann::json(resourceWriters[resourceIndex].front())}, + {"writer_count", resourceWriters[resourceIndex].size()}, + {"reader_count", resourceReaders[resourceIndex].size()}, + {"writers", resourceWriters[resourceIndex]}, + {"readers", resourceReaders[resourceIndex]}, + }; + resourceOwnership.push_back(ownership); + + if (!res.external && !res.hasInitialAccess && resourceReaders[resourceIndex].size() > 0 && resourceWriters[resourceIndex].empty()) { + resourcesWithoutWriters.push_back(ownership); + } + if (resourceWriters[resourceIndex].size() > 1) { + multiWriterResources.push_back(ownership); + } + } + const bool ownershipValidationPassed = invalidUses.empty(); + j["resource_ownership_validation"] = { + {"schema_version", 1}, + {"pass_count", passes.size()}, + {"compiled_pass_count", compiledOrder.size()}, + {"resource_count", resources.size()}, + {"invalid_use_count", invalidUses.size()}, + {"resources_without_declared_writer_count", resourcesWithoutWriters.size()}, + {"multi_writer_resource_count", multiWriterResources.size()}, + {"invalid_uses", std::move(invalidUses)}, + {"resources_without_declared_writer", std::move(resourcesWithoutWriters)}, + {"multi_writer_resources", std::move(multiWriterResources)}, + {"resource_ownership", std::move(resourceOwnership)}, + {"passed", ownershipValidationPassed}, + }; + + const uint32_t invalidPassIndex = std::numeric_limits::max(); + std::vector passExecutionPosition(passes.size(), invalidPassIndex); + for (uint32_t orderIndex = 0; orderIndex < compiledOrder.size(); ++orderIndex) { + const uint32_t passIndex = compiledOrder[orderIndex]; + if (passIndex < passExecutionPosition.size()) { + passExecutionPosition[passIndex] = orderIndex; + } + } + auto passIndexValid = [&](uint32_t passIndex) { + return passIndex == invalidPassIndex || passIndex < passes.size(); + }; + auto passExecutionOrder = [&](uint32_t passIndex) { + if (passIndex == invalidPassIndex || passIndex >= passExecutionPosition.size()) { + return invalidPassIndex; + } + return passExecutionPosition[passIndex]; + }; + auto passNameJson = [&](uint32_t passIndex) -> nlohmann::json { + if (passIndex == invalidPassIndex || passIndex >= passes.size()) { + return nullptr; + } + return passes[passIndex].name(); + }; + auto lifetimeResourceEvidence = [&](uint32_t resourceIndex, const TransientResourceLifetime& lifetime) { + const auto& res = resources[resourceIndex]; + return nlohmann::json{ + {"resource", res.debugName ? res.debugName : "unnamed"}, + {"index", resourceIndex}, + {"lifetime", resourceLifetimeName(res.lifetime)}, + {"external", res.external}, + {"first_use_pass", passNameJson(lifetime.firstUsePass)}, + {"last_use_pass", passNameJson(lifetime.lastUsePass)}, + {"first_read_pass", passNameJson(lifetime.firstReadPass)}, + {"last_read_pass", passNameJson(lifetime.lastReadPass)}, + {"first_write_pass", passNameJson(lifetime.firstWritePass)}, + {"last_write_pass", passNameJson(lifetime.lastWritePass)}, + {"alias_eligible", lifetime.aliasEligible}, + {"aliased", lifetime.aliased}, + {"alias_group", lifetime.aliasGroup}, + {"estimated_bytes", lifetime.estimatedBytes}, + }; + }; + auto lifetimeOverlapsByExecution = [&](const TransientResourceLifetime& a, const TransientResourceLifetime& b) { + const uint32_t aFirst = passExecutionOrder(a.firstUsePass); + const uint32_t aLast = passExecutionOrder(a.lastUsePass); + const uint32_t bFirst = passExecutionOrder(b.firstUsePass); + const uint32_t bLast = passExecutionOrder(b.lastUsePass); + if (aFirst == invalidPassIndex || aLast == invalidPassIndex || + bFirst == invalidPassIndex || bLast == invalidPassIndex) { + return true; + } + return !(aLast < bFirst || bLast < aFirst); + }; + + nlohmann::json lifetimeViolations = nlohmann::json::array(); + nlohmann::json lifetimeWarnings = nlohmann::json::array(); + auto addLifetimeViolation = [&](const char* code, const char* message, nlohmann::json evidence) { + lifetimeViolations.push_back({ + {"code", code}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + }; + auto addLifetimeWarning = [&](const char* code, const char* message, nlohmann::json evidence) { + lifetimeWarnings.push_back({ + {"code", code}, + {"message", message}, + {"evidence", std::move(evidence)}, + }); + }; + + uint32_t usedResourceCount = 0; + uint32_t transientResourceCount = 0; + uint32_t aliasEligibleResourceCount = 0; + uint32_t aliasedResourceCount = 0; + uint64_t estimatedTransientBytes = 0; + for (uint32_t resourceIndex = 0; resourceIndex < resources.size(); ++resourceIndex) { + const auto& res = resources[resourceIndex]; + const bool used = !resourceReaders[resourceIndex].empty() || !resourceWriters[resourceIndex].empty(); + const bool expectedAliasEligible = !res.external && res.lifetime == RenderGraphResource::Lifetime::Transient; + if (used) { + ++usedResourceCount; + } + if (res.lifetime == RenderGraphResource::Lifetime::Transient) { + ++transientResourceCount; + } + if (resourceIndex >= lifetimes.size()) { + addLifetimeViolation( + "missing_lifetime_record", + "Every render graph resource must have a compiled lifetime record.", + { + {"resource", res.debugName ? res.debugName : "unnamed"}, + {"index", resourceIndex}, + {"used", used}, + }); + continue; + } + + const TransientResourceLifetime& lifetime = lifetimes[resourceIndex]; + if (lifetime.resourceIndex != resourceIndex) { + addLifetimeViolation( + "lifetime_resource_index_mismatch", + "Compiled lifetime record must point back to its resource index.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (used && (lifetime.firstUsePass == invalidPassIndex || lifetime.lastUsePass == invalidPassIndex)) { + addLifetimeViolation( + "used_resource_missing_use_interval", + "Used resources must report first and last use passes.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (!passIndexValid(lifetime.firstUsePass) || + !passIndexValid(lifetime.lastUsePass) || + !passIndexValid(lifetime.firstReadPass) || + !passIndexValid(lifetime.lastReadPass) || + !passIndexValid(lifetime.firstWritePass) || + !passIndexValid(lifetime.lastWritePass)) { + addLifetimeViolation( + "lifetime_pass_index_out_of_range", + "Lifetime pass indices must reference compiled graph passes or be unset.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + const uint32_t firstUseOrder = passExecutionOrder(lifetime.firstUsePass); + const uint32_t lastUseOrder = passExecutionOrder(lifetime.lastUsePass); + if (used && firstUseOrder != invalidPassIndex && lastUseOrder != invalidPassIndex && firstUseOrder > lastUseOrder) { + addLifetimeViolation( + "lifetime_interval_inverted", + "Resource lifetime intervals must be ordered by compiled execution order.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (!resourceReaders[resourceIndex].empty() && + (lifetime.firstReadPass == invalidPassIndex || lifetime.lastReadPass == invalidPassIndex)) { + addLifetimeViolation( + "resource_readers_missing_read_interval", + "Resources with readers must report first and last read passes.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (!resourceWriters[resourceIndex].empty() && + (lifetime.firstWritePass == invalidPassIndex || lifetime.lastWritePass == invalidPassIndex)) { + addLifetimeViolation( + "resource_writers_missing_write_interval", + "Resources with writers must report first and last write passes.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + auto intervalWithinUse = [&](uint32_t passIndex) { + const uint32_t order = passExecutionOrder(passIndex); + return passIndex == invalidPassIndex || + (firstUseOrder != invalidPassIndex && + lastUseOrder != invalidPassIndex && + order != invalidPassIndex && + firstUseOrder <= order && + order <= lastUseOrder); + }; + if (!intervalWithinUse(lifetime.firstReadPass) || + !intervalWithinUse(lifetime.lastReadPass) || + !intervalWithinUse(lifetime.firstWritePass) || + !intervalWithinUse(lifetime.lastWritePass)) { + addLifetimeViolation( + "read_write_interval_outside_use_interval", + "Read/write lifetime intervals must be contained inside the first/last use interval.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (!used && lifetime.firstUsePass != invalidPassIndex) { + addLifetimeWarning( + "unused_resource_has_lifetime_interval", + "A resource with no compiled readers or writers still reports a lifetime interval.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (lifetime.aliasEligible != expectedAliasEligible) { + addLifetimeViolation( + "alias_eligibility_mismatch", + "Only non-external transient resources may be alias-eligible.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (!expectedAliasEligible && (lifetime.aliased || lifetime.aliasGroup != 0u)) { + addLifetimeViolation( + "non_transient_resource_alias_state", + "Persistent, temporal, or external resources must not be assigned transient alias state.", + lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (lifetime.aliasEligible) { + ++aliasEligibleResourceCount; + estimatedTransientBytes += static_cast(lifetime.estimatedBytes); + } + if (lifetime.aliased) { + ++aliasedResourceCount; + } + } + + nlohmann::json aliasGroups = nlohmann::json::array(); + uint32_t aliasGroupCount = 0; + uint64_t estimatedAliasSavingsBytes = 0; + std::vector observedAliasGroups; + for (const TransientResourceLifetime& lifetime : lifetimes) { + if (lifetime.aliasGroup == 0u || + std::find(observedAliasGroups.begin(), observedAliasGroups.end(), lifetime.aliasGroup) != observedAliasGroups.end()) { + continue; + } + observedAliasGroups.push_back(lifetime.aliasGroup); + } + for (uint32_t aliasGroup : observedAliasGroups) { + std::vector groupResources; + uint64_t groupBytes = 0; + uint64_t groupMaxBytes = 0; + nlohmann::json groupResourceJson = nlohmann::json::array(); + for (uint32_t resourceIndex = 0; resourceIndex < lifetimes.size() && resourceIndex < resources.size(); ++resourceIndex) { + const TransientResourceLifetime& lifetime = lifetimes[resourceIndex]; + if (lifetime.aliasGroup != aliasGroup) { + continue; + } + groupResources.push_back(resourceIndex); + groupBytes += static_cast(lifetime.estimatedBytes); + groupMaxBytes = std::max(groupMaxBytes, static_cast(lifetime.estimatedBytes)); + groupResourceJson.push_back(lifetimeResourceEvidence(resourceIndex, lifetime)); + } + if (groupResources.size() > 1) { + ++aliasGroupCount; + estimatedAliasSavingsBytes += groupBytes > groupMaxBytes ? groupBytes - groupMaxBytes : 0ull; + } + for (size_t a = 0; a < groupResources.size(); ++a) { + for (size_t b = a + 1; b < groupResources.size(); ++b) { + const uint32_t resourceA = groupResources[a]; + const uint32_t resourceB = groupResources[b]; + if (!resourcesAliasCompatible(resources[resourceA], resources[resourceB])) { + addLifetimeViolation( + "alias_group_incompatible_resources", + "Resources assigned to the same alias group must have compatible type, format, extent, and usage.", + { + {"alias_group", aliasGroup}, + {"resource_a", lifetimeResourceEvidence(resourceA, lifetimes[resourceA])}, + {"resource_b", lifetimeResourceEvidence(resourceB, lifetimes[resourceB])}, + }); + } + if (lifetimeOverlapsByExecution(lifetimes[resourceA], lifetimes[resourceB])) { + addLifetimeViolation( + "alias_group_lifetime_overlap", + "Resources assigned to the same alias group must not overlap in compiled execution order.", + { + {"alias_group", aliasGroup}, + {"resource_a", lifetimeResourceEvidence(resourceA, lifetimes[resourceA])}, + {"resource_b", lifetimeResourceEvidence(resourceB, lifetimes[resourceB])}, + }); + } + } + } + aliasGroups.push_back({ + {"alias_group", aliasGroup}, + {"resource_count", groupResources.size()}, + {"estimated_group_bytes", groupBytes}, + {"estimated_physical_bytes", groupMaxBytes}, + {"estimated_saved_bytes", groupBytes > groupMaxBytes ? groupBytes - groupMaxBytes : 0ull}, + {"resources", std::move(groupResourceJson)}, + }); + } + + nlohmann::json sharedPhysicalBacking = nlohmann::json::array(); + uint32_t sharedPhysicalBackingPairCount = 0; + uint32_t overlappingSharedPhysicalBackingPairCount = 0; + for (uint32_t i = 0; i < resources.size() && i < lifetimes.size(); ++i) { + if (lifetimes[i].firstUsePass == invalidPassIndex) { + continue; + } + for (uint32_t k = i + 1; k < resources.size() && k < lifetimes.size(); ++k) { + if (lifetimes[k].firstUsePass == invalidPassIndex || !resourcesSharePhysicalHandle(resources[i], resources[k])) { + continue; + } + const bool overlap = lifetimeOverlapsByExecution(lifetimes[i], lifetimes[k]); + const bool graphControlledAlias = + lifetimes[i].aliasEligible && + lifetimes[k].aliasEligible && + lifetimes[i].aliasGroup != 0u && + lifetimes[i].aliasGroup == lifetimes[k].aliasGroup; + ++sharedPhysicalBackingPairCount; + if (overlap) { + ++overlappingSharedPhysicalBackingPairCount; + } + nlohmann::json backingPair = { + {"resource_a", lifetimeResourceEvidence(i, lifetimes[i])}, + {"resource_b", lifetimeResourceEvidence(k, lifetimes[k])}, + {"lifetimes_overlap", overlap}, + {"graph_controlled_alias", graphControlledAlias}, + }; + sharedPhysicalBacking.push_back(backingPair); + if (overlap && (resources[i].external || resources[k].external)) { + addLifetimeWarning( + "external_shared_physical_backing_overlap", + "External persistent resources share the same physical backing over overlapping lifetimes; this is reported separately from graph-controlled transient aliasing.", + backingPair); + } else if (overlap && !graphControlledAlias) { + addLifetimeViolation( + "internal_shared_physical_backing_overlap", + "Internal resources that share physical backing over overlapping lifetimes must be represented by an explicit graph-controlled alias contract.", + backingPair); + } + } + } + + const bool lifetimeValidationPassed = lifetimeViolations.empty(); + j["resource_lifetime_validation"] = { + {"schema_version", 1}, + {"resource_count", resources.size()}, + {"lifetime_count", lifetimes.size()}, + {"used_resource_count", usedResourceCount}, + {"transient_resource_count", transientResourceCount}, + {"aliasing_enabled", graph.aliasingEnabled()}, + {"alias_eligible_resource_count", aliasEligibleResourceCount}, + {"aliased_resource_count", aliasedResourceCount}, + {"alias_group_count", aliasGroupCount}, + {"estimated_transient_bytes", estimatedTransientBytes}, + {"estimated_alias_savings_bytes", estimatedAliasSavingsBytes}, + {"alias_groups", std::move(aliasGroups)}, + {"shared_physical_backing_pair_count", sharedPhysicalBackingPairCount}, + {"overlapping_shared_physical_backing_pair_count", overlappingSharedPhysicalBackingPairCount}, + {"shared_physical_backing", std::move(sharedPhysicalBacking)}, + {"violation_count", lifetimeViolations.size()}, + {"warning_count", lifetimeWarnings.size()}, + {"violations", std::move(lifetimeViolations)}, + {"warnings", std::move(lifetimeWarnings)}, + {"passed", lifetimeValidationPassed}, + }; + const auto dir = outputPath.parent_path(); if (!dir.empty()) { std::filesystem::create_directories(dir); @@ -476,7 +927,7 @@ void dumpRenderGraphDot( const auto& pass = passes[passIndex]; const RenderGraphQueueDomain domain = pass.queueDomain(); - float gpuMs = timingForPassName(timings, pass.name()); + const TimingMapping timing = timingForPassName(timings, pass.name()); file << " \"" << pass.name() << "\" [fillcolor=" << domainColor(domain) << ", label=\"" << pass.name() << "\\n("; switch (domain) { @@ -486,7 +937,7 @@ void dumpRenderGraphDot( case RenderGraphQueueDomain::Graphics: file << "Graphics"; break; case RenderGraphQueueDomain::Transfer: file << "Transfer"; break; } - file << ")\\n" << gpuMs << "ms\"];\n"; + file << ")\\n" << timing.gpuMs << "ms\"];\n"; } file << "\n"; diff --git a/src/rtv/RenderSettingsPanel.cpp b/src/rtv/RenderSettingsPanel.cpp index 428e5d0..f8b97c9 100644 --- a/src/rtv/RenderSettingsPanel.cpp +++ b/src/rtv/RenderSettingsPanel.cpp @@ -9,12 +9,28 @@ #include #include +#include #include +#include namespace rtv { namespace { +std::string gRenderSettingsFilter; +bool gRenderSettingsRowTableOpen = false; + +bool containsInsensitive(const char* text, const std::string& needle) { + if (needle.empty()) { + return true; + } + std::string haystack = text != nullptr ? text : ""; + std::transform(haystack.begin(), haystack.end(), haystack.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return haystack.find(needle) != std::string::npos; +} + void tooltip(const char* text) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { ImGui::SetTooltip("%s", text); @@ -29,6 +45,111 @@ const char* serReorderingHintName(VkRayTracingInvocationReorderModeNV hint) { } } +float renderSettingsControlWidth() { + const float available = ImGui::GetContentRegionAvail().x; + return std::clamp(available * 0.62f, 190.0f, 390.0f); +} + +float renderSettingsLabelWidth() { + const float available = ImGui::GetContentRegionAvail().x; + return std::clamp(available * 0.38f, 128.0f, 210.0f); +} + +void renderSettingsBeginRow(const char* label) { + ImGui::PushID(label); + gRenderSettingsRowTableOpen = ImGui::BeginTable( + "##PropertyRow", + 2, + ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoSavedSettings); + if (gRenderSettingsRowTableOpen) { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch, 0.42f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.58f); + ImGui::TableNextRow(ImGuiTableRowFlags_None, EditorUiMetric::propertyRowHeight); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-FLT_MIN); + } +} + +void renderSettingsEndRow() { + if (gRenderSettingsRowTableOpen) { + ImGui::EndTable(); + gRenderSettingsRowTableOpen = false; + } + ImGui::PopID(); +} + +bool renderSettingsComboRow(const char* label, int* currentItem, const char* const items[], int itemsCount) { + renderSettingsBeginRow(label); + const bool changed = ImGui::Combo("##value", currentItem, items, itemsCount); + renderSettingsEndRow(); + return changed; +} + +bool renderSettingsComboRow(const char* label, int* currentItem, const char* itemsSeparatedByZeros) { + renderSettingsBeginRow(label); + const bool changed = ImGui::Combo("##value", currentItem, itemsSeparatedByZeros); + renderSettingsEndRow(); + return changed; +} + +bool renderSettingsSliderFloatRow(const char* label, float* value, float minValue, float maxValue, const char* format) { + renderSettingsBeginRow(label); + const bool changed = ImGui::SliderFloat("##value", value, minValue, maxValue, format); + renderSettingsEndRow(); + return changed; +} + +bool renderSettingsSliderScalarRow(const char* label, ImGuiDataType dataType, void* value, const void* minValue, const void* maxValue, const char* format = nullptr) { + renderSettingsBeginRow(label); + const bool changed = ImGui::SliderScalar("##value", dataType, value, minValue, maxValue, format); + renderSettingsEndRow(); + return changed; +} + +bool renderSettingsCheckboxRow(const char* label, bool* value) { + renderSettingsBeginRow(label); + const bool changed = ImGui::Checkbox("##value", value); + renderSettingsEndRow(); + return changed; +} + +void renderSettingsInfoRow(const char* label, const char* value) { + renderSettingsBeginRow(label); + ImGui::TextUnformatted(value); + renderSettingsEndRow(); +} + +bool renderSettingsBeginSection(const char* label, ImGuiTreeNodeFlags flags = 0, const char* keywords = nullptr) { + if (!gRenderSettingsFilter.empty() && + !containsInsensitive(label, gRenderSettingsFilter) && + !containsInsensitive(keywords, gRenderSettingsFilter)) { + return false; + } + ImGui::Spacing(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 6.0f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.120f, 0.137f, 0.160f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.160f, 0.180f, 0.212f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.160f, 0.300f, 0.490f, 0.96f)); + const bool open = ImGui::CollapsingHeader(label, flags); + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); + if (open) { + ImGui::Indent(8.0f); + ImGui::Spacing(); + ImGui::PushItemWidth(renderSettingsControlWidth()); + } + return open; +} + +void renderSettingsEndSection() { + ImGui::PopItemWidth(); + ImGui::Unindent(8.0f); + ImGui::Spacing(); +} + } // namespace void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& requests) { @@ -209,59 +330,54 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques const bool reflexCanRequest = nvidiaStatus.streamlineReflex.requestable || nvidiaStatus.streamlineReflex.supported; const bool nvperfCanRequest = nvidiaStatus.streamlineNvPerf.requestable || nvidiaStatus.streamlineNvPerf.supported; - ImGui::SeparatorText("Preview Actions"); - if (editorIconTextButton("RenderSettingsResetAccumulation", EditorGlyphIcon::Reset, "Reset Accumulation")) { - requests.resetAccumulation = AccumulationResetReason::Manual; - } - tooltip("Clear path tracing accumulation and rebuild the current preview from a fresh sample history."); - ImGui::SameLine(); - if (editorIconTextButton("RenderSettingsCycleDebugView", EditorGlyphIcon::DrawDebug, "Debug View")) { - requests.toggleDebugView = true; - } - tooltip("Cycle the active renderer debug view for the current viewport preview."); - if (editorIconTextButton("RenderSettingsCycleIntermediate", EditorGlyphIcon::Stats, "Intermediate")) { - requests.cycleIntermediateView = true; - } - tooltip("Cycle intermediate render targets exposed by the renderer diagnostic path."); - ImGui::SameLine(); - if (editorIconTextButton("RenderSettingsToggleDenoiser", EditorGlyphIcon::Render, "Denoiser", settings.denoiserEnabled)) { - requests.toggleDenoiser = true; - } - tooltip("Toggle denoising without leaving the docked Render Settings workflow."); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(7.0f, 4.0f)); + + static std::array settingsSearch{}; + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##RenderSettingsSearch", "Search render settings...", settingsSearch.data(), settingsSearch.size()); + gRenderSettingsFilter = settingsSearch.data(); + std::transform(gRenderSettingsFilter.begin(), gRenderSettingsFilter.end(), gRenderSettingsFilter.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + ImGui::Spacing(); - ImGui::SeparatorText("Rendering"); + if (renderSettingsBeginSection("Quality", ImGuiTreeNodeFlags_DefaultOpen, "preset debug bounces kernel samples spp resolution scale")) { + ImGui::SeparatorText("Preset"); const char* renderPresetItems[] = {"Custom", "Low", "Balanced", "Ultra", "Native 30"}; int renderPresetIndex = static_cast(settings.renderPreset); if (renderPresetIndex < 0 || renderPresetIndex > 4) { renderPresetIndex = 0; } - if (ImGui::Combo("Render Preset", &renderPresetIndex, renderPresetItems, 5)) { + if (renderSettingsComboRow("Render Preset", &renderPresetIndex, renderPresetItems, 5)) { applyRenderPreset(settings, static_cast(renderPresetIndex)); presetApplied = true; changed = true; } tooltip("Game-ready presets tune path tracing, ReSTIR, denoiser, TAA, and render scale together."); - editorDebugViewCombo("Debug View", settings, changed); - changed |= ImGui::SliderScalar("Max Bounces", ImGuiDataType_U32, &settings.maxBounces, &minBounces, &maxBounces); + renderSettingsBeginRow("Debug View"); + editorDebugViewCombo("##value", settings, changed); + renderSettingsEndRow(); + changed |= renderSettingsSliderScalarRow("Max Bounces", ImGuiDataType_U32, &settings.maxBounces, &minBounces, &maxBounces); tooltip("Number of ray bounces. Higher is more accurate and slower; 4-8 for preview, 16 for final."); const char* pathTraceKernelItems[] = {"Generic", "Native2B"}; int pathTraceKernelIndex = static_cast(settings.pathTraceKernelMode); if (pathTraceKernelIndex < 0 || pathTraceKernelIndex > 1) { pathTraceKernelIndex = 0; } - if (ImGui::Combo("Path Trace Kernel", &pathTraceKernelIndex, pathTraceKernelItems, 2)) { + if (renderSettingsComboRow("Path Trace Kernel", &pathTraceKernelIndex, pathTraceKernelItems, 2)) { settings.pathTraceKernelMode = static_cast(pathTraceKernelIndex); changed = true; } tooltip("Native2B is a strict native 1 SPP, 2-bounce beauty specialization. It falls back unless the current settings match its quality-safe gates."); const PathTraceKernelMode effectiveKernel = state.renderer.effectivePathTraceKernelMode(); - ImGui::TextDisabled("Effective: %s", pathTraceKernelModeName(effectiveKernel)); + renderSettingsInfoRow("Effective Kernel", pathTraceKernelModeName(effectiveKernel)); if (settings.pathTraceKernelMode == PathTraceKernelMode::Native2B) { const char* fallbackReason = state.renderer.pathTraceKernelFallbackReason(); if (fallbackReason != nullptr && fallbackReason[0] != '\0') { - ImGui::TextDisabled("Fallback: %s", fallbackReason); + renderSettingsInfoRow("Fallback", fallbackReason); } else { - ImGui::TextDisabled("Terminal payload: %s", state.renderer.native2BTerminalPayloadActive() ? "active" : "inactive"); + renderSettingsInfoRow("Terminal Payload", state.renderer.native2BTerminalPayloadActive() ? "active" : "inactive"); } } const char* blendedDecalShadowItems[] = {"Exact", "Opaque Shadow", "Alpha Cutout Proxy"}; @@ -269,7 +385,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques if (blendedDecalShadowIndex < 0 || blendedDecalShadowIndex > 2) { blendedDecalShadowIndex = 0; } - if (ImGui::Combo("Blended Decal Shadows", &blendedDecalShadowIndex, blendedDecalShadowItems, 3)) { + if (renderSettingsComboRow("Blended Decal Shadows", &blendedDecalShadowIndex, blendedDecalShadowItems, 3)) { settings.blendedDecalShadowMode = static_cast(blendedDecalShadowIndex); changed = true; } @@ -279,100 +395,114 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques if (native2BDirectReuseIndex < 0 || native2BDirectReuseIndex > 2) { native2BDirectReuseIndex = 0; } - if (ImGui::Combo("Native2B Direct Reuse", &native2BDirectReuseIndex, native2BDirectReuseItems, 3)) { + if (renderSettingsComboRow("Native2B Direct Reuse", &native2BDirectReuseIndex, native2BDirectReuseItems, 3)) { settings.native2BDirectReuseMode = static_cast(native2BDirectReuseIndex); changed = true; } tooltip("Experimental terminal direct-light reuse. Off keeps the exact estimator."); - changed |= ImGui::SliderFloat("Terminal Direct Rate", &settings.native2BTerminalDirectSampleProbability, 0.25f, 1.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("Terminal Direct Rate", &settings.native2BTerminalDirectSampleProbability, 0.25f, 1.0f, "%.2f"); tooltip("Native2B widened quality/performance gate. 1.00 samples every terminal env/sun direct light; lower values sample stochastically and rely on temporal accumulation."); - changed |= ImGui::SliderScalar("Environment Samples", ImGuiDataType_U32, &settings.environmentDirectSamples, &minEnvSamples, &maxEnvSamples); + ImGui::SeparatorText("Sampling"); + changed |= renderSettingsSliderScalarRow("Environment Samples", ImGuiDataType_U32, &settings.environmentDirectSamples, &minEnvSamples, &maxEnvSamples); tooltip("Environment light samples per bounce. Higher values reduce fireflies."); - changed |= ImGui::Checkbox("Limit to 1 SPP", &settings.limitSamplesPerPixel); + changed |= renderSettingsCheckboxRow("Limit to 1 SPP", &settings.limitSamplesPerPixel); tooltip("Keeps real-time path tracing at one path sample per pixel per frame. Disable for stills or high-end budgets."); - changed |= ImGui::SliderScalar("Samples Per Pixel", ImGuiDataType_U32, &settings.samplesPerPixel, &minSpp, &maxSpp); + changed |= renderSettingsSliderScalarRow("Samples Per Pixel", ImGuiDataType_U32, &settings.samplesPerPixel, &minSpp, &maxSpp); tooltip("Requested path samples per pixel per frame when the 1 SPP limiter is disabled."); - changed |= ImGui::Checkbox("Path Tracing", &settings.pathTracingEnabled); - changed |= ImGui::Checkbox("TAA Camera Jitter", &settings.cameraJitterEnabled); + renderSettingsEndSection(); + } + + if (renderSettingsBeginSection("Lighting", ImGuiTreeNodeFlags_DefaultOpen, "path tracing jitter direct indirect environment samples mis light")) { + changed |= renderSettingsCheckboxRow("Path Tracing", &settings.pathTracingEnabled); + changed |= renderSettingsCheckboxRow("TAA Camera Jitter", &settings.cameraJitterEnabled); tooltip("Halton sub-pixel jitter. It is only applied while TAA is enabled."); - changed |= ImGui::Checkbox("Direct Lighting", &settings.directLightingEnabled); - changed |= ImGui::Checkbox("Secondary Bounce Direct", &settings.secondaryDirectLightingEnabled); + changed |= renderSettingsCheckboxRow("Direct Lighting", &settings.directLightingEnabled); + changed |= renderSettingsCheckboxRow("Secondary Bounce Direct", &settings.secondaryDirectLightingEnabled); tooltip("Samples direct lighting at secondary surface hits so two-bounce renders keep bounce illumination."); - changed |= ImGui::SliderFloat("Indirect Strength", &settings.indirectStrength, 0.0f, 4.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("Indirect Strength", &settings.indirectStrength, 0.0f, 4.0f, "%.2f"); tooltip("Multiplier for indirect lighting contribution."); - const char* restirModeItems[] = {"Classic NEE", "ReSTIR Only", "Hybrid Compare"}; - int restirModeIndex = static_cast(settings.restirMode); - if (restirModeIndex < 0 || restirModeIndex > 2) { - restirModeIndex = 0; + renderSettingsEndSection(); } - if (ImGui::Combo("ReSTIR Mode", &restirModeIndex, restirModeItems, 3)) { - settings.restirMode = static_cast(restirModeIndex); - changed = true; - } - tooltip("Hybrid ReSTIR direct-light mode. Classic NEE remains the reference baseline."); - const char* restirDiModeItems[] = {"Off", "Legacy", "Production", "Reference Validation", "Hybrid Compare"}; - int restirDiModeIndex = static_cast(settings.restirDiMode); - if (ImGui::Combo("ReSTIR DI Pipeline", &restirDiModeIndex, restirDiModeItems, 5)) { - settings.restirDiMode = static_cast(restirDiModeIndex); - if (settings.restirDiMode == RestirDiMode::Off || settings.restirDiMode == RestirDiMode::Legacy) { - settings.restirDiReservoirLayout = RestirDiReservoirLayout::Legacy; - } else if (settings.restirDiMode == RestirDiMode::ReferenceValidation) { - settings.restirDiReservoirLayout = RestirDiReservoirLayout::ValidationFull; - settings.restirDiFinalVisibilityEnabled = true; - settings.restirDiProductionStabilizationEnabled = false; - } else if (settings.restirDiReservoirLayout == RestirDiReservoirLayout::Legacy) { - settings.restirDiReservoirLayout = RestirDiReservoirLayout::ProductionPacked; + + if (renderSettingsBeginSection("ReSTIR", ImGuiTreeNodeFlags_DefaultOpen, "restir di gi reservoir temporal spatial reuse visibility")) { + if (ImGui::TreeNodeEx("Direct Lighting", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth)) { + const char* restirModeItems[] = {"Classic NEE", "ReSTIR Only", "Hybrid Compare"}; + int restirModeIndex = static_cast(settings.restirMode); + if (restirModeIndex < 0 || restirModeIndex > 2) { + restirModeIndex = 0; } - changed = true; - } - tooltip("Selects the rollback path, shipping ReSTIR DI pipeline, strict reference estimator, or comparison mode."); - if (settings.restirDiMode == RestirDiMode::Production || - settings.restirDiMode == RestirDiMode::ReferenceValidation || - settings.restirDiMode == RestirDiMode::HybridCompare) { - if (ImGui::CollapsingHeader("ReSTIR DI Tuning")) { - const char* layoutItems[] = {"Legacy", "Production Packed", "Validation Full"}; - int layoutIndex = static_cast(settings.restirDiReservoirLayout); - if (ImGui::Combo("DI Reservoir Layout", &layoutIndex, layoutItems, 3)) { - const auto requested = static_cast(layoutIndex); - if (requested != RestirDiReservoirLayout::Legacy && - (settings.restirDiMode != RestirDiMode::ReferenceValidation || - requested == RestirDiReservoirLayout::ValidationFull)) { - settings.restirDiReservoirLayout = requested; - changed = true; + if (renderSettingsComboRow("ReSTIR Mode", &restirModeIndex, restirModeItems, 3)) { + settings.restirMode = static_cast(restirModeIndex); + changed = true; + } + tooltip("Hybrid ReSTIR direct-light mode. Classic NEE remains the reference baseline."); + const char* restirDiModeItems[] = {"Off", "Legacy", "Production", "Reference Validation", "Hybrid Compare"}; + int restirDiModeIndex = static_cast(settings.restirDiMode); + if (renderSettingsComboRow("ReSTIR DI Pipeline", &restirDiModeIndex, restirDiModeItems, 5)) { + settings.restirDiMode = static_cast(restirDiModeIndex); + if (settings.restirDiMode == RestirDiMode::Off || settings.restirDiMode == RestirDiMode::Legacy) { + settings.restirDiReservoirLayout = RestirDiReservoirLayout::Legacy; + } else if (settings.restirDiMode == RestirDiMode::ReferenceValidation) { + settings.restirDiReservoirLayout = RestirDiReservoirLayout::ValidationFull; + settings.restirDiFinalVisibilityEnabled = true; + settings.restirDiProductionStabilizationEnabled = false; + } else if (settings.restirDiReservoirLayout == RestirDiReservoirLayout::Legacy) { + settings.restirDiReservoirLayout = RestirDiReservoirLayout::ProductionPacked; + } + changed = true; + } + tooltip("Selects the rollback path, shipping ReSTIR DI pipeline, strict reference estimator, or comparison mode."); + if (settings.restirDiMode == RestirDiMode::Production || + settings.restirDiMode == RestirDiMode::ReferenceValidation || + settings.restirDiMode == RestirDiMode::HybridCompare) { + if (ImGui::CollapsingHeader("ReSTIR DI Tuning")) { + const char* layoutItems[] = {"Legacy", "Production Packed", "Validation Full"}; + int layoutIndex = static_cast(settings.restirDiReservoirLayout); + if (renderSettingsComboRow("DI Reservoir Layout", &layoutIndex, layoutItems, 3)) { + const auto requested = static_cast(layoutIndex); + if (requested != RestirDiReservoirLayout::Legacy && + (settings.restirDiMode != RestirDiMode::ReferenceValidation || + requested == RestirDiReservoirLayout::ValidationFull)) { + settings.restirDiReservoirLayout = requested; + changed = true; + } } + tooltip("Production Packed is the shipping ABI. Validation Full is the inspectable reference ABI."); + changed |= renderSettingsCheckboxRow("DI Temporal Reuse", &settings.restirDiTemporalEnabled); + changed |= renderSettingsCheckboxRow("DI Spatial Reuse", &settings.restirDiSpatialEnabled); + changed |= renderSettingsCheckboxRow("DI Final Visibility", &settings.restirDiFinalVisibilityEnabled); + tooltip("Required in Reference Validation; traces current visibility before finalizing a reused sample."); + const uint32_t minRounds = 1u; + const uint32_t maxRounds = 16u; + const uint32_t minAge = 1u; + const uint32_t maxAge = 255u; + const uint32_t minM = 1u; + const uint32_t maxM = 255u; + const uint32_t minVisibilityRays = 0u; + const uint32_t maxVisibilityRays = 4u; + changed |= renderSettingsSliderScalarRow("DI Spatial Rounds", ImGuiDataType_U32, &settings.restirDiSpatialRounds, &minRounds, &maxRounds); + changed |= renderSettingsSliderFloatRow("DI Spatial Radius", &settings.restirDiSpatialRadius, 0.5f, 32.0f, "%.2f"); + changed |= renderSettingsSliderScalarRow("DI Temporal Max Age", ImGuiDataType_U32, &settings.restirDiTemporalMaxAge, &minAge, &maxAge); + changed |= renderSettingsSliderScalarRow("DI Max M", ImGuiDataType_U32, &settings.restirDiMaxM, &minM, &maxM); + changed |= renderSettingsSliderScalarRow("DI Visibility Ray Budget", ImGuiDataType_U32, &settings.restirDiVisibilityRayBudget, &minVisibilityRays, &maxVisibilityRays); + tooltip("Maximum shifted-sample visibility queries per pixel in each DI reuse stage. Zero falls back to the current-frame candidate."); + changed |= renderSettingsCheckboxRow("DI Production Stabilization", &settings.restirDiProductionStabilizationEnabled); + tooltip("Biased temporal and luminance stabilization for production. Reference Validation disables it."); + changed |= renderSettingsSliderFloatRow("DI Luminance Clamp", &settings.restirDiClampLuminance, 0.0f, 1000.0f, "%.1f"); + ImGui::BeginDisabled(); + bool sunOutside = false; + bool environmentOutside = false; + renderSettingsCheckboxRow("Sample Sun In DI", &sunOutside); + renderSettingsCheckboxRow("Sample Environment In DI", &environmentOutside); + ImGui::EndDisabled(); + tooltip("Sun and environment use specialized samplers outside ReSTIR DI and are composed exactly once."); } - tooltip("Production Packed is the shipping ABI. Validation Full is the inspectable reference ABI."); - changed |= ImGui::Checkbox("DI Temporal Reuse", &settings.restirDiTemporalEnabled); - changed |= ImGui::Checkbox("DI Spatial Reuse", &settings.restirDiSpatialEnabled); - changed |= ImGui::Checkbox("DI Final Visibility", &settings.restirDiFinalVisibilityEnabled); - tooltip("Required in Reference Validation; traces current visibility before finalizing a reused sample."); - const uint32_t minRounds = 1u; - const uint32_t maxRounds = 16u; - const uint32_t minAge = 1u; - const uint32_t maxAge = 255u; - const uint32_t minM = 1u; - const uint32_t maxM = 255u; - const uint32_t minVisibilityRays = 0u; - const uint32_t maxVisibilityRays = 4u; - changed |= ImGui::SliderScalar("DI Spatial Rounds", ImGuiDataType_U32, &settings.restirDiSpatialRounds, &minRounds, &maxRounds); - changed |= ImGui::SliderFloat("DI Spatial Radius", &settings.restirDiSpatialRadius, 0.5f, 32.0f, "%.2f"); - changed |= ImGui::SliderScalar("DI Temporal Max Age", ImGuiDataType_U32, &settings.restirDiTemporalMaxAge, &minAge, &maxAge); - changed |= ImGui::SliderScalar("DI Max M", ImGuiDataType_U32, &settings.restirDiMaxM, &minM, &maxM); - changed |= ImGui::SliderScalar("DI Visibility Ray Budget", ImGuiDataType_U32, &settings.restirDiVisibilityRayBudget, &minVisibilityRays, &maxVisibilityRays); - tooltip("Maximum shifted-sample visibility queries per pixel in each DI reuse stage. Zero falls back to the current-frame candidate."); - changed |= ImGui::Checkbox("DI Production Stabilization", &settings.restirDiProductionStabilizationEnabled); - tooltip("Biased temporal and luminance stabilization for production. Reference Validation disables it."); - changed |= ImGui::SliderFloat("DI Luminance Clamp", &settings.restirDiClampLuminance, 0.0f, 1000.0f, "%.1f"); - ImGui::BeginDisabled(); - bool sunOutside = false; - bool environmentOutside = false; - ImGui::Checkbox("Sample Sun In DI", &sunOutside); - ImGui::Checkbox("Sample Environment In DI", &environmentOutside); - ImGui::EndDisabled(); - tooltip("Sun and environment use specialized samplers outside ReSTIR DI and are composed exactly once."); } + ImGui::TreePop(); } - if (ImGui::Checkbox("ReSTIR GI", &settings.restirGiEnabled)) { + + if (ImGui::TreeNodeEx("Global Illumination", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth)) { + if (renderSettingsCheckboxRow("Enable ReSTIR GI", &settings.restirGiEnabled)) { settings.restirGiMode = settings.restirGiEnabled ? RestirGiMode::Production : RestirGiMode::Off; settings.restirGiReservoirLayout = settings.restirGiEnabled ? RestirGiReservoirLayout::ProductionPacked @@ -388,7 +518,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques settings.restirGiMode = RestirGiMode::Production; changed = true; } - if (ImGui::Combo("GI Mode", &giMode, giModeItems, 4)) { + if (renderSettingsComboRow("GI Mode", &giMode, giModeItems, 4)) { settings.restirGiMode = static_cast(giMode); if (settings.restirGiMode == RestirGiMode::Off || settings.restirGiMode == RestirGiMode::LegacyCache) { @@ -414,7 +544,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques settings.restirGiMode == RestirGiMode::LegacyCache; const bool forceValidationLayout = settings.restirGiMode == RestirGiMode::ReferenceValidation; ImGui::BeginDisabled(forceLegacyLayout || forceValidationLayout); - if (ImGui::Combo("GI Reservoir Layout", &layout, layoutItems, 3)) { + if (renderSettingsComboRow("GI Reservoir Layout", &layout, layoutItems, 3)) { settings.restirGiReservoirLayout = static_cast(layout); changed = true; } @@ -435,7 +565,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques if (ImGui::CollapsingHeader("ReSTIR GI Tuning")) { const char* presetItems[] = {"Custom", "Reference", "Balanced", "Performance"}; int preset = 0; - if (ImGui::Combo("GI Preset", &preset, presetItems, 4) && preset != 0) { + if (renderSettingsComboRow("GI Preset", &preset, presetItems, 4) && preset != 0) { if (preset == 1) { settings.restirGiTemporalMaxAge = 32; settings.restirGiSpatialRounds = 6; @@ -467,31 +597,36 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques changed = true; } tooltip("Applies ReSTIR GI reservoir reuse presets. Custom values remain editable below."); - changed |= ImGui::Checkbox("GI Final Stabilization", &settings.restirGiFinalStabilizationEnabled); + changed |= renderSettingsCheckboxRow("GI Final Stabilization", &settings.restirGiFinalStabilizationEnabled); tooltip("Applies confidence, motion, history, and luminance clamps to the final ReSTIR GI contribution. Disable for raw reservoir A/B checks."); - changed |= ImGui::Checkbox("GI Half Resolution Reuse", &settings.restirGiHalfResolution); + changed |= renderSettingsCheckboxRow("GI Half Resolution Reuse", &settings.restirGiHalfResolution); tooltip("Uses one spatial GI reservoir per 2x2 pixel group for the GI debug/final path."); int activeTileMaskMode = static_cast(settings.restirGiActiveTileMaskMode); - if (ImGui::Combo("GI Active Tile Mask", &activeTileMaskMode, "Off\0On\0Auto\0")) { + if (renderSettingsComboRow("GI Active Tile Mask", &activeTileMaskMode, "Off\0On\0Auto\0")) { settings.restirGiActiveTileMaskMode = static_cast(activeTileMaskMode); changed = true; } tooltip("Skips production GI reuse/final work on 16x16 tiles without reusable GI candidates. Auto probes off/on and keeps the faster mode."); int historyCopyMode = static_cast(settings.restirHistoryCopyMode); - if (ImGui::Combo("ReSTIR History Copy", &historyCopyMode, "Copy\0Ping-pong\0")) { + if (renderSettingsComboRow("ReSTIR History Copy", &historyCopyMode, "Copy\0Ping-pong\0")) { settings.restirHistoryCopyMode = static_cast(historyCopyMode); changed = true; } tooltip("Experimental: ping-pong can avoid DI/GI history copies when supported. Copy remains the default."); - changed |= ImGui::SliderScalar("GI Temporal Max Age", ImGuiDataType_U32, &settings.restirGiTemporalMaxAge, &minRestirGiAge, &maxRestirGiAge); - changed |= ImGui::SliderScalar("GI Spatial Rounds", ImGuiDataType_U32, &settings.restirGiSpatialRounds, &minRestirGiRounds, &maxRestirGiRounds); - changed |= ImGui::SliderFloat("GI Spatial Radius", &settings.restirGiSpatialRadius, 1.0f, 8.0f, "%.2f"); - changed |= ImGui::SliderFloat("GI Depth Threshold Scale", &settings.restirGiDepthThresholdScale, 0.5f, 2.0f, "%.2f"); - changed |= ImGui::SliderFloat("GI Compatibility Cutoff", &settings.restirGiSpatialCompatibilityThreshold, 0.0f, 0.85f, "%.2f"); - changed |= ImGui::SliderScalar("GI Visibility Rays", ImGuiDataType_U32, &settings.restirGiVisibilityRayBudget, &minRestirGiVisibilityRays, &maxRestirGiVisibilityRays); + changed |= renderSettingsSliderScalarRow("GI Temporal Max Age", ImGuiDataType_U32, &settings.restirGiTemporalMaxAge, &minRestirGiAge, &maxRestirGiAge); + changed |= renderSettingsSliderScalarRow("GI Spatial Rounds", ImGuiDataType_U32, &settings.restirGiSpatialRounds, &minRestirGiRounds, &maxRestirGiRounds); + changed |= renderSettingsSliderFloatRow("GI Spatial Radius", &settings.restirGiSpatialRadius, 1.0f, 8.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("GI Depth Threshold Scale", &settings.restirGiDepthThresholdScale, 0.5f, 2.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("GI Compatibility Cutoff", &settings.restirGiSpatialCompatibilityThreshold, 0.0f, 0.85f, "%.2f"); + changed |= renderSettingsSliderScalarRow("GI Visibility Rays", ImGuiDataType_U32, &settings.restirGiVisibilityRayBudget, &minRestirGiVisibilityRays, &maxRestirGiVisibilityRays); tooltip("Ray-query visibility budget per pixel for temporal/spatial GI reuse. Zero validates all configured GI reuse candidates."); } - if (ImGui::CollapsingHeader("Renderer Roadmap")) { + ImGui::TreePop(); + } + renderSettingsEndSection(); + } + + if (renderSettingsBeginSection("Advanced", 0, "kernel reuse reservoir material gpu ser opacity atmosphere camera exposure environment artifacts")) { const char* lightingReuseItems[] = {"Legacy DI/GI", "Legacy DI/GI + ReGIR", "Experimental ReSTIR PT", "Validate ReSTIR PT"}; int lightingReuseIndex = static_cast(settings.lightingReuseMode); if (lightingReuseIndex < 0 || lightingReuseIndex > 3) { @@ -613,11 +748,14 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques changed |= ImGui::SliderFloat("Volumetric", &settings.adaptiveWeightVolumetric, 0.0f, 1.0f, "%.2f"); ImGui::TreePop(); } + renderSettingsEndSection(); } + if (renderSettingsBeginSection("Temporal & Denoising", ImGuiTreeNodeFlags_DefaultOpen, "temporal upscaling dlss tsr taa denoiser sharpening reflex frame generation")) { + ImGui::SeparatorText("Upscaling"); const char* tsrPresetItems[] = {"Native", "Quality", "Balanced", "Performance"}; int tsrPreset = settings.renderResolutionScale >= 0.99f ? 0 : (settings.renderResolutionScale >= 0.74f ? 1 : (settings.renderResolutionScale >= 0.59f ? 2 : 3)); - if (ImGui::Combo("TSR Preset", &tsrPreset, tsrPresetItems, 4)) { + if (renderSettingsComboRow("TSR Preset", &tsrPreset, tsrPresetItems, 4)) { const float presetScales[] = {1.0f, 0.77f, 0.67f, 0.50f}; settings.renderResolutionScale = presetScales[tsrPreset]; changed = true; @@ -627,7 +765,8 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques if (temporalIndex < 0 || temporalIndex > 2) { temporalIndex = 0; } - if (ImGui::BeginCombo("Temporal Upscaler", temporalItems[temporalIndex])) { + renderSettingsBeginRow("Temporal Upscaler"); + if (ImGui::BeginCombo("##value", temporalItems[temporalIndex])) { if (ImGui::Selectable("TAA / TSR", settings.temporalUpscaler == TemporalUpscaler::TaaTsr)) { settings.temporalUpscaler = TemporalUpscaler::TaaTsr; settings.dlssRayReconstructionEnabled = false; @@ -648,20 +787,21 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques } ImGui::EndCombo(); } + renderSettingsEndRow(); tooltip(settings.temporalUpscaler == TemporalUpscaler::Dlss && !dlssCanRequest ? nvidiaStatus.dlssUnavailableReason.c_str() : (settings.temporalUpscaler == TemporalUpscaler::Nis ? nvidiaStatus.streamlineNis.unavailableReason.c_str() - : "Selects the post-denoise temporal resolve/upscale backend.")); + : "Selects the post-denoise temporal resolve/upscale backend. Q5D fallback order keeps TAA/TSR as the safe default, uses DLSS only when requested/available, and treats DLSS RR as opt-in guide-validated mode.")); ImGui::BeginDisabled(!dlssCanRequest); - changed |= ImGui::SliderFloat("DLSS Sharpening", &settings.dlssSharpeningStrength, 0.0f, 1.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("DLSS Sharpening", &settings.dlssSharpeningStrength, 0.0f, 1.0f, "%.2f"); ImGui::EndDisabled(); tooltip(dlssCanRequest ? "Sharpening amount passed to DLSS Super Resolution." : nvidiaStatus.dlssUnavailableReason.c_str()); bool rrEnabled = settings.dlssRayReconstructionEnabled; ImGui::BeginDisabled(!dlssRayReconstructionCanRequest); - if (ImGui::Checkbox("DLSS Ray Reconstruction", &rrEnabled)) { + if (renderSettingsCheckboxRow("DLSS Ray Reconstruction", &rrEnabled)) { settings.dlssRayReconstructionEnabled = rrEnabled; if (rrEnabled) { settings.temporalUpscaler = TemporalUpscaler::Dlss; @@ -674,7 +814,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques : nvidiaStatus.dlssRayReconstructionUnavailableReason.c_str()); bool fgEnabled = settings.dlssFrameGenerationEnabled; ImGui::BeginDisabled(!dlssFrameGenerationCanRequest); - if (ImGui::Checkbox("DLSS Frame Generation", &fgEnabled)) { + if (renderSettingsCheckboxRow("DLSS Frame Generation", &fgEnabled)) { settings.dlssFrameGenerationEnabled = fgEnabled; if (fgEnabled) { settings.temporalUpscaler = TemporalUpscaler::Dlss; @@ -687,7 +827,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques : nvidiaStatus.dlssFrameGenerationUnavailableReason.c_str()); bool reflexEnabled = settings.streamlineReflexEnabled; ImGui::BeginDisabled(!reflexCanRequest); - if (ImGui::Checkbox("Streamline Reflex", &reflexEnabled)) { + if (renderSettingsCheckboxRow("Streamline Reflex", &reflexEnabled)) { settings.streamlineReflexEnabled = reflexEnabled; changed = true; } @@ -697,7 +837,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques : nvidiaStatus.streamlineReflex.unavailableReason.c_str()); bool nvperfEnabled = settings.streamlineNvPerfEnabled; ImGui::BeginDisabled(!nvperfCanRequest); - if (ImGui::Checkbox("Streamline NvPerf", &nvperfEnabled)) { + if (renderSettingsCheckboxRow("Streamline NvPerf", &nvperfEnabled)) { settings.streamlineNvPerfEnabled = nvperfEnabled; changed = true; } @@ -705,23 +845,24 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques tooltip(nvperfCanRequest ? "Requests Streamline NvPerf evaluation for Nsight Perf HUD and per-frame performance diagnostics." : nvidiaStatus.streamlineNvPerf.unavailableReason.c_str()); - changed |= ImGui::SliderFloat("Render Resolution Scale", &settings.renderResolutionScale, 0.25f, 1.0f, "%.2f"); - changed |= ImGui::SliderFloat("Material Anisotropy", &settings.materialTextureAnisotropy, 1.0f, 16.0f, "%.1fx"); + changed |= renderSettingsSliderFloatRow("Render Resolution Scale", &settings.renderResolutionScale, 0.25f, 1.0f, "%.2f"); + ImGui::SeparatorText("Material & GPU Features"); + changed |= renderSettingsSliderFloatRow("Material Anisotropy", &settings.materialTextureAnisotropy, 1.0f, 16.0f, "%.1fx"); tooltip("Anisotropic filtering level for material textures. Unsupported devices clamp to 1x."); - changed |= ImGui::Checkbox("Specular AA", &settings.specularAaEnabled); + changed |= renderSettingsCheckboxRow("Specular AA", &settings.specularAaEnabled); tooltip("Raises effective specular roughness for high-frequency normal maps without changing material roughness."); const OpacityMicromapDeviceInfo& ommInfo = state.renderer.opacityMicromapInfo(); if (!ommInfo.supported) { settings.opacityMicromapsEnabled = false; } ImGui::BeginDisabled(!ommInfo.supported); - changed |= ImGui::Checkbox("Opacity Micromaps", &settings.opacityMicromapsEnabled); + changed |= renderSettingsCheckboxRow("Opacity Micromaps", &settings.opacityMicromapsEnabled); ImGui::EndDisabled(); tooltip(ommInfo.supported ? "Builds hardware opacity micromaps for eligible alpha-tested BLAS geometry." : ommInfo.disabledReason.c_str()); const SerDeviceInfo& serInfo = state.renderer.serInfo(); - ImGui::Text("SER: %s", serInfo.supported ? "available" : "unavailable"); + renderSettingsInfoRow("SER", serInfo.supported ? "available" : "unavailable"); tooltip(serInfo.supported ? serReorderingHintName(serInfo.reorderingHint) : serInfo.disabledReason.c_str()); @@ -729,7 +870,7 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques settings.shaderExecutionReorderingEnabled = false; } ImGui::BeginDisabled(!serInfo.supported); - changed |= ImGui::Checkbox("Wavefront SER", &settings.shaderExecutionReorderingEnabled); + changed |= renderSettingsCheckboxRow("Wavefront SER", &settings.shaderExecutionReorderingEnabled); ImGui::EndDisabled(); tooltip(serInfo.supported ? "Enables shader execution reordering hints for the opt-in wavefront trace raygen path." @@ -739,15 +880,18 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques if (adaptiveIndex < 0 || adaptiveIndex > 3) { adaptiveIndex = 0; } - if (ImGui::Combo("Adaptive Quality", &adaptiveIndex, adaptiveItems, 4)) { + if (renderSettingsComboRow("Adaptive Quality", &adaptiveIndex, adaptiveItems, 4)) { settings.adaptiveQualityMode = static_cast(adaptiveIndex); changed = true; } tooltip("Dynamically lowers expensive path-tracing controls while moving or over the GPU frame target."); - changed |= ImGui::SliderFloat("Adaptive GPU Target", &settings.adaptiveGpuFrameTargetMs, 4.0f, 100.0f, "%.1f ms"); + changed |= renderSettingsSliderFloatRow("Adaptive GPU Target", &settings.adaptiveGpuFrameTargetMs, 4.0f, 100.0f, "%.1f ms"); tooltip("Target smoothed GPU frame time used by adaptive quality modes."); + renderSettingsEndSection(); + } - if (ImGui::CollapsingHeader("Tone Mapping", ImGuiTreeNodeFlags_DefaultOpen)) { + if (renderSettingsBeginSection("Appearance & Camera", 0, "tone mapper exposure camera depth of field bloom color")) { + ImGui::SeparatorText("Color & Exposure"); const char* toneMapperItems2[] = {"Linear", "Reinhard", "Reinhard White", "ACES", "PBR Neutral", "AgX"}; int toneMapperIndex2 = static_cast(settings.toneMapper); if (toneMapperIndex2 < 0 || toneMapperIndex2 > 5) { @@ -846,9 +990,10 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques tooltip("Controls the target percentile used by auto-exposure metering."); ImGui::TreePop(); } + renderSettingsEndSection(); } - if (ImGui::CollapsingHeader("Sun / Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { + if (renderSettingsBeginSection("Scene Lighting", 0, "sun lighting lux direction")) { if (state.sceneDocument != nullptr) { const SunDerivedState sun = SunController::derivedState(*state.sceneDocument); ImGui::Text("Primary Sun: %s", SunController::primarySunEntity(*state.sceneDocument).valid() ? "Scene" : "Missing"); @@ -859,9 +1004,10 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques requests.sceneUpdate = SceneUpdateKind::LightOnly; } } + renderSettingsEndSection(); } - if (ImGui::CollapsingHeader("Atmosphere")) { + if (renderSettingsBeginSection("Atmosphere & Sky")) { changed |= ImGui::SliderFloat("Sky Intensity", &settings.skyIntensity, 0.0f, 3.0f, "%.2f"); tooltip("Multiplier for atmospheric sky radiance."); changed |= ImGui::SliderFloat("Rayleigh Scale Height", &settings.rayleighScaleHeight, 1000.0f, 20000.0f, "%.0f m"); @@ -872,23 +1018,27 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques tooltip("Forward/backward scattering asymmetry. Higher = more forward scattered light."); changed |= ImGui::SliderFloat("Ground Albedo", &settings.groundAlbedo, 0.0f, 1.0f, "%.2f"); tooltip("Planetary ground reflectance. Affects atmospheric light bouncing off the terrain."); + renderSettingsEndSection(); } - if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) { + if (renderSettingsBeginSection("Environment", 0, "environment hdri intensity rotation background")) { changed |= ImGui::Checkbox("Show Environment", &settings.environmentEnabled); changed |= ImGui::SliderFloat("Environment Intensity", &settings.environmentIntensity, 0.0f, 8.0f, "%.2f"); changed |= ImGui::SliderFloat("Background Intensity", &settings.environmentBackgroundIntensity, 0.0f, 2.0f, "%.2f"); changed |= ImGui::SliderFloat("Environment Rotation", &settings.environmentRotation, -6.28318f, 6.28318f, "%.2f"); + renderSettingsEndSection(); } - if (ImGui::CollapsingHeader("Denoiser / TAA", ImGuiTreeNodeFlags_DefaultOpen)) { - changed |= ImGui::Checkbox("Denoiser", &settings.denoiserEnabled); + if (renderSettingsBeginSection("Denoiser & TAA", 0, "denoiser nrd taa feedback history atrous sharpening")) { + ImGui::SeparatorText("Denoiser"); + changed |= renderSettingsCheckboxRow("Denoiser", &settings.denoiserEnabled); const char* denoiserBackendItems[] = {"Engine", "NRD"}; int denoiserBackendIndex = static_cast(settings.denoiserBackend); if (denoiserBackendIndex < 0 || denoiserBackendIndex > 1) { denoiserBackendIndex = 0; } - if (ImGui::BeginCombo("Denoiser Backend", denoiserBackendItems[denoiserBackendIndex])) { + renderSettingsBeginRow("Denoiser Backend"); + if (ImGui::BeginCombo("##value", denoiserBackendItems[denoiserBackendIndex])) { if (ImGui::Selectable("Engine", settings.denoiserBackend == DenoiserBackend::Engine)) { settings.denoiserBackend = DenoiserBackend::Engine; changed = true; @@ -901,41 +1051,47 @@ void RenderSettingsPanel::draw(EditorRuntimeState& state, EditorRequests& reques ImGui::EndDisabled(); ImGui::EndCombo(); } + renderSettingsEndRow(); tooltip(settings.denoiserBackend == DenoiserBackend::Nrd && !nrdCanRequest ? nvidiaStatus.nrdUnavailableReason.c_str() - : "Selects the active denoiser backend."); - changed |= ImGui::Checkbox("Denoise While Moving", &settings.denoiseWhileMoving); - changed |= ImGui::SliderScalar("A-trous Iterations", ImGuiDataType_U32, &settings.atrousIterations, &minAtrous, &maxAtrous); + : "Selects the active denoiser backend. Q5D fallback order is Engine + TAA/TSR, NRD + TAA/TSR when validated, Engine + DLSS, NRD + DLSS, then opt-in DLSS RR."); + changed |= renderSettingsCheckboxRow("Denoise While Moving", &settings.denoiseWhileMoving); + changed |= renderSettingsSliderScalarRow("A-trous Iterations", ImGuiDataType_U32, &settings.atrousIterations, &minAtrous, &maxAtrous); tooltip("Denoiser iterations. More is smoother and slower."); - changed |= ImGui::SliderFloat("Denoiser Strength", &settings.denoiserStrength, 0.05f, 4.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("Denoiser Strength", &settings.denoiserStrength, 0.05f, 4.0f, "%.2f"); tooltip("Higher values denoise more aggressively and may lose detail."); const uint32_t minHistory = 4; const uint32_t maxHistory = 256; - changed |= ImGui::SliderScalar("Max History Length", ImGuiDataType_U32, &settings.denoiserMaxHistoryLength, &minHistory, &maxHistory); + changed |= renderSettingsSliderScalarRow("Max History Length", ImGuiDataType_U32, &settings.denoiserMaxHistoryLength, &minHistory, &maxHistory); tooltip("Maximum temporal history length for moment tracking. Higher values stabilize static scenes more."); - changed |= ImGui::SliderFloat("Moment Validity Threshold", &settings.momentValidityThreshold, 0.05f, 0.75f, "%.2f"); + changed |= renderSettingsSliderFloatRow("Moment Validity Threshold", &settings.momentValidityThreshold, 0.05f, 0.75f, "%.2f"); tooltip("Threshold for moment history validity. Lower = more history, more ghosting. Higher = stricter, less ghosting, more noise."); - changed |= ImGui::Checkbox("TAA", &settings.taaEnabled); + ImGui::SeparatorText("Temporal AA"); + changed |= renderSettingsCheckboxRow("TAA", &settings.taaEnabled); tooltip("HDR temporal anti-aliasing pass after denoising and before tone mapping."); - changed |= ImGui::SliderFloat("TAA Feedback", &settings.taaFeedback, 0.01f, 0.5f, "%.2f"); + changed |= renderSettingsSliderFloatRow("TAA Feedback", &settings.taaFeedback, 0.01f, 0.5f, "%.2f"); tooltip("Lower values keep more history; higher values react faster to motion and lighting changes."); - changed |= ImGui::SliderFloat("TAA Motion Feedback", &settings.taaMotionFeedback, 0.25f, 0.98f, "%.2f"); + changed |= renderSettingsSliderFloatRow("TAA Motion Feedback", &settings.taaMotionFeedback, 0.25f, 0.98f, "%.2f"); tooltip("Current-frame blend target while the camera is moving. Lower values stabilize noisy motion; higher values reduce ghosting."); - changed |= ImGui::SliderFloat("TAA Reactive Feedback", &settings.taaReactiveFeedback, 0.25f, 0.99f, "%.2f"); + changed |= renderSettingsSliderFloatRow("TAA Reactive Feedback", &settings.taaReactiveFeedback, 0.25f, 0.99f, "%.2f"); tooltip("Current-frame blend used for strong reactive or disocclusion cases while moving."); - changed |= ImGui::SliderFloat("TAA Sharpening", &settings.taaSharpeningStrength, 0.0f, 1.0f, "%.2f"); + changed |= renderSettingsSliderFloatRow("TAA Sharpening", &settings.taaSharpeningStrength, 0.0f, 1.0f, "%.2f"); tooltip("Unsharp mask amount applied by the TAA resolve."); + renderSettingsEndSection(); } - if (ImGui::CollapsingHeader("Artifact Controls")) { + if (renderSettingsBeginSection("Artifact Controls", 0, "shadow bias firefly clamp artifact")) { changed |= ImGui::SliderFloat("Shadow Ray Bias", &settings.shadowRayBias, 0.00001f, 0.05f, "%.5f"); tooltip("Surface offset used for secondary shadow rays."); changed |= ImGui::SliderFloat("Shadow Distance Bias", &settings.shadowDistanceBias, 0.0f, 0.1f, "%.5f"); tooltip("Reduces the maximum distance of finite shadow rays to avoid self hits at the light."); changed |= ImGui::SliderFloat("Firefly Clamp", &settings.fireflyClamp, 1.0f, 512.0f, "%.1f"); tooltip("Luminance clamp for single path samples before accumulation."); + renderSettingsEndSection(); } + ImGui::PopStyleVar(2); + if (changed) { if (!presetApplied) { settings.renderPreset = RenderPreset::Custom; diff --git a/src/rtv/RendererCoreRegressionTests.cpp b/src/rtv/RendererCoreRegressionTests.cpp new file mode 100644 index 0000000..57882fb --- /dev/null +++ b/src/rtv/RendererCoreRegressionTests.cpp @@ -0,0 +1,143 @@ +#include "rtv/RendererCoreRegressionTests.h" + +#include "rtv/FreeListAllocator.h" +#include "rtv/RenderGraph.h" +#include "rtv/passes/RestirGIPass.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace rtv { + +namespace { + +bool check(bool condition, std::string_view message, std::ostream& output) { + if (!condition) { + output << "FAIL: " << message << '\n'; + } + return condition; +} + +bool testFreeListAllocator(std::ostream& output) { + FreeListAllocator allocator(2u); + bool ok = true; + ok &= check(allocator.allocate() == 0u, "free-list first allocation", output); + ok &= check(allocator.allocate() == 1u, "free-list second allocation", output); + ok &= check(allocator.allocate() == UINT32_MAX, "free-list exhaustion", output); + ok &= check(allocator.allocatedCount() == 2u, "free-list allocated count at capacity", output); + + allocator.free(0u); + allocator.free(0u); + ok &= check(allocator.allocatedCount() == 1u, "free-list duplicate free is ignored", output); + ok &= check(allocator.allocate() == 0u, "free-list reuses a released slot", output); + ok &= check(allocator.allocate() == UINT32_MAX, "free-list does not allocate a live slot twice", output); + return ok; +} + +RenderGraphResource makeExternalBuffer(const char* name) { + RenderGraphResource resource{}; + resource.type = RenderGraphResource::Type::Buffer; + resource.lifetime = RenderGraphResource::Lifetime::Persistent; + resource.size = 256u; + resource.bufferUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + resource.external = true; + resource.debugName = name; + return resource; +} + +bool testRenderGraphReaderOrdering(std::ostream& output) { + RenderGraph graph(nullptr, false); + const RenderGraphResourceId target = graph.createBuffer(makeExternalBuffer("target")); + const RenderGraphResourceId dependency = graph.createBuffer(makeExternalBuffer("dependency")); + const RenderGraphResourceId outputA = graph.createBuffer(makeExternalBuffer("output a")); + const RenderGraphResourceId outputB = graph.createBuffer(makeExternalBuffer("output b")); + + graph.addPass("initial write").addStorageWrite(target, PipelineDomain::Transfer); + graph.addPass("delayed compute reader") + .addStorageRead(target, PipelineDomain::Compute) + .addStorageRead(dependency, PipelineDomain::Compute) + .addStorageWrite(outputA, PipelineDomain::Compute); + graph.addPass("ray reader") + .addStorageRead(target, PipelineDomain::RayTracing) + .addStorageWrite(outputB, PipelineDomain::RayTracing); + graph.addPass("final write").addStorageWrite(target, PipelineDomain::Transfer); + graph.addPass("late dependency producer").addStorageWrite(dependency, PipelineDomain::Transfer); + graph.compile(); + + std::array position{}; + position.fill(std::numeric_limits::max()); + const auto& order = graph.compiledPassOrder(); + for (uint32_t i = 0; i < order.size(); ++i) { + if (order[i] < position.size()) { + position[order[i]] = i; + } + } + + bool ok = true; + ok &= check(position[1] < position[3], "compute reader completes before the following writer", output); + ok &= check(position[2] < position[3], "ray reader completes before the following writer", output); + + const auto barrierIt = std::find_if( + graph.compiledBarriers().begin(), + graph.compiledBarriers().end(), + [&](const RenderGraphBarrier& barrier) { + return barrier.resource.index == target.index && barrier.afterPass == 3u; + }); + ok &= check(barrierIt != graph.compiledBarriers().end(), "reader-to-writer barrier exists", output); + if (barrierIt != graph.compiledBarriers().end()) { + ok &= check( + (barrierIt->before.stage & VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) != 0, + "reader barrier includes compute stage", + output); + ok &= check( + (barrierIt->before.stage & VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR) != 0, + "reader barrier includes ray-tracing stage", + output); + ok &= check( + (barrierIt->before.access & VK_ACCESS_2_SHADER_READ_BIT) != 0, + "reader barrier includes shader reads", + output); + } + return ok; +} + +bool testRestirGiHalfResolutionUpsampleContract(std::ostream& output) { + RendererSettings settings{}; + settings.restirGiMode = RestirGiMode::Production; + settings.restirGiSpatialRounds = 0u; + + bool ok = true; + ok &= check( + passes::RestirGIPass::requestsUpsample(settings, false, true), + "half-resolution production GI upsamples without spatial reuse", + output); + ok &= check( + !passes::RestirGIPass::requestsUpsample(settings, false, false), + "full-resolution production GI does not request upsample", + output); + + settings.restirGiMode = RestirGiMode::Off; + ok &= check( + !passes::RestirGIPass::requestsUpsample(settings, false, true), + "disabled GI does not request upsample", + output); + return ok; +} + +} // namespace + +int runRendererCoreRegressionTests(std::ostream& output) { + const bool ok = testFreeListAllocator(output) && + testRenderGraphReaderOrdering(output) && + testRestirGiHalfResolutionUpsampleContract(output); + output << (ok ? "Renderer core regression tests passed.\n" : "Renderer core regression tests failed.\n"); + return ok ? 0 : 1; +} + +} // namespace rtv diff --git a/src/rtv/RendererDebug.cpp b/src/rtv/RendererDebug.cpp index ca25a28..5f67b68 100644 --- a/src/rtv/RendererDebug.cpp +++ b/src/rtv/RendererDebug.cpp @@ -53,14 +53,19 @@ const char* restirDiModeName(RestirDiMode mode) { return "off"; } -RestirDiMode parseRestirDiMode(std::string_view value) { +bool tryParseRestirDiMode(std::string_view value, RestirDiMode& out) { const std::string key = normalized(value); - if (key == "off" || key == "disabled" || key == "none") { return RestirDiMode::Off; } - if (key == "legacy" || key == "old" || key == "current") { return RestirDiMode::Legacy; } - if (key == "production" || key == "prod" || key == "new" || key == "default") { return RestirDiMode::Production; } - if (key == "reference" || key == "validation" || key == "referencevalidation" || key == "ref") { return RestirDiMode::ReferenceValidation; } - if (key == "hybrid" || key == "hybridcompare" || key == "compare") { return RestirDiMode::HybridCompare; } - return RestirDiMode::Off; + if (key == "off" || key == "disabled" || key == "none") { out = RestirDiMode::Off; return true; } + if (key == "legacy" || key == "old" || key == "current") { out = RestirDiMode::Legacy; return true; } + if (key == "production" || key == "prod" || key == "new" || key == "default") { out = RestirDiMode::Production; return true; } + if (key == "reference" || key == "validation" || key == "referencevalidation" || key == "ref") { out = RestirDiMode::ReferenceValidation; return true; } + if (key == "hybrid" || key == "hybridcompare" || key == "compare") { out = RestirDiMode::HybridCompare; return true; } + return false; +} + +RestirDiMode parseRestirDiMode(std::string_view value) { + RestirDiMode mode = RestirDiMode::Off; + return tryParseRestirDiMode(value, mode) ? mode : RestirDiMode::Off; } const char* restirDiReservoirLayoutName(RestirDiReservoirLayout layout) { @@ -90,14 +95,19 @@ const char* restirGiModeName(RestirGiMode mode) { return "off"; } -RestirGiMode parseRestirGiMode(std::string_view value) { +bool tryParseRestirGiMode(std::string_view value, RestirGiMode& out) { const std::string key = normalized(value); - if (key == "off" || key == "false" || key == "0" || key == "disabled" || key == "none") { return RestirGiMode::Off; } - if (key == "legacy" || key == "legacycache" || key == "old" || key == "current") { return RestirGiMode::LegacyCache; } + if (key == "off" || key == "false" || key == "0" || key == "disabled" || key == "none") { out = RestirGiMode::Off; return true; } + if (key == "legacy" || key == "legacycache" || key == "old" || key == "current") { out = RestirGiMode::LegacyCache; return true; } if (key == "on" || key == "true" || key == "1" || - key == "production" || key == "prod" || key == "new" || key == "default") { return RestirGiMode::Production; } - if (key == "reference" || key == "validation" || key == "referencevalidation" || key == "ref") { return RestirGiMode::ReferenceValidation; } - return RestirGiMode::LegacyCache; + key == "production" || key == "prod" || key == "new" || key == "default") { out = RestirGiMode::Production; return true; } + if (key == "reference" || key == "validation" || key == "referencevalidation" || key == "ref") { out = RestirGiMode::ReferenceValidation; return true; } + return false; +} + +RestirGiMode parseRestirGiMode(std::string_view value) { + RestirGiMode mode = RestirGiMode::LegacyCache; + return tryParseRestirGiMode(value, mode) ? mode : RestirGiMode::LegacyCache; } const char* restirGiReservoirLayoutName(RestirGiReservoirLayout layout) { @@ -407,6 +417,7 @@ RendererDebugView parseRendererDebugView(std::string_view value) { if (key == "restirdireceiver" || key == "restirdireceiverposition" || key == "direceiver") { return RendererDebugView::RestirDiReceiverPosition; } if (key == "restirdinormal" || key == "restirdireceivernormal" || key == "dinormal") { return RendererDebugView::RestirDiReceiverNormal; } if (key == "restirdilightversion" || key == "dilightversion") { return RendererDebugView::RestirDiLightVersion; } + if (key == "restirdilightmapstatus" || key == "dilightmapstatus" || key == "restirdilightmapping" || key == "dilightmapping") { return RendererDebugView::RestirDiLightMapStatus; } if (key == "restirdiweightsum" || key == "diweightsum" || key == "restirdiw") { return RendererDebugView::RestirDiWeightSum; } if (key == "restirdim" || key == "dim" || key == "restirdisamplecount") { return RendererDebugView::RestirDiM; } if (key == "restirdilightclass" || key == "dilightclass" || key == "restirdilighttype" || key == "dilighttype") { return RendererDebugView::RestirDiLightClass; } @@ -463,6 +474,46 @@ RendererDebugView parseRendererDebugView(std::string_view value) { if (key == "denoiserbasedisocclusion" || key == "basedisocclusion") { return RendererDebugView::DenoiserBaseDisocclusion; } if (key == "denoiserspecularchannelconfidence" || key == "specularchannelconfidence" || key == "specularconfidence") { return RendererDebugView::DenoiserSpecularChannelConfidence; } if (key == "denoiserspecularhistoryweight" || key == "specularhistoryweight") { return RendererDebugView::DenoiserSpecularHistoryWeight; } + if (key == "nrdvalidation" || key == "nrdvalidationoutput") { return RendererDebugView::NrdValidation; } + if (key == "nrddiffuseconfidence" || key == "nrddiffconfidence" || key == "nrddiffusehistoryconfidence") { + return RendererDebugView::NrdDiffuseConfidence; + } + if (key == "nrdspecularconfidence" || key == "nrdspecconfidence" || key == "nrdspecularhistoryconfidence") { + return RendererDebugView::NrdSpecularConfidence; + } + if (key == "nrdrawconfidencegradient" || key == "nrdrawgradient" || key == "nrdconfidencegradient") { + return RendererDebugView::NrdRawConfidenceGradient; + } + if (key == "nrdfilteredconfidencegradient" || key == "nrdfilteredgradient" || key == "nrdconfidencegradientfiltered") { + return RendererDebugView::NrdFilteredConfidenceGradient; + } + if (key == "nrdconfidencehistory" || key == "nrdhistoryconfidence" || key == "nrdhistory" || + key == "nrdconfidencehistoryvalidity" || key == "nrdsourcepixelhistory") { + return RendererDebugView::NrdConfidenceHistory; + } + if (key == "psractivemask" || key == "psractive" || key == "primarysurfacereplacement") { return RendererDebugView::PsrActiveMask; } + if (key == "psrdepth" || key == "psrviewz" || key == "replacementdepth") { return RendererDebugView::PsrDepth; } + if (key == "psrmotion" || key == "psrvelocity" || key == "replacementmotion") { return RendererDebugView::PsrMotion; } + if (key == "psrnormalroughness" || key == "psrnormal" || key == "replacementnormal") { return RendererDebugView::PsrNormalRoughness; } + if (key == "psrhitdistance" || key == "replacementhitdistance") { return RendererDebugView::PsrHitDistance; } + if (key == "psralbedof0" || key == "psralbedo" || key == "psrf0") { return RendererDebugView::PsrAlbedoF0; } + if (key == "psrraydirection" || key == "psrdirection" || key == "reflectionraydirection") { return RendererDebugView::PsrRayDirection; } + if (key == "dlssdepth" || key == "dlssguidedepth") { return RendererDebugView::DlssDepth; } + if (key == "dlssmotion" || key == "dlssmotionvectors" || key == "dlssguidevelocity") { return RendererDebugView::DlssMotionVectors; } + if (key == "dlssinput" || key == "dlssinputcolor" || key == "dlssscalinginputcolor") { return RendererDebugView::DlssInputColor; } + if (key == "dlssoutput" || key == "dlssoutputcolor" || key == "dlssscalingoutputcolor") { return RendererDebugView::DlssOutputColor; } + if (key == "dlssrrdiffusealbedo" || key == "rrdiffusealbedo") { return RendererDebugView::DlssRrDiffuseAlbedo; } + if (key == "dlssrrspecularalbedo" || key == "rrspecularalbedo" || key == "dlssrrspecularf0") { return RendererDebugView::DlssRrSpecularAlbedo; } + if (key == "dlssrrnormals" || key == "dlssrrnormal" || key == "rrnormals") { return RendererDebugView::DlssRrNormals; } + if (key == "dlssrrroughness" || key == "rrroughness") { return RendererDebugView::DlssRrRoughness; } + if (key == "dlssrrdiffusehitdistance" || key == "rrdiffusehitdistance") { return RendererDebugView::DlssRrDiffuseHitDistance; } + if (key == "dlssrrspecularhitdistance" || key == "rrspecularhitdistance") { return RendererDebugView::DlssRrSpecularHitDistance; } + if (key == "dlssrrreflectedalbedo" || key == "rrreflectedalbedo") { return RendererDebugView::DlssRrReflectedAlbedo; } + if (key == "dlssrrdisocclusion" || key == "dlssrrdisocclusionmask" || key == "rrdisocclusion") { return RendererDebugView::DlssRrDisocclusionMask; } + if (key == "dlssrrdiffuseraydirection" || key == "rrdiffuseraydirection") { return RendererDebugView::DlssRrDiffuseRayDirection; } + if (key == "dlssrrspecularraydirection" || key == "rrspecularraydirection") { return RendererDebugView::DlssRrSpecularRayDirection; } + if (key == "dlssrrdiffuseraydirectionhitdistance" || key == "rrdiffuseraydirectionhitdistance") { return RendererDebugView::DlssRrDiffuseRayDirectionHitDistance; } + if (key == "dlssrrspecularraydirectionhitdistance" || key == "rrspecularraydirectionhitdistance") { return RendererDebugView::DlssRrSpecularRayDirectionHitDistance; } if (key == "directsample" || key == "directsampletype" || key == "sampletype") { return RendererDebugView::DirectSampleType; } if (key == "albedo" || key == "basecolor" || key == "basecolour") { return RendererDebugView::Albedo; } if (key == "occlusion" || key == "ao" || key == "materialocclusion" || key == "aotexture") { @@ -551,6 +602,24 @@ RendererDebugView parseRendererDebugView(std::string_view value) { if (key == "restirgipathclass" || key == "gipathclass" || key == "restirgiclass" || key == "giclass") { return RendererDebugView::RestirGiPathClass; } + if (key == "restirgitarget" || key == "gitarget" || key == "restirgitargetfunction") { + return RendererDebugView::RestirGiTarget; + } + if (key == "restirgisourcepdf" || key == "gisourcepdf" || key == "restirgipdf") { + return RendererDebugView::RestirGiSourcePdf; + } + if (key == "restirgiweightsum" || key == "giweightsum" || key == "restirgiw") { + return RendererDebugView::RestirGiWeightSum; + } + if (key == "restirgim" || key == "gim" || key == "restirgisamplecount") { + return RendererDebugView::RestirGiM; + } + if (key == "restirgiconfidence" || key == "giconfidence") { + return RendererDebugView::RestirGiConfidence; + } + if (key == "restirgivisibility" || key == "restirgivis" || key == "givisibility") { + return RendererDebugView::RestirGiVisibility; + } if (key == "adaptivedensity" || key == "adaptivedensitymap" || key == "samplingdensity" || key == "densitymap") { return RendererDebugView::AdaptiveDensityMap; } @@ -760,6 +829,35 @@ const char* rendererDebugViewName(RendererDebugView view) { case RendererDebugView::DenoiserBaseDisocclusion: return "denoiser-base-disocclusion"; case RendererDebugView::DenoiserSpecularChannelConfidence: return "denoiser-specular-channel-confidence"; case RendererDebugView::DenoiserSpecularHistoryWeight: return "denoiser-specular-history-weight"; + case RendererDebugView::NrdValidation: return "nrd-validation"; + case RendererDebugView::NrdDiffuseConfidence: return "nrd-diffuse-confidence"; + case RendererDebugView::NrdSpecularConfidence: return "nrd-specular-confidence"; + case RendererDebugView::NrdRawConfidenceGradient: return "nrd-raw-confidence-gradient"; + case RendererDebugView::NrdFilteredConfidenceGradient: return "nrd-filtered-confidence-gradient"; + case RendererDebugView::NrdConfidenceHistory: return "nrd-confidence-history"; + case RendererDebugView::PsrActiveMask: return "psr-active-mask"; + case RendererDebugView::PsrDepth: return "psr-depth"; + case RendererDebugView::PsrMotion: return "psr-motion"; + case RendererDebugView::PsrNormalRoughness: return "psr-normal-roughness"; + case RendererDebugView::PsrHitDistance: return "psr-hit-distance"; + case RendererDebugView::PsrAlbedoF0: return "psr-albedo-f0"; + case RendererDebugView::PsrRayDirection: return "psr-ray-direction"; + case RendererDebugView::DlssDepth: return "dlss-depth"; + case RendererDebugView::DlssMotionVectors: return "dlss-motion-vectors"; + case RendererDebugView::DlssInputColor: return "dlss-input-color"; + case RendererDebugView::DlssOutputColor: return "dlss-output-color"; + case RendererDebugView::DlssRrDiffuseAlbedo: return "dlss-rr-diffuse-albedo"; + case RendererDebugView::DlssRrSpecularAlbedo: return "dlss-rr-specular-albedo"; + case RendererDebugView::DlssRrNormals: return "dlss-rr-normals"; + case RendererDebugView::DlssRrRoughness: return "dlss-rr-roughness"; + case RendererDebugView::DlssRrDiffuseHitDistance: return "dlss-rr-diffuse-hit-distance"; + case RendererDebugView::DlssRrSpecularHitDistance: return "dlss-rr-specular-hit-distance"; + case RendererDebugView::DlssRrReflectedAlbedo: return "dlss-rr-reflected-albedo"; + case RendererDebugView::DlssRrDisocclusionMask: return "dlss-rr-disocclusion-mask"; + case RendererDebugView::DlssRrDiffuseRayDirection: return "dlss-rr-diffuse-ray-direction"; + case RendererDebugView::DlssRrSpecularRayDirection: return "dlss-rr-specular-ray-direction"; + case RendererDebugView::DlssRrDiffuseRayDirectionHitDistance: return "dlss-rr-diffuse-ray-direction-hit-distance"; + case RendererDebugView::DlssRrSpecularRayDirectionHitDistance: return "dlss-rr-specular-ray-direction-hit-distance"; case RendererDebugView::RestirPairwiseMis: return "restir-pairwise-mis"; case RendererDebugView::RestirGiValidity: return "restir-gi-validity"; case RendererDebugView::RestirGiAge: return "restir-gi-age"; @@ -771,6 +869,12 @@ const char* rendererDebugViewName(RendererDebugView view) { case RendererDebugView::RestirGiHitDistance: return "restir-gi-hit-distance"; case RendererDebugView::RestirGiGrid: return "restir-gi-grid"; case RendererDebugView::RestirGiPathClass: return "restir-gi-path-class"; + case RendererDebugView::RestirGiTarget: return "restir-gi-target"; + case RendererDebugView::RestirGiSourcePdf: return "restir-gi-source-pdf"; + case RendererDebugView::RestirGiWeightSum: return "restir-gi-weight-sum"; + case RendererDebugView::RestirGiM: return "restir-gi-m"; + case RendererDebugView::RestirGiConfidence: return "restir-gi-confidence"; + case RendererDebugView::RestirGiVisibility: return "restir-gi-visibility"; case RendererDebugView::AdaptiveDensityMap: return "adaptive-density-map"; case RendererDebugView::AdaptiveSampleCount: return "adaptive-sample-count"; case RendererDebugView::AdaptiveUnsampledPixels: return "adaptive-unsampled-pixels"; @@ -805,6 +909,7 @@ const char* rendererDebugViewName(RendererDebugView view) { case RendererDebugView::RestirDiReceiverPosition: return "restir-di-receiver-position"; case RendererDebugView::RestirDiReceiverNormal: return "restir-di-receiver-normal"; case RendererDebugView::RestirDiLightVersion: return "restir-di-light-version"; + case RendererDebugView::RestirDiLightMapStatus: return "restir-di-light-map-status"; case RendererDebugView::RestirDiInitialReservoir: return "restir-di-initial-reservoir"; case RendererDebugView::RestirDiTemporalReservoir: return "restir-di-temporal-reservoir"; case RendererDebugView::RestirDiSpatialReservoir: return "restir-di-spatial-reservoir"; diff --git a/src/rtv/RtxdiRuntime.cpp b/src/rtv/RtxdiRuntime.cpp new file mode 100644 index 0000000..1d66b67 --- /dev/null +++ b/src/rtv/RtxdiRuntime.cpp @@ -0,0 +1,142 @@ +#include "rtv/RtxdiRuntime.h" + +#include +#include + +namespace rtv { + +namespace { + +uint64_t reservoirBytes( + const RTXDI_ReservoirBufferParameters& params, + uint32_t bufferCount, + uint64_t reservoirStride) { + return static_cast(params.reservoirArrayPitch) * bufferCount * reservoirStride; +} + +} // namespace + +RtxdiRuntime::RtxdiRuntime(const RtxdiRuntimeConfig& config) { + recreate(config); +} + +void RtxdiRuntime::recreate(const RtxdiRuntimeConfig& config) { + if (config.renderWidth == 0 || config.renderHeight == 0) { + throw std::invalid_argument("RTXDI render extent must be non-zero"); + } + + config_ = config; + rtxdi::ImportanceSamplingContext_StaticParameters params{}; + params.renderWidth = config.renderWidth; + params.renderHeight = config.renderHeight; + params.CheckerboardSamplingMode = config.checkerboard + ? rtxdi::CheckerboardMode::Black + : rtxdi::CheckerboardMode::Off; + context_ = std::make_unique(params); + applyQualityPreset(); +} + +void RtxdiRuntime::beginFrame(uint32_t frameIndex) { + context_->GetReSTIRDIContext().SetFrameIndex(frameIndex); + context_->GetReSTIRGIContext().SetFrameIndex(frameIndex); + context_->GetReSTIRPTContext().SetFrameIndex(frameIndex); +} + +RtxdiMemoryRequirements RtxdiRuntime::memoryRequirements() const { + RtxdiMemoryRequirements result{}; + result.diReservoirBytes = reservoirBytes( + context_->GetReSTIRDIContext().GetReservoirBufferParameters(), + rtxdi::c_NumReSTIRDIReservoirBuffers, + sizeof(RTXDI_PackedDIReservoir)); + result.giReservoirBytes = reservoirBytes( + context_->GetReSTIRGIContext().GetReservoirBufferParameters(), + rtxdi::c_NumReSTIRGIReservoirBuffers, + sizeof(RTXDI_PackedGIReservoir)); + result.ptReservoirBytes = reservoirBytes( + context_->GetReSTIRPTContext().GetReservoirBufferParameters(), + rtxdi::c_NumReSTIRPTReservoirBuffers, + sizeof(RTXDI_PackedPTReservoir)); + result.totalReservoirBytes = result.diReservoirBytes + result.giReservoirBytes + result.ptReservoirBytes; + return result; +} + +void RtxdiRuntime::applyQualityPreset() { + auto& di = context_->GetReSTIRDIContext(); + auto initial = rtxdi::GetDefaultReSTIRDIInitialSamplingParams(); + auto temporal = rtxdi::GetDefaultReSTIRDITemporalResamplingParams(); + auto boiling = rtxdi::GetDefaultReSTIRDIBoilingFilterParams(); + auto spatial = rtxdi::GetDefaultReSTIRDISpatialResamplingParams(); + auto shading = rtxdi::GetDefaultReSTIRDIShadingParams(); + + di.SetResamplingMode(rtxdi::ReSTIRDI_ResamplingMode::TemporalAndSpatial); + switch (config_.qualityPreset) { + case RtxdiQualityPreset::Fast: + initial.localLightSamplingMode = ReSTIRDI_LocalLightSamplingMode::Power_RIS; + initial.numLocalLightSamples = 4; + initial.numBrdfSamples = 0; + initial.numInfiniteLightSamples = 1; + temporal.enableVisibilityShortcut = true; + temporal.biasCorrectionMode = ReSTIRDI_TemporalBiasCorrectionMode::Off; + boiling.enableBoilingFilter = true; + boiling.boilingFilterStrength = 0.2f; + spatial.biasCorrectionMode = ReSTIRDI_SpatialBiasCorrectionMode::Off; + spatial.numSamples = 1; + spatial.numDisocclusionBoostSamples = 2; + shading.reuseFinalVisibility = true; + break; + case RtxdiQualityPreset::Medium: + initial.localLightSamplingMode = ReSTIRDI_LocalLightSamplingMode::ReGIR_RIS; + initial.numLocalLightSamples = 8; + initial.numBrdfSamples = 1; + initial.numInfiniteLightSamples = 1; + temporal.enableVisibilityShortcut = true; + temporal.biasCorrectionMode = ReSTIRDI_TemporalBiasCorrectionMode::Raytraced; + boiling.enableBoilingFilter = true; + boiling.boilingFilterStrength = 0.2f; + spatial.biasCorrectionMode = ReSTIRDI_SpatialBiasCorrectionMode::Basic; + spatial.numSamples = 1; + spatial.numDisocclusionBoostSamples = 8; + shading.reuseFinalVisibility = true; + break; + case RtxdiQualityPreset::Unbiased: + case RtxdiQualityPreset::Reference: + initial.localLightSamplingMode = ReSTIRDI_LocalLightSamplingMode::Uniform; + initial.numLocalLightSamples = config_.qualityPreset == RtxdiQualityPreset::Reference ? 16 : 8; + initial.numBrdfSamples = 1; + initial.numInfiniteLightSamples = 1; + temporal.enableVisibilityShortcut = false; + temporal.biasCorrectionMode = ReSTIRDI_TemporalBiasCorrectionMode::Raytraced; + boiling.enableBoilingFilter = false; + spatial.biasCorrectionMode = ReSTIRDI_SpatialBiasCorrectionMode::Raytraced; + spatial.numSamples = config_.qualityPreset == RtxdiQualityPreset::Reference ? 8 : 1; + spatial.numDisocclusionBoostSamples = config_.qualityPreset == RtxdiQualityPreset::Reference ? 32 : 8; + shading.reuseFinalVisibility = false; + break; + case RtxdiQualityPreset::Ultra: + initial.localLightSamplingMode = ReSTIRDI_LocalLightSamplingMode::ReGIR_RIS; + initial.numLocalLightSamples = 16; + initial.numBrdfSamples = 1; + initial.numInfiniteLightSamples = 1; + temporal.enableVisibilityShortcut = false; + temporal.biasCorrectionMode = ReSTIRDI_TemporalBiasCorrectionMode::Raytraced; + boiling.enableBoilingFilter = false; + spatial.biasCorrectionMode = ReSTIRDI_SpatialBiasCorrectionMode::Raytraced; + spatial.numSamples = 4; + spatial.numDisocclusionBoostSamples = 16; + shading.reuseFinalVisibility = false; + break; + } + + di.SetInitialSamplingParameters(initial); + di.SetTemporalResamplingParameters(temporal); + di.SetBoilingFilterParameters(boiling); + di.SetSpatialResamplingParameters(spatial); + di.SetShadingParameters(shading); + + auto& gi = context_->GetReSTIRGIContext(); + gi.SetResamplingMode(rtxdi::ReSTIRGI_ResamplingMode::TemporalAndSpatial); + auto& pt = context_->GetReSTIRPTContext(); + pt.SetResamplingMode(rtxdi::ReSTIRPT_ResamplingMode::TemporalAndSpatial); +} + +} // namespace rtv diff --git a/src/rtv/SceneDocument.cpp b/src/rtv/SceneDocument.cpp index 9b9e554..a674d38 100644 --- a/src/rtv/SceneDocument.cpp +++ b/src/rtv/SceneDocument.cpp @@ -806,7 +806,7 @@ bool SceneDocument::saveJson(const std::filesystem::path& path) const { if (header_.sceneGuid.empty()) { header_.sceneGuid = generateSceneGuid(); } - header_.formatVersion = 4; + header_.formatVersion = 5; header_.engineVersion = "0.1"; header_.projectRelativePaths = true; @@ -1471,6 +1471,19 @@ bool SceneDocument::loadJson(const std::filesystem::path& path) { renderSettings_.restirDiTemporalMaxAge = render.value("restirDiTemporalMaxAge", renderSettings_.restirDiTemporalMaxAge); renderSettings_.restirDiMaxM = render.value("restirDiMaxM", renderSettings_.restirDiMaxM); renderSettings_.restirDiVisibilityRayBudget = render.value("restirDiVisibilityRayBudget", renderSettings_.restirDiVisibilityRayBudget); + if (restirDiMode == RestirDiMode::Off) { + renderSettings_.restirMode = RestirMode::ClassicNee; + } else if (restirDiMode == RestirDiMode::Legacy) { + if (renderSettings_.restirMode == RestirMode::ClassicNee) { + renderSettings_.restirMode = RestirMode::RestirOnly; + } + } else { + renderSettings_.restirMode = restirDiMode == RestirDiMode::HybridCompare + ? RestirMode::HybridCompare + : RestirMode::ClassicNee; + renderSettings_.restirDiFinalVisibilityEnabled = true; + renderSettings_.restirDiVisibilityRayBudget = std::max(renderSettings_.restirDiVisibilityRayBudget, 1u); + } renderSettings_.restirDiProductionStabilizationEnabled = render.value("restirDiProductionStabilizationEnabled", renderSettings_.restirDiProductionStabilizationEnabled); renderSettings_.restirDiClampLuminance = render.value("restirDiClampLuminance", renderSettings_.restirDiClampLuminance); if (loadedFormatVersion < 4u) { @@ -1484,6 +1497,15 @@ bool SceneDocument::loadJson(const std::filesystem::path& path) { renderSettings_.restirDiIncludeSun = render.value("restirDiIncludeSun", renderSettings_.restirDiIncludeSun); renderSettings_.restirDiIncludeEnvironment = render.value("restirDiIncludeEnvironment", renderSettings_.restirDiIncludeEnvironment); } + if (restirDiMode == RestirDiMode::ReferenceValidation) { + renderSettings_.restirDiFinalVisibilityEnabled = true; + renderSettings_.restirDiProductionStabilizationEnabled = false; + renderSettings_.restirDiVisibilityRayBudget = std::max(renderSettings_.restirDiVisibilityRayBudget, 1u); + } else if (restirDiMode == RestirDiMode::Production || + restirDiMode == RestirDiMode::HybridCompare) { + renderSettings_.restirDiFinalVisibilityEnabled = true; + renderSettings_.restirDiVisibilityRayBudget = std::max(renderSettings_.restirDiVisibilityRayBudget, 1u); + } if (restirDiMode == RestirDiMode::ReferenceValidation && (!renderSettings_.restirDiFinalVisibilityEnabled || restirDiLayout != RestirDiReservoirLayout::ValidationFull)) { @@ -1570,6 +1592,9 @@ bool SceneDocument::loadJson(const std::filesystem::path& path) { renderSettings_.restirHistoryCopyMode = historyCopyMode <= static_cast(RestirHistoryCopyMode::PingPong) ? static_cast(historyCopyMode) : RestirHistoryCopyMode::Copy; + if (loadedFormatVersion < 5u && renderSettings_.restirHistoryCopyMode == RestirHistoryCopyMode::Copy) { + renderSettings_.restirHistoryCopyMode = RestirHistoryCopyMode::PingPong; + } const uint32_t lightingReuseMode = render.value("lightingReuseMode", static_cast(renderSettings_.lightingReuseMode)); if (lightingReuseMode > static_cast(LightingReuseMode::ValidateRestirPTAgainstLegacy)) { return false; diff --git a/src/rtv/SceneHierarchyPanel.cpp b/src/rtv/SceneHierarchyPanel.cpp index 5a6ca73..3ad0562 100644 --- a/src/rtv/SceneHierarchyPanel.cpp +++ b/src/rtv/SceneHierarchyPanel.cpp @@ -551,26 +551,124 @@ void SceneHierarchyPanel::draw(const EditorRuntimeState& state, EditorSelection& SceneDocument& document = *state.sceneDocument; SceneRegistry& registry = document.registry(); - ImGui::BeginGroup(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterMesh, EditorGlyphIcon::Model, "Filter mesh objects"); - ImGui::SameLine(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterCamera, EditorGlyphIcon::Camera, "Filter cameras"); - ImGui::SameLine(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterLight, EditorGlyphIcon::Light, "Filter lights and suns"); - ImGui::SameLine(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterWorld, EditorGlyphIcon::Environment, "Filter world environment actors"); + static std::array filterBuffer{}; + ImGui::SetNextItemWidth(-72.0f); + ImGui::InputTextWithHint("##entityFilter", "Search scene...", filterBuffer.data(), filterBuffer.size()); ImGui::SameLine(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterAtmosphere, EditorGlyphIcon::Sky, "Filter atmosphere, fog, and cloud actors"); + const bool filtersActive = typeFilterMask_ != 0 || !layerFilter_.empty() || !tagFilter_.empty() || !collectionFilter_.empty(); + if (editorIconButton("HierarchyFilter", EditorGlyphIcon::ViewSettings, filtersActive)) { + ImGui::OpenPopup("HierarchyFilterPopup"); + } + hierarchyTooltip("Filter hierarchy"); ImGui::SameLine(); - hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterEffects, EditorGlyphIcon::PostProcess, "Filter post-process and effects actors"); - if (typeFilterMask_ != 0) { + if (editorIconButton("HierarchyAddEntity", EditorGlyphIcon::Add, false)) { + ImGui::OpenPopup("HierarchyCreatePopup"); + } + hierarchyTooltip("Create entity"); + + if (ImGui::BeginPopup("HierarchyCreatePopup")) { + if (editorGlyphMenuItem(EditorGlyphIcon::Entity, "Empty Entity")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::Empty}; + requests.sceneUpdate = SceneUpdateKind::None; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Camera, "Camera")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::Camera}; + requests.sceneUpdate = SceneUpdateKind::CameraOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Light, "Point Light")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::Light}; + requests.sceneUpdate = SceneUpdateKind::LightOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Light, "Spot Light")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::SpotLight}; + requests.sceneUpdate = SceneUpdateKind::LightOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Light, "Area Light")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::AreaLight}; + requests.sceneUpdate = SceneUpdateKind::LightOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Sun, "Sun")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::Sun}; + requests.sceneUpdate = SceneUpdateKind::LightOnly; + } + ImGui::Separator(); + if (editorGlyphMenuItem(EditorGlyphIcon::Environment, "Environment Light")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::EnvironmentLight}; + requests.sceneUpdate = SceneUpdateKind::RendererSettingsOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Sky, "Sky Atmosphere")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::SkyAtmosphere}; + requests.sceneUpdate = SceneUpdateKind::RendererSettingsOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Fog, "Height Fog")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::HeightFog}; + requests.sceneUpdate = SceneUpdateKind::RendererSettingsOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::Cloud, "Volumetric Cloud")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::VolumetricCloud}; + requests.sceneUpdate = SceneUpdateKind::RendererSettingsOnly; + } + if (editorGlyphMenuItem(EditorGlyphIcon::PostProcess, "Post Process Volume")) { + requests.createEntity = EditorEntityCreateRequest{.kind = EditorEntityCreateKind::PostProcessVolume}; + requests.sceneUpdate = SceneUpdateKind::RendererSettingsOnly; + } + ImGui::EndPopup(); + } + + std::string filter = filterBuffer.data(); + std::transform(filter.begin(), filter.end(), filter.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + if (ImGui::BeginPopup("HierarchyFilterPopup")) { + ImGui::TextDisabled("TYPE"); + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterMesh, EditorGlyphIcon::Model, "Mesh objects"); ImGui::SameLine(); - if (editorIconButton("HierarchyClearTypeFilters", EditorGlyphIcon::Exit, false, ImVec2(18.0f, 18.0f))) { + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterCamera, EditorGlyphIcon::Camera, "Cameras"); + ImGui::SameLine(); + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterLight, EditorGlyphIcon::Light, "Lights"); + ImGui::SameLine(); + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterWorld, EditorGlyphIcon::Environment, "World actors"); + ImGui::SameLine(); + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterAtmosphere, EditorGlyphIcon::Sky, "Atmosphere"); + ImGui::SameLine(); + hierarchyTypeFilterButton(typeFilterMask_, HierarchyTypeFilterEffects, EditorGlyphIcon::PostProcess, "Effects"); + ImGui::Separator(); + drawHierarchyLayerControls(document, requests, layerFilter_); + drawHierarchyTagControls(document, tagFilter_); + drawHierarchyCollectionControls(document, collectionFilter_); + const std::vector popupSelection = collectFilteredSelectableEntities( + registry, + filter, + layerFilter_, + tagFilter_, + collectionFilter_, + typeFilterMask_); + ImGui::Separator(); + ImGui::BeginDisabled(popupSelection.empty()); + if (editorIconTextButton("HierarchySelectFiltered", EditorGlyphIcon::Select, "Select Filtered")) { + selection.selectEntities(popupSelection); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (editorIconTextButton("HierarchyResetFilters", EditorGlyphIcon::Reset, "Reset")) { typeFilterMask_ = 0; + layerFilter_.clear(); + tagFilter_.clear(); + collectionFilter_.clear(); } - hierarchyTooltip("Clear hierarchy type filters"); + ImGui::EndPopup(); + } + + ImGui::TextDisabled("%zu entities", registry.entities().size()); + if (selection.selectionCount() > 0) { + ImGui::SameLine(); + ImGui::TextColored(editorAccentColor(), "%zu selected", selection.selectionCount()); + } + if (filtersActive) { + ImGui::SameLine(); + ImGui::TextDisabled("filtered"); } - ImGui::EndGroup(); + if (ImGui::BeginDragDropTarget()) { if (const auto* payload = ImGui::AcceptDragDropPayload("PREFAB_ASSET")) { requests.placeAsset = std::string(static_cast(payload->Data)); @@ -578,35 +676,6 @@ void SceneHierarchyPanel::draw(const EditorRuntimeState& state, EditorSelection& ImGui::EndDragDropTarget(); } - static std::array filterBuffer{}; - ImGui::SetNextItemWidth(-FLT_MIN); - ImGui::InputTextWithHint("##entityFilter", "Search...", filterBuffer.data(), filterBuffer.size()); - std::string filter = filterBuffer.data(); - std::transform(filter.begin(), filter.end(), filter.begin(), [](unsigned char ch) { - return static_cast(std::tolower(ch)); - }); - drawHierarchyLayerControls(document, requests, layerFilter_); - drawHierarchyTagControls(document, tagFilter_); - drawHierarchyCollectionControls(document, collectionFilter_); - const std::vector filteredSelectable = collectFilteredSelectableEntities( - registry, - filter, - layerFilter_, - tagFilter_, - collectionFilter_, - typeFilterMask_); - ImGui::BeginDisabled(filteredSelectable.empty()); - if (editorIconTextButton("HierarchySelectFiltered", EditorGlyphIcon::Select, "Select Filtered")) { - selection.selectEntities(filteredSelectable); - } - ImGui::EndDisabled(); - hierarchyTooltip("Select all unlocked entities matching the current hierarchy filters."); - ImGui::SameLine(); - ImGui::BeginDisabled(selection.selectionCount() == 0); - if (editorIconTextButton("HierarchyClearSelection", EditorGlyphIcon::Exit, "Clear Selection")) { - selection.clear(); - } - ImGui::EndDisabled(); ImGui::Separator(); const EntityId selectedEntity = selection.entityId(); diff --git a/src/rtv/SceneRenderSettingsSync.cpp b/src/rtv/SceneRenderSettingsSync.cpp index 5545e57..4103c47 100644 --- a/src/rtv/SceneRenderSettingsSync.cpp +++ b/src/rtv/SceneRenderSettingsSync.cpp @@ -163,7 +163,13 @@ void applySceneWorldComponentsToRendererSettings(const SceneDocument& document, settings.mieAnisotropy = std::clamp(selection.skyAtmosphere->mieAnisotropy, 0.0f, 0.99f); settings.groundAlbedo = std::clamp(selection.skyAtmosphere->groundAlbedo, 0.0f, 1.0f); } else { - settings.skyIntensity = 0.0f; + const bool proceduralEnvironment = + selection.environmentLight != nullptr && + selection.environmentLight->enabled && + selection.environmentLight->hdrPath.empty(); + settings.skyIntensity = proceduralEnvironment + ? safeNonNegative(document.renderSettings().skyIntensity) + : 0.0f; } if (selection.heightFog != nullptr) { diff --git a/src/rtv/SceneStatsPanel.cpp b/src/rtv/SceneStatsPanel.cpp index 09395dd..cc63abe 100644 --- a/src/rtv/SceneStatsPanel.cpp +++ b/src/rtv/SceneStatsPanel.cpp @@ -1,58 +1,117 @@ #include "rtv/SceneStatsPanel.h" +#include "rtv/CameraBookmark.h" +#include "rtv/EditorUiStyle.h" #include "rtv/PathTracerRenderer.h" #include +#include +#include + namespace rtv { -void SceneStatsPanel::draw(const EditorRuntimeState& state) { - if (!ImGui::Begin("Scene Stats")) { +namespace { + +void drawStatRow(const char* label, const char* value) { + ImGui::TableNextRow(ImGuiTableRowFlags_None, EditorUiMetric::propertyRowHeight); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(value); +} + +template +void drawStatRow(const char* label, T value) { + const std::string text = std::to_string(value); + drawStatRow(label, text.c_str()); +} + +} // namespace + +void SceneStatsPanel::draw(const EditorRuntimeState& state, EditorRequests& requests) { + if (!ImGui::Begin("Scene Explorer")) { ImGui::End(); return; } - const MeshParamsUniform& meshParams = state.renderer.scene().meshParams(); - const RayTracingRendererStats rt = state.renderer.rayTracingStats(); + if (ImGui::BeginTabBar("SceneExplorerTabs")) { + if (ImGui::BeginTabItem("Statistics")) { + const MeshParamsUniform& meshParams = state.renderer.scene().meshParams(); + const RayTracingRendererStats rt = state.renderer.rayTracingStats(); + if (ImGui::BeginTable("SceneStatisticsTable", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoSavedSettings)) { + ImGui::TableSetupColumn("Metric", ImGuiTableColumnFlags_WidthStretch, 0.58f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 0.42f); + if (state.sceneDocument != nullptr) { + drawStatRow("Entities", state.sceneDocument->registry().liveCount()); + } + drawStatRow("Primitives", meshParams.primitiveCount); + drawStatRow("Instances", meshParams.instanceCount); + drawStatRow("Triangles", meshParams.triangleCount); + drawStatRow("Lights", meshParams.lightCount); + drawStatRow("Materials", meshParams.materialCount); + char textures[64]{}; + std::snprintf( + textures, + sizeof(textures), + "%u / %u", + state.renderer.scene().materialTextureCount(), + state.renderer.scene().materialTextureSlotCapacity()); + drawStatRow("Textures", static_cast(textures)); + if (rt.active) { + char memory[64]{}; + std::snprintf(memory, sizeof(memory), "%.2f MB", static_cast(rt.accelerationStructureBytes) / (1024.0 * 1024.0)); + drawStatRow("RT memory", static_cast(memory)); + } + ImGui::EndTable(); + } + ImGui::EndTabItem(); + } - ImGui::SeparatorText("Scene"); - if (state.sceneDocument != nullptr) { - ImGui::Text("Entities: %zu", state.sceneDocument->registry().liveCount()); - } - ImGui::Text("Meshes: %u", meshParams.meshCount); - ImGui::Text("Primitives: %u", meshParams.primitiveCount); - ImGui::Text("Triangles: %u", meshParams.triangleCount); - ImGui::Text("Vertices: %u", meshParams.vertexCount); - ImGui::Text("Instances: %u", meshParams.instanceCount); - ImGui::Text("Materials: %u", meshParams.materialCount); - ImGui::Text("Textures: %u resident / %u capacity", - state.renderer.scene().materialTextureCount(), - state.renderer.scene().materialTextureSlotCapacity()); - - ImGui::SeparatorText("Geometry"); - ImGui::Text("Local vertices: %u", meshParams.localVertexCount); - ImGui::Text("Local triangles: %u", meshParams.localTriangleCount); - ImGui::Text("Spheres: %u", meshParams.sphereCount); - ImGui::Text("Lights: %u", meshParams.lightCount); - ImGui::Text("Emissive area: %.3f", meshParams.emissiveTotalArea); - - ImGui::SeparatorText("BVH"); - ImGui::Text("BVH nodes: %u", meshParams.bvhNodeCount); - ImGui::Text("Local BVH nodes: %u", meshParams.localBvhNodeCount); - ImGui::Text("TLAS nodes: %u", meshParams.tlasNodeCount); - ImGui::Text("TLAS indices: %u", meshParams.tlasInstanceIndexCount); - - if (rt.active) { - ImGui::SeparatorText("Hardware RT"); - ImGui::Text("BLAS: %u", rt.blasCount); - ImGui::Text("Instances: %u", rt.instanceCount); - ImGui::Text("Opaque primitives: %u", rt.geometry.opaquePrimitiveCount); - ImGui::Text("Alpha-tested primitives: %u", rt.geometry.alphaTestedPrimitiveCount); - ImGui::Text("Blended primitives: %u", rt.geometry.blendedPrimitiveCount); - constexpr double mb = 1024.0 * 1024.0; - ImGui::Text("AS memory: %.2f MB", static_cast(rt.accelerationStructureBytes) / mb); - ImGui::Text("SBT: %.2f KB", static_cast(rt.sbtBytes) / 1024.0); - ImGui::Text("TLAS refit: %.3f ms", rt.lastTlasRefitMs); + if (ImGui::BeginTabItem("Bookmarks")) { + static std::array name{}; + ImGui::SetNextItemWidth(-92.0f); + ImGui::InputTextWithHint("##BookmarkName", "Bookmark name", name.data(), name.size()); + ImGui::SameLine(); + if (editorIconTextButton("SaveCameraBookmark", EditorGlyphIcon::Save, "Save")) { + std::string label = name.data(); + if (label.empty()) { + label = "Camera " + std::to_string(state.cameraBookmarks != nullptr ? state.cameraBookmarks->count() + 1u : 1u); + } + requests.saveCameraBookmark = std::move(label); + name.fill('\0'); + } + + ImGui::Separator(); + if (state.cameraBookmarks == nullptr || state.cameraBookmarks->bookmarks().empty()) { + ImGui::TextDisabled("No camera bookmarks"); + } else { + const auto& bookmarks = state.cameraBookmarks->bookmarks(); + for (size_t index = 0; index < bookmarks.size(); ++index) { + ImGui::PushID(static_cast(index)); + const CameraBookmark& bookmark = bookmarks[index]; + if (ImGui::Selectable(bookmark.name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + requests.loadCameraBookmarkIndex = index; + } + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("Double-click to load"); + } + ImGui::SameLine(ImGui::GetContentRegionMax().x - 56.0f); + if (editorIconButton("LoadBookmark", EditorGlyphIcon::Camera, false, ImVec2(24.0f, 22.0f))) { + requests.loadCameraBookmarkIndex = index; + } + ImGui::SameLine(); + if (editorIconButton("DeleteBookmark", EditorGlyphIcon::Trash, false, ImVec2(24.0f, 22.0f))) { + requests.deleteCameraBookmarkIndex = index; + } + ImGui::PopID(); + } + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); diff --git a/src/rtv/StreamingGpuTransferExecutor.cpp b/src/rtv/StreamingGpuTransferExecutor.cpp index e25c636..3b03f3a 100644 --- a/src/rtv/StreamingGpuTransferExecutor.cpp +++ b/src/rtv/StreamingGpuTransferExecutor.cpp @@ -3,6 +3,7 @@ #include "rtv/AccelerationStructure.h" #include "rtv/Buffer.h" #include "rtv/Check.h" +#include "rtv/GpuValidation.h" #include "rtv/Image.h" #include "rtv/ImageBarrier.h" #include "rtv/ResourceAllocator.h" @@ -102,6 +103,20 @@ void StreamingGpuTransferExecutor::shutdown() { waitInfo.pValues = &submittedTimeline_; (void)vkWaitSemaphores(device_, &waitInfo, UINT64_MAX); } + if (computeTimeline_ != VK_NULL_HANDLE && submittedComputeTimeline_ > 0) { + VkSemaphoreWaitInfo waitInfo{}; + waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + waitInfo.semaphoreCount = 1; + waitInfo.pSemaphores = &computeTimeline_; + waitInfo.pValues = &submittedComputeTimeline_; + (void)vkWaitSemaphores(device_, &waitInfo, UINT64_MAX); + } + const bool computeSharesTransferTimeline = computeTimeline_ == timeline_; + if (computeTimelineOwned_ && computeTimeline_ != VK_NULL_HANDLE && !computeSharesTransferTimeline) { + vkDestroySemaphore(device_, computeTimeline_, nullptr); + } + computeTimeline_ = VK_NULL_HANDLE; + computeTimelineOwned_ = false; if (timeline_ != VK_NULL_HANDLE) { vkDestroySemaphore(device_, timeline_, nullptr); timeline_ = VK_NULL_HANDLE; @@ -119,6 +134,7 @@ void StreamingGpuTransferExecutor::shutdown() { } pendingCompactionQueries_.clear(); compactedBlasSizes_.clear(); + imageLayouts_.clear(); if (commandPool_ != VK_NULL_HANDLE) { vkDestroyCommandPool(device_, commandPool_, nullptr); commandPool_ = VK_NULL_HANDLE; @@ -127,20 +143,10 @@ void StreamingGpuTransferExecutor::shutdown() { vkDestroyCommandPool(device_, graphicsCommandPool_, nullptr); graphicsCommandPool_ = VK_NULL_HANDLE; } - if (computeCommandPool_ != VK_NULL_HANDLE) { - vkDestroyCommandPool(device_, computeCommandPool_, nullptr); - computeCommandPool_ = VK_NULL_HANDLE; - } - if (computeTimeline_ != VK_NULL_HANDLE) { - vkDestroySemaphore(device_, computeTimeline_, nullptr); - computeTimeline_ = VK_NULL_HANDLE; - } stagingRing_ = StreamingStagingRing(); inFlight_.clear(); - computeInFlight_.clear(); freeCommandBuffers_.clear(); freeGraphicsCommandBuffers_.clear(); - freeComputeCommandBuffers_.clear(); openBatch_ = VK_NULL_HANDLE; openBatchCopies_ = 0; openGraphicsBatch_ = VK_NULL_HANDLE; @@ -149,6 +155,10 @@ void StreamingGpuTransferExecutor::shutdown() { allocator_ = nullptr; graphicsQueue_ = VK_NULL_HANDLE; graphicsQueueFamily_ = UINT32_MAX; + computeQueue_ = VK_NULL_HANDLE; + nextComputeTimelineValue_ = 1; + submittedComputeTimeline_ = 0; + completedComputeTimeline_ = 0; } VkCommandBuffer StreamingGpuTransferExecutor::beginBatch() { @@ -220,6 +230,7 @@ bool StreamingGpuTransferExecutor::stageBufferUpload(Buffer& destination, const return false; } std::memcpy(alloc->mapped, src, static_cast(bytes)); + stagingRing_.flush(alloc->offset, bytes); VkCommandBuffer cmd = beginBatch(); VkBufferCopy copy{}; @@ -254,11 +265,18 @@ bool StreamingGpuTransferExecutor::stageImageMipUpload( return false; } std::memcpy(alloc->mapped, src, static_cast(bytes)); + stagingRing_.flush(alloc->offset, bytes); VkCommandBuffer cmd = beginBatch(); + TrackedImageLayouts& tracked = imageLayouts_[&destination]; + if (tracked.image != destination.handle() || tracked.mips.size() != destination.mipLevels()) { + tracked.image = destination.handle(); + tracked.mips.assign(destination.mipLevels(), destination.layout()); + } + const VkImageLayout oldMipLayout = tracked.mips[mipLevel]; barrier::cmdTransitionImage(cmd, { .image = destination.handle(), - .oldLayout = destination.layout(), + .oldLayout = oldMipLayout, .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .range = barrier::colorRange(mipLevel, 1), .srcStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, @@ -296,6 +314,7 @@ bool StreamingGpuTransferExecutor::stageImageMipUpload( .dstStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, .dstAccess = VK_ACCESS_2_MEMORY_READ_BIT, }); + tracked.mips[mipLevel] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; destination.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ++openBatchCopies_; @@ -362,11 +381,15 @@ bool StreamingGpuTransferExecutor::stageBlasBuild(const BlasTriangleBuild& build barrier.srcStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; barrier.srcAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_2_MEMORY_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR | + VK_ACCESS_2_MEMORY_READ_BIT | + VK_ACCESS_2_MEMORY_WRITE_BIT; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; + recordManualBarrierEscape("StreamingGpuTransferExecutor", "streaming_blas_build", dependency); vkCmdPipelineBarrier2(cmd, &dependency); if ((build.flags & VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_COMPACTION_BIT_KHR) != 0u) { @@ -417,14 +440,20 @@ bool StreamingGpuTransferExecutor::stageBlasCompaction(AccelerationStructure& so VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; - barrier.srcStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_COPY_BIT_KHR; + // The dedicated copy stage requires VK_KHR_ray_tracing_maintenance1. The + // build stage also covers acceleration-structure copies on baseline KHR RT. + barrier.srcStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; barrier.srcAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_2_MEMORY_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR | + VK_ACCESS_2_MEMORY_READ_BIT | + VK_ACCESS_2_MEMORY_WRITE_BIT; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; + recordManualBarrierEscape("StreamingGpuTransferExecutor", "streaming_blas_compaction", dependency); vkCmdPipelineBarrier2(cmd, &dependency); ++openGraphicsBatchOps_; @@ -496,11 +525,15 @@ bool StreamingGpuTransferExecutor::stageTlasBuild(const TlasBuild& build) { barrier.srcStageMask = VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR; barrier.srcAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR; barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | VK_ACCESS_2_MEMORY_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR | + VK_ACCESS_2_ACCELERATION_STRUCTURE_WRITE_BIT_KHR | + VK_ACCESS_2_MEMORY_READ_BIT | + VK_ACCESS_2_MEMORY_WRITE_BIT; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; + recordManualBarrierEscape("StreamingGpuTransferExecutor", "streaming_tlas_build", dependency); vkCmdPipelineBarrier2(cmd, &dependency); ++openGraphicsBatchOps_; @@ -522,6 +555,7 @@ uint64_t StreamingGpuTransferExecutor::submitFrame() { checkVk(vkEndCommandBuffer(openBatch_), "vkEndCommandBuffer(streaming transfer)"); const uint64_t signalValue = nextTimelineValue_++; + const uint64_t waitValue = submittedTimeline_; VkCommandBufferSubmitInfo cmdInfo{}; cmdInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; @@ -533,10 +567,20 @@ uint64_t StreamingGpuTransferExecutor::submitFrame() { signalInfo.value = signalValue; signalInfo.stageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + VkSemaphoreSubmitInfo waitInfo{}; + waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + waitInfo.semaphore = timeline_; + waitInfo.value = waitValue; + waitInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + VkSubmitInfo2 submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; submit.commandBufferInfoCount = 1; submit.pCommandBufferInfos = &cmdInfo; + if (waitValue != 0) { + submit.waitSemaphoreInfoCount = 1; + submit.pWaitSemaphoreInfos = &waitInfo; + } submit.signalSemaphoreInfoCount = 1; submit.pSignalSemaphoreInfos = &signalInfo; checkVk(vkQueueSubmit2(queue_, 1, &submit, VK_NULL_HANDLE), "vkQueueSubmit2(streaming transfer)"); @@ -562,6 +606,7 @@ uint64_t StreamingGpuTransferExecutor::submitGraphicsFrame(uint64_t waitTimeline checkVk(vkEndCommandBuffer(openGraphicsBatch_), "vkEndCommandBuffer(streaming graphics)"); const uint64_t signalValue = nextTimelineValue_++; + const uint64_t orderedWaitTimelineValue = std::max(waitTimelineValue, submittedTimeline_); VkCommandBufferSubmitInfo cmdInfo{}; cmdInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; @@ -570,7 +615,7 @@ uint64_t StreamingGpuTransferExecutor::submitGraphicsFrame(uint64_t waitTimeline VkSemaphoreSubmitInfo waitInfo{}; waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; waitInfo.semaphore = timeline_; - waitInfo.value = waitTimelineValue; + waitInfo.value = orderedWaitTimelineValue; waitInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; VkSemaphoreSubmitInfo signalInfo{}; @@ -583,7 +628,7 @@ uint64_t StreamingGpuTransferExecutor::submitGraphicsFrame(uint64_t waitTimeline submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; submit.commandBufferInfoCount = 1; submit.pCommandBufferInfos = &cmdInfo; - if (waitTimelineValue != 0) { + if (orderedWaitTimelineValue != 0) { submit.waitSemaphoreInfoCount = 1; submit.pWaitSemaphoreInfos = &waitInfo; } @@ -638,6 +683,13 @@ uint64_t StreamingGpuTransferExecutor::submitTimelineMarker() { // Submit an empty batch whose only purpose is to advance the device timeline // so streaming completion can be gated on real GPU progress (never faked). const uint64_t signalValue = nextTimelineValue_++; + const uint64_t waitValue = submittedTimeline_; + + VkSemaphoreSubmitInfo waitInfo{}; + waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + waitInfo.semaphore = timeline_; + waitInfo.value = waitValue; + waitInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; VkSemaphoreSubmitInfo signalInfo{}; signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; @@ -647,6 +699,10 @@ uint64_t StreamingGpuTransferExecutor::submitTimelineMarker() { VkSubmitInfo2 submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + if (waitValue != 0) { + submit.waitSemaphoreInfoCount = 1; + submit.pWaitSemaphoreInfos = &waitInfo; + } submit.signalSemaphoreInfoCount = 1; submit.pSignalSemaphoreInfos = &signalInfo; checkVk(vkQueueSubmit2(queue_, 1, &submit, VK_NULL_HANDLE), "vkQueueSubmit2(streaming transfer marker)"); @@ -658,18 +714,15 @@ uint64_t StreamingGpuTransferExecutor::submitTimelineMarker() { void StreamingGpuTransferExecutor::recycleCompleted(uint64_t completedTimeline) { while (!inFlight_.empty() && inFlight_.front().timelineValue <= completedTimeline) { - if (inFlight_.front().graphics) { - freeGraphicsCommandBuffers_.push_back(inFlight_.front().commandBuffer); - } else { - freeCommandBuffers_.push_back(inFlight_.front().commandBuffer); + if (inFlight_.front().commandBuffer != VK_NULL_HANDLE) { + if (inFlight_.front().graphics) { + freeGraphicsCommandBuffers_.push_back(inFlight_.front().commandBuffer); + } else { + freeCommandBuffers_.push_back(inFlight_.front().commandBuffer); + } } inFlight_.pop_front(); } - // Recycle completed compute-queue command buffers. - while (!computeInFlight_.empty() && computeInFlight_.front().timelineValue <= completedTimeline) { - freeComputeCommandBuffers_.push_back(computeInFlight_.front().commandBuffer); - computeInFlight_.pop_front(); - } (void)stagingRing_.retire(completedTimeline); auto queryOut = pendingCompactionQueries_.begin(); for (auto queryIt = pendingCompactionQueries_.begin(); queryIt != pendingCompactionQueries_.end(); ++queryIt) { @@ -712,78 +765,90 @@ uint64_t StreamingGpuTransferExecutor::poll() { if (device_ == VK_NULL_HANDLE || timeline_ == VK_NULL_HANDLE) { return 0; } - uint64_t value = 0; - if (vkGetSemaphoreCounterValue(device_, timeline_, &value) != VK_SUCCESS) { - return completedTimeline_; + uint64_t transferValue = 0; + if (vkGetSemaphoreCounterValue(device_, timeline_, &transferValue) == VK_SUCCESS) { + completedTimeline_ = transferValue; + recycleCompleted(transferValue); } - completedTimeline_ = value; - recycleCompleted(value); - return value; -} - -void StreamingGpuTransferExecutor::setComputeQueue(VkQueue queue, uint32_t familyIndex, VkSemaphore timelineSemaphore) { - computeQueue_ = queue; - computeQueueFamily_ = familyIndex; - computeTimeline_ = timelineSemaphore; - nextComputeTimelineValue_ = 1; - - if (device_ != VK_NULL_HANDLE && queue != VK_NULL_HANDLE && computeCommandPool_ == VK_NULL_HANDLE) { - VkCommandPoolCreateInfo poolInfo{}; - poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - poolInfo.queueFamilyIndex = familyIndex; - vkCreateCommandPool(device_, &poolInfo, nullptr, &computeCommandPool_); + if (computeTimeline_ != VK_NULL_HANDLE) { + uint64_t computeValue = 0; + if (vkGetSemaphoreCounterValue(device_, computeTimeline_, &computeValue) == VK_SUCCESS) { + completedComputeTimeline_ = computeValue; + } } + return completedTimeline_; } -bool StreamingGpuTransferExecutor::recordComputeDispatch(VkCommandBuffer externalCommandBuffer, - uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { - if (computeQueue_ == VK_NULL_HANDLE || externalCommandBuffer == VK_NULL_HANDLE) { +bool StreamingGpuTransferExecutor::setComputeQueue( + VkQueue queue, + VkSemaphore timelineSemaphore, + ComputeTimelineOwnership ownership) { + if ((queue == VK_NULL_HANDLE) != (timelineSemaphore == VK_NULL_HANDLE) || + (timelineSemaphore != VK_NULL_HANDLE && timelineSemaphore == timeline_) || + (device_ == VK_NULL_HANDLE && queue != VK_NULL_HANDLE)) { return false; } - if (groupCountX == 0 || groupCountY == 0 || groupCountZ == 0) { + + uint64_t newCompletedTimeline = 0; + if (device_ != VK_NULL_HANDLE && timelineSemaphore != VK_NULL_HANDLE && + vkGetSemaphoreCounterValue(device_, timelineSemaphore, &newCompletedTimeline) != VK_SUCCESS) { return false; } - vkCmdDispatch(externalCommandBuffer, groupCountX, groupCountY, groupCountZ); + + if (device_ != VK_NULL_HANDLE && computeTimeline_ != VK_NULL_HANDLE && + computeTimeline_ != timelineSemaphore && submittedComputeTimeline_ > completedComputeTimeline_) { + VkSemaphoreWaitInfo waitInfo{}; + waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + waitInfo.semaphoreCount = 1; + waitInfo.pSemaphores = &computeTimeline_; + waitInfo.pValues = &submittedComputeTimeline_; + if (vkWaitSemaphores(device_, &waitInfo, UINT64_MAX) != VK_SUCCESS) { + return false; + } + completedComputeTimeline_ = submittedComputeTimeline_; + } + + if (device_ != VK_NULL_HANDLE && computeTimelineOwned_ && + computeTimeline_ != VK_NULL_HANDLE && computeTimeline_ != timelineSemaphore && + computeTimeline_ != timeline_) { + vkDestroySemaphore(device_, computeTimeline_, nullptr); + } + + const bool sameTimeline = computeTimeline_ == timelineSemaphore && timelineSemaphore != VK_NULL_HANDLE; + computeQueue_ = queue; + computeTimeline_ = timelineSemaphore; + computeTimelineOwned_ = ownership == ComputeTimelineOwnership::Owned && + timelineSemaphore != VK_NULL_HANDLE && timelineSemaphore != timeline_; + if (sameTimeline) { + completedComputeTimeline_ = std::max(completedComputeTimeline_, newCompletedTimeline); + nextComputeTimelineValue_ = std::max(nextComputeTimelineValue_, completedComputeTimeline_ + 1); + } else { + completedComputeTimeline_ = newCompletedTimeline; + submittedComputeTimeline_ = newCompletedTimeline; + nextComputeTimelineValue_ = newCompletedTimeline + 1; + } return true; } -uint64_t StreamingGpuTransferExecutor::submitComputeFrame(uint64_t waitTimelineValue) { +uint64_t StreamingGpuTransferExecutor::submitComputeTimelineMarker(uint64_t waitTimelineValue) { if (device_ == VK_NULL_HANDLE || computeQueue_ == VK_NULL_HANDLE || - computeTimeline_ == VK_NULL_HANDLE || computeCommandPool_ == VK_NULL_HANDLE) { + computeTimeline_ == VK_NULL_HANDLE || waitTimelineValue > submittedTimeline_) { return 0; } - // Allocate a fresh compute command buffer for this frame. - VkCommandBufferAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandPool = computeCommandPool_; - allocInfo.commandBufferCount = 1; - - VkCommandBuffer computeCmd = VK_NULL_HANDLE; - if (vkAllocateCommandBuffers(device_, &allocInfo, &computeCmd) != VK_SUCCESS) { + uint64_t currentComputeTimeline = 0; + if (vkGetSemaphoreCounterValue(device_, computeTimeline_, ¤tComputeTimeline) != VK_SUCCESS) { return 0; } - - VkCommandBufferBeginInfo beginInfo{}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(computeCmd, &beginInfo); - - // No work recorded yet — this is a placeholder for future compute-shader decompression. - // The external caller records dispatches via recordComputeDispatch before submission. - - vkEndCommandBuffer(computeCmd); - - const uint64_t signalValue = nextComputeTimelineValue_++; + completedComputeTimeline_ = std::max(completedComputeTimeline_, currentComputeTimeline); + const uint64_t signalValue = std::max(nextComputeTimelineValue_, currentComputeTimeline + 1); // Wait on the transfer timeline. VkSemaphoreSubmitInfo waitInfo{}; waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; waitInfo.semaphore = timeline_; waitInfo.value = waitTimelineValue; - waitInfo.stageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + waitInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; // Signal compute timeline. VkSemaphoreSubmitInfo signalInfo{}; @@ -792,32 +857,23 @@ uint64_t StreamingGpuTransferExecutor::submitComputeFrame(uint64_t waitTimelineV signalInfo.value = signalValue; signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - VkCommandBufferSubmitInfo cmdInfo{}; - cmdInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; - cmdInfo.commandBuffer = computeCmd; - VkSubmitInfo2 submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; if (waitTimelineValue > 0) { submit.waitSemaphoreInfoCount = 1; submit.pWaitSemaphoreInfos = &waitInfo; } - submit.commandBufferInfoCount = 1; - submit.pCommandBufferInfos = &cmdInfo; submit.signalSemaphoreInfoCount = 1; submit.pSignalSemaphoreInfos = &signalInfo; const VkResult result = vkQueueSubmit2(computeQueue_, 1, &submit, VK_NULL_HANDLE); if (result != VK_SUCCESS) { - vkFreeCommandBuffers(device_, computeCommandPool_, 1, &computeCmd); return 0; } - // Track the compute command buffer for recycling when the timeline is - // signaled (freed in poll/recycleCompleted alongside in-flight tracking). - computeInFlight_.push_back({signalValue, computeCmd, true}); - // Also add to the general in-flight tracking so poll() can drain both queues. - inFlight_.push_back({signalValue, VK_NULL_HANDLE, true}); + nextComputeTimelineValue_ = signalValue + 1; + submittedComputeTimeline_ = signalValue; + ++totalSubmissions_; return signalValue; } @@ -829,7 +885,10 @@ StreamingGpuTransferExecutor::Stats StreamingGpuTransferExecutor::stats() const out.transferQueueFamily = queueFamily_; out.submittedTimeline = submittedTimeline_; out.completedTimeline = completedTimeline_; - out.inFlightSubmissions = static_cast(inFlight_.size()); + const uint64_t pendingComputeMarkers = submittedComputeTimeline_ > completedComputeTimeline_ + ? submittedComputeTimeline_ - completedComputeTimeline_ + : 0; + out.inFlightSubmissions = static_cast(inFlight_.size() + pendingComputeMarkers); out.totalSubmissions = totalSubmissions_; out.totalBufferCopies = totalBufferCopies_; out.totalImageCopies = totalImageCopies_; @@ -921,40 +980,166 @@ bool StreamingGpuTransferExecutor::runSelfTest(std::string& errorOut) { return false; } - std::array imagePattern{}; - for (size_t i = 0; i < imagePattern.size(); ++i) { - imagePattern[i] = static_cast((i * 17u + 3u) & 0xFFu); + struct SelfTestMipUpload { + uint32_t mipLevel = 0; + uint32_t width = 1; + uint32_t height = 1; + VkDeviceSize readbackOffset = 0; + std::vector bytes; + }; + std::array mipUploads{{ + {.mipLevel = 2, .width = 1, .height = 1, .bytes = std::vector(1u * 1u * 4u)}, + {.mipLevel = 0, .width = 4, .height = 4, .bytes = std::vector(4u * 4u * 4u)}, + {.mipLevel = 1, .width = 2, .height = 2, .bytes = std::vector(2u * 2u * 4u)}, + }}; + for (SelfTestMipUpload& upload : mipUploads) { + for (size_t i = 0; i < upload.bytes.size(); ++i) { + upload.bytes[i] = static_cast( + (i * 17u + static_cast(upload.mipLevel) * 53u + 3u) & 0xFFu); + } + } + constexpr VkDeviceSize kImageCopyOffsetAlignment = 16; + VkDeviceSize imageReadbackBytes = 0; + for (SelfTestMipUpload& upload : mipUploads) { + upload.readbackOffset = Buffer::alignUp(imageReadbackBytes, kImageCopyOffsetAlignment); + imageReadbackBytes = upload.readbackOffset + static_cast(upload.bytes.size()); } Image image(*allocator_, ImageDesc{ .width = 4, .height = 4, .depth = 1, - .mipLevels = 1, + .mipLevels = 3, .format = VK_FORMAT_R8G8B8A8_UNORM, - .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT, .aspect = VK_IMAGE_ASPECT_COLOR_BIT, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, .createDefaultView = true, .debugName = "streaming transfer selftest image", }); - if (!stageImageMipUpload(image, imagePattern.data(), imagePattern.size(), 0, 4, 4)) { - errorOut = "stageImageMipUpload failed"; - return false; + uint64_t imageTimeline = 0; + for (const SelfTestMipUpload& upload : mipUploads) { + if (!stageImageMipUpload( + image, + upload.bytes.data(), + upload.bytes.size(), + upload.mipLevel, + upload.width, + upload.height)) { + errorOut = "stageImageMipUpload(multi-mip) failed"; + return false; + } + const uint64_t mipTimeline = submitFrame(); + if (mipTimeline == 0 || mipTimeline <= imageTimeline) { + errorOut = "multi-mip image uploads did not produce increasing timeline values"; + return false; + } + imageTimeline = mipTimeline; } - const uint64_t imageTimeline = submitFrame(); VkSemaphoreWaitInfo imageWait{}; imageWait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; imageWait.semaphoreCount = 1; imageWait.pSemaphores = &timeline_; imageWait.pValues = &imageTimeline; if (vkWaitSemaphores(device_, &imageWait, UINT64_MAX) != VK_SUCCESS) { - errorOut = "vkWaitSemaphores(image upload) failed"; + errorOut = "vkWaitSemaphores(multi-mip image uploads) failed"; return false; } if (poll() < imageTimeline) { - errorOut = "image upload timeline did not complete after wait"; + errorOut = "multi-mip image upload timeline did not complete after final wait"; + return false; + } + + Buffer imageReadback(*allocator_, BufferDesc{ + .size = imageReadbackBytes, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::Readback, + .persistentMapped = true, + .debugName = "streaming transfer selftest image readback", + }); + VkCommandBuffer imageReadbackCmd = beginBatch(); + std::array imageCopies{}; + TrackedImageLayouts& imageTrackedLayouts = imageLayouts_[&image]; + for (size_t i = 0; i < mipUploads.size(); ++i) { + const SelfTestMipUpload& upload = mipUploads[i]; + barrier::cmdTransitionImage(imageReadbackCmd, { + .image = image.handle(), + .oldLayout = imageTrackedLayouts.mips[upload.mipLevel], + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .range = barrier::colorRange(upload.mipLevel, 1), + .srcStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .srcAccess = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, + .dstStage = VK_PIPELINE_STAGE_2_COPY_BIT, + .dstAccess = VK_ACCESS_2_TRANSFER_READ_BIT, + }); + imageTrackedLayouts.mips[upload.mipLevel] = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + + VkBufferImageCopy& imageCopy = imageCopies[i]; + imageCopy.bufferOffset = upload.readbackOffset; + imageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageCopy.imageSubresource.mipLevel = upload.mipLevel; + imageCopy.imageSubresource.baseArrayLayer = 0; + imageCopy.imageSubresource.layerCount = 1; + imageCopy.imageExtent = {upload.width, upload.height, 1}; + } + vkCmdCopyImageToBuffer( + imageReadbackCmd, + image.handle(), + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + imageReadback.handle(), + static_cast(imageCopies.size()), + imageCopies.data()); + for (const SelfTestMipUpload& upload : mipUploads) { + barrier::cmdTransitionImage(imageReadbackCmd, { + .image = image.handle(), + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .range = barrier::colorRange(upload.mipLevel, 1), + .srcStage = VK_PIPELINE_STAGE_2_COPY_BIT, + .srcAccess = VK_ACCESS_2_TRANSFER_READ_BIT, + .dstStage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .dstAccess = VK_ACCESS_2_MEMORY_READ_BIT, + }); + imageTrackedLayouts.mips[upload.mipLevel] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + image.setLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + openBatchCopies_ += static_cast(imageCopies.size()); + totalImageCopies_ += static_cast(imageCopies.size()); + + const uint64_t imageReadbackTimeline = submitFrame(); + if (imageReadbackTimeline <= imageTimeline) { + errorOut = "multi-mip image readback did not produce a later timeline value"; + return false; + } + VkSemaphoreWaitInfo imageReadbackWait{}; + imageReadbackWait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + imageReadbackWait.semaphoreCount = 1; + imageReadbackWait.pSemaphores = &timeline_; + imageReadbackWait.pValues = &imageReadbackTimeline; + if (vkWaitSemaphores(device_, &imageReadbackWait, UINT64_MAX) != VK_SUCCESS) { + errorOut = "vkWaitSemaphores(multi-mip image readback) failed"; + return false; + } + if (poll() < imageReadbackTimeline) { + errorOut = "multi-mip image readback timeline did not complete after wait"; + return false; + } + imageReadback.invalidate(imageReadbackBytes); + const uint8_t* imageReadbackData = static_cast(imageReadback.mappedData()); + if (imageReadbackData == nullptr) { + errorOut = "multi-mip image readback buffer not mapped"; return false; } + for (const SelfTestMipUpload& upload : mipUploads) { + if (std::memcmp( + imageReadbackData + upload.readbackOffset, + upload.bytes.data(), + upload.bytes.size()) != 0) { + errorOut = "multi-mip image readback bytes did not match uploaded pattern"; + return false; + } + } struct SelfTestVertex { float x = 0.0f; @@ -1046,20 +1231,48 @@ bool StreamingGpuTransferExecutor::runSelfTest(std::string& errorOut) { return false; } const uint64_t blasTimeline = submitTimelineMarker(); - VkSemaphoreWaitInfo blasWait{}; - blasWait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; - blasWait.semaphoreCount = 1; - blasWait.pSemaphores = &timeline_; - blasWait.pValues = &blasTimeline; - if (blasTimeline == 0 || vkWaitSemaphores(device_, &blasWait, UINT64_MAX) != VK_SUCCESS) { - errorOut = "vkWaitSemaphores(BLAS build) failed"; + if (blasTimeline == 0) { + errorOut = "BLAS build did not produce a timeline value"; + return false; + } + + std::array postBlasPattern{}; + for (size_t i = 0; i < postBlasPattern.size(); ++i) { + postBlasPattern[i] = static_cast((i * 29u + 11u) & 0xFFu); + } + Buffer postBlasTransfer(*allocator_, BufferDesc{ + .size = postBlasPattern.size(), + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = BufferMemory::GpuOnly, + .debugName = "streaming transfer selftest post-BLAS transfer", + }); + if (!stageBufferUpload(postBlasTransfer, postBlasPattern.data(), postBlasPattern.size(), 0)) { + errorOut = "stageBufferUpload(post-BLAS transfer) failed"; + return false; + } + const uint64_t orderedTransferTimeline = submitFrame(); + if (orderedTransferTimeline <= blasTimeline) { + errorOut = "post-BLAS transfer did not follow the graphics timeline value"; + return false; + } + VkSemaphoreWaitInfo orderedWait{}; + orderedWait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + orderedWait.semaphoreCount = 1; + orderedWait.pSemaphores = &timeline_; + orderedWait.pValues = &orderedTransferTimeline; + if (vkWaitSemaphores(device_, &orderedWait, UINT64_MAX) != VK_SUCCESS) { + errorOut = "vkWaitSemaphores(transfer-BLAS-transfer chain) failed"; return false; } - if (poll() < blasTimeline) { - errorOut = "BLAS build timeline did not complete after wait"; + if (poll() < orderedTransferTimeline) { + errorOut = "transfer-BLAS-transfer timeline did not complete after final wait"; return false; } const uint64_t compactedBlasBytes = consumeCompactedBlasSize(blas); + if (compactedBlasBytes == 0) { + errorOut = "BLAS compaction-size query was unavailable after the ordered final transfer"; + return false; + } AccelerationStructure compactedBlas; const AccelerationStructure* tlasBlas = &blas; if (compactedBlasBytes != 0 && compactedBlasBytes < static_cast(blas.size())) { @@ -1193,6 +1406,55 @@ bool StreamingGpuTransferExecutor::runSelfTest(std::string& errorOut) { return false; } + if (!hasAsyncComputeQueue()) { + VkSemaphoreTypeCreateInfo computeTimelineInfo{}; + computeTimelineInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + computeTimelineInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + computeTimelineInfo.initialValue = 0; + VkSemaphoreCreateInfo computeSemaphoreInfo{}; + computeSemaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + computeSemaphoreInfo.pNext = &computeTimelineInfo; + VkSemaphore selfTestComputeTimeline = VK_NULL_HANDLE; + if (vkCreateSemaphore(device_, &computeSemaphoreInfo, nullptr, &selfTestComputeTimeline) != VK_SUCCESS) { + errorOut = "vkCreateSemaphore(compute self-test timeline) failed"; + return false; + } + if (!setComputeQueue( + graphicsQueue_, + selfTestComputeTimeline, + ComputeTimelineOwnership::Owned)) { + vkDestroySemaphore(device_, selfTestComputeTimeline, nullptr); + errorOut = "setComputeQueue(compute self-test timeline) failed"; + return false; + } + } + const size_t transferInFlightBeforeCompute = inFlight_.size(); + const uint64_t computeTimelineValue = submitComputeTimelineMarker(markerTimeline); + if (computeTimelineValue == 0) { + errorOut = "submitComputeTimelineMarker failed"; + return false; + } + VkSemaphoreWaitInfo computeWait{}; + computeWait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + computeWait.semaphoreCount = 1; + computeWait.pSemaphores = &computeTimeline_; + computeWait.pValues = &computeTimelineValue; + if (vkWaitSemaphores(device_, &computeWait, UINT64_MAX) != VK_SUCCESS) { + errorOut = "vkWaitSemaphores(compute self-test) failed"; + return false; + } + (void)poll(); + if (completedComputeTimeline_ < computeTimelineValue) { + errorOut = "compute timeline marker did not complete on its own semaphore"; + return false; + } + if (inFlight_.size() != transferInFlightBeforeCompute || + std::find(freeGraphicsCommandBuffers_.begin(), freeGraphicsCommandBuffers_.end(), VK_NULL_HANDLE) != + freeGraphicsCommandBuffers_.end()) { + errorOut = "compute completion corrupted transfer/graphics in-flight tracking"; + return false; + } + StreamingStagingRing cpuRing(1024u, 1u); const std::optional a = cpuRing.allocate(700, 1); const std::optional b = cpuRing.allocate(200, 2); diff --git a/src/rtv/StreamingStagingRing.cpp b/src/rtv/StreamingStagingRing.cpp index b155b86..afcf272 100644 --- a/src/rtv/StreamingStagingRing.cpp +++ b/src/rtv/StreamingStagingRing.cpp @@ -112,6 +112,13 @@ std::optional StreamingStagingRing::allocate(uint64_ return out; } +void StreamingStagingRing::flush(uint64_t offset, uint64_t bytes) const { + if (buffer_.handle() == VK_NULL_HANDLE || bytes == 0 || offset >= capacityBytes_) { + return; + } + buffer_.flush(std::min(bytes, capacityBytes_ - offset), offset); +} + uint64_t StreamingStagingRing::retire(uint64_t completedTimeline) { uint64_t reclaimed = 0; while (!pending_.empty() && pending_.front().timelineValue <= completedTimeline) { diff --git a/src/rtv/Swapchain.cpp b/src/rtv/Swapchain.cpp index 65a0447..cb942e5 100644 --- a/src/rtv/Swapchain.cpp +++ b/src/rtv/Swapchain.cpp @@ -25,6 +25,21 @@ uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, Vk throw std::runtime_error("Failed to find suitable memory type"); } +VkCompositeAlphaFlagBitsKHR chooseCompositeAlpha(VkCompositeAlphaFlagsKHR supported) { + constexpr std::array preferred{ + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, + }; + for (VkCompositeAlphaFlagBitsKHR mode : preferred) { + if ((supported & mode) != 0) { + return mode; + } + } + throw std::runtime_error("Swapchain surface exposes no supported composite alpha mode"); +} + } // namespace Swapchain::Swapchain(const VulkanContext& context, GLFWwindow* window) @@ -33,7 +48,11 @@ Swapchain::Swapchain(const VulkanContext& context, GLFWwindow* window) } Swapchain::Swapchain(const VulkanContext& context, VkExtent2D extent) - : context_(context), headless_(true), imageFormat_(VK_FORMAT_B8G8R8A8_UNORM), extent_(extent) { + : context_(context), + headless_(true), + imageFormat_(VK_FORMAT_B8G8R8A8_UNORM), + imageUsage_(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT), + extent_(extent) { createHeadlessImages(); } @@ -100,6 +119,12 @@ Swapchain::SupportDetails Swapchain::querySupport() const { } VkSurfaceFormatKHR Swapchain::chooseSurfaceFormat(const std::vector& formats) const { + if (formats.size() == 1 && formats.front().format == VK_FORMAT_UNDEFINED) { + return VkSurfaceFormatKHR{ + VK_FORMAT_B8G8R8A8_UNORM, + formats.front().colorSpace, + }; + } const auto preferred = std::find_if(formats.begin(), formats.end(), [](const VkSurfaceFormatKHR& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; @@ -145,6 +170,14 @@ void Swapchain::create() { const VkPresentModeKHR presentMode = choosePresentMode(support.presentModes); extent_ = chooseExtent(support.capabilities); + if ((support.capabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) == 0) { + throw std::runtime_error("Swapchain surface does not support color-attachment images"); + } + imageUsage_ = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + if ((support.capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0) { + imageUsage_ |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + } + uint32_t imageCount = support.capabilities.minImageCount + 1; if (support.capabilities.maxImageCount > 0) { imageCount = std::min(imageCount, support.capabilities.maxImageCount); @@ -164,7 +197,7 @@ void Swapchain::create() { createInfo.imageColorSpace = surfaceFormat.colorSpace; createInfo.imageExtent = extent_; createInfo.imageArrayLayers = 1; - createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + createInfo.imageUsage = imageUsage_; if (queues.graphics != queues.present) { createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; createInfo.queueFamilyIndexCount = static_cast(queueFamilyIndices.size()); @@ -173,7 +206,7 @@ void Swapchain::create() { createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; } createInfo.preTransform = support.capabilities.currentTransform; - createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + createInfo.compositeAlpha = chooseCompositeAlpha(support.capabilities.supportedCompositeAlpha); createInfo.presentMode = presentMode; createInfo.clipped = VK_TRUE; @@ -226,7 +259,7 @@ void Swapchain::createHeadlessImages() { imageInfo.arrayLayers = 1; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.usage = imageUsage_; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; diff --git a/src/rtv/UiOverlay.cpp b/src/rtv/UiOverlay.cpp index 1a415a7..3daffb6 100644 --- a/src/rtv/UiOverlay.cpp +++ b/src/rtv/UiOverlay.cpp @@ -92,18 +92,16 @@ std::filesystem::path findEditorTablerIconFont() { return {}; } -std::filesystem::path findEditorUiTextFont() { +std::filesystem::path findEditorUiTextFont(bool bold = false) { #if defined(_WIN32) char* windir = nullptr; size_t windirLength = 0; if (_dupenv_s(&windir, &windirLength, "WINDIR") == 0 && windir != nullptr) { const std::filesystem::path fonts = std::filesystem::path(windir) / "Fonts"; std::free(windir); - const std::array candidates{{ - fonts / "segoeui.ttf", - fonts / "segoeuib.ttf", - fonts / "tahoma.ttf", - }}; + const std::array candidates = bold + ? std::array{{fonts / "segoeuib.ttf", fonts / "segoeui.ttf", fonts / "tahomabd.ttf"}} + : std::array{{fonts / "segoeui.ttf", fonts / "tahoma.ttf", fonts / "segoeuib.ttf"}}; for (const std::filesystem::path& candidate : candidates) { if (std::filesystem::exists(candidate)) { return candidate; @@ -145,8 +143,8 @@ const char* debugViewLabel(RendererDebugView view) { return rendererDebugViewName(view); } -const std::array& rendererOnlyDebugViews() { - static const std::array views{{ +const std::array& rendererOnlyDebugViews() { + static const std::array views{{ RendererDebugView::Beauty, RendererDebugView::DirectLighting, RendererDebugView::IndirectLighting, @@ -157,13 +155,49 @@ const std::array& rendererOnlyDebugViews() { RendererDebugView::Roughness, RendererDebugView::Variance, RendererDebugView::DenoiserRejection, + RendererDebugView::NrdValidation, + RendererDebugView::NrdDiffuseConfidence, + RendererDebugView::NrdSpecularConfidence, + RendererDebugView::NrdRawConfidenceGradient, + RendererDebugView::NrdFilteredConfidenceGradient, + RendererDebugView::NrdConfidenceHistory, + RendererDebugView::PsrActiveMask, + RendererDebugView::PsrDepth, + RendererDebugView::PsrMotion, + RendererDebugView::PsrNormalRoughness, + RendererDebugView::PsrHitDistance, + RendererDebugView::PsrAlbedoF0, + RendererDebugView::PsrRayDirection, + RendererDebugView::DlssDepth, + RendererDebugView::DlssMotionVectors, + RendererDebugView::DlssInputColor, + RendererDebugView::DlssOutputColor, + RendererDebugView::DlssRrDiffuseAlbedo, + RendererDebugView::DlssRrSpecularAlbedo, + RendererDebugView::DlssRrNormals, + RendererDebugView::DlssRrRoughness, + RendererDebugView::DlssRrDiffuseHitDistance, + RendererDebugView::DlssRrSpecularHitDistance, + RendererDebugView::DlssRrReflectedAlbedo, + RendererDebugView::DlssRrDisocclusionMask, + RendererDebugView::DlssRrDiffuseRayDirection, + RendererDebugView::DlssRrSpecularRayDirection, + RendererDebugView::DlssRrDiffuseRayDirectionHitDistance, + RendererDebugView::DlssRrSpecularRayDirectionHitDistance, RendererDebugView::ReprojectionConfidence, RendererDebugView::RestirDiFinalContribution, RendererDebugView::RestirDiTemporalReservoir, RendererDebugView::RestirDiSpatialReservoir, + RendererDebugView::RestirDiLightMapStatus, RendererDebugView::RestirGiFinal, RendererDebugView::RestirGiTemporal, RendererDebugView::RestirGiSpatial, + RendererDebugView::RestirGiTarget, + RendererDebugView::RestirGiSourcePdf, + RendererDebugView::RestirGiWeightSum, + RendererDebugView::RestirGiM, + RendererDebugView::RestirGiConfidence, + RendererDebugView::RestirGiVisibility, RendererDebugView::AdaptiveDensityMap, RendererDebugView::RegirGridOccupancy, RendererDebugView::RegirReservoirWeight, @@ -1153,11 +1187,25 @@ void UiOverlay::loadEditorFonts() { textConfig.PixelSnapH = true; textConfig.OversampleH = 2; textConfig.OversampleV = 1; - if (ImFont* textFont = io.Fonts->AddFontFromFileTTF(textFontPath.string().c_str(), 14.0f, &textConfig, io.Fonts->GetGlyphRangesDefault())) { + if (ImFont* textFont = io.Fonts->AddFontFromFileTTF(textFontPath.string().c_str(), 15.0f, &textConfig, io.Fonts->GetGlyphRangesDefault())) { io.FontDefault = textFont; } } + editorSetHeadingFont(nullptr); + const std::filesystem::path headingFontPath = findEditorUiTextFont(true); + if (!headingFontPath.empty()) { + ImFontConfig headingConfig{}; + headingConfig.PixelSnapH = true; + headingConfig.OversampleH = 2; + headingConfig.OversampleV = 1; + editorSetHeadingFont(io.Fonts->AddFontFromFileTTF( + headingFontPath.string().c_str(), + 15.0f, + &headingConfig, + io.Fonts->GetGlyphRangesDefault())); + } + if (io.Fonts->Fonts.empty()) { io.Fonts->AddFontDefault(); } @@ -1189,15 +1237,15 @@ void UiOverlay::loadEditorFonts() { void UiOverlay::applyDarkStyle() { ImGui::StyleColorsDark(); ImGuiStyle& style = ImGui::GetStyle(); - style.WindowPadding = ImVec2(5.0f, 4.0f); - style.FramePadding = ImVec2(5.0f, 3.0f); - style.ItemSpacing = ImVec2(5.0f, 4.0f); - style.ItemInnerSpacing = ImVec2(4.0f, 3.0f); - style.ScrollbarSize = 10.0f; + style.WindowPadding = ImVec2(EditorUiMetric::panelPaddingX, EditorUiMetric::panelPaddingY); + style.FramePadding = ImVec2(EditorUiMetric::rowPaddingX, EditorUiMetric::rowPaddingY); + style.ItemSpacing = ImVec2(7.0f, 5.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); + style.ScrollbarSize = 11.0f; style.WindowRounding = 0.0f; - style.FrameRounding = 1.0f; - style.GrabRounding = 1.0f; - style.TabRounding = 1.0f; + style.FrameRounding = EditorUiMetric::compactButtonRounding; + style.GrabRounding = EditorUiMetric::compactButtonRounding; + style.TabRounding = EditorUiMetric::dockTabRounding; style.WindowBorderSize = 1.0f; style.FrameBorderSize = 0.0f; style.WindowMenuButtonPosition = ImGuiDir_Left; diff --git a/src/rtv/UploadContext.cpp b/src/rtv/UploadContext.cpp index 199d46e..325bcba 100644 --- a/src/rtv/UploadContext.cpp +++ b/src/rtv/UploadContext.cpp @@ -2,6 +2,8 @@ #include "rtv/Check.h" +#include + namespace rtv { UploadContext::UploadContext(VkDevice device, VkQueue queue, uint32_t queueFamilyIndex) @@ -12,6 +14,13 @@ UploadContext::UploadContext(VkDevice device, VkQueue queue, uint32_t queueFamil poolInfo.queueFamilyIndex = queueFamilyIndex_; checkVk(vkCreateCommandPool(device_, &poolInfo, nullptr, &commandPool_), "vkCreateCommandPool(upload)"); + VkCommandBufferAllocateInfo allocateInfo{}; + allocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocateInfo.commandPool = commandPool_; + allocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocateInfo.commandBufferCount = 1; + checkVk(vkAllocateCommandBuffers(device_, &allocateInfo, &commandBuffer_), "vkAllocateCommandBuffers(upload)"); + VkFenceCreateInfo fenceInfo{}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; @@ -30,27 +39,21 @@ UploadContext::~UploadContext() { VkCommandBuffer UploadContext::begin() { checkVk(vkWaitForFences(device_, 1, &fence_, VK_TRUE, UINT64_MAX), "vkWaitForFences(upload)"); - checkVk(vkResetFences(device_, 1, &fence_), "vkResetFences(upload)"); checkVk(vkResetCommandPool(device_, commandPool_, 0), "vkResetCommandPool(upload)"); - VkCommandBufferAllocateInfo allocateInfo{}; - allocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocateInfo.commandPool = commandPool_; - allocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocateInfo.commandBufferCount = 1; - - VkCommandBuffer commandBuffer = VK_NULL_HANDLE; - checkVk(vkAllocateCommandBuffers(device_, &allocateInfo, &commandBuffer), "vkAllocateCommandBuffers(upload)"); - VkCommandBufferBeginInfo beginInfo{}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - checkVk(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(upload)"); - return commandBuffer; + checkVk(vkBeginCommandBuffer(commandBuffer_, &beginInfo), "vkBeginCommandBuffer(upload)"); + return commandBuffer_; } void UploadContext::submitAndWait(VkCommandBuffer commandBuffer) { + if (commandBuffer != commandBuffer_) { + throw std::runtime_error("UploadContext can only submit its reusable command buffer"); + } checkVk(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(upload)"); + checkVk(vkResetFences(device_, 1, &fence_), "vkResetFences(upload)"); VkCommandBufferSubmitInfo commandBufferInfo{}; commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; diff --git a/src/rtv/ViewportPanel.cpp b/src/rtv/ViewportPanel.cpp index 9437008..f70f1de 100644 --- a/src/rtv/ViewportPanel.cpp +++ b/src/rtv/ViewportPanel.cpp @@ -2189,7 +2189,7 @@ void drawViewportOverlayBackdrop(ImDrawList* drawList, ImVec2 min, ImVec2 max) { void drawViewportTopRail(ImDrawList* drawList, ImVec2 imagePos, ImVec2 avail) { const ImVec2 min(imagePos.x, imagePos.y); - const ImVec2 max(imagePos.x + avail.x, imagePos.y + editorIconButtonSize().y + 3.0f); + const ImVec2 max(imagePos.x + avail.x, imagePos.y + EditorUiMetric::toolbarButtonHeight + 6.0f); drawList->AddRectFilled(min, max, ImGui::GetColorU32(editorViewportOverlayBgColor()), 0.0f); drawList->AddLine(ImVec2(min.x, max.y), max, ImGui::GetColorU32(editorViewportOverlayBorderColor()), 1.0f); } @@ -2218,6 +2218,20 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, const ImVec2 imagePos = ImGui::GetCursorScreenPos(); lastContentExtent_.width = static_cast(std::max(1.0f, std::floor(avail.x))); lastContentExtent_.height = static_cast(std::max(1.0f, std::floor(avail.y))); + if (stableRenderExtent_.width == 0 || stableRenderExtent_.height == 0) { + stableRenderExtent_ = lastContentExtent_; + pendingRenderExtent_ = lastContentExtent_; + pendingRenderExtentFrames_ = 0; + } else if (pendingRenderExtent_.width == lastContentExtent_.width && + pendingRenderExtent_.height == lastContentExtent_.height) { + if (++pendingRenderExtentFrames_ >= 2u) { + stableRenderExtent_ = pendingRenderExtent_; + pendingRenderExtentFrames_ = 0; + } + } else { + pendingRenderExtent_ = lastContentExtent_; + pendingRenderExtentFrames_ = 1u; + } state.viewport.imageOrigin = {imagePos.x, imagePos.y}; state.viewport.imageSize = {avail.x, avail.y}; const ImVec2 mousePos = ImGui::GetIO().MousePos; @@ -2229,8 +2243,8 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, state.viewport.leftClicked = hovered_ && !state.viewport.mouseCaptureActive && ImGui::IsMouseClicked(ImGuiMouseButton_Left); const bool imageMatchesPanel = - lastContentExtent_.width == state.viewport.displayExtent.width && - lastContentExtent_.height == state.viewport.displayExtent.height; + stableRenderExtent_.width == state.viewport.displayExtent.width && + stableRenderExtent_.height == state.viewport.displayExtent.height; if (imageMatchesPanel && state.viewport.textureReady && state.viewport.texture != VK_NULL_HANDLE) { ImGui::Image( @@ -2656,6 +2670,7 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(EditorUiMetric::rowPaddingX, EditorUiMetric::rowPaddingY)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(3.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, EditorUiMetric::compactButtonRounding); + const bool compactViewportToolbar = avail.x < 940.0f; ImGui::SetCursorScreenPos(ImVec2(imagePos.x + 4.0f, imagePos.y + 2.0f)); ImGui::BeginGroup(); auto toolButton = [&](EditorGlyphIcon icon, const char* id, EditorCommandId command, bool active) { @@ -2669,29 +2684,35 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, } ImGui::SameLine(); }; - toolButton(EditorGlyphIcon::Select, "ViewportSelect", EditorCommandId::ViewportSelect, transformGizmoMode_ < 0); - toolButton(EditorGlyphIcon::Move, "ViewportMove", EditorCommandId::ViewportMove, transformGizmoMode_ == 0); - toolButton(EditorGlyphIcon::Rotate, "ViewportRotate", EditorCommandId::ViewportRotate, transformGizmoMode_ == 1); - toolButton(EditorGlyphIcon::Scale, "ViewportScale", EditorCommandId::ViewportScale, transformGizmoMode_ == 2); - toolButton(localGizmoMode_ ? EditorGlyphIcon::LocalSpace : EditorGlyphIcon::WorldSpace, "ViewportSpace", EditorCommandId::ViewportToggleLocal, localGizmoMode_); - if (state.editorPrefs != nullptr && selection.entityId().valid() && transformGizmoMode_ == 2) { - const bool linkedScale = state.editorPrefs->linkedScale; - const bool pressed = editorIconButton( - "ViewportLinkedScale", - linkedScale ? EditorGlyphIcon::Lock : EditorGlyphIcon::Unlock, - linkedScale); + toolButton(EditorGlyphIcon::Grid, "ViewportGrid", EditorCommandId::ViewportToggleGrid, showGrid_); + toolButton(EditorGlyphIcon::Axes, "ViewportAxes", EditorCommandId::ViewportToggleAxes, showAxes_); + ImGui::Dummy(ImVec2(EditorUiMetric::toolbarGroupGap, 1.0f)); + ImGui::SameLine(); + auto toolbarActionButton = [&](const char* id, EditorGlyphIcon icon, const char* label, bool active, const char* tooltip, auto&& action) { + const bool pressed = compactViewportToolbar + ? editorIconButton(id, icon, active) + : editorToolbarTextButton(id, icon, label, active); viewportUiHovered = viewportUiHovered || ImGui::IsItemHovered(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Linked Scale"); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("%s", tooltip); } if (pressed) { - state.editorPrefs->linkedScale = !linkedScale; + action(); } ImGui::SameLine(); - } - toolButton(EditorGlyphIcon::Snap, "ViewportSnap", EditorCommandId::ViewportToggleSnap, snap_.enabled); - toolButton(EditorGlyphIcon::Grid, "ViewportGrid", EditorCommandId::ViewportToggleGrid, showGrid_); - toolButton(EditorGlyphIcon::Axes, "ViewportAxes", EditorCommandId::ViewportToggleAxes, showAxes_); + }; + toolbarActionButton("ViewportResetAccumulation", EditorGlyphIcon::Reset, "Reset", false, "Reset accumulation", [&] { + requests.resetAccumulation = AccumulationResetReason::Manual; + }); + toolbarActionButton("ViewportDebugView", EditorGlyphIcon::DrawDebug, "Debug", settings.debugView != RendererDebugView::Beauty, "Cycle debug view", [&] { + requests.toggleDebugView = true; + }); + toolbarActionButton("ViewportIntermediate", EditorGlyphIcon::Details, "Intermediate", false, "Cycle intermediate render targets", [&] { + requests.cycleIntermediateView = true; + }); + toolbarActionButton("ViewportDenoiser", EditorGlyphIcon::Render, "Denoiser", settings.denoiserEnabled, "Toggle denoiser", [&] { + requests.toggleDenoiser = true; + }); if (selection.entityId().valid()) { if (editorIconButton("ViewportFrameSelected", EditorGlyphIcon::Frame, false)) { requests.focusOnEntity = selection.entityId(); } viewportUiHovered = viewportUiHovered || ImGui::IsItemHovered(); @@ -2718,6 +2739,7 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, } } ImGui::EndGroup(); + const float leftControlsRight = ImGui::GetItemRectMax().x; ImGui::PopStyleVar(3); const float gpuTotal = timings.totalMs(); @@ -2746,12 +2768,26 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, cameraSpeed << std::fixed << std::setprecision(3) << state.camera->moveSpeed(); cameraSpeedText = cameraSpeed.str(); } - const float cameraSpeedWidth = cameraSpeedText.empty() ? 0.0f : editorIconTextButtonWidth(cameraSpeedText.c_str()) + ImGui::GetStyle().ItemSpacing.x; - const float overlayWidth = editorIconTextButtonWidth("View Settings") + - (hudVisible ? editorIconTextButtonWidth("Stats") + editorIconTextButtonWidth("Draw Debug") + cameraSpeedWidth + ImGui::GetStyle().ItemSpacing.x * 3.0f : 0.0f); - const float controlsX = std::max(imagePos.x + 8.0f, statusRight - overlayWidth); - const float statusX = std::max(imagePos.x + 8.0f, controlsX - statusSize.x - 14.0f); + const float overlaySpacing = ImGui::GetStyle().ItemSpacing.x; + const float viewSettingsWidth = compactViewportToolbar ? editorIconButtonSize().x : editorIconTextButtonWidth("View Settings"); + const float statsWidth = compactViewportToolbar ? editorIconButtonSize().x : editorIconTextButtonWidth("Stats"); + const float drawDebugWidth = compactViewportToolbar ? editorIconButtonSize().x : editorIconTextButtonWidth("Draw Debug"); + const float cameraSpeedWidth = cameraSpeedText.empty() ? 0.0f : editorIconTextReadoutSize(cameraSpeedText.c_str()).x; + float overlayWidth = viewSettingsWidth; + int overlayItemCount = 1; if (hudVisible) { + overlayWidth += statsWidth + drawDebugWidth; + overlayItemCount += 2; + } + if (hudVisible && !cameraSpeedText.empty()) { + overlayWidth += cameraSpeedWidth; + ++overlayItemCount; + } + overlayWidth += overlaySpacing * static_cast(overlayItemCount - 1); + const float controlsX = std::max(imagePos.x + 8.0f, statusRight - overlayWidth); + const float statusX = controlsX - statusSize.x - 14.0f; + const bool statusFits = statusX >= leftControlsRight + 10.0f; + if (hudVisible && statusFits) { dl->AddText(nullptr, ImGui::GetFontSize() * hudScale, ImVec2(statusX, statusY), IM_COL32(216, 221, 228, 245), statusText.c_str()); } @@ -2759,7 +2795,10 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, ImGui::BeginGroup(); auto overlayButton = [&](EditorGlyphIcon icon, const char* label, const char* popupName, const char* buttonId, const char* tooltip) { const bool open = ImGui::IsPopupOpen(popupName); - if (editorIconTextButton(buttonId, icon, label, open)) { + const bool pressed = compactViewportToolbar + ? editorIconButton(buttonId, icon, open) + : editorIconTextButton(buttonId, icon, label, open); + if (pressed) { ImGui::OpenPopup(popupName); } viewportUiHovered = viewportUiHovered || ImGui::IsItemHovered(); @@ -3470,10 +3509,10 @@ void ViewportPanel::draw(EditorRuntimeState& state, EditorSelection& selection, } VkExtent2D ViewportPanel::desiredRenderExtent(VkExtent2D fallback) const { - if (lastContentExtent_.width == 0 || lastContentExtent_.height == 0) { + if (stableRenderExtent_.width == 0 || stableRenderExtent_.height == 0) { return fallback; } - return lastContentExtent_; + return stableRenderExtent_; } void ViewportPanel::commitGizmoDrag(EditorRequests& requests, SceneDocument& document) { diff --git a/src/rtv/VulkanContext.cpp b/src/rtv/VulkanContext.cpp index 2c40cec..bb52e65 100644 --- a/src/rtv/VulkanContext.cpp +++ b/src/rtv/VulkanContext.cpp @@ -10,12 +10,16 @@ #endif #include +#include +#include #include #include #include #include #include #include +#include +#include namespace rtv { @@ -30,7 +34,6 @@ const std::vector requiredDeviceExtensions = { const std::vector optionalRayTracingDeviceExtensions = { VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME, - VK_KHR_RAY_TRACING_MAINTENANCE_1_EXTENSION_NAME, VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_RAY_QUERY_EXTENSION_NAME, @@ -90,18 +93,30 @@ std::vector cStringView(const std::vector& values) { return result; } -VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( +} // namespace + +VKAPI_ATTR VkBool32 VKAPI_CALL VulkanContext::debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, - void*) { + void* userData) { + auto* context = static_cast(userData); + if (context != nullptr) { + if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) { + context->validationErrorCount_.fetch_add(1, std::memory_order_relaxed); + } else if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0) { + context->validationWarningCount_.fetch_add(1, std::memory_order_relaxed); + } + } if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { - std::cerr << "Vulkan validation: " << callbackData->pMessage << '\n'; + std::cerr << "Vulkan validation: " + << (callbackData != nullptr && callbackData->pMessage != nullptr ? callbackData->pMessage : "unknown message") + << '\n'; } return VK_FALSE; } -VkDebugUtilsMessengerCreateInfoEXT debugMessengerCreateInfo() { +VkDebugUtilsMessengerCreateInfoEXT VulkanContext::debugMessengerCreateInfo() { VkDebugUtilsMessengerCreateInfoEXT info{}; info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; info.messageSeverity = @@ -111,11 +126,15 @@ VkDebugUtilsMessengerCreateInfoEXT debugMessengerCreateInfo() { VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; - info.pfnUserCallback = debugCallback; + info.pfnUserCallback = &VulkanContext::debugCallback; + info.pUserData = this; return info; } -} // namespace +void VulkanContext::resetValidationMessageCounts() const noexcept { + validationErrorCount_.store(0, std::memory_order_relaxed); + validationWarningCount_.store(0, std::memory_order_relaxed); +} VulkanContext::VulkanContext(GLFWwindow* window) { headless_ = false; @@ -275,6 +294,7 @@ void VulkanContext::createInstance(GLFWwindow* window) { if (validationRequested() && !validationAvailable()) { throw std::runtime_error("Vulkan validation layer VK_LAYER_KHRONOS_validation is not installed"); } + validationEnabled_ = validationRequested(); VkApplicationInfo appInfo{}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; @@ -288,6 +308,9 @@ void VulkanContext::createInstance(GLFWwindow* window) { debugUtilsExtensionEnabled_ = std::any_of(extensions.begin(), extensions.end(), [](const char* extension) { return std::strcmp(extension, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0; }); + if (validationEnabled_ && !debugUtilsExtensionEnabled_) { + throw std::runtime_error("Vulkan validation requires VK_EXT_debug_utils so validation messages can be recorded"); + } const auto debugInfo = debugMessengerCreateInfo(); VkInstanceCreateInfo createInfo{}; @@ -296,7 +319,7 @@ void VulkanContext::createInstance(GLFWwindow* window) { createInfo.enabledExtensionCount = static_cast(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); - if (validationRequested()) { + if (validationEnabled_) { createInfo.enabledLayerCount = 1; createInfo.ppEnabledLayerNames = &validationLayer; createInfo.pNext = &debugInfo; @@ -306,7 +329,7 @@ void VulkanContext::createInstance(GLFWwindow* window) { } void VulkanContext::createDebugMessenger() { - if (!validationRequested()) { + if (!validationEnabled_) { return; } @@ -338,7 +361,7 @@ void VulkanContext::pickPhysicalDevice() { } if (physicalDevice_ == VK_NULL_HANDLE || bestScore < 0) { - throw std::runtime_error("No suitable Vulkan 1.3 device with swapchain, dynamic rendering, and synchronization2 support was found"); + throw std::runtime_error("No suitable Vulkan 1.3 device with swapchain, dynamic rendering, synchronization2, and shaderFloat64 support was found"); } vkGetPhysicalDeviceProperties(physicalDevice_, &physicalDeviceProperties_); @@ -532,14 +555,6 @@ void VulkanContext::createDevice() { featureTail = &timelineSemaphore; } - VkPhysicalDeviceRayTracingMaintenance1FeaturesKHR rtMaintenance1Features{}; - rtMaintenance1Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_MAINTENANCE_1_FEATURES_KHR; - rtMaintenance1Features.rayTracingMaintenance1 = VK_FALSE; - if (rayTracingInfo_.capabilities.supported) { - rtMaintenance1Features.pNext = featureTail; - featureTail = &rtMaintenance1Features; - } - VkPhysicalDevice16BitStorageFeatures storage16BitFeatures{}; storage16BitFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES; storage16BitFeatures.storageBuffer16BitAccess = storageBuffer16BitAccess_ ? VK_TRUE : VK_FALSE; @@ -582,7 +597,6 @@ void VulkanContext::createDevice() { VkPhysicalDeviceFeatures2 features2{}; features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features2.features.shaderFloat64 = VK_TRUE; - features2.features.pipelineStatisticsQuery = VK_TRUE; features2.features.samplerAnisotropy = samplerAnisotropy_ ? VK_TRUE : VK_FALSE; features2.pNext = featureTail; @@ -670,6 +684,28 @@ void VulkanContext::createDevice() { } bool VulkanContext::validationRequested() const { + auto parseEnvironmentFlag = [](std::string_view value) { + std::string normalized(value); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return !normalized.empty() && normalized != "0" && normalized != "false" && + normalized != "off" && normalized != "no"; + }; +#if defined(_WIN32) + char* forced = nullptr; + size_t forcedLength = 0; + if (_dupenv_s(&forced, &forcedLength, "RTV_ENABLE_VULKAN_VALIDATION") == 0 && forced != nullptr) { + const bool enabled = parseEnvironmentFlag(forced); + std::free(forced); + return enabled; + } + std::free(forced); +#else + if (const char* forced = std::getenv("RTV_ENABLE_VULKAN_VALIDATION"); forced != nullptr) { + return parseEnvironmentFlag(forced); + } +#endif #if defined(NDEBUG) return false; #else @@ -840,7 +876,9 @@ bool VulkanContext::deviceSupportsRequiredFeatures(VkPhysicalDevice physicalDevi features2.pNext = &features13; vkGetPhysicalDeviceFeatures2(physicalDevice, &features2); - return features13.dynamicRendering == VK_TRUE && features13.synchronization2 == VK_TRUE; + return features13.dynamicRendering == VK_TRUE && + features13.synchronization2 == VK_TRUE && + features2.features.shaderFloat64 == VK_TRUE; } bool VulkanContext::deviceSupportsExtension(VkPhysicalDevice physicalDevice, const char* extensionName) const { @@ -1118,7 +1156,7 @@ void VulkanContext::pickPhysicalDeviceHeadless() { } if (physicalDevice_ == VK_NULL_HANDLE || bestScore < 0) { - throw std::runtime_error("No suitable Vulkan 1.3 device with dynamic rendering and synchronization2 support was found"); + throw std::runtime_error("No suitable Vulkan 1.3 device with dynamic rendering, synchronization2, and shaderFloat64 support was found"); } vkGetPhysicalDeviceProperties(physicalDevice_, &physicalDeviceProperties_); diff --git a/src/third_party/imgui/imgui_impl_vulkan.cpp b/src/third_party/imgui/imgui_impl_vulkan.cpp index 924e699..d8e17c9 100644 --- a/src/third_party/imgui/imgui_impl_vulkan.cpp +++ b/src/third_party/imgui/imgui_impl_vulkan.cpp @@ -292,8 +292,9 @@ struct ImGui_ImplVulkan_ViewportData bool WindowOwned; bool SwapChainNeedRebuild; // Flag when viewport swapchain resized in the middle of processing a frame bool SwapChainSuboptimal; // Flag when VK_SUBOPTIMAL_KHR was returned. + bool FrameRendered; // This viewport submitted work and may be presented. - ImGui_ImplVulkan_ViewportData() { WindowOwned = SwapChainNeedRebuild = SwapChainSuboptimal = false; memset((void*)&RenderBuffers, 0, sizeof(RenderBuffers)); } + ImGui_ImplVulkan_ViewportData() { WindowOwned = SwapChainNeedRebuild = SwapChainSuboptimal = FrameRendered = false; memset((void*)&RenderBuffers, 0, sizeof(RenderBuffers)); } ~ImGui_ImplVulkan_ViewportData() { } }; @@ -2037,10 +2038,7 @@ static void ImGui_ImplVulkan_CreateWindow(ImGuiViewport* viewport) wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(v->PhysicalDevice, wd->Surface, requestSurfaceImageFormats.Data, requestSurfaceImageFormats.Size, requestSurfaceColorSpace); // Select Present Mode - // Editor platform windows should be throttled. Prefer FIFO so detached panels - // such as Content/Project Manager do not spin an unbounded present loop when - // focused. - VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR }; + VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR }; wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(v->PhysicalDevice, wd->Surface, &present_modes[0], IM_COUNTOF(present_modes)); //printf("[vulkan] Secondary window selected PresentMode = %d\n", wd->PresentMode); @@ -2110,6 +2108,7 @@ static void ImGui_ImplVulkan_RenderWindow(ImGuiViewport* viewport, void*) ImGui_ImplVulkanH_Window* wd = &vd->Window; ImGui_ImplVulkan_InitInfo* v = &bd->VulkanInitInfo; VkResult err; + vd->FrameRendered = false; if (vd->SwapChainNeedRebuild || vd->SwapChainSuboptimal) { @@ -2123,7 +2122,9 @@ static void ImGui_ImplVulkan_RenderWindow(ImGuiViewport* viewport, void*) ImGui_ImplVulkanH_FrameSemaphores* render_fsd = nullptr; { { - err = vkAcquireNextImageKHR(v->Device, wd->Swapchain, UINT64_MAX, acquire_fsd->ImageAcquiredSemaphore, VK_NULL_HANDLE, &wd->FrameIndex); + err = vkAcquireNextImageKHR(v->Device, wd->Swapchain, 0, acquire_fsd->ImageAcquiredSemaphore, VK_NULL_HANDLE, &wd->FrameIndex); + if (err == VK_NOT_READY || err == VK_TIMEOUT) + return; if (err == VK_ERROR_OUT_OF_DATE_KHR) { vd->SwapChainNeedRebuild = true; // Since we are not going to swap this frame anyway, it's ok that recreation happens on next frame. @@ -2251,6 +2252,7 @@ static void ImGui_ImplVulkan_RenderWindow(ImGuiViewport* viewport, void*) check_vk_result(err); err = vkQueueSubmit(v->Queue, 1, &info, fd->Fence); check_vk_result(err); + vd->FrameRendered = true; } } } @@ -2264,7 +2266,7 @@ static void ImGui_ImplVulkan_SwapBuffers(ImGuiViewport* viewport, void*) ImGui_ImplVulkanH_Window* wd = &vd->Window; ImGui_ImplVulkan_InitInfo* v = &bd->VulkanInitInfo; - if (vd->SwapChainNeedRebuild) // Frame data became invalid in the middle of rendering + if (vd->SwapChainNeedRebuild || !vd->FrameRendered) // Frame data became invalid or acquisition was skipped. return; VkResult err; diff --git a/third_party/RTXDI-Library b/third_party/RTXDI-Library new file mode 160000 index 0000000..d28e20f --- /dev/null +++ b/third_party/RTXDI-Library @@ -0,0 +1 @@ +Subproject commit d28e20f11c6bdd5a1cca273cfbad5e8493e4ba05