From a1071d94419c3bd58a6306cf72566d5225222fcc Mon Sep 17 00:00:00 2001 From: acmarrs-nvidia Date: Tue, 22 Mar 2022 10:45:20 -0400 Subject: [PATCH] v1.2.07 release --- .gitmodules | 8 + CMakeLists.txt | 22 +- ChangeLog.md | 29 ++ docs/DDGIVolume.md | 4 +- docs/ShaderAPI.md | 9 +- docs/images/ddgivolume-textures-probedata.jpg | Bin 21494 -> 73241 bytes rtxgi-sdk/.gitignore | 1 - rtxgi-sdk/include/rtxgi/Common.h | 4 +- .../include/rtxgi/ddgi/DDGIVolumeDescGPU.h | 2 +- .../include/rtxgi/ddgi/gfx/DDGIVolume_D3D12.h | 4 +- .../include/rtxgi/ddgi/gfx/DDGIVolume_VK.h | 4 +- rtxgi-sdk/shaders/ddgi/Irradiance.hlsl | 19 +- rtxgi-sdk/shaders/ddgi/ProbeBlendingCS.hlsl | 9 +- .../shaders/ddgi/ProbeClassificationCS.hlsl | 4 +- rtxgi-sdk/shaders/ddgi/ProbeRelocationCS.hlsl | 4 +- .../shaders/ddgi/include/ProbeCommon.hlsl | 4 +- .../shaders/ddgi/include/ProbeIndexing.hlsl | 69 +++-- samples/test-harness/CMakeLists.txt | 26 +- samples/test-harness/config/cornell.ini | 8 +- samples/test-harness/config/furnace.ini | 8 +- samples/test-harness/config/multi-cornell.ini | 24 +- samples/test-harness/config/sponza.ini | 2 +- samples/test-harness/config/tunnel.ini | 10 +- samples/test-harness/config/two-rooms.ini | 10 +- samples/test-harness/include/Benchmark.h | 30 +++ samples/test-harness/include/Configs.h | 1 + samples/test-harness/include/Graphics.h | 2 +- samples/test-harness/include/ImageCapture.h | 20 ++ samples/test-harness/include/Inputs.h | 3 +- .../test-harness/include/Instrumentation.h | 39 ++- samples/test-harness/include/Vulkan.h | 3 +- samples/test-harness/include/graphics/DDGI.h | 2 + samples/test-harness/include/graphics/RTAO.h | 1 + .../shaders/ddgi/ProbeTraceRGS.hlsl | 2 +- .../ddgi/visualizations/ProbesRGS.hlsl | 4 +- .../ddgi/visualizations/VolumeTexturesCS.hlsl | 56 ++-- samples/test-harness/src/Benchmark.cpp | 92 +++++++ samples/test-harness/src/Configs.cpp | 1 + samples/test-harness/src/Direct3D12.cpp | 240 ++++++++++++++++- samples/test-harness/src/ImageCapture.cpp | 68 +++++ samples/test-harness/src/Inputs.cpp | 10 +- samples/test-harness/src/Instrumentation.cpp | 16 ++ samples/test-harness/src/Vulkan.cpp | 253 ++++++++++++++++-- .../test-harness/src/graphics/DDGI_D3D12.cpp | 35 ++- samples/test-harness/src/graphics/DDGI_VK.cpp | 49 +++- .../src/graphics/GBuffer_D3D12.cpp | 26 +- .../test-harness/src/graphics/GBuffer_VK.cpp | 31 ++- .../test-harness/src/graphics/RTAO_D3D12.cpp | 16 ++ samples/test-harness/src/graphics/RTAO_VK.cpp | 19 +- samples/test-harness/src/main.cpp | 29 +- thirdparty/libpng | 1 + thirdparty/zlib | 1 + 52 files changed, 1133 insertions(+), 201 deletions(-) delete mode 100644 rtxgi-sdk/.gitignore create mode 100644 samples/test-harness/include/Benchmark.h create mode 100644 samples/test-harness/include/ImageCapture.h create mode 100644 samples/test-harness/src/Benchmark.cpp create mode 100644 samples/test-harness/src/ImageCapture.cpp create mode 160000 thirdparty/libpng create mode 160000 thirdparty/zlib diff --git a/.gitmodules b/.gitmodules index 40411be..ec54d46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,11 @@ path = thirdparty/Vulkan-Headers url = https://github.com/KhronosGroup/Vulkan-Headers.git branch = sdk-1.2.170 +[submodule "thirdparty/libpng"] + path = thirdparty/libpng + url = https://github.com/glennrp/libpng.git + branch = libpng16 +[submodule "thirdparty/zlib"] + path = thirdparty/zlib + url = https://github.com/madler/zlib.git + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b63b15..c1013d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "bin") set_property(GLOBAL PROPERTY USE_FOLDERS ON) -# Add Vulkan headers on ARM since there is no official Vulkan SDK +# Add Vulkan headers on ARM since there is no official ARM Vulkan SDK if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64") add_subdirectory(thirdparty/Vulkan-Headers) set(RTXGI_API_VULKAN_SDK "1") @@ -47,6 +47,26 @@ if(RTXGI_BUILD_SAMPLES) target_compile_definitions(tinygltf PUBLIC _CRT_SECURE_NO_WARNINGS) # suppress the sprintf CRT warnings set_target_properties(tinygltf PROPERTIES FOLDER "Thirdparty/") + # zlib + set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) + set(CMAKE_DEBUG_POSTFIX "" CACHE STRING "override debug postfix") + option(ASM686 "" OFF) + option(AMD64 "" OFF) + add_subdirectory(thirdparty/zlib) + target_compile_options(zlib BEFORE PUBLIC /wd4267) # suppress the implicit conversion warning + set_target_properties(zlib zlibstatic example minigzip PROPERTIES FOLDER "Thirdparty/zlib" DEBUG_POSTFIX "") + + # libpng + option(PNG_BUILD_ZLIB "" ON) + option(PNG_STATIC "" ON) + option(PNG_SHARED "" OFF) + option(PNG_EXECUTABLES "" OFF) + option(PNG_FRAMEWORK "" OFF) + option(PNG_TESTS "" OFF) + set(ZLIB_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/thirdparty/zlib" "${CMAKE_BINARY_DIR}/thirdparty/zlib") + add_subdirectory(thirdparty/libpng) + set_target_properties(png_static genfiles PROPERTIES FOLDER "Thirdparty/libpng" DEBUG_POSTFIX "") + # Samples add_subdirectory(samples) endif() diff --git a/ChangeLog.md b/ChangeLog.md index 50f40e4..2b224bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,34 @@ # RTXGI SDK Change Log +## 1.2.07 + +### SDK + +Features and Improvements: +- Shader improvements for probe data reads/writes + - The probe data texture now uses the same layout as the irradiance and distance textures + - This change makes probe data visualizations easier to understand +- Adds a RWTexture2D variant of ```DDGILoadProbeState()``` +- Uses the ```DDGILoadProbeState()``` convenience function where appropriate +- Moved the ```data``` member of the ```DDGIVolumeDescGPU``` struct inside the ```GetPackedData()``` function so the struct size is the same on the CPU and GPU +- Updated stale code comments (non-functional) +- Bumped SDK revision number and version string + +Bug Fixes: +- ```DDGIGetVolumeBlendWeight```: fixed regression related to volume edge fade not respecting rotations + +### Test Harness + +Features and Improvements: +- ```ProbeTraceRGS.hlsl```: no longer performs lighting for fixed rays when probe relocation is enabled (performance optimization) +- Adds a performance benchmark mode that collects CPU and GPU performance data and outputs the results to ```*.csv``` files + - Press the ```F2``` key to run a benchmark +- Adds the ability to store intermediate textures (back buffer, GBuffer, RTAO, and DDGIVolume textures) to disk + - Uses the zlib and libpng libraries for cross-platform compatibility + - Press the ```F1``` key to save the current image data to disk +- ```VolumeTexturesCS.hlsl```: minor updates to the layout order of visualized textures +- Config file and documentation updates + ## 1.2.04 ### SDK diff --git a/docs/DDGIVolume.md b/docs/DDGIVolume.md index e137e3e..0ab3e72 100644 --- a/docs/DDGIVolume.md +++ b/docs/DDGIVolume.md @@ -369,11 +369,11 @@ This texture stores world-space offsets and classification states for all probes - ```ProbeDataCommon.hlsl``` contains helper functions for reading and writing world-space offset data. - Probe classification state is stored in the W channel. -The texture's layout is the same as the irradiance and distance texture altases, except each probe uses just *one texel*. Below is a visualization of the probe data texture's world-space offsets (top) and probe states (bottom). +The texture's layout is the same as the irradiance and distance texture altases, except here each probe is represented by just *one texel*. Below is a visualization of the probe data texture's world-space offsets (top) and probe states (bottom).
-
Figure 6: The Probe Data texture (zoomed) from the Crytek Sponza scene
+
Figure 6: A visualization of the Probe Data texture (zoomed) for the Crytek Sponza scene
diff --git a/docs/ShaderAPI.md b/docs/ShaderAPI.md index 9ad2008..1042bc1 100644 --- a/docs/ShaderAPI.md +++ b/docs/ShaderAPI.md @@ -52,9 +52,14 @@ The Test Harness sample application provides [an example ray generation shader]( Computes the 3D grid-space coordinates for the probe at a given probe index in the range [0, numProbes]. Provide these coordinates to ```DDGIGetProbeWorldPosition()``` and ```DDGIGetScrollingProbeIndex()```. -#### ```DDGIGetProbeState(...)``` +#### ```DDGILoadProbeState(...)``` - float DDGIGetProbeState( + float DDGILoadProbeState( + int probeIndex, + RWTexture2D probeData, + DDGIVolumeDescGPU volume) + + float DDGILoadProbeState( int probeIndex, Texture2D probeData, DDGIVolumeDescGPU volume) diff --git a/docs/images/ddgivolume-textures-probedata.jpg b/docs/images/ddgivolume-textures-probedata.jpg index 7b067ebfecac2250b2ebd626a63307e944c57941..af31fee74f140360f0eb800088768cc5ed200c14 100644 GIT binary patch literal 73241 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 literal 21494 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 volume.probeBackfaceThreshold) @@ -369,7 +369,7 @@ void DDGIProbeClassificationResetCS(uint3 DispatchThreadID : SV_DispatchThreadID #endif // Get the probe's texel coordinates in the Probe Data texture - uint2 probeDataCoords = DDGIGetProbeDataTexelCoords(DispatchThreadID.x, volume); + uint2 probeDataCoords = DDGIGetProbeTexelCoords(DispatchThreadID.x, volume); ProbeData[probeDataCoords].w = RTXGI_DDGI_PROBE_STATE_ACTIVE; } diff --git a/rtxgi-sdk/shaders/ddgi/ProbeRelocationCS.hlsl b/rtxgi-sdk/shaders/ddgi/ProbeRelocationCS.hlsl index ba26c9b..b6c4224 100644 --- a/rtxgi-sdk/shaders/ddgi/ProbeRelocationCS.hlsl +++ b/rtxgi-sdk/shaders/ddgi/ProbeRelocationCS.hlsl @@ -268,7 +268,7 @@ void DDGIProbeRelocationCS(uint3 DispatchThreadID : SV_DispatchThreadID) #endif // Get the probe's texel coordinates in the Probe Data texture - uint2 coords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + uint2 coords = DDGIGetProbeTexelCoords(probeIndex, volume); // Read the current world position offset float3 offset = DDGILoadProbeDataOffset(ProbeData, coords, volume); @@ -387,7 +387,7 @@ void DDGIProbeRelocationResetCS(uint3 DispatchThreadID : SV_DispatchThreadID) #endif // Get the probe's texel coordinates in the Probe Data texture - uint2 probeDataCoords = DDGIGetProbeDataTexelCoords(DispatchThreadID.x, volume); + uint2 probeDataCoords = DDGIGetProbeTexelCoords(DispatchThreadID.x, volume); // Write the probe offset ProbeData[probeDataCoords].xyz = float3(0.f, 0.f, 0.f); diff --git a/rtxgi-sdk/shaders/ddgi/include/ProbeCommon.hlsl b/rtxgi-sdk/shaders/ddgi/include/ProbeCommon.hlsl index a08e345..8856cfe 100644 --- a/rtxgi-sdk/shaders/ddgi/include/ProbeCommon.hlsl +++ b/rtxgi-sdk/shaders/ddgi/include/ProbeCommon.hlsl @@ -62,7 +62,7 @@ float3 DDGIGetProbeWorldPosition(int3 probeCoords, DDGIVolumeDescGPU volume, Tex int probeIndex = DDGIGetScrollingProbeIndex(probeCoords, volume); // Find the texture coordinates of the probe in the Probe Data texture - uint2 coords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + uint2 coords = DDGIGetProbeTexelCoords(probeIndex, volume); // Load the probe's world-space position offset and add it to the current world position probeWorldPosition += DDGILoadProbeDataOffset(probeData, coords, volume); @@ -88,7 +88,7 @@ float3 DDGIGetProbeWorldPosition(int3 probeCoords, DDGIVolumeDescGPU volume, RWT int probeIndex = DDGIGetScrollingProbeIndex(probeCoords, volume); // Find the texture coordinates of the probe in the Probe Data texture - uint2 coords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + uint2 coords = DDGIGetProbeTexelCoords(probeIndex, volume); // Load the probe's world-space position offset and add it to the current world position probeWorldPosition += DDGILoadProbeDataOffset(probeData, coords, volume); diff --git a/rtxgi-sdk/shaders/ddgi/include/ProbeIndexing.hlsl b/rtxgi-sdk/shaders/ddgi/include/ProbeIndexing.hlsl index 70b1894..660ae5e 100644 --- a/rtxgi-sdk/shaders/ddgi/include/ProbeIndexing.hlsl +++ b/rtxgi-sdk/shaders/ddgi/include/ProbeIndexing.hlsl @@ -172,22 +172,17 @@ int3 DDGIGetBaseProbeGridCoords(float3 worldPosition, DDGIVolumeDescGPU volume) //------------------------------------------------------------------------ /** - * Computes the normalized texture UVs for the Probe Irradiance and Probe Distance textures - * used in blending given the probe index and 2D normalized octant coordinates [-1, 1]. - * + * Computes the 2D texture coordinates of the probe at the given probe index. + * * When infinite scrolling is enbled, probeIndex is expected to be the scroll adjusted probe index. * Obtain the adjusted index with DDGIGetScrollingProbeIndex(). */ -float2 DDGIGetProbeUV(int probeIndex, float2 octantCoordinates, int numTexels, DDGIVolumeDescGPU volume) +uint2 DDGIGetProbeTexelCoords(int probeIndex, DDGIVolumeDescGPU volume) { // Find the probe's plane index int probesPerPlane = DDGIGetProbesPerPlane(volume.probeCounts); int planeIndex = int(probeIndex / probesPerPlane); - // Account for the border texels - float probeInteriorTexels = float(numTexels); - float probeTexels = (probeInteriorTexels + 2.f); - #if RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_LEFT || RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_RIGHT int gridSpaceX = (probeIndex % volume.probeCounts.x); int gridSpaceY = (probeIndex / volume.probeCounts.x); @@ -208,6 +203,25 @@ float2 DDGIGetProbeUV(int probeIndex, float2 octantCoordinates, int numTexels, D int y = gridSpaceY % volume.probeCounts.y; #endif + return uint2(x, y); +} + +/** + * Computes the normalized texture UVs for the Probe Irradiance and Probe Distance textures + * (used in blending) given the probe index and 2D normalized octant coordinates [-1, 1]. + * + * When infinite scrolling is enbled, probeIndex is expected to be the scroll adjusted probe index. + * Obtain the adjusted index with DDGIGetScrollingProbeIndex(). + */ +float2 DDGIGetProbeUV(int probeIndex, float2 octantCoordinates, int numTexels, DDGIVolumeDescGPU volume) +{ + // Get the probe's texel coordinates, assuming one texel per probe + uint2 coords = DDGIGetProbeTexelCoords(probeIndex, volume); + + // Adjust for the number of interior and border texels + float probeInteriorTexels = float(numTexels); + float probeTexels = (probeInteriorTexels + 2.f); + #if RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_LEFT || RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_RIGHT float textureWidth = probeTexels * (volume.probeCounts.x * volume.probeCounts.y); float textureHeight = probeTexels * volume.probeCounts.z; @@ -219,34 +233,33 @@ float2 DDGIGetProbeUV(int probeIndex, float2 octantCoordinates, int numTexels, D float textureHeight = probeTexels * volume.probeCounts.y; #endif - float2 uv = float2(x * probeTexels, y * probeTexels) + (probeTexels * 0.5f); + float2 uv = float2(coords.x * probeTexels, coords.y * probeTexels) + (probeTexels * 0.5f); uv += octantCoordinates.xy * (probeInteriorTexels * 0.5f); uv /= float2(textureWidth, textureHeight); return uv; } +//------------------------------------------------------------------------ +// Probe Classification +//------------------------------------------------------------------------ + /** - * Computes the 2D texture coordinates of the probe at the given probe index - * for the Probe Data texture used in relocation and classification. - * - * When infinite scrolling is enbled, probeIndex is expected to be the scroll adjusted probe index. - * Obtain the adjusted index with DDGIGetScrollingProbeIndex(). + * Loads and returns the probe's classification state (from a RWTexture2D). */ -uint2 DDGIGetProbeDataTexelCoords(int probeIndex, DDGIVolumeDescGPU volume) +float DDGILoadProbeState(int probeIndex, RWTexture2D probeData, DDGIVolumeDescGPU volume) { - // Compute the probe texel coordinates for this probe -#if RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_LEFT || RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_RIGHT - return uint2(probeIndex % (volume.probeCounts.x * volume.probeCounts.y), probeIndex / (volume.probeCounts.x * volume.probeCounts.y)); -#elif RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_LEFT_Z_UP - return uint2(probeIndex % (volume.probeCounts.y * volume.probeCounts.z), probeIndex / (volume.probeCounts.y * volume.probeCounts.z)); -#elif RTXGI_COORDINATE_SYSTEM == RTXGI_COORDINATE_SYSTEM_RIGHT_Z_UP - return uint2(probeIndex % (volume.probeCounts.x * volume.probeCounts.z), probeIndex / (volume.probeCounts.x * volume.probeCounts.z)); -#endif -} + float state = RTXGI_DDGI_PROBE_STATE_ACTIVE; + if (volume.probeClassificationEnabled) + { + // Get the probe's texel coordinates in the Probe Data texture + int2 probeDataCoords = DDGIGetProbeTexelCoords(probeIndex, volume); -//------------------------------------------------------------------------ -// Probe Classification -//------------------------------------------------------------------------ + // Get the probe's classification state + state = probeData[probeDataCoords].w; + } + + return state; +} /** * Loads and returns the probe's classification state (from a Texture2D). @@ -257,7 +270,7 @@ float DDGILoadProbeState(int probeIndex, Texture2D probeData, DDGIVolume if (volume.probeClassificationEnabled) { // Get the probe's texel coordinates in the Probe Data texture - int2 probeDataCoords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + int2 probeDataCoords = DDGIGetProbeTexelCoords(probeIndex, volume); // Get the probe's classification state state = probeData.Load(int3(probeDataCoords, 0)).w; diff --git a/samples/test-harness/CMakeLists.txt b/samples/test-harness/CMakeLists.txt index a49e52e..b6e0374 100644 --- a/samples/test-harness/CMakeLists.txt +++ b/samples/test-harness/CMakeLists.txt @@ -35,21 +35,25 @@ file(GLOB TEST_HARNESS_CONFIG ) file(GLOB TEST_HARNESS_SOURCE + "include/Benchmark.h" "include/Caches.h" "include/Common.h" "include/Configs.h" "include/Geometry.h" "include/Graphics.h" + "include/ImageCapture.h" "include/Inputs.h" "include/Instrumentation.h" "include/Scenes.h" "include/Shaders.h" "include/Textures.h" "include/Window.h" + "src/Benchmark.cpp" "src/Caches.cpp" "src/Configs.cpp" "src/Geometry.cpp" "src/Inputs.cpp" + "src/ImageCapture.cpp" "src/Instrumentation.cpp" "src/main.cpp" "src/Scenes.cpp" @@ -264,6 +268,7 @@ set(GLFW_INCLUDE "${ROOT_DIR}/thirdparty/glfw/include") set(IMGUI_INCLUDE "${ROOT_DIR}/thirdparty/imgui") set(IMGUI_BACKENDS_INCLUDE "${ROOT_DIR}/thirdparty/imgui/backends") set(TINYGLTF_INCLUDE "${ROOT_DIR}/thirdparty/tinygltf") +set(LIBPNG_INCLUDE "${ROOT_DIR}/thirdparty/libpng" "${CMAKE_BINARY_DIR}/thirdparty/libpng") # ---- WINDOWS / D3D12 -------------------------------------------------------------------------------------- @@ -294,7 +299,7 @@ if(RTXGI_API_D3D12_ENABLE) ) # Add dependencies - add_dependencies(${TARGET_EXE} glfw tinygltf) + add_dependencies(${TARGET_EXE} glfw tinygltf png_static zlibstatic) # Add the include directories target_include_directories(${TARGET_EXE} PRIVATE @@ -309,6 +314,7 @@ if(RTXGI_API_D3D12_ENABLE) ${IMGUI_INCLUDE} ${IMGUI_BACKENDS_INCLUDE} ${TINYGLTF_INCLUDE} + ${LIBPNG_INCLUDE} ) # Add statically linked libs @@ -318,6 +324,8 @@ if(RTXGI_API_D3D12_ENABLE) d3d11 d3d12 dxgi + ../../thirdparty/libpng/$/libpng16_static + ../../thirdparty/zlib/$/zlibstatic ) # Add common compiler definitions for exposed Test Harness options @@ -330,7 +338,7 @@ if(RTXGI_API_D3D12_ENABLE) set_target_properties(${TARGET_EXE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ../bin/d3d12/$) # Set the default scene config - set_target_properties(${TARGET_EXE} PROPERTIES VS_DEBUGGER_COMMAND_ARGUMENTS ../../../samples/test-harness/config/cornell.ini) + set_target_properties(${TARGET_EXE} PROPERTIES VS_DEBUGGER_COMMAND_ARGUMENTS ${CMAKE_SOURCE_DIR}/samples/test-harness/config/cornell.ini) # Add to the samples folder set_target_properties(${TARGET_EXE} PROPERTIES FOLDER "RTXGI Samples") @@ -376,6 +384,7 @@ if(RTXGI_API_VULKAN_ENABLE) ${IMGUI_INCLUDE} ${IMGUI_BACKENDS_INCLUDE} ${TINYGLTF_INCLUDE} + ${LIBPNG_INCLUDE} ) # Add VS filters @@ -425,12 +434,13 @@ if(RTXGI_API_VULKAN_ENABLE) ${IMGUI_INCLUDE} ${IMGUI_BACKENDS_INCLUDE} ${TINYGLTF_INCLUDE} + ${LIBPNG_INCLUDE} ) endif() # Add dependencies - add_dependencies(${TARGET_EXE} glfw tinygltf) + add_dependencies(${TARGET_EXE} glfw tinygltf png_static zlibstatic) # Add compiler definitions target_compile_definitions(${TARGET_EXE} PUBLIC API_VULKAN) @@ -444,17 +454,17 @@ if(RTXGI_API_VULKAN_ENABLE) # Add statically linked libs if(WIN32) # Note: Even when targeting Vulkan, Windows uses D3D11 GPU-based texture compression with DirectXTex - target_link_libraries(${TARGET_EXE} RTXGI-VK ${Vulkan_LIBRARY} ../../thirdparty/glfw/src/$/glfw3 d3d11) + target_link_libraries(${TARGET_EXE} RTXGI-VK ${Vulkan_LIBRARY} ../../thirdparty/glfw/src/$/glfw3 d3d11 ../../thirdparty/libpng/$/libpng16_static ../../thirdparty/zlib/$/zlibstatic) elseif(UNIX AND NOT APPLE) # Note: UNIX can't use D3D11 GPU-based texture compression with DirectXTex - target_link_libraries(${TARGET_EXE} RTXGI-VK -lglfw -lvulkan -ldl -lpthread -lX11 -lXrandr -lXi -lstdc++fs) + target_link_libraries(${TARGET_EXE} RTXGI-VK -llibpng16_static -lz -lglfw -lvulkan -ldl -lpthread -lX11 -lXrandr -lXi -lstdc++fs) endif() # Set the binary output directory set_target_properties(${TARGET_EXE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ../bin/vulkan/$) # Set the default scene config - set_target_properties(${TARGET_EXE} PROPERTIES VS_DEBUGGER_COMMAND_ARGUMENTS ../../../samples/test-harness/config/cornell.ini) + set_target_properties(${TARGET_EXE} PROPERTIES VS_DEBUGGER_COMMAND_ARGUMENTS ${CMAKE_SOURCE_DIR}/samples/test-harness/config/cornell.ini) # Add to the samples folder set_target_properties(${TARGET_EXE} PROPERTIES FOLDER "RTXGI Samples") @@ -514,14 +524,14 @@ if(UNIX AND RTXGI_API_VULKAN_ENABLE) if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64") add_custom_command( TARGET TestHarness-VK POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/vulkan/$ - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_SOURCE_DIR}/build/samples/test-harness + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_BINARY_DIR}/samples/test-harness COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/docs/nvidia.jpg ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/vulkan/$ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/docs/nvidia.jpg ${CMAKE_BINARY_DIR}/samples/test-harness ) elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") add_custom_command( TARGET TestHarness-VK POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/vulkan/$ - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_SOURCE_DIR}/build/samples/test-harness + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/dxc/lib/libdxcompiler.so ${CMAKE_BINARY_DIR}/samples/test-harness COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/docs/nvidia.jpg ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/vulkan/$ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${ROOT_DIR}/docs/nvidia.jpg ${CMAKE_BINARY_DIR}/samples/test-harness ) diff --git a/samples/test-harness/config/cornell.ini b/samples/test-harness/config/cornell.ini index ec736bf..5b4ec11 100644 --- a/samples/test-harness/config/cornell.ini +++ b/samples/test-harness/config/cornell.ini @@ -27,14 +27,14 @@ scene.lights.0.radius=4.0 scene.lights.1.name=Sun scene.lights.1.type=0 -scene.lights.1.direction=0.6 -0.435, -0.816 # right hand, y-up +scene.lights.1.direction=0.6 -0.435 -0.816 # right hand, y-up scene.lights.1.color=1.0 1.0 1.0 scene.lights.1.power=1.0 scene.lights.2.name=Spot Light 1 scene.lights.2.type=1 scene.lights.2.position=0.0 1.0 0.5 -scene.lights.2.direction=0.6 -0.435, -0.816 # right hand, y-up +scene.lights.2.direction=0.6 -0.435 -0.816 # right hand, y-up scene.lights.2.color=1.0 1.0 1.0 scene.lights.2.power=0.0 scene.lights.2.radius=4.0 @@ -96,8 +96,8 @@ ddgi.volume.0.vis.showProbes=1 ddgi.volume.0.vis.texture.rayDataScale=0.75 ddgi.volume.0.vis.texture.irradianceScale=2.1 ddgi.volume.0.vis.texture.distanceScale=1.05 -ddgi.volume.0.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.0.vis.texture.classificationStateScale=5.9 +ddgi.volume.0.vis.texture.relocationOffsetScale=16.8 +ddgi.volume.0.vis.texture.classificationStateScale=16.8 # ray traced ambient occlusion rtao.enable=1 diff --git a/samples/test-harness/config/furnace.ini b/samples/test-harness/config/furnace.ini index 118238b..91d8fe2 100644 --- a/samples/test-harness/config/furnace.ini +++ b/samples/test-harness/config/furnace.ini @@ -53,8 +53,8 @@ ddgi.volume.0.textures.rayData.format=0 ddgi.volume.0.textures.irradiance.format=0 ddgi.volume.0.textures.distance.format=0 ddgi.volume.0.textures.data.format=0 -ddgi.volume.0.origin=0.0, 0.5, 0.0 # left and right hand, y-up -ddgi.volume.0.probeCounts=8, 3, 8 +ddgi.volume.0.origin=0.0 0.5 0.0 # left and right hand, y-up +ddgi.volume.0.probeCounts=8 3 8 ddgi.volume.0.probeSpacing=2 1 2 ddgi.volume.0.probeNumRays=288 ddgi.volume.0.probeNumIrradianceTexels=6 @@ -68,9 +68,9 @@ ddgi.volume.0.probeBrightnessThreshold=1.0 ddgi.volume.0.vis.probeRadius=0.2 ddgi.volume.0.vis.probeDistanceDivisor=30 ddgi.volume.0.vis.showProbes=1 -ddgi.volume.0.vis.texture.rayDataScale=0.25 +ddgi.volume.0.vis.texture.rayDataScale=1.0 ddgi.volume.0.vis.texture.irradianceScale=1.0 -ddgi.volume.0.vis.texture.distanceScale=1.3 +ddgi.volume.0.vis.texture.distanceScale=1.0 ddgi.volume.0.vis.texture.relocationOffsetScale=7.0 ddgi.volume.0.vis.texture.classificationStateScale=7.0 diff --git a/samples/test-harness/config/multi-cornell.ini b/samples/test-harness/config/multi-cornell.ini index 124360c..a4ffe0f 100644 --- a/samples/test-harness/config/multi-cornell.ini +++ b/samples/test-harness/config/multi-cornell.ini @@ -72,8 +72,8 @@ ddgi.volume.0.vis.showProbes=1 ddgi.volume.0.vis.texture.rayDataScale=0.75 ddgi.volume.0.vis.texture.irradianceScale=2.1 ddgi.volume.0.vis.texture.distanceScale=1.05 -ddgi.volume.0.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.0.vis.texture.classificationStateScale=5.9 +ddgi.volume.0.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.0.vis.texture.classificationStateScale=16.81 ddgi.volume.1.name=Cornell Box 2 Volume ddgi.volume.1.probeRelocation.enabled=1 @@ -103,8 +103,8 @@ ddgi.volume.1.vis.showProbes=1 ddgi.volume.1.vis.texture.rayDataScale=0.75 ddgi.volume.1.vis.texture.irradianceScale=2.1 ddgi.volume.1.vis.texture.distanceScale=1.05 -ddgi.volume.1.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.1.vis.texture.classificationStateScale=5.9 +ddgi.volume.1.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.1.vis.texture.classificationStateScale=16.81 ddgi.volume.2.name=Cornell Box 3 Volume ddgi.volume.2.probeRelocation.enabled=1 @@ -134,8 +134,8 @@ ddgi.volume.2.vis.showProbes=1 ddgi.volume.2.vis.texture.rayDataScale=0.75 ddgi.volume.2.vis.texture.irradianceScale=2.1 ddgi.volume.2.vis.texture.distanceScale=1.05 -ddgi.volume.2.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.2.vis.texture.classificationStateScale=5.9 +ddgi.volume.2.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.2.vis.texture.classificationStateScale=16.81 ddgi.volume.3.name=Cornell Box 4 Volume ddgi.volume.3.probeRelocation.enabled=1 @@ -165,8 +165,8 @@ ddgi.volume.3.vis.showProbes=1 ddgi.volume.3.vis.texture.rayDataScale=0.75 ddgi.volume.3.vis.texture.irradianceScale=2.1 ddgi.volume.3.vis.texture.distanceScale=1.05 -ddgi.volume.3.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.3.vis.texture.classificationStateScale=5.9 +ddgi.volume.3.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.3.vis.texture.classificationStateScale=16.81 ddgi.volume.4.name=Cornell Box 5 Volume ddgi.volume.4.probeRelocation.enabled=1 @@ -196,8 +196,8 @@ ddgi.volume.4.vis.showProbes=1 ddgi.volume.4.vis.texture.rayDataScale=0.75 ddgi.volume.4.vis.texture.irradianceScale=2.1 ddgi.volume.4.vis.texture.distanceScale=1.05 -ddgi.volume.4.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.4.vis.texture.classificationStateScale=5.9 +ddgi.volume.4.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.4.vis.texture.classificationStateScale=16.81 ddgi.volume.5.name=Cornell Box 6 Volume ddgi.volume.5.probeRelocation.enabled=1 @@ -227,8 +227,8 @@ ddgi.volume.5.vis.showProbes=1 ddgi.volume.5.vis.texture.rayDataScale=0.75 ddgi.volume.5.vis.texture.irradianceScale=2.1 ddgi.volume.5.vis.texture.distanceScale=1.05 -ddgi.volume.5.vis.texture.relocationOffsetScale=5.9 -ddgi.volume.5.vis.texture.classificationStateScale=5.9 +ddgi.volume.5.vis.texture.relocationOffsetScale=16.81 +ddgi.volume.5.vis.texture.classificationStateScale=16.81 # ray traced ambient occlusion rtao.enable=1 diff --git a/samples/test-harness/config/sponza.ini b/samples/test-harness/config/sponza.ini index 4d3fd4b..6f815a8 100644 --- a/samples/test-harness/config/sponza.ini +++ b/samples/test-harness/config/sponza.ini @@ -77,7 +77,7 @@ ddgi.volume.0.vis.probeRadius=0.1 ddgi.volume.0.vis.probeAlpha=0.9 ddgi.volume.0.vis.probeDistanceDivisor=200 ddgi.volume.0.vis.showProbes=1 -ddgi.volume.0.vis.texture.rayDataScale=0.067 +ddgi.volume.0.vis.texture.rayDataScale=0.4 ddgi.volume.0.vis.texture.irradianceScale=0.395 ddgi.volume.0.vis.texture.distanceScale=0.24 ddgi.volume.0.vis.texture.relocationOffsetScale=3.9 diff --git a/samples/test-harness/config/tunnel.ini b/samples/test-harness/config/tunnel.ini index 1b4f64d..176bbc3 100644 --- a/samples/test-harness/config/tunnel.ini +++ b/samples/test-harness/config/tunnel.ini @@ -20,7 +20,7 @@ scene.skyIntensity=1.0 # scene lights scene.lights.0.name=Sun scene.lights.0.type=0 -scene.lights.0.direction=1.0 -1.0, -0.7 # right hand, y-up +scene.lights.0.direction=1.0 -1.0 -0.7 # right hand, y-up scene.lights.0.color=1.0 1.0 1.0 scene.lights.0.power=2.2 @@ -70,10 +70,10 @@ ddgi.volume.0.vis.probeRadius=1.0 ddgi.volume.0.vis.probeDistanceDivisor=3 ddgi.volume.0.vis.showProbes=1 ddgi.volume.0.vis.texture.rayDataScale=0.3 -ddgi.volume.0.vis.texture.irradianceScale=0.805 -ddgi.volume.0.vis.texture.distanceScale=0.403 -ddgi.volume.0.vis.texture.relocationOffsetScale=4.5 -ddgi.volume.0.vis.texture.classificationStateScale=4.5 +ddgi.volume.0.vis.texture.irradianceScale=0.8 +ddgi.volume.0.vis.texture.distanceScale=0.4 +ddgi.volume.0.vis.texture.relocationOffsetScale=6.4 +ddgi.volume.0.vis.texture.classificationStateScale=6.4 # ray traced ambient occlusion rtao.enable=1 diff --git a/samples/test-harness/config/two-rooms.ini b/samples/test-harness/config/two-rooms.ini index 400cc29..1b125de 100644 --- a/samples/test-harness/config/two-rooms.ini +++ b/samples/test-harness/config/two-rooms.ini @@ -95,11 +95,11 @@ ddgi.volume.0.probeBrightnessThreshold=2.0 ddgi.volume.0.vis.probeRadius=2.0 ddgi.volume.0.vis.probeDistanceDivisor=30 ddgi.volume.0.vis.showProbes=1 -ddgi.volume.0.vis.texture.rayDataScale=0.25 -ddgi.volume.0.vis.texture.irradianceScale=1.2 -ddgi.volume.0.vis.texture.distanceScale=0.6 -ddgi.volume.0.vis.texture.relocationOffsetScale=4.0 -ddgi.volume.0.vis.texture.classificationStateScale=4.0 +ddgi.volume.0.vis.texture.rayDataScale=0.15 +ddgi.volume.0.vis.texture.irradianceScale=0.7 +ddgi.volume.0.vis.texture.distanceScale=0.35 +ddgi.volume.0.vis.texture.relocationOffsetScale=5.6 +ddgi.volume.0.vis.texture.classificationStateScale=5.6 # ray traced ambient occlusion rtao.enable=1 diff --git a/samples/test-harness/include/Benchmark.h b/samples/test-harness/include/Benchmark.h new file mode 100644 index 0000000..c44cd15 --- /dev/null +++ b/samples/test-harness/include/Benchmark.h @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved. +* +* NVIDIA CORPORATION and its licensors retain all intellectual property +* and proprietary rights in and to this software, related documentation +* and any modifications thereto. Any use, reproduction, disclosure or +* distribution of this software and related documentation without an express +* license agreement from NVIDIA CORPORATION is strictly prohibited. +*/ + +#pragma once + +#include "Instrumentation.h" +#include "Configs.h" +#include "Graphics.h" + +#include + +namespace Benchmark +{ + const static uint32_t NumBenchmarkFrames = 1024; + + struct BenchmarkRun + { + std::stringstream cpuTimingCsv; + std::stringstream gpuTimingCsv; + }; + void StartBenchmark(BenchmarkRun& benchmarkRun, Instrumentation::Performance& perf, Configs::Config& config, Graphics::Globals& gfx); + bool UpdateBenchmark(BenchmarkRun& benchmarkRun, Instrumentation::Performance& perf, Configs::Config& config, Graphics::Globals& gfx, std::ofstream& log); +} \ No newline at end of file diff --git a/samples/test-harness/include/Configs.h b/samples/test-harness/include/Configs.h index 9826d2f..d41cc6c 100644 --- a/samples/test-harness/include/Configs.h +++ b/samples/test-harness/include/Configs.h @@ -33,6 +33,7 @@ namespace Configs { std::string name = ""; uint32_t index = 0; + uint32_t rngSeed = 0; bool insertPerfMarkers = false; bool showProbes = false; diff --git a/samples/test-harness/include/Graphics.h b/samples/test-harness/include/Graphics.h index 7a8e19c..6580f44 100644 --- a/samples/test-harness/include/Graphics.h +++ b/samples/test-harness/include/Graphics.h @@ -62,7 +62,7 @@ namespace Graphics bool UpdateTimestamps(Globals& gfx, GlobalResources& gfxResources, Instrumentation::Performance& performance); #endif - bool WriteGBufferToDisk(Globals& gfx, GlobalResources& gfxResources, std::string directory); + bool WriteBackBufferToDisk(Globals& gfx, std::string directory); namespace BindlessResourceOffsets { diff --git a/samples/test-harness/include/ImageCapture.h b/samples/test-harness/include/ImageCapture.h new file mode 100644 index 0000000..f36ff4c --- /dev/null +++ b/samples/test-harness/include/ImageCapture.h @@ -0,0 +1,20 @@ +/* +* Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved. +* +* NVIDIA CORPORATION and its licensors retain all intellectual property +* and proprietary rights in and to this software, related documentation +* and any modifications thereto. Any use, reproduction, disclosure or +* distribution of this software and related documentation without an express +* license agreement from NVIDIA CORPORATION is strictly prohibited. +*/ + +#pragma once + +#include +#include +#include + +namespace ImageCapture +{ + bool CapturePng(std::string file, uint32_t width, uint32_t height, std::vector& rows); +} \ No newline at end of file diff --git a/samples/test-harness/include/Inputs.h b/samples/test-harness/include/Inputs.h index f7262e3..af4e7f0 100644 --- a/samples/test-harness/include/Inputs.h +++ b/samples/test-harness/include/Inputs.h @@ -24,6 +24,7 @@ namespace Inputs SAVE_IMAGE, CAMERA_MOVEMENT, FULLSCREEN_CHANGE, + RUN_BENCHMARK, COUNT }; @@ -34,7 +35,7 @@ namespace Inputs DirectX::XMINT2 prevMousePos = { INT_MAX, INT_MAX }; bool mouseLeftBtnDown = false; bool mouseRightBtnDown = false; - bool saveImages = false; + bool runBenchmark = false; }; bool Initialize(GLFWwindow* window, Input& input, Configs::Config& config, Scenes::Scene& scene); diff --git a/samples/test-harness/include/Instrumentation.h b/samples/test-harness/include/Instrumentation.h index 28885e7..91dddf5 100644 --- a/samples/test-harness/include/Instrumentation.h +++ b/samples/test-harness/include/Instrumentation.h @@ -14,6 +14,7 @@ #include #include +#include namespace Instrumentation { @@ -42,12 +43,14 @@ namespace Instrumentation this->sampleSize = sampleSize; } + const static uint32_t FallbackSampleSize = 10; + std::string name = ""; EStatType type; uint32_t index = 0; uint64_t timestamp = 0; - uint32_t sampleSize = 10; + uint32_t sampleSize = FallbackSampleSize; double elapsed = 0; // milliseconds double average = 0; double total = 0; @@ -56,6 +59,15 @@ namespace Instrumentation uint32_t GetQueryBeginIndex() { return (index * 2); } uint32_t GetQueryEndIndex() { return (index * 2) + 1; } + void Reset(uint32_t sampleSize = FallbackSampleSize) + { + elapsed = 0; + average = 0; + total = 0; + samples = {}; + this->sampleSize = sampleSize; + } + }; struct Performance @@ -63,27 +75,42 @@ namespace Instrumentation std::vector gpuTimes; std::vector cpuTimes; + const static uint32_t DefaultSampleSize = 50; + uint32_t GetNumGPUQueries() { return static_cast(gpuTimes.size() * 2); }; - Stat*& AddCPUStat(std::string name, uint32_t sampleSize = 50) + Stat*& AddCPUStat(std::string name, uint32_t sampleSize = DefaultSampleSize) { cpuTimes.emplace_back(new Stat(EStatType::CPU, name, sampleSize)); return cpuTimes.back(); } - Stat*& AddGPUStat(std::string name, uint32_t sampleSize = 50) + Stat*& AddGPUStat(std::string name, uint32_t sampleSize = DefaultSampleSize) { uint32_t index = static_cast(gpuTimes.size()); gpuTimes.emplace_back(new Stat(EStatType::GPU, index, name, sampleSize)); return gpuTimes.back(); } - void AddStat(std::string name, Stat*& cpu, Stat*& gpu, uint32_t sampleSize = 50) + void AddStat(std::string name, Stat*& cpu, Stat*& gpu, uint32_t sampleSize = DefaultSampleSize) { cpu = AddCPUStat(name, sampleSize); gpu = AddGPUStat(name, sampleSize); } + void Reset(uint32_t sampleSize = DefaultSampleSize) + { + for (Stat* stat : cpuTimes) + { + stat->Reset(sampleSize); + } + + for (Stat* stat : gpuTimes) + { + stat->Reset(sampleSize); + } + } + void Cleanup() { size_t index; @@ -102,6 +129,7 @@ namespace Instrumentation cpuTimes.clear(); gpuTimes.clear(); } + }; void Begin(Stat* s); @@ -109,6 +137,9 @@ namespace Instrumentation void Resolve(Stat* s); void EndAndResolve(Stat* s); + std::ostream& operator<<(std::ostream& os, const Stat& stat); + std::ostream& operator<<(std::ostream& os, std::vector& stats); + } #define CPU_TIMESTAMP_BEGIN(x) Begin(x) diff --git a/samples/test-harness/include/Vulkan.h b/samples/test-harness/include/Vulkan.h index 0d48dc7..1408a79 100644 --- a/samples/test-harness/include/Vulkan.h +++ b/samples/test-harness/include/Vulkan.h @@ -397,8 +397,7 @@ namespace Graphics void BeginRenderPass(Globals& vk); - /*bool WriteResourceImageToDisk(Globals& gfx, ID3D12Resource* pResource, std::string folder, std::string filename, D3D12_RESOURCE_STATES state); - bool WriteResourceImagesToDisk(Globals& gfx, Resources& resources, std::string folder);*/ + bool WriteResourceToDisk(Globals& vk, std::string file, VkImage image, uint32_t width, uint32_t height, VkFormat imageFormat, VkImageLayout originalLayout); #ifdef GFX_NAME_OBJECTS void SetObjectName(VkDevice device, uint64_t handle, const char* name, VkObjectType type); diff --git a/samples/test-harness/include/graphics/DDGI.h b/samples/test-harness/include/graphics/DDGI.h index 3ede156..f7a5fab 100644 --- a/samples/test-harness/include/graphics/DDGI.h +++ b/samples/test-harness/include/graphics/DDGI.h @@ -36,5 +36,7 @@ namespace Graphics void AddCommonShaderDefines(Shaders::ShaderProgram& shader, const DDGIVolumeDesc& volumeDesc, bool spirv); bool CompileDDGIVolumeShaders(Globals& vk, const DDGIVolumeDesc& volumeDesc, std::vector& volumeShaders, bool spirv, std::ofstream& log); + + bool WriteVolumesToDisk(Globals& globals, GlobalResources& gfxResources, Resources& resources, std::string directory); } } diff --git a/samples/test-harness/include/graphics/RTAO.h b/samples/test-harness/include/graphics/RTAO.h index e7bdf4f..f883ea1 100644 --- a/samples/test-harness/include/graphics/RTAO.h +++ b/samples/test-harness/include/graphics/RTAO.h @@ -32,5 +32,6 @@ namespace Graphics void Update(Globals& globals, GlobalResources& gfxResources, Resources& resources, const Configs::Config& config); void Execute(Globals& globals, GlobalResources& gfxResources, Resources& resources); void Cleanup(Globals& globals, Resources& resources); + bool WriteRTAOBuffersToDisk(Globals& globals, GlobalResources& gfxResources, Resources& resources, std::string directory); } } diff --git a/samples/test-harness/shaders/ddgi/ProbeTraceRGS.hlsl b/samples/test-harness/shaders/ddgi/ProbeTraceRGS.hlsl index 12e2271..576e372 100644 --- a/samples/test-harness/shaders/ddgi/ProbeTraceRGS.hlsl +++ b/samples/test-harness/shaders/ddgi/ProbeTraceRGS.hlsl @@ -107,7 +107,7 @@ void RayGen() // Early out: a "fixed" ray hit a front facing surface. Fixed rays are not blended since their direction // is not random and they would bias the irradiance estimate. Don't perform lighting for these rays. - if(volume.probeClassificationEnabled && rayIndex < RTXGI_DDGI_NUM_FIXED_RAYS) + if((volume.probeRelocationEnabled || volume.probeClassificationEnabled) && rayIndex < RTXGI_DDGI_NUM_FIXED_RAYS) { // Store the ray front face hit distance (only) DDGIStoreProbeRayFrontfaceHit(RayData, texCoords, volume, payload.hitT); diff --git a/samples/test-harness/shaders/ddgi/visualizations/ProbesRGS.hlsl b/samples/test-harness/shaders/ddgi/visualizations/ProbesRGS.hlsl index fd19678..d71cb02 100644 --- a/samples/test-harness/shaders/ddgi/visualizations/ProbesRGS.hlsl +++ b/samples/test-harness/shaders/ddgi/visualizations/ProbesRGS.hlsl @@ -164,7 +164,7 @@ void RayGen() // Get the probe's location in the probe data texture Texture2D ProbeData = GetDDGIVolumeProbeDataSRV(volumeIndex); - uint2 probeStateTexCoords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + uint2 probeStateTexCoords = DDGIGetProbeTexelCoords(probeIndex, volume); // Get the probe's state float probeState = ProbeData[probeStateTexCoords].w; @@ -257,7 +257,7 @@ void RayGenHideInactive() // Get the probe's state Texture2D ProbeData = GetDDGIVolumeProbeDataSRV(volumeIndex); - uint2 probeStateTexCoords = DDGIGetProbeDataTexelCoords(probeIndex, volume); + uint2 probeStateTexCoords = DDGIGetProbeTexelCoords(probeIndex, volume); float probeState = ProbeData[probeStateTexCoords].w; if(probeState == RTXGI_DDGI_PROBE_STATE_INACTIVE) continue; diff --git a/samples/test-harness/shaders/ddgi/visualizations/VolumeTexturesCS.hlsl b/samples/test-harness/shaders/ddgi/visualizations/VolumeTexturesCS.hlsl index f33220f..2464bea 100644 --- a/samples/test-harness/shaders/ddgi/visualizations/VolumeTexturesCS.hlsl +++ b/samples/test-harness/shaders/ddgi/visualizations/VolumeTexturesCS.hlsl @@ -114,36 +114,9 @@ void CS(uint3 DispatchThreadID : SV_DispatchThreadID) return; } - // Ray Data - float2 radianceRect = float2(DDGIVolume.probeNumRays, DDGIVolume.probeCounts.x * DDGIVolume.probeCounts.y * DDGIVolume.probeCounts.z) *GetGlobalConst(ddgivis, rayDataTextureScale); - xmax = radianceRect.x; - ymin += distanceRect.y + 5; - ymax = (ymin + radianceRect.y); - if (DispatchThreadID.x <= xmax && DispatchThreadID.y > ymin && DispatchThreadID.y <= ymax) - { - Texture2D RayData = GetDDGIVolumeRayDataSRV(volumeIndex); - - // Sample the ray data texture - coords = float2(DispatchThreadID.x, (DispatchThreadID.y - ymin)) / radianceRect.xy; - - if(DDGIVolume.probeRayDataFormat == RTXGI_DDGI_FORMAT_PROBE_RAY_DATA_R32G32B32A32_FLOAT) - { - color = RayData.SampleLevel(PointClampSampler, coords, 0).rgb; - } - else if(DDGIVolume.probeRayDataFormat == RTXGI_DDGI_FORMAT_PROBE_RAY_DATA_R32G32_FLOAT) - { - color = RTXGIUintToFloat3(asuint(RayData.SampleLevel(PointClampSampler, coords, 0).r)); - } - - // Overwrite GBufferA's albedo and mark the pixel to not be lit - GBufferA[DispatchThreadID.xy] = float4(color, 0.f); - - return; - } - // Relocation offsets float2 offsetRect = 0; - ymin += radianceRect.y + 5; + ymin += distanceRect.y + 5; if (DDGIVolume.probeRelocationEnabled) { offsetRect = numProbes.xy * GetGlobalConst(ddgivis, relocationOffsetTextureScale); @@ -204,4 +177,31 @@ void CS(uint3 DispatchThreadID : SV_DispatchThreadID) } } + // Ray Data + float2 radianceRect = float2(DDGIVolume.probeNumRays, DDGIVolume.probeCounts.x * DDGIVolume.probeCounts.y * DDGIVolume.probeCounts.z) *GetGlobalConst(ddgivis, rayDataTextureScale); + xmax = radianceRect.x; + ymin = ymax + 5; + ymax = (ymin + radianceRect.y); + if (DispatchThreadID.x <= xmax && DispatchThreadID.y > ymin && DispatchThreadID.y <= ymax) + { + Texture2D RayData = GetDDGIVolumeRayDataSRV(volumeIndex); + + // Sample the ray data texture + coords = float2(DispatchThreadID.x, (DispatchThreadID.y - ymin)) / radianceRect.xy; + + if (DDGIVolume.probeRayDataFormat == RTXGI_DDGI_FORMAT_PROBE_RAY_DATA_R32G32B32A32_FLOAT) + { + color = RayData.SampleLevel(PointClampSampler, coords, 0).rgb; + } + else if (DDGIVolume.probeRayDataFormat == RTXGI_DDGI_FORMAT_PROBE_RAY_DATA_R32G32_FLOAT) + { + color = RTXGIUintToFloat3(asuint(RayData.SampleLevel(PointClampSampler, coords, 0).r)); + } + + // Overwrite GBufferA's albedo and mark the pixel to not be lit + GBufferA[DispatchThreadID.xy] = float4(color, 0.f); + + return; + } + } diff --git a/samples/test-harness/src/Benchmark.cpp b/samples/test-harness/src/Benchmark.cpp new file mode 100644 index 0000000..8cac600 --- /dev/null +++ b/samples/test-harness/src/Benchmark.cpp @@ -0,0 +1,92 @@ +/* +* Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved. +* +* NVIDIA CORPORATION and its licensors retain all intellectual property +* and proprietary rights in and to this software, related documentation +* and any modifications thereto. Any use, reproduction, disclosure or +* distribution of this software and related documentation without an express +* license agreement from NVIDIA CORPORATION is strictly prohibited. +*/ + +#include "Benchmark.h" + +namespace Benchmark +{ + void StartBenchmark(BenchmarkRun& benchmarkRun, Instrumentation::Performance& perf, Configs::Config& config, Graphics::Globals& gfx) + { + benchmarkRun.cpuTimingCsv.str(""); + benchmarkRun.gpuTimingCsv.str(""); + // Clear timer history if beginning benchmark mode, this should not break timers that are currently running + perf.Reset(NumBenchmarkFrames); + if (config.app.renderMode == ERenderMode::DDGI) + { + // reload ddgi configs in order to reset RNG state + config.ddgi.reload = true; + } + gfx.frameNumber = 1; + } + + bool UpdateBenchmark(BenchmarkRun& benchmarkRun, Instrumentation::Performance& perf, Configs::Config& config, Graphics::Globals& gfx, std::ofstream& log) + { + // if benchmark is currently running, make a csv row for the frame's timings + benchmarkRun.cpuTimingCsv << gfx.frameNumber << ","; + benchmarkRun.gpuTimingCsv << gfx.frameNumber << ","; + + // print the timer values to the row + benchmarkRun.cpuTimingCsv << perf.cpuTimes; + benchmarkRun.gpuTimingCsv << perf.gpuTimes; + + // if benchmark is done, print the timing results to files + if (gfx.frameNumber >= NumBenchmarkFrames) + { + // generate the header row of the csvs + std::stringstream header; + header << "FrameIndex,"; + for (std::vector::const_iterator& it = perf.cpuTimes.cbegin(); it != perf.cpuTimes.cend(); it++) + { + Instrumentation::Stat* stat = *it; + header << stat->name << ","; + } + std::string cpuHeader = header.str(); + + header.str(""); + header << "FrameIndex,"; + for (std::vector::const_iterator& it = perf.gpuTimes.cbegin(); it != perf.gpuTimes.cend(); it++) + { + Instrumentation::Stat* stat = *it; + header << stat->name << ","; + } + std::string gpuHeader = header.str(); + + // write csv to file + std::ofstream csv; + csv.open(config.scene.screenshotPath + "\\benchmarkCpu.csv", std::ios::out); + if (csv.is_open()) + { + csv << cpuHeader << std::endl << benchmarkRun.cpuTimingCsv.str(); + } + csv.close(); + csv.open(config.scene.screenshotPath + "\\benchmarkGpu.csv", std::ios::out); + if (csv.is_open()) + { + csv << gpuHeader << std::endl << benchmarkRun.gpuTimingCsv.str(); + } + csv.close(); + log << "wrote benchmark results to csv." << std::endl; + + // print averages to the log file + log << "Benchmark Timings:" << std::endl; + for (Instrumentation::Stat* stat : perf.cpuTimes) + { + log << "\t" << stat->name << "=" << stat->average << "ms(CPU)" << std::endl;; + } + for (Instrumentation::Stat* stat : perf.gpuTimes) + { + log << "\t" << stat->name << "=" << stat->average << "ms(GPU)" << std::endl;; + } + + return false; + } + return true; + } +} \ No newline at end of file diff --git a/samples/test-harness/src/Configs.cpp b/samples/test-harness/src/Configs.cpp index b961ac2..fb42656 100644 --- a/samples/test-harness/src/Configs.cpp +++ b/samples/test-harness/src/Configs.cpp @@ -172,6 +172,7 @@ namespace Configs if (tokens[3].compare("probeMaxRayDistance") == 0) { Store(data, config.ddgi.volumes[volumeIndex].probeMaxRayDistance); return true; } if (tokens[3].compare("probeIrradianceThreshold") == 0) { Store(data, config.ddgi.volumes[volumeIndex].probeIrradianceThreshold); return true; } if (tokens[3].compare("probeBrightnessThreshold") == 0) { Store(data, config.ddgi.volumes[volumeIndex].probeBrightnessThreshold); return true; } + if (tokens[3].compare("rngSeed") == 0) { Store(data, config.ddgi.volumes[volumeIndex].rngSeed); return true; } if (tokens[3].compare("probeRelocation") == 0) { diff --git a/samples/test-harness/src/Direct3D12.cpp b/samples/test-harness/src/Direct3D12.cpp index 0b3f9ae..d4956fe 100644 --- a/samples/test-harness/src/Direct3D12.cpp +++ b/samples/test-harness/src/Direct3D12.cpp @@ -10,6 +10,7 @@ #include "Graphics.h" #include "UI.h" +#include "ImageCapture.h" #include #include @@ -1592,6 +1593,25 @@ namespace Graphics // Debug Functions //---------------------------------------------------------------------------------------------------------- + IWICImagingFactory2* _GetWIC() + { + static INIT_ONCE s_initOnce = INIT_ONCE_STATIC_INIT; + + IWICImagingFactory2* factory = nullptr; + (void)InitOnceExecuteOnce(&s_initOnce, + [](PINIT_ONCE, PVOID, PVOID* ifactory) -> BOOL + { + return SUCCEEDED(CoCreateInstance( + CLSID_WICImagingFactory2, + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IWICImagingFactory2), + ifactory)) ? TRUE : FALSE; + }, nullptr, reinterpret_cast(&factory)); + + return factory; + } + /** * Write an image to disk from the given D3D12 resource. */ @@ -1599,7 +1619,208 @@ namespace Graphics { CoInitialize(NULL); std::wstring filename = std::wstring(file.begin(), file.end()); - if(FAILED(SaveWICTextureToFile(d3d.cmdQueue, pResource, GUID_ContainerFormatPng, filename.c_str(), state, state))) return false; + //if(FAILED(SaveWICTextureToFile(d3d.cmdQueue, pResource, GUID_ContainerFormatPng, filename.c_str(), state, state))) return false; + + // copied from SaveWICTextureToFile() from DirectXTK, but using libpng instead of WIC + const D3D12_RESOURCE_DESC desc = pResource->GetDesc(); + UINT64 totalResourceSize = 0, fpRowPitch = 0; + UINT fpRowCount = 0; + // Get the rowcount, pitch and size of the top mip + d3d.device->GetCopyableFootprints( + &desc, + 0, + 1, + 0, + nullptr, + &fpRowCount, + &fpRowPitch, + &totalResourceSize); + // Round up the srcPitch to multiples of 256 + UINT64 dstRowPitch = (fpRowPitch + 255) & ~0xFF; + ID3D12Resource* pStaging = nullptr; + + D3D12_HEAP_PROPERTIES sourceHeapProperties = {}; + D3D12_HEAP_FLAGS sourceHeapFlags = {}; + HRESULT hr = pResource->GetHeapProperties(&sourceHeapProperties, &sourceHeapFlags); + ID3D12CommandAllocator* commandAlloc = nullptr; + hr = d3d.device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAlloc)); + ID3D12GraphicsCommandList* commandList = nullptr; + hr = d3d.device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAlloc, nullptr, IID_PPV_ARGS(&commandList)); + ID3D12Fence* fence = nullptr; + hr = d3d.device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)); + D3D12_HEAP_PROPERTIES defaultHeapProperties = {}, readBackHeapProperties = {}; + defaultHeapProperties.Type = D3D12_HEAP_TYPE_DEFAULT; + defaultHeapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + defaultHeapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + defaultHeapProperties.CreationNodeMask = 1; + defaultHeapProperties.VisibleNodeMask = 1; + readBackHeapProperties.Type = D3D12_HEAP_TYPE_READBACK; + readBackHeapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + readBackHeapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + readBackHeapProperties.CreationNodeMask = 1; + readBackHeapProperties.VisibleNodeMask = 1; + // Readback resources must be buffers + D3D12_RESOURCE_DESC bufferDesc = {}; + bufferDesc.Alignment = desc.Alignment; + bufferDesc.DepthOrArraySize = 1; + bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE; + bufferDesc.Format = DXGI_FORMAT_UNKNOWN; + bufferDesc.Height = 1; + bufferDesc.Width = dstRowPitch * desc.Height; + bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + bufferDesc.MipLevels = 1; + bufferDesc.SampleDesc.Count = 1; + bufferDesc.SampleDesc.Quality = 0; + // Create a staging texture + hr = d3d.device->CreateCommittedResource( + &readBackHeapProperties, + D3D12_HEAP_FLAG_NONE, + &bufferDesc, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&pStaging)); + + { + D3D12_RESOURCE_BARRIER barrierDesc = {}; + barrierDesc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrierDesc.Transition.pResource = pResource; + barrierDesc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrierDesc.Transition.StateBefore = state; + barrierDesc.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + commandList->ResourceBarrier(1, &barrierDesc); + } + + // Get the copy target location + D3D12_PLACED_SUBRESOURCE_FOOTPRINT bufferFootprint = {}; + bufferFootprint.Footprint.Width = static_cast(desc.Width); + bufferFootprint.Footprint.Height = desc.Height; + bufferFootprint.Footprint.Depth = 1; + bufferFootprint.Footprint.RowPitch = static_cast(dstRowPitch); + bufferFootprint.Footprint.Format = desc.Format; + + D3D12_TEXTURE_COPY_LOCATION copySrc = {}, copyDest = {}; + copySrc.pResource = pResource; + copySrc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + copySrc.SubresourceIndex = 0; + copyDest.pResource = pStaging; + copyDest.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + copyDest.PlacedFootprint = bufferFootprint; + + // Copy the texture + commandList->CopyTextureRegion(©Dest, 0, 0, 0, ©Src, nullptr); + + { + D3D12_RESOURCE_BARRIER barrierDesc = {}; + barrierDesc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrierDesc.Transition.pResource = pResource; + barrierDesc.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrierDesc.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barrierDesc.Transition.StateAfter = state; + commandList->ResourceBarrier(1, &barrierDesc); + } + + hr = commandList->Close(); + + // Execute the command list + d3d.cmdQueue->ExecuteCommandLists(1, reinterpret_cast(&commandList)); + // Signal the fence + hr = d3d.cmdQueue->Signal(fence, 1); + // Block until the copy is complete + while (fence->GetCompletedValue() < 1) + SwitchToThread(); + + UINT64 imageSize = dstRowPitch * fpRowCount; + unsigned char* pMappedMemory = nullptr; + D3D12_RANGE readRange = { 0, static_cast(imageSize) }; + D3D12_RANGE writeRange = { 0, 0 }; + hr = pStaging->Map(0, &readRange, (void**)&pMappedMemory); + + // convert to RGBA8 UNORM using WIC + std::vector converted(desc.Width * desc.Height * 4); + { + // Determine source format's WIC equivalent + WICPixelFormatGUID pfGuid; + bool sRGB = false; + switch (desc.Format) + { + case DXGI_FORMAT_R32G32B32A32_FLOAT: pfGuid = GUID_WICPixelFormat128bppRGBAFloat; break; + case DXGI_FORMAT_R16G16B16A16_FLOAT: pfGuid = GUID_WICPixelFormat64bppRGBAHalf; break; + case DXGI_FORMAT_R16G16B16A16_UNORM: pfGuid = GUID_WICPixelFormat64bppRGBA; break; + case DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM: pfGuid = GUID_WICPixelFormat32bppRGBA1010102XR; break; + case DXGI_FORMAT_R10G10B10A2_UNORM: pfGuid = GUID_WICPixelFormat32bppRGBA1010102; break; + case DXGI_FORMAT_B5G5R5A1_UNORM: pfGuid = GUID_WICPixelFormat16bppBGRA5551; break; + case DXGI_FORMAT_B5G6R5_UNORM: pfGuid = GUID_WICPixelFormat16bppBGR565; break; + case DXGI_FORMAT_R32_FLOAT: pfGuid = GUID_WICPixelFormat32bppGrayFloat; break; + case DXGI_FORMAT_R16_FLOAT: pfGuid = GUID_WICPixelFormat16bppGrayHalf; break; + case DXGI_FORMAT_R16_UNORM: pfGuid = GUID_WICPixelFormat16bppGray; break; + case DXGI_FORMAT_R8_UNORM: pfGuid = GUID_WICPixelFormat8bppGray; break; + case DXGI_FORMAT_A8_UNORM: pfGuid = GUID_WICPixelFormat8bppAlpha; break; + + case DXGI_FORMAT_R8G8B8A8_UNORM: + pfGuid = GUID_WICPixelFormat32bppRGBA; + break; + + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + pfGuid = GUID_WICPixelFormat32bppRGBA; + sRGB = true; + break; + + case DXGI_FORMAT_B8G8R8A8_UNORM: + pfGuid = GUID_WICPixelFormat32bppBGRA; + break; + + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + pfGuid = GUID_WICPixelFormat32bppBGRA; + sRGB = true; + break; + + case DXGI_FORMAT_B8G8R8X8_UNORM: + pfGuid = GUID_WICPixelFormat32bppBGR; + break; + + case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + pfGuid = GUID_WICPixelFormat32bppBGR; + sRGB = true; + break; + + default: + return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED); + } + + IWICImagingFactory2* pWIC = _GetWIC(); + IWICBitmap* bitmap = nullptr; + hr = pWIC->CreateBitmapFromMemory(static_cast(desc.Width), desc.Height, pfGuid, + static_cast(dstRowPitch), static_cast(imageSize), + static_cast(pMappedMemory), &bitmap); + + IWICFormatConverter* converter = nullptr; + hr = pWIC->CreateFormatConverter(&converter); + hr = converter->Initialize(bitmap, GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeMedianCut); + + WICRect rect = { 0, 0, static_cast(desc.Width), static_cast(desc.Height) }; + converter->CopyPixels(&rect, static_cast(desc.Width * 4), static_cast(converted.size()), converted.data()); + + converter->Release(); + bitmap->Release(); + } + + { + // libpng wants pointers to each row + std::vector rows(desc.Height, nullptr); + for (uint32_t i = 0; i < desc.Height; i++) + { + rows[i] = &converted[desc.Width * 4 * i]; + } + ImageCapture::CapturePng(file, static_cast(desc.Width), static_cast(desc.Height), rows); + } + pStaging->Unmap(0, &writeRange); + + pStaging->Release(); + fence->Release(); + commandList->Release(); + commandAlloc->Release(); + return true; } @@ -2324,6 +2545,15 @@ namespace Graphics Cleanup(d3d); } + /** + * Write the back buffer texture resources to disk. + */ + bool WriteBackBufferToDisk(Globals& d3d, std::string directory) + { + CoInitialize(NULL); + bool success = WriteResourceToDisk(d3d, directory + "\\backbuffer.png", d3d.backBuffer[d3d.frameIndex], D3D12_RESOURCE_STATE_PRESENT); + return success; + } } /** @@ -2435,4 +2665,12 @@ namespace Graphics { Graphics::D3D12::Cleanup(gfx, gfxResources); } + + /** + * Write the back buffer texture resources to disk. + */ + bool WriteBackBufferToDisk(Globals& d3d, std::string directory) + { + return Graphics::D3D12::WriteBackBufferToDisk(d3d, directory); + } } diff --git a/samples/test-harness/src/ImageCapture.cpp b/samples/test-harness/src/ImageCapture.cpp new file mode 100644 index 0000000..4dee7fc --- /dev/null +++ b/samples/test-harness/src/ImageCapture.cpp @@ -0,0 +1,68 @@ +/* +* Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved. +* +* NVIDIA CORPORATION and its licensors retain all intellectual property +* and proprietary rights in and to this software, related documentation +* and any modifications thereto. Any use, reproduction, disclosure or +* distribution of this software and related documentation without an express +* license agreement from NVIDIA CORPORATION is strictly prohibited. +*/ + +#include "ImageCapture.h" +#include +#include + +namespace ImageCapture +{ + bool CapturePng(std::string file, uint32_t width, uint32_t height, std::vector& rows) + { + FILE* fp = nullptr; + errno_t ferror = fopen_s(&fp, file.c_str(), "wb"); + if (ferror != 0) + { + return false; + } + + png_structp pngWrite = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!pngWrite) + { + return false; + } + png_infop pngInfo = png_create_info_struct(pngWrite); + if (!pngInfo) + { + png_destroy_write_struct(&pngWrite, (png_infopp)nullptr); + return false; + } + + if (setjmp(png_jmpbuf(pngWrite))) + { + png_destroy_write_struct(&pngWrite, &pngInfo); + fclose(fp); + return false; + } + + png_init_io(pngWrite, fp); + png_set_IHDR( + pngWrite, + pngInfo, + width, + height, + 8, + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT + ); + png_write_info(pngWrite, pngInfo); + // stripping alpha channel for captured images + // too bad if there's useful data there... + png_set_filler(pngWrite, 0, PNG_FILLER_AFTER); + png_write_image(pngWrite, rows.data()); + png_write_end(pngWrite, NULL); + fclose(fp); + png_destroy_write_struct(&pngWrite, &pngInfo); + + return true; + } +} \ No newline at end of file diff --git a/samples/test-harness/src/Inputs.cpp b/samples/test-harness/src/Inputs.cpp index 14c6f81..bcc22b3 100644 --- a/samples/test-harness/src/Inputs.cpp +++ b/samples/test-harness/src/Inputs.cpp @@ -64,8 +64,16 @@ void KeyHandler(GLFWwindow* window, int key, int scancode, int action, int mods) // Save debug images if(IsKeyReleased(key, action, GLFW_KEY_F1)) { - inputPtr->saveImages = true; inputPtr->event = Inputs::EInputEvent::SAVE_IMAGE; + return; + } + + // Run benchmark + if (IsKeyReleased(key, action, GLFW_KEY_F2)) + { + inputPtr->event = Inputs::EInputEvent::RUN_BENCHMARK; + inputPtr->runBenchmark = true; + return; } // Toggle pan inversion diff --git a/samples/test-harness/src/Instrumentation.cpp b/samples/test-harness/src/Instrumentation.cpp index 3116621..51acd50 100644 --- a/samples/test-harness/src/Instrumentation.cpp +++ b/samples/test-harness/src/Instrumentation.cpp @@ -96,4 +96,20 @@ namespace Instrumentation Resolve(s); } + std::ostream& operator<<(std::ostream& os, const Stat& stat) + { + os << stat.elapsed; + return os; + } + + std::ostream& operator<<(std::ostream& os, std::vector& stats) + { + for (std::vector::const_iterator& it = stats.cbegin(); it != stats.cend(); it++) + { + os << **it << ","; + } + os << std::endl; + return os; + } + } diff --git a/samples/test-harness/src/Vulkan.cpp b/samples/test-harness/src/Vulkan.cpp index 9d0cb82..f9ec73e 100644 --- a/samples/test-harness/src/Vulkan.cpp +++ b/samples/test-harness/src/Vulkan.cpp @@ -11,6 +11,7 @@ #include "Graphics.h" #include "VulkanExtensions.h" #include "UI.h" +#include "ImageCapture.h" namespace Graphics { @@ -1364,7 +1365,7 @@ namespace Graphics bool CreateRenderTargets(Globals& vk, Resources& resources) { // Create the GBufferA (R8G8B8A8_UNORM) texture resource - TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT }; + TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT }; if(!CreateTexture(vk, desc, &resources.rt.GBufferA, &resources.rt.GBufferAMemory, &resources.rt.GBufferAView)) return false; #ifdef GFX_NAME_OBJECTS SetObjectName(vk.device, reinterpret_cast(resources.rt.GBufferA), "GBufferA", VK_OBJECT_TYPE_IMAGE); @@ -1994,10 +1995,223 @@ namespace Graphics /** * Write an image to disk from the given Vulkan resource. */ - bool WriteResourceToDisk(Globals& d3d, std::string file) + bool WriteResourceToDisk(Globals& vk, std::string file, VkImage image, uint32_t width, uint32_t height, VkFormat imageFormat, VkImageLayout originalLayout) { - // TODO - return false; + VkCommandPool pool; + VkCommandBuffer cmd; + + // create custom command pool + { + VkCommandPoolCreateInfo commandPoolCreateInfo = {}; + commandPoolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + commandPoolCreateInfo.queueFamilyIndex = vk.queueFamilyIndex; + commandPoolCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + VKCHECK(vkCreateCommandPool(vk.device, &commandPoolCreateInfo, nullptr, &pool)); +#ifdef GFX_NAME_OBJECTS + SetObjectName(vk.device, reinterpret_cast(pool), "Image capture Command Pool", VK_OBJECT_TYPE_COMMAND_POOL); +#endif + } + + // create custom command buffer + { + uint32_t numCommandBuffers = 1; + + VkCommandBufferAllocateInfo commandBufferAllocateInfo = {}; + commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + commandBufferAllocateInfo.commandBufferCount = numCommandBuffers; + commandBufferAllocateInfo.commandPool = pool; + commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + + VKCHECK(vkAllocateCommandBuffers(vk.device, &commandBufferAllocateInfo, &cmd)); + +#ifdef GFX_NAME_OBJECTS + SetObjectName(vk.device, reinterpret_cast(cmd), "Image capture Command Buffer", VK_OBJECT_TYPE_COMMAND_BUFFER); +#endif + VkCommandBufferBeginInfo commandBufferBeginInfo = {}; + commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + VKCHECK(vkBeginCommandBuffer(cmd, &commandBufferBeginInfo)); + } + + // using vr_sli_vk demo for reference + VkImage linearScreenshotImage, optimalScreenshotImage; + VkDeviceMemory linearScreenshotImageMemory, optimalScreenshotImageMemory; + { + // same as CreateTexture() but don't create a view + VkImageCreateInfo imageCreateInfo = {}; + imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageCreateInfo.imageType = VK_IMAGE_TYPE_2D; + imageCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageCreateInfo.extent.width = width; + imageCreateInfo.extent.height = height; + imageCreateInfo.extent.depth = 1; + imageCreateInfo.mipLevels = 1; + imageCreateInfo.arrayLayers = 1; + imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageCreateInfo.tiling = VK_IMAGE_TILING_LINEAR; + imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VKCHECK(vkCreateImage(vk.device, &imageCreateInfo, nullptr, &linearScreenshotImage)); + imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VKCHECK(vkCreateImage(vk.device, &imageCreateInfo, nullptr, &optimalScreenshotImage)); + + AllocateMemoryDesc desc = {}; + vkGetImageMemoryRequirements(vk.device, linearScreenshotImage, &desc.requirements); + desc.properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + desc.flags = 0; + + if (!AllocateMemory(vk, desc, &linearScreenshotImageMemory)) return false; + VKCHECK(vkBindImageMemory(vk.device, linearScreenshotImage, linearScreenshotImageMemory, 0)); + + vkGetImageMemoryRequirements(vk.device, optimalScreenshotImage, &desc.requirements); + desc.properties = 0; + desc.flags = 0; + if (!AllocateMemory(vk, desc, &optimalScreenshotImageMemory)) return false; + VKCHECK(vkBindImageMemory(vk.device, optimalScreenshotImage, optimalScreenshotImageMemory, 0)); + } + + { + ImageBarrierDesc barrier = + { + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 } + }; + SetImageMemoryBarrier(cmd, linearScreenshotImage, barrier); + SetImageMemoryBarrier(cmd, optimalScreenshotImage, barrier); + barrier.oldLayout = originalLayout; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + SetImageMemoryBarrier(cmd, image, barrier); + } + + { + // blit image (format conversion if necessary) + VkImageBlit region = {}; + VkImageSubresourceLayers subres = {}; + subres.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + subres.mipLevel = 0; + subres.baseArrayLayer = 0; + subres.layerCount = 1; + region.srcSubresource = subres; + region.dstSubresource = subres; + region.srcOffsets[0] = {}; + region.srcOffsets[1] = { (int32_t) width, (int32_t) height, 1 }; + region.dstOffsets[0] = {}; + region.dstOffsets[1] = { (int32_t) width, (int32_t) height, 1 }; + vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, optimalScreenshotImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_NEAREST); + } + + { + ImageBarrierDesc barrier = + { + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 } + }; + SetImageMemoryBarrier(cmd, optimalScreenshotImage, barrier); + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.newLayout = originalLayout; + SetImageMemoryBarrier(cmd, image, barrier); + } + + { + // copy to linear tiling image for CPU copy + VkImageSubresourceLayers subResource = {}; + subResource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + subResource.baseArrayLayer = 0; + subResource.mipLevel = 0; + subResource.layerCount = 1; + VkImageCopy region = {}; + region.srcSubresource = subResource; + region.dstSubresource = subResource; + region.srcOffset = { 0, 0, 0 }; + region.dstOffset = { 0, 0, 0 }; + region.extent.width = width; + region.extent.height = height; + region.extent.depth = 1; + vkCmdCopyImage( + cmd, + optimalScreenshotImage, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + linearScreenshotImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, ®ion + ); + } + + { + ImageBarrierDesc barrier = + { + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 } + }; + SetImageMemoryBarrier(cmd, linearScreenshotImage, barrier); + } + + { + // Execute GPU work to finish initialization + VKCHECK(vkEndCommandBuffer(cmd)); + + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmd; + + VKCHECK(vkQueueSubmit(vk.queue, 1, &submitInfo, VK_NULL_HANDLE)); + VKCHECK(vkQueueWaitIdle(vk.queue)); + + WaitForGPU(vk); + } + + // copy screenshot image to cpu-side memory + { + unsigned char* rawData = nullptr; + + VkImageSubresource subResource{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 }; + VkSubresourceLayout subResourceLayout; + vkGetImageSubresourceLayout(vk.device, linearScreenshotImage, &subResource, &subResourceLayout); + + VkResult result = vkMapMemory(vk.device, linearScreenshotImageMemory, 0, VK_WHOLE_SIZE, 0, (void**)&rawData); + if (result != VK_SUCCESS) + { + return false; + } + + // libpng wants pointers to each row + std::vector rows(height, nullptr); + for (uint32_t i = 0; i < height; i++) + { + rows[i] = rawData + i * subResourceLayout.rowPitch; + } + + // output the image file to disk + ImageCapture::CapturePng(file, width, height, rows); + + vkUnmapMemory(vk.device, linearScreenshotImageMemory); + } + + { + // tear-down screenshot images + vkFreeMemory(vk.device, linearScreenshotImageMemory, nullptr); + vkDestroyImage(vk.device, linearScreenshotImage, nullptr); + vkFreeMemory(vk.device, optimalScreenshotImageMemory, nullptr); + vkDestroyImage(vk.device, optimalScreenshotImage, nullptr); + // tear-down temporary command list and pool + vkFreeCommandBuffers(vk.device, pool, 1, &cmd); + vkDestroyCommandPool(vk.device, pool, nullptr); + } + + return true; } #ifdef GFX_NAME_OBJECTS @@ -3033,15 +3247,6 @@ namespace Graphics } #endif - /** - * Write the GBuffer texture resources to disk. - */ - bool WriteGBufferToDisk(Globals& vk, Resources& resources, std::string directory) - { - // TODO: implement - return true; - } - /** * Release Vulkan resources. */ @@ -3051,6 +3256,16 @@ namespace Graphics Cleanup(vk); } + /** + * Write the back buffer texture resources to disk. + */ + bool WriteBackBufferToDisk(Globals& vk, std::string directory) + { + CoInitialize(NULL); + bool success = WriteResourceToDisk(vk, directory + "\\backbuffer.png", vk.swapChainImage[vk.frameIndex], vk.width, vk.height, vk.swapChainFormat, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + return success; + } + } /** @@ -3156,19 +3371,19 @@ namespace Graphics #endif /** - * Write GBuffer resources to disk. + * Cleanup global graphics resources. */ - bool WriteGBufferToDisk(Globals& gfx, GlobalResources& gfxResources, std::string directory) + void Cleanup(Globals& gfx, GlobalResources& gfxResources) { - return Graphics::Vulkan::WriteGBufferToDisk(gfx, gfxResources, directory); + Graphics::Vulkan::Cleanup(gfx, gfxResources); } /** - * Cleanup global graphics resources. + * Write the back buffer texture resources to disk. */ - void Cleanup(Globals& gfx, GlobalResources& gfxResources) + bool WriteBackBufferToDisk(Globals& vk, std::string directory) { - Graphics::Vulkan::Cleanup(gfx, gfxResources); + return Graphics::Vulkan::WriteBackBufferToDisk(vk, directory); } } diff --git a/samples/test-harness/src/graphics/DDGI_D3D12.cpp b/samples/test-harness/src/graphics/DDGI_D3D12.cpp index 32d1557..299d818 100644 --- a/samples/test-harness/src/graphics/DDGI_D3D12.cpp +++ b/samples/test-harness/src/graphics/DDGI_D3D12.cpp @@ -372,6 +372,7 @@ namespace Graphics { volumeDesc.name = config.name; volumeDesc.index = config.index; + volumeDesc.rngSeed = config.rngSeed; volumeDesc.origin = { config.origin.x, config.origin.y, config.origin.z }; volumeDesc.eulerAngles = { config.eulerAngles.x, config.eulerAngles.y, config.eulerAngles.z, }; volumeDesc.probeSpacing = { config.probeSpacing.x, config.probeSpacing.y, config.probeSpacing.z }; @@ -940,8 +941,8 @@ namespace Graphics // Validate the SDK version assert(RTXGI_VERSION::major == 1); assert(RTXGI_VERSION::minor == 2); - assert(RTXGI_VERSION::revision == 4); - assert(std::strcmp(RTXGI_VERSION::getVersionString(), "1.2.04") == 0); + assert(RTXGI_VERSION::revision == 7); + assert(std::strcmp(RTXGI_VERSION::getVersionString(), "1.2.07") == 0); UINT numVolumes = static_cast(config.ddgi.volumes.size()); @@ -1181,6 +1182,31 @@ namespace Graphics } } + /** + * Write the DDGI Volume texture resources to disk. + */ + bool WriteVolumesToDisk(Globals& globals, GlobalResources& gfxResources, Resources& resources, std::string directory) + { + CoInitialize(NULL); + bool success = true; + for (rtxgi::DDGIVolumeBase* volumeBase : resources.volumes) + { + std::string baseName = directory + "\\" + volumeBase->GetName(); + std::string filename = baseName + "-irradiance.png"; + + rtxgi::d3d12::DDGIVolume* volume = static_cast(volumeBase); + success &= WriteResourceToDisk(globals, filename, volume->GetProbeIrradiance(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + // not capturing distances because WIC doesn't like two-channel textures + //filename = baseName + "-distance.png"; + //success &= WriteResourceToDisk(globals, filename, volume->GetProbeDistance(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + filename = baseName + "-data.png"; + success &= WriteResourceToDisk(globals, filename, volume->GetProbeData(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + return success; + } + } // namespace Graphics::D3D12::RTAO } // namespace Graphics::D3D12 @@ -1218,5 +1244,10 @@ namespace Graphics Graphics::D3D12::DDGI::Cleanup(resources); } + bool WriteVolumesToDisk(Globals& globals, GlobalResources& gfxResources, Resources& resources, std::string directory) + { + return Graphics::D3D12::DDGI::WriteVolumesToDisk(globals, gfxResources, resources, directory); + } + } // namespace Graphics::DDGI } diff --git a/samples/test-harness/src/graphics/DDGI_VK.cpp b/samples/test-harness/src/graphics/DDGI_VK.cpp index 0277923..794f93c 100644 --- a/samples/test-harness/src/graphics/DDGI_VK.cpp +++ b/samples/test-harness/src/graphics/DDGI_VK.cpp @@ -189,7 +189,7 @@ namespace Graphics GetDDGIVolumeTextureDimensions(volumeDesc, EDDGIVolumeTextureType::Irradiance, width, height); format = GetDDGIVolumeTextureFormat(EDDGIVolumeTextureType::Irradiance, volumeDesc.probeIrradianceFormat); - TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; + TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; CHECK(CreateTexture(vk, desc, &volumeResources.unmanaged.probeIrradiance, &volumeResources.unmanaged.probeIrradianceMemory, &volumeResources.unmanaged.probeIrradianceView), "create DDGIVolume irradiance texture!", log); #ifdef GFX_NAME_OBJECTS std::string n = "DDGIVolume[" + std::to_string(volumeDesc.index) + "], Probe Irradiance"; @@ -205,7 +205,7 @@ namespace Graphics GetDDGIVolumeTextureDimensions(volumeDesc, EDDGIVolumeTextureType::Distance, width, height); format = GetDDGIVolumeTextureFormat(EDDGIVolumeTextureType::Distance, volumeDesc.probeDistanceFormat); - TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; + TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT }; CHECK(CreateTexture(vk, desc, &volumeResources.unmanaged.probeDistance, &volumeResources.unmanaged.probeDistanceMemory, &volumeResources.unmanaged.probeDistanceView), "create DDGIVolume distance texture!", log); #ifdef GFX_NAME_OBJECTS std::string n = "DDGIVolume[" + std::to_string(volumeDesc.index) + "], Probe Distance"; @@ -222,7 +222,7 @@ namespace Graphics if (width <= 0) return false; format = GetDDGIVolumeTextureFormat(EDDGIVolumeTextureType::Data, volumeDesc.probeDataFormat); - TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT }; + TextureDesc desc = { width, height, 1, format, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT }; CHECK(CreateTexture(vk, desc, &volumeResources.unmanaged.probeData, &volumeResources.unmanaged.probeDataMemory, &volumeResources.unmanaged.probeDataView), "", log); #ifdef GFX_NAME_OBJECTS std::string n = "DDGIVolume[" + std::to_string(volumeDesc.index) + "], Probe Data"; @@ -579,6 +579,7 @@ namespace Graphics { volumeDesc.name = config.name; volumeDesc.index = config.index; + volumeDesc.rngSeed = config.rngSeed; volumeDesc.origin = { config.origin.x, config.origin.y, config.origin.z }; volumeDesc.eulerAngles = { config.eulerAngles.x, config.eulerAngles.y, config.eulerAngles.z, }; volumeDesc.probeSpacing = { config.probeSpacing.x, config.probeSpacing.y, config.probeSpacing.z }; @@ -795,7 +796,7 @@ namespace Graphics vkFreeMemory(vk.device, resources.outputMemory, nullptr); // Create the output (R16G16B16A16_FLOAT) texture resource - TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT }; + TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT }; CHECK(CreateTexture(vk, desc, &resources.output, &resources.outputMemory, &resources.outputView), "create DDGI output texture resource!\n", log); #ifdef GFX_NAME_OBJECTS SetObjectName(vk.device, reinterpret_cast(resources.output), "DDGI Output", VK_OBJECT_TYPE_IMAGE); @@ -1367,8 +1368,8 @@ namespace Graphics // Validate the SDK version assert(RTXGI_VERSION::major == 1); assert(RTXGI_VERSION::minor == 2); - assert(RTXGI_VERSION::revision == 4); - assert(std::strcmp(RTXGI_VERSION::getVersionString(), "1.2.04") == 0); + assert(RTXGI_VERSION::revision == 7); + assert(std::strcmp(RTXGI_VERSION::getVersionString(), "1.2.07") == 0); // Reset the command list before initialization CHECK(ResetCmdList(vk), "reset command list!", log); @@ -1650,6 +1651,37 @@ namespace Graphics } } + /** + * Write the DDGI Volume texture resources to disk. + */ + bool WriteVolumesToDisk(Globals& vk, GlobalResources& vkResources, Resources& resources, std::string directory) + { + CoInitialize(NULL); + bool success = true; + for (rtxgi::DDGIVolumeBase* volumeBase : resources.volumes) + { + std::string baseName = directory + "\\" + volumeBase->GetName(); + std::string filename = baseName + "-irradiance.png"; + + rtxgi::vulkan::DDGIVolume* volume = static_cast(volumeBase); + rtxgi::DDGIVolumeDesc desc = volumeBase->GetDesc(); + uint32_t width = 0, height = 0; + GetDDGIVolumeTextureDimensions(desc, EDDGIVolumeTextureType::Irradiance, width, height); + VkFormat format = GetDDGIVolumeTextureFormat(EDDGIVolumeTextureType::Irradiance, desc.probeIrradianceFormat); + success &= WriteResourceToDisk(vk, filename, volume->GetProbeIrradiance(), width, height, format, VK_IMAGE_LAYOUT_GENERAL); + + // not capturing distances because WIC doesn't like two-channel textures + //filename = baseName + "-distance.png"; + //success &= WriteResourceToDisk(globals, filename, volume->GetProbeDistance(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + filename = baseName + "-data.png"; + GetDDGIVolumeTextureDimensions(desc, EDDGIVolumeTextureType::Data, width, height); + format = GetDDGIVolumeTextureFormat(EDDGIVolumeTextureType::RayData, desc.probeDataFormat); + success &= WriteResourceToDisk(vk, filename, volume->GetProbeData(), width, height, format,VK_IMAGE_LAYOUT_GENERAL); + } + return success; + } + } // namespace Graphics::Vulkan::DDGI } // namespace Graphics::Vulkan @@ -1687,5 +1719,10 @@ namespace Graphics Graphics::Vulkan::DDGI::Cleanup(vk.device, resources); } + bool WriteVolumesToDisk(Globals& vk, GlobalResources& vkResources, Resources& resources, std::string directory) + { + return Graphics::Vulkan::DDGI::WriteVolumesToDisk(vk, vkResources, resources, directory); + } + } // namespace Graphics::DDGI } diff --git a/samples/test-harness/src/graphics/GBuffer_D3D12.cpp b/samples/test-harness/src/graphics/GBuffer_D3D12.cpp index a59653a..3ccbfe9 100644 --- a/samples/test-harness/src/graphics/GBuffer_D3D12.cpp +++ b/samples/test-harness/src/graphics/GBuffer_D3D12.cpp @@ -304,19 +304,6 @@ namespace Graphics #endif } - /** - * Write the GBuffer texture resources to disk. - */ - bool WriteGBufferToDisk(Globals& d3d, GlobalResources& d3dResources, std::string directory) - { - CoInitialize(NULL); - bool success = WriteResourceToDisk(d3d, directory + "GBufferA.png", d3dResources.rt.GBufferA, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - success &= WriteResourceToDisk(d3d, directory + "GBufferB.png", d3dResources.rt.GBufferB, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - success &= WriteResourceToDisk(d3d, directory + "GBufferC.png", d3dResources.rt.GBufferC, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - success &= WriteResourceToDisk(d3d, directory + "GBufferD.png", d3dResources.rt.GBufferD, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - return success; - } - /** * Release resources. */ @@ -332,6 +319,19 @@ namespace Graphics SAFE_RELEASE(resources.rtpso); } + /** + * Write the GBuffer texture resources to disk. + */ + bool WriteGBufferToDisk(Globals& d3d, GlobalResources& d3dResources, std::string directory) + { + CoInitialize(NULL); + bool success = WriteResourceToDisk(d3d, directory + "\\GBufferA.png", d3dResources.rt.GBufferA, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + success &= WriteResourceToDisk(d3d, directory + "\\GBufferB.png", d3dResources.rt.GBufferB, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + success &= WriteResourceToDisk(d3d, directory + "\\GBufferC.png", d3dResources.rt.GBufferC, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + success &= WriteResourceToDisk(d3d, directory + "\\GBufferD.png", d3dResources.rt.GBufferD, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + return success; + } + } // namespace Graphics::D3D12::GBuffer } // namespace Graphics::D3D12 diff --git a/samples/test-harness/src/graphics/GBuffer_VK.cpp b/samples/test-harness/src/graphics/GBuffer_VK.cpp index fb70524..9a302a2 100644 --- a/samples/test-harness/src/graphics/GBuffer_VK.cpp +++ b/samples/test-harness/src/graphics/GBuffer_VK.cpp @@ -536,15 +536,6 @@ namespace Graphics CPU_TIMESTAMP_ENDANDRESOLVE(resources.cpuStat); } - /** - * Write the GBuffer texture resources to disk. - */ - bool WriteGBufferToDisk(Globals& d3d, GlobalResources& vkResources, std::string directory) - { - // TODO: implement - return true; - } - /** * Release resources. */ @@ -568,6 +559,20 @@ namespace Graphics resources.shaderTableHitGroupTableSize = 0; } + /** + * Write the GBuffer texture resources to disk. + */ + bool WriteGBufferToDisk(Globals& vk, GlobalResources& vkResources, std::string directory) + { + CoInitialize(NULL); + // formats should match those from Graphics::Vulkan::CreateRenderTargets() in Vulkan.cpp + bool success = WriteResourceToDisk(vk, directory + "\\GBufferA.png", vkResources.rt.GBufferA, vk.width, vk.height, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_GENERAL); + success &= WriteResourceToDisk(vk, directory + "\\GBufferB.png", vkResources.rt.GBufferB, vk.width, vk.height, VK_FORMAT_R32G32B32A32_SFLOAT, VK_IMAGE_LAYOUT_GENERAL); + success &= WriteResourceToDisk(vk, directory + "\\GBufferC.png", vkResources.rt.GBufferC, vk.width, vk.height, VK_FORMAT_R32G32B32A32_SFLOAT, VK_IMAGE_LAYOUT_GENERAL); + success &= WriteResourceToDisk(vk, directory + "\\GBufferD.png", vkResources.rt.GBufferD, vk.width, vk.height, VK_FORMAT_R32G32B32A32_SFLOAT, VK_IMAGE_LAYOUT_GENERAL); + return success; + } + } // namespace Graphics::Vulkan::GBuffer } // namespace Graphics::Vulkan @@ -600,14 +605,14 @@ namespace Graphics return Graphics::Vulkan::GBuffer::Execute(vk, vkResources, resources); } - bool WriteGBufferToDisk(Globals& vk, GlobalResources& vkResources, std::string directory) + void Cleanup(Globals& vk, Resources& resources) { - return Graphics::Vulkan::GBuffer::WriteGBufferToDisk(vk, vkResources, directory); + Graphics::Vulkan::GBuffer::Cleanup(vk.device, resources); } - void Cleanup(Globals& vk, Resources& resources) + bool WriteGBufferToDisk(Globals& vk, GlobalResources& vkResources, std::string directory) { - Graphics::Vulkan::GBuffer::Cleanup(vk.device, resources); + return Graphics::Vulkan::GBuffer::WriteGBufferToDisk(vk, vkResources, directory); } } // namespace Graphics::GBuffer diff --git a/samples/test-harness/src/graphics/RTAO_D3D12.cpp b/samples/test-harness/src/graphics/RTAO_D3D12.cpp index 2cd0b9a..20a8099 100644 --- a/samples/test-harness/src/graphics/RTAO_D3D12.cpp +++ b/samples/test-harness/src/graphics/RTAO_D3D12.cpp @@ -434,6 +434,17 @@ namespace Graphics resources.shaderTableHitGroupTableStartAddress = 0; } + /** + * Write the RTAO texture resources to disk. + */ + bool WriteRTAOBuffersToDisk(Globals& d3d, GlobalResources& d3dResources, Resources& resources, std::string directory) + { + CoInitialize(NULL); + bool success = WriteResourceToDisk(d3d, directory + "\\rtaoraw.png", resources.RTAORaw, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + success &= WriteResourceToDisk(d3d, directory + "\\rtaofiltered.png", resources.RTAOOutput, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + return success; + } + } // namespace Graphics::D3D12::RTAO } // namespace Graphics::D3D12 @@ -471,5 +482,10 @@ namespace Graphics Graphics::D3D12::RTAO::Cleanup(resources); } + bool WriteRTAOBuffersToDisk(Globals& d3d, GlobalResources& d3dResources, Resources& resources, std::string directory) + { + return Graphics::D3D12::RTAO::WriteRTAOBuffersToDisk(d3d, d3dResources, resources, directory); + } + } // namespace Graphics::RTAO } diff --git a/samples/test-harness/src/graphics/RTAO_VK.cpp b/samples/test-harness/src/graphics/RTAO_VK.cpp index d086818..a449a82 100644 --- a/samples/test-harness/src/graphics/RTAO_VK.cpp +++ b/samples/test-harness/src/graphics/RTAO_VK.cpp @@ -25,7 +25,7 @@ namespace Graphics bool CreateTextures(Globals& vk, GlobalResources& vkResources, Resources& resources, std::ofstream& log) { // Create the output (R8G8B8A8_UNORM) texture resource - TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_R8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT }; + TextureDesc desc = { static_cast(vk.width), static_cast(vk.height), 1, VK_FORMAT_R8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT }; CHECK(CreateTexture(vk, desc, &resources.RTAOOutput, &resources.RTAOOutputMemory, &resources.RTAOOutputView), "create RTAO output texture resource!\n", log); #ifdef GFX_NAME_OBJECTS SetObjectName(vk.device, reinterpret_cast(resources.RTAOOutput), "RTAO Output", VK_OBJECT_TYPE_IMAGE); @@ -651,6 +651,18 @@ namespace Graphics resources.shaderTableHitGroupTableStartAddress = 0; } + /** + * Write the RTAO texture resources to disk. + */ + bool WriteRTAOBuffersToDisk(Globals& vk, GlobalResources& vkResources, Resources& resources, std::string directory) + { + CoInitialize(NULL); + // format should match those from CreateTextures() function above + bool success = WriteResourceToDisk(vk, directory + "\\rtaoraw.png", resources.RTAORaw, vk.width, vk.height, VK_FORMAT_R8_UNORM, VK_IMAGE_LAYOUT_GENERAL); + success &= WriteResourceToDisk(vk, directory + "\\rtaofiltered.png", resources.RTAOOutput, vk.width, vk.height, VK_FORMAT_R8_UNORM, VK_IMAGE_LAYOUT_GENERAL); + return success; + } + } // namespace Graphics::Vulkan::RTAO } // namespace Graphics::Vulkan @@ -688,5 +700,10 @@ namespace Graphics Graphics::Vulkan::RTAO::Cleanup(vk.device, resources); } + bool WriteRTAOBuffersToDisk(Globals& vk, GlobalResources& vkResources, Resources& resources, std::string directory) + { + return Graphics::Vulkan::RTAO::WriteRTAOBuffersToDisk(vk, vkResources, resources, directory); + } + } // namespace Graphics::RTAO } diff --git a/samples/test-harness/src/main.cpp b/samples/test-harness/src/main.cpp index 1ead7ce..9e37068 100644 --- a/samples/test-harness/src/main.cpp +++ b/samples/test-harness/src/main.cpp @@ -16,6 +16,7 @@ #include "Graphics.h" #include "UI.h" #include "Window.h" +#include "Benchmark.h" #include "graphics/PathTracing.h" #include "graphics/GBuffer.h" @@ -24,6 +25,8 @@ #include "graphics/RTAO.h" #include "graphics/Composite.h" +#include + /** * Run the Test Harness. */ @@ -57,6 +60,7 @@ int Run(const std::vector& arguments) perf.AddGPUStat("Frame"); perf.AddCPUStat("Input"); perf.AddCPUStat("Update"); + Benchmark::BenchmarkRun benchmarkRun; CPU_TIMESTAMP_BEGIN(&startupShutdown); @@ -200,6 +204,12 @@ int Run(const std::vector& arguments) continue; } + if (input.event == Inputs::EInputEvent::RUN_BENCHMARK) + { + Benchmark::StartBenchmark(benchmarkRun, perf, config, gfx); + input.event = Inputs::EInputEvent::NONE; + } + // Reload shaders and PSOs for graphics workloads { if (config.pathTrace.reload) @@ -325,17 +335,32 @@ int Run(const std::vector& arguments) if (!Graphics::SubmitCmdList(gfx)) break; if (!Graphics::Present(gfx)) continue; if (!Graphics::WaitForGPU(gfx)) break; + + bool saveImages = (input.event == Inputs::EInputEvent::SAVE_IMAGE) || (input.runBenchmark && gfx.frameNumber >= Benchmark::NumBenchmarkFrames); + + if (saveImages) + { + CreateDirectory(config.scene.screenshotPath.c_str(), NULL); + Graphics::WriteBackBufferToDisk(gfx, config.scene.screenshotPath); + Graphics::GBuffer::WriteGBufferToDisk(gfx, gfxResources, config.scene.screenshotPath); + Graphics::RTAO::WriteRTAOBuffersToDisk(gfx, gfxResources, rtao, config.scene.screenshotPath); + Graphics::DDGI::WriteVolumesToDisk(gfx, gfxResources, ddgi, config.scene.screenshotPath); + input.event = Inputs::EInputEvent::NONE; + } + if (!Graphics::MoveToNextFrame(gfx)) break; if (!Graphics::ResetCmdList(gfx)) break; CPU_TIMESTAMP_ENDANDRESOLVE(perf.cpuTimes.back()); #ifdef GFX_PERF_INSTRUMENTATION if (!Graphics::UpdateTimestamps(gfx, gfxResources, perf)) break; + if (input.runBenchmark) + { + input.runBenchmark = Benchmark::UpdateBenchmark(benchmarkRun, perf, config, gfx, log); + } Graphics::BeginFrame(gfx, gfxResources, perf); #endif - // TODO: add GBuffer image dump code for debugging - CPU_TIMESTAMP_ENDANDRESOLVE(perf.cpuTimes[0]); } diff --git a/thirdparty/libpng b/thirdparty/libpng new file mode 160000 index 0000000..a37d483 --- /dev/null +++ b/thirdparty/libpng @@ -0,0 +1 @@ +Subproject commit a37d4836519517bdce6cb9d956092321eca3e73b diff --git a/thirdparty/zlib b/thirdparty/zlib new file mode 160000 index 0000000..cacf7f1 --- /dev/null +++ b/thirdparty/zlib @@ -0,0 +1 @@ +Subproject commit cacf7f1d4e3d44d871b605da3b647f07d718623f