diff --git a/engine/assets/builtin/shaders/volumetric.shader b/engine/assets/builtin/shaders/volumetric.shader index a8fcc61a..479725a6 100644 --- a/engine/assets/builtin/shaders/volumetric.shader +++ b/engine/assets/builtin/shaders/volumetric.shader @@ -9,6 +9,9 @@ Shader "Builtin Volumetric" _AmbientStrength ("Ambient Strength", Float) = 0.005 _LightDirection ("Light Direction", Vector) = (0.5, 0.8, 0.3, 0.0) _LightSamples ("Light Samples", Float) = 8.0 + _EnableEntryHdda ("Enable Entry HDDA", Float) = 1.0 + _EnableEmptySpaceSkipping ("Enable Empty Space Skipping", Float) = 1.0 + _EnableEarlyTermination ("Enable Early Termination", Float) = 1.0 } HLSLINCLUDE #define PNANOVDB_HLSL @@ -41,6 +44,9 @@ Shader "Builtin Volumetric" float4 gAmbientStrength; float4 gLightDirection; float4 gLightSamples; + float4 gEnableEntryHdda; + float4 gEnableEmptySpaceSkipping; + float4 gEnableEarlyTermination; }; StructuredBuffer VolumeData; @@ -192,6 +198,9 @@ Shader "Builtin Volumetric" const int maxSteps = max((int)gMaxSteps.x, 1); const float ambientStrength = max(gAmbientStrength.x, 0.0f); const float lightSamples = max(gLightSamples.x, 0.0f); + const bool enableEntryHdda = gEnableEntryHdda.x >= 0.5f; + const bool enableEmptySpaceSkipping = gEnableEmptySpaceSkipping.x >= 0.5f; + const bool enableEarlyTermination = gEnableEarlyTermination.x >= 0.5f; const float3 cameraLocalPosition = mul(gInverseModelMatrix, float4(gCameraWorldPosition.xyz, 1.0f)).xyz; @@ -206,7 +215,8 @@ Shader "Builtin Volumetric" const bool cameraInside = IsPointInsideAabb(cameraLocalPosition, boxMin, boxMax); const float surfaceT = cameraInside ? tExit : max(tEnter, 0.0f); const float3 expectedSurfacePosition = cameraLocalPosition + rayDirection * surfaceT; - if (distance(expectedSurfacePosition, input.localPosition) > stepSize * 1.5f + 0.01f) { + const float surfaceTolerance = max(stepSize * 1.5f, 1.0f); + if (distance(expectedSurfacePosition, input.localPosition) > surfaceTolerance + 0.01f) { discard; } @@ -216,14 +226,14 @@ Shader "Builtin Volumetric" NanoVolume volume; InitNanoVolume(volume); - if (!GetHddaHit(volume.accessor, t, cameraLocalPosition, rayDirection, marchEnd, hitValue)) { + if (enableEntryHdda && + !GetHddaHit(volume.accessor, t, cameraLocalPosition, rayDirection, marchEnd, hitValue)) { discard; } float skipDistance = 0.0f; float3 integratedLight = 0.0f.xxx; float transmittance = 1.0f; - float accumulatedDensity = 0.0f; float3 resolvedLightDirectionWS = gLightDirection.xyz; float3 resolvedLightRadiance = 1.0f.xxx; @@ -251,21 +261,23 @@ Shader "Builtin Volumetric" } float3 localPosition = cameraLocalPosition + rayDirection * t; - const uint dim = GetDimCoord(volume.accessor, localPosition); - if (dim > 1u) { + const uint dim = enableEmptySpaceSkipping + ? GetDimCoord(volume.accessor, localPosition) + : 1u; + if (enableEmptySpaceSkipping && dim > 1u) { skipDistance = 15.0f; t += skipDistance; continue; } - float density = GetValueCoord(volume.accessor, localPosition) * densityScale; - if (density < 0.01f) { + float density = max(GetValueCoord(volume.accessor, localPosition) * densityScale, 0.0f); + if (enableEmptySpaceSkipping && density < 0.01f) { skipDistance = 5.0f; t += skipDistance; continue; } - if (skipDistance > 0.0f) { + if (enableEmptySpaceSkipping && skipDistance > 0.0f) { t -= skipDistance * 0.8f; localPosition = cameraLocalPosition + rayDirection * t; skipDistance = 0.0f; @@ -273,7 +285,6 @@ Shader "Builtin Volumetric" const float sigmaS = density; const float sigmaE = max(0.000001f, sigmaS); - accumulatedDensity += sigmaS; const float shadow = ComputeVolumetricShadow( @@ -283,15 +294,12 @@ Shader "Builtin Volumetric" lightSamples, volume.accessor); const float3 S = sigmaS * shadow.xxx * resolvedLightRadiance; - const float3 integratedSegment = (S - S * exp(-sigmaE * stepSize)) / sigmaE; + const float segmentTransmittance = exp(-sigmaE * stepSize); + const float3 integratedSegment = (S - S * segmentTransmittance) / sigmaE; integratedLight += transmittance * integratedSegment; - transmittance *= exp(-sigmaE * stepSize); + transmittance *= segmentTransmittance; - if (accumulatedDensity > 1.0f) { - break; - } - - if (transmittance < 0.05f) { + if (enableEarlyTermination && transmittance < 0.05f) { transmittance = 0.0f; break; } @@ -300,11 +308,12 @@ Shader "Builtin Volumetric" } const float3 ambientLight = ambientStrength.xxx; - float3 finalColor = (integratedLight + ambientLight) * accumulatedDensity; + const float opacity = saturate(1.0f - transmittance); + float3 finalColor = integratedLight + ambientLight * opacity; finalColor *= gVolumeTint.rgb; finalColor = pow(max(finalColor, 0.0f.xxx), 1.0f / 2.2f); - const float alpha = saturate(accumulatedDensity) * saturate(gVolumeTint.a); + const float alpha = opacity * saturate(gVolumeTint.a); if (alpha <= 0.001f) { discard; } diff --git a/project/Assets/Materials/VolumeCloud.mat b/project/Assets/Materials/VolumeCloud.mat index f55fe665..cde4e4ce 100644 --- a/project/Assets/Materials/VolumeCloud.mat +++ b/project/Assets/Materials/VolumeCloud.mat @@ -2,12 +2,15 @@ "shader": "builtin://shaders/volumetric", "renderQueue": "Transparent", "properties": { - "_Tint": [1.0, 1.0, 1.0, 0.95], - "_DensityScale": 0.24, - "_StepSize": 0.75, - "_MaxSteps": 2400.0, - "_AmbientStrength": 0.02, + "_Tint": [1.0, 1.0, 1.0, 1.0], + "_DensityScale": 0.2, + "_StepSize": 1.0, + "_MaxSteps": 2000.0, + "_AmbientStrength": 0.005, "_LightDirection": [0.5, 0.8, 0.3, 0.0], - "_LightSamples": 10.0 + "_LightSamples": 8.0, + "_EnableEntryHdda": 1.0, + "_EnableEmptySpaceSkipping": 1.0, + "_EnableEarlyTermination": 1.0 } } diff --git a/tests/Rendering/CMakeLists.txt b/tests/Rendering/CMakeLists.txt index f6e7a0f8..2035b23b 100644 --- a/tests/Rendering/CMakeLists.txt +++ b/tests/Rendering/CMakeLists.txt @@ -26,6 +26,7 @@ add_custom_target(rendering_integration_tests rendering_integration_offscreen_scene rendering_integration_skybox_scene rendering_integration_post_process_scene + rendering_integration_volume_performance_suite rendering_integration_nahida_preview_scene ) diff --git a/tests/Rendering/integration/CMakeLists.txt b/tests/Rendering/integration/CMakeLists.txt index 56b4b17b..84363318 100644 --- a/tests/Rendering/integration/CMakeLists.txt +++ b/tests/Rendering/integration/CMakeLists.txt @@ -24,5 +24,6 @@ add_subdirectory(alpha_cutout_scene) add_subdirectory(volume_scene) add_subdirectory(volume_occlusion_scene) add_subdirectory(volume_transform_scene) +add_subdirectory(volume_performance_suite) add_subdirectory(gaussian_splat_scene) add_subdirectory(nahida_preview_scene) diff --git a/tests/Rendering/integration/VolumeIntegrationSceneFixture.h b/tests/Rendering/integration/VolumeIntegrationSceneFixture.h index 3a06f1b3..73882fdd 100644 --- a/tests/Rendering/integration/VolumeIntegrationSceneFixture.h +++ b/tests/Rendering/integration/VolumeIntegrationSceneFixture.h @@ -25,6 +25,8 @@ #include "../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include +#include +#include #include #include @@ -41,6 +43,15 @@ constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; constexpr const char* kCloudVolumeRelativePath = "Res/Volumes/cloud.nvdb"; +struct StableFrameStats { + int warmupFrames = 0; + int measuredFrames = 0; + double averageFrameTimeMs = 0.0; + double minFrameTimeMs = 0.0; + double maxFrameTimeMs = 0.0; + double averageFps = 0.0; +}; + inline Material* CreateVolumetricMaterial( const char* name, const char* path, @@ -50,7 +61,10 @@ inline Material* CreateVolumetricMaterial( float maxSteps = 2000.0f, float ambientStrength = 0.005f, const Vector3& lightDirection = Vector3(0.5f, 0.8f, 0.3f), - float lightSamples = 8.0f) { + float lightSamples = 8.0f, + float enableEntryHdda = 1.0f, + float enableEmptySpaceSkipping = 1.0f, + float enableEarlyTermination = 1.0f) { auto* material = new Material(); IResource::ConstructParams params = {}; params.name = name; @@ -66,6 +80,9 @@ inline Material* CreateVolumetricMaterial( material->SetFloat("_AmbientStrength", ambientStrength); material->SetFloat4("_LightDirection", Vector4(lightDirection, 0.0f)); material->SetFloat("_LightSamples", lightSamples); + material->SetFloat("_EnableEntryHdda", enableEntryHdda); + material->SetFloat("_EnableEmptySpaceSkipping", enableEmptySpaceSkipping); + material->SetFloat("_EnableEarlyTermination", enableEarlyTermination); return material; } @@ -250,6 +267,72 @@ protected: } } + StableFrameStats RenderAndMeasureStableFrames( + int warmupFrameCount, + int measuredFrameCount, + const char* screenshotFilename = nullptr) { + StableFrameStats stats = {}; + stats.warmupFrames = warmupFrameCount; + stats.measuredFrames = measuredFrameCount; + + RHICommandQueue* commandQueue = GetCommandQueue(); + RHISwapChain* swapChain = GetSwapChain(); + if (commandQueue == nullptr || swapChain == nullptr) { + ADD_FAILURE() << "RenderAndMeasureStableFrames requires a valid command queue and swap chain"; + return stats; + } + + auto renderPresentedFrame = [&]() { + BeginRender(); + RenderFrame(); + swapChain->Present(0, 0); + }; + + for (int frameIndex = 0; frameIndex < warmupFrameCount; ++frameIndex) { + if (frameIndex > 0) { + commandQueue->WaitForPreviousFrame(); + } + renderPresentedFrame(); + } + + if (warmupFrameCount > 0) { + commandQueue->WaitForPreviousFrame(); + } + + using Clock = std::chrono::steady_clock; + double totalFrameTimeMs = 0.0; + double minFrameTimeMs = std::numeric_limits::max(); + double maxFrameTimeMs = 0.0; + for (int frameIndex = 0; frameIndex < measuredFrameCount; ++frameIndex) { + const Clock::time_point frameStart = Clock::now(); + renderPresentedFrame(); + commandQueue->WaitForPreviousFrame(); + const double frameTimeMs = std::chrono::duration( + Clock::now() - frameStart).count(); + totalFrameTimeMs += frameTimeMs; + minFrameTimeMs = (std::min)(minFrameTimeMs, frameTimeMs); + maxFrameTimeMs = (std::max)(maxFrameTimeMs, frameTimeMs); + } + + if (measuredFrameCount > 0) { + stats.averageFrameTimeMs = totalFrameTimeMs / static_cast(measuredFrameCount); + stats.minFrameTimeMs = minFrameTimeMs; + stats.maxFrameTimeMs = maxFrameTimeMs; + stats.averageFps = stats.averageFrameTimeMs > 0.0 + ? 1000.0 / stats.averageFrameTimeMs + : 0.0; + } + + if (screenshotFilename != nullptr && screenshotFilename[0] != '\0') { + BeginRender(); + RenderFrame(); + commandQueue->WaitForIdle(); + EXPECT_TRUE(TakeScreenshot(screenshotFilename)); + } + + return stats; + } + RHIResourceView* GetCurrentBackBufferView() { const int backBufferIndex = GetCurrentBackBufferIndex(); if (backBufferIndex < 0) { diff --git a/tests/Rendering/integration/volume_performance_suite/CMakeLists.txt b/tests/Rendering/integration/volume_performance_suite/CMakeLists.txt new file mode 100644 index 00000000..57910d92 --- /dev/null +++ b/tests/Rendering/integration/volume_performance_suite/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +project(rendering_integration_volume_performance_suite) + +set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine) +set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package) + +get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE) + +find_package(Vulkan QUIET) + +add_executable(rendering_integration_volume_performance_suite + main.cpp + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp + ${PACKAGE_DIR}/src/glad.c +) + +target_include_directories(rendering_integration_volume_performance_suite PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures + ${ENGINE_ROOT_DIR}/include + ${PACKAGE_DIR}/include + ${PROJECT_ROOT_DIR}/engine/src +) + +target_link_libraries(rendering_integration_volume_performance_suite PRIVATE + d3d12 + dxgi + d3dcompiler + winmm + opengl32 + XCEngine + GTest::gtest +) + +if(TARGET Vulkan::Vulkan) + target_link_libraries(rendering_integration_volume_performance_suite PRIVATE Vulkan::Vulkan) + target_compile_definitions(rendering_integration_volume_performance_suite PRIVATE XCENGINE_SUPPORT_VULKAN) +endif() + +target_compile_definitions(rendering_integration_volume_performance_suite PRIVATE + UNICODE + _UNICODE + XCENGINE_SUPPORT_OPENGL + XCENGINE_SUPPORT_D3D12 +) + +add_custom_command(TARGET rendering_integration_volume_performance_suite POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Res/Volumes + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/mvs/VolumeRenderer/Res/NanoVDB/cloud.nvdb + $/Res/Volumes/cloud.nvdb + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll + $/ +) + +include(GoogleTest) +gtest_discover_tests(rendering_integration_volume_performance_suite) diff --git a/tests/Rendering/integration/volume_performance_suite/main.cpp b/tests/Rendering/integration/volume_performance_suite/main.cpp new file mode 100644 index 00000000..7a9cc43c --- /dev/null +++ b/tests/Rendering/integration/volume_performance_suite/main.cpp @@ -0,0 +1,317 @@ +#include + +#include "../VolumeIntegrationSceneFixture.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace VolumeIntegrationTestUtils; + +namespace { + +constexpr int kWarmupFrames = 20; +constexpr int kMeasuredFrames = 60; + +struct QualityProfile { + const char* name = ""; + float stepSize = 1.0f; + float maxSteps = 2000.0f; + const char* screenshotFilename = ""; +}; + +struct OptimizationProfile { + const char* name = ""; + float enableEntryHdda = 1.0f; + float enableEmptySpaceSkipping = 1.0f; + float enableEarlyTermination = 1.0f; + const char* screenshotFilename = ""; +}; + +struct QualityResult { + std::string name; + float stepSize = 0.0f; + float maxSteps = 0.0f; + StableFrameStats stats = {}; + std::string screenshotFilename; +}; + +struct OptimizationResult { + std::string name; + float enableEntryHdda = 1.0f; + float enableEmptySpaceSkipping = 1.0f; + float enableEarlyTermination = 1.0f; + StableFrameStats stats = {}; + std::string screenshotFilename; +}; + +std::string FormatNumber(double value, int precision = 3) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(precision) << value; + return stream.str(); +} + +void WriteQualityResults(const std::vector& results) { + const std::filesystem::path tsvPath = + RenderingIntegrationTestUtils::ResolveRuntimePath("volume_quality_results.tsv"); + std::ofstream tsv(tsvPath, std::ios::trunc); + ASSERT_TRUE(tsv.is_open()) << tsvPath.string(); + tsv << "profile\tstep_size\tmax_steps\twarmup_frames\tmeasured_frames\tavg_frame_ms\tmin_frame_ms\tmax_frame_ms\tavg_fps\tscreenshot\n"; + for (const QualityResult& result : results) { + tsv << result.name << '\t' + << result.stepSize << '\t' + << result.maxSteps << '\t' + << result.stats.warmupFrames << '\t' + << result.stats.measuredFrames << '\t' + << FormatNumber(result.stats.averageFrameTimeMs) << '\t' + << FormatNumber(result.stats.minFrameTimeMs) << '\t' + << FormatNumber(result.stats.maxFrameTimeMs) << '\t' + << FormatNumber(result.stats.averageFps) << '\t' + << result.screenshotFilename << '\n'; + } + + const std::filesystem::path markdownPath = + RenderingIntegrationTestUtils::ResolveRuntimePath("volume_quality_results.md"); + std::ofstream markdown(markdownPath, std::ios::trunc); + ASSERT_TRUE(markdown.is_open()) << markdownPath.string(); + markdown << "|profile|StepSize|MaxSteps|warmup_frames|measured_frames|avg_frame_ms|min_frame_ms|max_frame_ms|avg_fps|screenshot|\n"; + markdown << "|---|---:|---:|---:|---:|---:|---:|---:|---:|---|\n"; + for (const QualityResult& result : results) { + markdown << '|' + << result.name << '|' + << result.stepSize << '|' + << result.maxSteps << '|' + << result.stats.warmupFrames << '|' + << result.stats.measuredFrames << '|' + << FormatNumber(result.stats.averageFrameTimeMs) << '|' + << FormatNumber(result.stats.minFrameTimeMs) << '|' + << FormatNumber(result.stats.maxFrameTimeMs) << '|' + << FormatNumber(result.stats.averageFps) << '|' + << result.screenshotFilename << "|\n"; + } +} + +void WriteOptimizationResults(const std::vector& results) { + const std::filesystem::path tsvPath = + RenderingIntegrationTestUtils::ResolveRuntimePath("volume_optimization_results.tsv"); + std::ofstream tsv(tsvPath, std::ios::trunc); + ASSERT_TRUE(tsv.is_open()) << tsvPath.string(); + tsv << "profile\tenable_entry_hdda\tenable_empty_space_skipping\tenable_early_termination\twarmup_frames\tmeasured_frames\tavg_frame_ms\tmin_frame_ms\tmax_frame_ms\tavg_fps\tscreenshot\n"; + for (const OptimizationResult& result : results) { + tsv << result.name << '\t' + << result.enableEntryHdda << '\t' + << result.enableEmptySpaceSkipping << '\t' + << result.enableEarlyTermination << '\t' + << result.stats.warmupFrames << '\t' + << result.stats.measuredFrames << '\t' + << FormatNumber(result.stats.averageFrameTimeMs) << '\t' + << FormatNumber(result.stats.minFrameTimeMs) << '\t' + << FormatNumber(result.stats.maxFrameTimeMs) << '\t' + << FormatNumber(result.stats.averageFps) << '\t' + << result.screenshotFilename << '\n'; + } + + const std::filesystem::path markdownPath = + RenderingIntegrationTestUtils::ResolveRuntimePath("volume_optimization_results.md"); + std::ofstream markdown(markdownPath, std::ios::trunc); + ASSERT_TRUE(markdown.is_open()) << markdownPath.string(); + markdown << "|profile|enable_entry_hdda|enable_empty_space_skipping|enable_early_termination|warmup_frames|measured_frames|avg_frame_ms|min_frame_ms|max_frame_ms|avg_fps|screenshot|\n"; + markdown << "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|\n"; + for (const OptimizationResult& result : results) { + markdown << '|' + << result.name << '|' + << result.enableEntryHdda << '|' + << result.enableEmptySpaceSkipping << '|' + << result.enableEarlyTermination << '|' + << result.stats.warmupFrames << '|' + << result.stats.measuredFrames << '|' + << FormatNumber(result.stats.averageFrameTimeMs) << '|' + << FormatNumber(result.stats.minFrameTimeMs) << '|' + << FormatNumber(result.stats.maxFrameTimeMs) << '|' + << FormatNumber(result.stats.averageFps) << '|' + << result.screenshotFilename << "|\n"; + } +} + +class VolumePerformanceSuiteTest : public VolumeIntegrationSceneFixture { +protected: + const char* GetSceneName() const override { return "VolumePerformanceSuite"; } + void BuildScene() override; + void ReleaseSceneResources() override; + + void ApplyQualityProfile(const QualityProfile& profile); + void ApplyOptimizationProfile(const OptimizationProfile& profile); + +private: + Material* mVolumeMaterial = nullptr; + VolumeField* mVolumeField = nullptr; +}; + +void VolumePerformanceSuiteTest::BuildScene() { + mVolumeMaterial = CreateVolumetricMaterial( + "VolumePerformanceMaterial", + "Tests/Rendering/VolumePerformanceSuite/Volume.material", + Vector4(1.0f, 1.0f, 1.0f, 1.0f), + 0.2f, + 1.0f, + 2000.0f, + 0.005f, + Vector3(-0.72f, 0.16f, 0.67f), + 8.0f, + 1.0f, + 1.0f, + 1.0f); + ASSERT_NE(mVolumeMaterial, nullptr); + + mVolumeField = LoadCloudVolumeField(); + ASSERT_NE(mVolumeField, nullptr); + + GameObject* cameraObject = mScene->CreateGameObject("MainCamera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetFieldOfView(45.0f); + camera->SetNearClipPlane(0.1f); + camera->SetFarClipPlane(5000.0f); + camera->SetClearColor(XCEngine::Math::Color(0.03f, 0.04f, 0.06f, 1.0f)); + cameraObject->GetTransform()->SetLocalPosition(Vector3(-10.0f, 300.0f, -1200.0f)); + cameraObject->GetTransform()->LookAt(Vector3(-10.0f, 73.0f, 0.0f)); + + GameObject* lightObject = mScene->CreateGameObject("MainDirectionalLight"); + auto* light = lightObject->AddComponent(); + light->SetLightType(LightType::Directional); + light->SetColor(XCEngine::Math::Color(1.0f, 1.0f, 1.0f, 1.0f)); + light->SetIntensity(1.0f); + lightObject->GetTransform()->LookAt(Vector3(-0.5f, -0.8f, -0.3f)); + + GameObject* volumeObject = mScene->CreateGameObject("CloudVolume"); + auto* volumeRenderer = volumeObject->AddComponent(); + volumeRenderer->SetVolumeField(mVolumeField); + volumeRenderer->SetMaterial(mVolumeMaterial); + volumeRenderer->SetCastShadows(false); + volumeRenderer->SetReceiveShadows(false); + + RenderSceneExtractor extractor; + const RenderSceneData sceneData = extractor.Extract(*mScene, nullptr, kFrameWidth, kFrameHeight); + ASSERT_EQ(sceneData.visibleItems.size(), 0u); + ASSERT_EQ(sceneData.visibleVolumes.size(), 1u); + ASSERT_TRUE(sceneData.lighting.HasMainDirectionalLight()); +} + +void VolumePerformanceSuiteTest::ReleaseSceneResources() { + delete mVolumeField; + mVolumeField = nullptr; + delete mVolumeMaterial; + mVolumeMaterial = nullptr; +} + +void VolumePerformanceSuiteTest::ApplyQualityProfile(const QualityProfile& profile) { + ASSERT_NE(mVolumeMaterial, nullptr); + mVolumeMaterial->SetFloat("_StepSize", profile.stepSize); + mVolumeMaterial->SetFloat("_MaxSteps", profile.maxSteps); + mVolumeMaterial->SetFloat("_LightSamples", 8.0f); + mVolumeMaterial->SetFloat("_EnableEntryHdda", 1.0f); + mVolumeMaterial->SetFloat("_EnableEmptySpaceSkipping", 1.0f); + mVolumeMaterial->SetFloat("_EnableEarlyTermination", 1.0f); +} + +void VolumePerformanceSuiteTest::ApplyOptimizationProfile(const OptimizationProfile& profile) { + ASSERT_NE(mVolumeMaterial, nullptr); + mVolumeMaterial->SetFloat("_StepSize", 1.0f); + mVolumeMaterial->SetFloat("_MaxSteps", 2000.0f); + mVolumeMaterial->SetFloat("_LightSamples", 8.0f); + mVolumeMaterial->SetFloat("_EnableEntryHdda", profile.enableEntryHdda); + mVolumeMaterial->SetFloat("_EnableEmptySpaceSkipping", profile.enableEmptySpaceSkipping); + mVolumeMaterial->SetFloat("_EnableEarlyTermination", profile.enableEarlyTermination); +} + +TEST_P(VolumePerformanceSuiteTest, MeasureQualityProfiles) { + const std::array profiles = { + QualityProfile{ "high_quality", 0.25f, 8000.0f, "volume_quality_high_d3d12.ppm" }, + QualityProfile{ "balanced", 1.0f, 2000.0f, "volume_quality_balanced_d3d12.ppm" }, + QualityProfile{ "high_performance", 4.0f, 512.0f, "volume_quality_performance_d3d12.ppm" } + }; + + std::vector results; + results.reserve(profiles.size()); + for (const QualityProfile& profile : profiles) { + ApplyQualityProfile(profile); + const StableFrameStats stats = RenderAndMeasureStableFrames( + kWarmupFrames, + kMeasuredFrames, + profile.screenshotFilename); + EXPECT_GT(stats.averageFrameTimeMs, 0.0); + EXPECT_GT(stats.averageFps, 0.0); + + const RenderingIntegrationTestUtils::PpmImage image = + RenderingIntegrationTestUtils::LoadPpmImage(profile.screenshotFilename); + EXPECT_EQ(image.width, kFrameWidth); + EXPECT_EQ(image.height, kFrameHeight); + + results.push_back(QualityResult{ + profile.name, + profile.stepSize, + profile.maxSteps, + stats, + profile.screenshotFilename + }); + } + + WriteQualityResults(results); +} + +TEST_P(VolumePerformanceSuiteTest, MeasureOptimizationVariants) { + const std::array profiles = { + OptimizationProfile{ "full_optimization", 1.0f, 1.0f, 1.0f, "volume_opt_full_d3d12.ppm" }, + OptimizationProfile{ "disable_entry_hdda", 0.0f, 1.0f, 1.0f, "volume_opt_no_entry_hdda_d3d12.ppm" }, + OptimizationProfile{ "disable_empty_space_skipping", 1.0f, 0.0f, 1.0f, "volume_opt_no_empty_space_skip_d3d12.ppm" }, + OptimizationProfile{ "disable_early_termination", 1.0f, 1.0f, 0.0f, "volume_opt_no_early_termination_d3d12.ppm" } + }; + + std::vector results; + results.reserve(profiles.size()); + for (const OptimizationProfile& profile : profiles) { + ApplyOptimizationProfile(profile); + const StableFrameStats stats = RenderAndMeasureStableFrames( + kWarmupFrames, + kMeasuredFrames, + profile.screenshotFilename); + EXPECT_GT(stats.averageFrameTimeMs, 0.0); + EXPECT_GT(stats.averageFps, 0.0); + + const RenderingIntegrationTestUtils::PpmImage image = + RenderingIntegrationTestUtils::LoadPpmImage(profile.screenshotFilename); + EXPECT_EQ(image.width, kFrameWidth); + EXPECT_EQ(image.height, kFrameHeight); + + results.push_back(OptimizationResult{ + profile.name, + profile.enableEntryHdda, + profile.enableEmptySpaceSkipping, + profile.enableEarlyTermination, + stats, + profile.screenshotFilename + }); + } + + WriteOptimizationResults(results); +} + +} // namespace + +INSTANTIATE_TEST_SUITE_P(D3D12, VolumePerformanceSuiteTest, ::testing::Values(XCEngine::RHI::RHIType::D3D12)); + +GTEST_API_ int main(int argc, char** argv) { + return RunRenderingIntegrationTestMain(argc, argv); +}