From 666e6930c5da582343883eb24f6d194bc68f63d8 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 10:51:59 +0200 Subject: [PATCH 01/24] Fix cascade camera allocation and frustum planes, add shadowFreezeCascades --- .../Systems/CalculateShadowCameraMatrices.cpp | 112 +++++++++--------- .../Rendering/Shadow/ShadowRenderer.cpp | 6 - .../Rendering/Shadow/ShadowRenderer.h | 21 ---- 3 files changed, 54 insertions(+), 85 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp index e368a42..16f8547 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp @@ -14,23 +14,24 @@ #include #include -#include -#include +#include AutoCVar_Int CVAR_ShadowsStable(CVarCategory::Client | CVarCategory::Rendering, "shadowStable", "stable shadows", 1, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_ShadowsFreezeCascades(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades", "freeze cascade cameras and culling planes for debugging", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 0); AutoCVar_Float CVAR_ShadowCascadeSplitLambda(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda", "split lambda for cascades, between 0.0f and 1.0f", 0.8f); -AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side)", 4096); +AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side), only applies to cascades created after it is set", 4096); namespace ECS::Systems { - inline vec4 EncodePlane(vec3 position, vec3 normal) + // Converts a Gribb-Hartmann clip plane row (inside if dot(n, p) + d >= 0) to the + // encoding the culling shaders expect: vec4(n, dot(n, pointOnPlane)) with normalized inward n + inline vec4 ExtractPlane(const vec4& row) { - vec3 normalizedNormal = glm::normalize(normal); - vec4 result = vec4(normalizedNormal, glm::dot(normalizedNormal, position)); - return result; + f32 normalLength = glm::length(vec3(row)); + return vec4(vec3(row) / normalLength, -row.w / normalLength); } void CalculateShadowCameraMatrices::Update(entt::registry& registry, f32 deltaTime) @@ -47,30 +48,34 @@ namespace ECS::Systems bool stableShadows = CVAR_ShadowsStable.Get() == 1; // Initialize any new shadow cascades - if (numCascades != renderResources.shadowDepthCascades.size()) + // Cascade count can shrink at runtime, we keep the excess cameras and depth images around unused since neither can shrink + u32 numCamerasNeeded = numCascades + 1; // Main camera + cascades + u32 numCameras = renderResources.cameras.Count(); + if (numCamerasNeeded > numCameras) { - u32 numCameras = renderResources.cameras.Count(); - u32 numCamerasToAdd = numCascades - numCameras; - renderResources.cameras.AddCount(numCamerasToAdd); + renderResources.cameras.AddCount(numCamerasNeeded - numCameras); + } - while (numCascades > renderResources.shadowDepthCascades.size()) - { - u32 cascadeIndex = static_cast(renderResources.shadowDepthCascades.size()); - - // Shadow depth rendertarget - Renderer::DepthImageDesc shadowDepthDesc; - shadowDepthDesc.dimensions = vec2(cascadeTextureSize, cascadeTextureSize); - shadowDepthDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_ABSOLUTE; - shadowDepthDesc.format = Renderer::DepthImageFormat::D32_FLOAT; - shadowDepthDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; - shadowDepthDesc.depthClearValue = 0.0f; - shadowDepthDesc.debugName = "ShadowDepthCascade" + std::to_string(cascadeIndex); - - Renderer::DepthImageID cascadeDepthImage = renderer->CreateDepthImage(shadowDepthDesc); - renderResources.shadowDepthCascades.push_back(cascadeDepthImage); - } + while (renderResources.shadowDepthCascades.size() < numCascades) + { + u32 cascadeIndex = static_cast(renderResources.shadowDepthCascades.size()); + + // Shadow depth rendertarget + Renderer::DepthImageDesc shadowDepthDesc; + shadowDepthDesc.dimensions = vec2(cascadeTextureSize, cascadeTextureSize); + shadowDepthDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_ABSOLUTE; + shadowDepthDesc.format = Renderer::DepthImageFormat::D32_FLOAT; + shadowDepthDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; + shadowDepthDesc.depthClearValue = 0.0f; + shadowDepthDesc.debugName = "ShadowDepthCascade" + std::to_string(cascadeIndex); + + Renderer::DepthImageID cascadeDepthImage = renderer->CreateDepthImage(shadowDepthDesc); + renderResources.shadowDepthCascades.push_back(cascadeDepthImage); } + if (CVAR_ShadowsFreezeCascades.Get()) + return; + entt::registry::context& ctx = registry.ctx(); auto& dayNightCycle = ctx.get(); @@ -212,8 +217,6 @@ namespace ECS::Systems maxExtents.y *= scale; } - vec3 cascadeExtents = maxExtents - minExtents; - // Get postion of the shadow camera vec3 shadowCameraPos = frustumCenter + lightDirection * -minExtents.z; @@ -257,35 +260,28 @@ namespace ECS::Systems f32 splitDepth = (farClip - (nearClip + splitDist * clipRange));// *-1.0f; cascadeCamera.eyePosition = vec4(shadowCameraPos, splitDepth); // w holds split depth - vec3 scale; - quat rotation; - vec3 translation; - vec3 skew; - vec4 perspective; - glm::decompose(viewMatrix, scale, rotation, translation, skew, perspective); - vec3 eulerAngles = glm::eulerAngles(rotation); - - f32 pitch = eulerAngles.x; - f32 yaw = eulerAngles.y; - f32 roll = eulerAngles.z; - - cascadeCamera.eyeRotation = vec4(roll, pitch, yaw, 0.0f); // Is this order correct? - - // Calculate frustum planes - glm::vec3 front = glm::vec3(0, 0, 1); - glm::vec3 right = glm::vec3(1, 0, 0); - glm::vec3 up = glm::vec3(0, 1, 0); - - front = vec3(viewMatrix * vec4(front, 0.0f)); - right = vec3(viewMatrix * vec4(right, 0.0f)); - up = vec3(viewMatrix * vec4(up, 0.0f)); - - cascadeCamera.frustum[(size_t)FrustumPlane::Near] = EncodePlane(shadowCameraPos + front * nearPlane, front); - cascadeCamera.frustum[(size_t)FrustumPlane::Far] = EncodePlane(shadowCameraPos + front * farPlane , -front); - cascadeCamera.frustum[(size_t)FrustumPlane::Right] = EncodePlane(shadowCameraPos - right * cascadeExtents.x, right); - cascadeCamera.frustum[(size_t)FrustumPlane::Left] = EncodePlane(shadowCameraPos + right * cascadeExtents.x, -right); - cascadeCamera.frustum[(size_t)FrustumPlane::Top] = EncodePlane(shadowCameraPos + up * cascadeExtents.z, -up); - cascadeCamera.frustum[(size_t)FrustumPlane::Bottom] = EncodePlane(shadowCameraPos - up * cascadeExtents.z, up); + // Extract world-space frustum planes from the view-projection matrix (Gribb-Hartmann) + const mat4x4& m = cascadeCamera.worldToClip; + vec4 row0 = glm::row(m, 0); + vec4 row1 = glm::row(m, 1); + vec4 row2 = glm::row(m, 2); + vec4 row3 = glm::row(m, 3); + + cascadeCamera.frustum[(size_t)FrustumPlane::Left] = ExtractPlane(row3 + row0); + cascadeCamera.frustum[(size_t)FrustumPlane::Right] = ExtractPlane(row3 - row0); + cascadeCamera.frustum[(size_t)FrustumPlane::Bottom] = ExtractPlane(row3 + row1); + cascadeCamera.frustum[(size_t)FrustumPlane::Top] = ExtractPlane(row3 - row1); + cascadeCamera.frustum[(size_t)FrustumPlane::Near] = ExtractPlane(row3 - row2); // Reversed Z, depth 1 is near + cascadeCamera.frustum[(size_t)FrustumPlane::Far] = ExtractPlane(row2); + +#if NC_DEBUG + for (u32 j = 0; j < 6; j++) + { + const vec4& plane = cascadeCamera.frustum[j]; + f32 distance = glm::dot(vec3(plane), frustumCenter) - plane.w; + NC_ASSERT(distance > 0.0f, "CalculateShadowCameraMatrices : Cascade frustum plane {0} does not contain the frustum center", j); + } +#endif renderResources.cameras.SetDirtyElement(i+1); lastSplitDist = cascadeSplits[i]; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 0d31151..c93bdbe 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -11,8 +11,6 @@ #include #include -#include -#include #include AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 0, CVarFlags::EditCheckbox); @@ -30,8 +28,6 @@ AutoCVar_Float CVAR_ShadowDepthBiasConstantFactor(CVarCategory::Client | CVarCat AutoCVar_Float CVAR_ShadowDepthBiasClamp(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp", "clamp of depth bias to prevent shadow acne", 0.0f); AutoCVar_Float CVAR_ShadowDepthBiasSlopeFactor(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope", "slope factor of depth bias to prevent shadow acne", -5.0f); -#define TIMESLICED_CASCADES 0 - ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) , _gameRenderer(gameRenderer) @@ -103,8 +99,6 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes if (numCascades == 0) return; - const bool shadowEnabled = CVAR_ShadowEnabled.Get(); - renderGraph->AddPass("Shadow Pass", [=, &resources](ShadowPassData& data, Renderer::RenderGraphBuilder& builder) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 76fb006..390c487 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -32,15 +32,6 @@ class ShadowRenderer private: void CreatePermanentResources(RenderResources& resources); -private: - struct ShadowCascadeDebugInformation - { - public: - vec3 frustumCorners[8]; - vec3 frustumPlanePos; - vec4 frustumPlanes[6]; - }; - private: Renderer::Renderer* _renderer = nullptr; GameRenderer* _gameRenderer = nullptr; @@ -50,16 +41,4 @@ class ShadowRenderer Renderer::SamplerID _shadowCmpSampler; Renderer::SamplerID _shadowPointClampSampler; - - Renderer::TextureArrayID _shadowDepthTextures; - u32 _numInitializedShadowDepthImages = 0; - - mat4x4 _cascadeProjectionMatrices[Renderer::Settings::MAX_SHADOW_CASCADES]; - - ShadowCascadeDebugInformation _cascadeDebugInformation[Renderer::Settings::MAX_SHADOW_CASCADES]; - - vec3 _boundingBoxMin; - vec3 _boundingBoxMax; - - u32 _currentCascade = 0; }; \ No newline at end of file From 4cebb8cbf11f06d479bf35733afe57c5ec08e11e Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 10:53:31 +0200 Subject: [PATCH 02/24] Bind terrain shadow pipeline and enable depth write on model shadow pipeline --- Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp | 7 +++++++ .../Game-Lib/Rendering/Terrain/TerrainRenderer.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 923c7e7..67eebf1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -2655,6 +2655,13 @@ void ModelRenderer::CreateModelPipelines() // Shadows { Renderer::GraphicsPipelineDesc pipelineDesc; + pipelineDesc.debugName = "Model Draw Shadow"; + + // Depth state + pipelineDesc.states.depthStencilState.depthEnable = true; + pipelineDesc.states.depthStencilState.depthWriteEnable = true; + pipelineDesc.states.depthStencilState.depthFunc = Renderer::ComparisonFunc::GREATER; + // Rasterizer state pipelineDesc.states.rasterizerState.cullMode = Renderer::CullMode::NONE; pipelineDesc.states.rasterizerState.depthBiasEnabled = true; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index 20f2b84..d678053 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -1199,7 +1199,7 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend commandList.BeginRenderPass(renderPassDesc); // Set pipeline - Renderer::GraphicsPipelineID pipeline = _drawPipeline; + Renderer::GraphicsPipelineID pipeline = params.shadowPass ? _drawShadowPipeline : _drawPipeline; commandList.BeginPipeline(pipeline); // Set index buffer From a1cb513fd103acf67da8fd50c3e0c5a95ebb5084 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:02:12 +0200 Subject: [PATCH 03/24] Add per-cascade culling for instanced models and shadowDebugCullingView --- .../Game-Lib/Rendering/CulledRenderer.cpp | 124 ++++++++++++++++-- .../Game-Lib/Rendering/CulledRenderer.h | 12 +- .../Game-Lib/Rendering/CullingResources.cpp | 4 +- .../Rendering/Model/ModelRenderer.cpp | 19 ++- .../Rendering/Terrain/TerrainRenderer.cpp | 2 + .../Shaders/Shaders/Terrain/Culling.cs.slang | 29 ++-- .../Shaders/Utils/CullingInstanced.cs.slang | 40 ++++++ ...FillInstancedDrawCallsFromBitmask.cs.slang | 5 +- 8 files changed, 201 insertions(+), 34 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index cdbf3b9..d12fa92 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -4,6 +4,10 @@ #include "Game-Lib/Rendering/GameRenderer.h" #include "Game-Lib/Rendering/RenderUtils.h" +#include + +AutoCVar_Int CVAR_ShadowDebugCullingView(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView", "debug draw culling results for a view as AABBs (green = kept, red = culled), 0 = main view, 1..N = cascades, -1 = off", -1); + bool CulledRenderer::_pipelinesCreated = false; Renderer::ComputePipelineID CulledRenderer::_fillInstancedDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed Renderer::ComputePipelineID CulledRenderer::_fillDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed @@ -90,7 +94,8 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) if (params.disableTwoStepCulling) { - params.commandList->FillBuffer(params.culledDrawCallsBitMaskBuffer, 0, RenderUtils::CalcCullingBitmaskSize(numInstances), 0); + u32 sizePerView = params.cullingResources->GetBitMaskBufferSizePerView(); + params.commandList->FillBuffer(params.culledDrawCallsBitMaskBuffer, 0, sizePerView * Renderer::Settings::MAX_VIEWS, 0); params.commandList->BufferBarrier(params.culledDrawCallsBitMaskBuffer, Renderer::BufferPassUsage::TRANSFER); } @@ -108,6 +113,7 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored u32 drawCallDataSize; u32 currentBitmaskIndex; + u32 bitmaskOffset; }; FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); @@ -115,6 +121,7 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; fillConstants->drawCallDataSize = params.drawCallDataSize; fillConstants->currentBitmaskIndex = !params.frameIndex; // Occluders consume last frame's culling output + fillConstants->bitmaskOffset = 0; // Occluders only draw the main view params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); @@ -379,6 +386,9 @@ void CulledRenderer::CullingPass(CullingPassParams& params) u32 cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... u32 debugDrawColliders; u32 currentBitmaskIndex; + u32 numCascades; + u32 bitMaskBufferUintsPerView; + u32 debugDrawView; }; CullConstants* cullConstants = params.graphResources->FrameNew(); cullConstants->viewportSizeX = u32(viewportSize.x); @@ -397,6 +407,9 @@ void CulledRenderer::CullingPass(CullingPassParams& params) cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; cullConstants->debugDrawColliders = params.debugDrawColliders; cullConstants->currentBitmaskIndex = params.frameIndex; + cullConstants->numCascades = params.numCascades; + cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); + cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); params.cullingDescriptorSet.Bind("_depthPyramid"_h, params.depthPyramid); @@ -555,22 +568,22 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); params.commandList->PushMarker(markerName, Color::PastelYellow); + if (i == 1) + { + uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); + + params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); + params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); + + params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); + } + // Reset the counters if (!params.cullingResources->IsInstanced() && params.cullingResources->HasSupportForTwoStepCulling()) { params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); - - if (i == 1) - { - uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); - - params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - - params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); - } // Fill the geometry to draw { @@ -610,6 +623,95 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); } + else if (params.cullingResources->IsInstanced() && params.cullingResources->HasSupportForTwoStepCulling() && i > 0) + { + // The main view (i == 0) consumes the culling pass's output directly, cascades rebuild the + // shared instance counts/lookup/argument buffers from their bitmask slice before drawing + const u32 numInstances = params.cullingResources->GetNumInstances(); + + // Reset the counters and per-drawcall instance counts + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); + params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); + params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + + // Fill the instances visible in this cascade + { + std::string debugName = params.passName + " Instanced Geometry Fill"; + params.commandList->PushMarker(debugName, Color::White); + + Renderer::ComputePipelineID pipeline = _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct FillDrawCallConstants + { + u32 numTotalInstances; + u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored + u32 drawCallDataSize; + u32 currentBitmaskIndex; + u32 bitmaskOffset; + }; + + FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); + fillConstants->numTotalInstances = numInstances; + fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + fillConstants->drawCallDataSize = params.drawCallDataSize; + fillConstants->currentBitmaskIndex = params.frameIndex; + fillConstants->bitmaskOffset = i * params.cullingResources->GetBitMaskBufferUintsPerView(); + params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); + + params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); + } + + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); + + params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); + params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); + + // Create indirect argument buffer + { + std::string debugName = params.passName + " Create Indirect"; + params.commandList->PushMarker(debugName, Color::Yellow); + + Renderer::ComputePipelineID pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct CreateIndirectConstants + { + u32 numTotalDrawCalls; + u32 baseInstanceLookupOffset; + u32 drawCallDataSize; + }; + CreateIndirectConstants* createIndirectConstants = params.graphResources->FrameNew(); + + createIndirectConstants->numTotalDrawCalls = numDrawCalls; + createIndirectConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + createIndirectConstants->drawCallDataSize = params.drawCallDataSize; + params.commandList->PushConstant(createIndirectConstants, 0, sizeof(CreateIndirectConstants)); + + params.commandList->BindDescriptorSet(params.createIndirectDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); + } + + params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); + } if (!params.cullingEnabled) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index f845033..b587b04 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -246,15 +246,15 @@ class CulledRenderer data.drawCallsBuffer = builder.Write(cullingResources->GetDrawCalls().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.culledDrawCallsBuffer = builder.Write(cullingResources->GetCulledDrawsBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - data.culledDrawCallCountBuffer = builder.Write(cullingResources->GetCulledDrawCallCountBuffer(), BufferUsage::GRAPHICS); + data.culledDrawCallCountBuffer = builder.Write(cullingResources->GetCulledDrawCallCountBuffer(), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.drawCountBuffer = builder.Write(cullingResources->GetDrawCountBuffer(), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.triangleCountBuffer = builder.Write(cullingResources->GetTriangleCountBuffer(), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.drawCountReadBackBuffer = builder.Write(cullingResources->GetDrawCountReadBackBuffer(), BufferUsage::TRANSFER); data.triangleCountReadBackBuffer = builder.Write(cullingResources->GetTriangleCountReadBackBuffer(), BufferUsage::TRANSFER); - builder.Read(cullingResources->GetInstanceRefs().GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(cullingResources->GetCulledInstanceLookupTableBuffer(), BufferUsage::GRAPHICS); + builder.Read(cullingResources->GetInstanceRefs().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Write(cullingResources->GetCulledInstanceLookupTableBuffer(), BufferUsage::COMPUTE | BufferUsage::GRAPHICS); data.drawSet = builder.Use(cullingResources->GetGeometryPassDescriptorSet()); } @@ -291,6 +291,8 @@ class CulledRenderer Renderer::BufferMutableResource culledDrawCallCountBuffer; + Renderer::BufferMutableResource culledInstanceCountsBuffer; // Instanced only, used to rebuild per-cascade draw sets + Renderer::BufferMutableResource drawCountBuffer; Renderer::BufferMutableResource triangleCountBuffer; Renderer::BufferMutableResource drawCountReadBackBuffer; @@ -298,10 +300,14 @@ class CulledRenderer Renderer::DescriptorSetResource globalDescriptorSet; Renderer::DescriptorSetResource fillDescriptorSet; + Renderer::DescriptorSetResource createIndirectDescriptorSet; // Instanced only Renderer::DescriptorSetResource drawDescriptorSet; std::function drawCallback; + u32 baseInstanceLookupOffset = 0; // Instanced only + u32 drawCallDataSize = 0; // Instanced only + u32 numCascades = 0; f32 biasConstantFactor = 0.0f; diff --git a/Source/Game-Lib/Game-Lib/Rendering/CullingResources.cpp b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.cpp index cbd4d62..759ce26 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CullingResources.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.cpp @@ -157,8 +157,8 @@ bool CullingResourcesBase::SyncToGPU(bool forceRecount) { _occluderFillDescriptorSet.Bind("_instanceRefTable"_h, _instanceRefs.GetBuffer()); _cullingDescriptorSet.Bind("_instanceRefTable"_h, _instanceRefs.GetBuffer()); + _geometryFillDescriptorSet.Bind("_instanceRefTable"_h, _instanceRefs.GetBuffer()); } - //_geometryFillDescriptorSet.Bind2("_instanceRefTable"_h, _instanceRefs.GetBuffer()); _geometryPassDescriptorSet.Bind("_instanceRefTable"_h, _instanceRefs.GetBuffer(), true); if (_materialPassDescriptorSet != nullptr) { @@ -180,8 +180,8 @@ bool CullingResourcesBase::SyncToGPU(bool forceRecount) { _occluderFillDescriptorSet.Bind("_culledInstanceLookupTable"_h, _culledInstanceLookupTableBuffer); _cullingDescriptorSet.Bind("_culledInstanceLookupTable"_h, _culledInstanceLookupTableBuffer); + _geometryFillDescriptorSet.Bind("_culledInstanceLookupTable"_h, _culledInstanceLookupTableBuffer); } - //_geometryFillDescriptorSet.Bind("_culledInstanceLookupTable"_h, _culledInstanceLookupTableBuffer); _geometryPassDescriptorSet.Bind("_culledInstanceLookupTable"_h, _culledInstanceLookupTableBuffer, true); if (_materialPassDescriptorSet != nullptr) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 67eebf1..3fbb64a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -603,7 +603,11 @@ void ModelRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderRes params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.createIndirectAfterCullSet; - params.numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + params.numCascades = 0; + if (CVAR_ModelsCastShadow.Get() == 1) + { + params.numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + } params.occlusionCull = CVAR_ModelOcclusionCullingEnabled.Get(); params.disableTwoStepCulling = CVAR_ModelDisableTwoStepCulling.Get(); @@ -646,6 +650,8 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe Renderer::BufferMutableResource culledDrawCallsBuffer; Renderer::BufferMutableResource culledDrawCallCountBuffer; + Renderer::BufferMutableResource culledInstanceCountsBuffer; + Renderer::BufferMutableResource drawCountBuffer; Renderer::BufferMutableResource triangleCountBuffer; Renderer::BufferMutableResource drawCountReadBackBuffer; @@ -654,6 +660,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe Renderer::DescriptorSetResource globalSet; Renderer::DescriptorSetResource modelSet; Renderer::DescriptorSetResource fillSet; + Renderer::DescriptorSetResource createIndirectSet; Renderer::DescriptorSetResource drawSet; }; @@ -685,11 +692,14 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS); + data.culledInstanceCountsBuffer = builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::TRANSFER | BufferUsage::COMPUTE); + + builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.globalSet = builder.Use(resources.globalDescriptorSet); data.modelSet = builder.Use(resources.modelDescriptorSet); data.fillSet = builder.Use(_opaqueCullingResources.GetGeometryFillDescriptorSet()); + data.createIndirectSet = builder.Use(_opaqueCullingResources.GetCreateIndirectAfterCullDescriptorSet()); return true; // Return true from setup to enable this pass, return false to disable it }, @@ -713,6 +723,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.drawCallsBuffer = data.drawCallsBuffer; params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; + params.culledInstanceCountsBuffer = data.culledInstanceCountsBuffer; params.drawCountBuffer = data.drawCountBuffer; params.triangleCountBuffer = data.triangleCountBuffer; @@ -721,6 +732,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.globalDescriptorSet = data.globalSet; params.fillDescriptorSet = data.fillSet; + params.createIndirectDescriptorSet = data.createIndirectSet; params.drawDescriptorSet = data.drawSet; params.drawCallback = [&](DrawParams& drawParams) @@ -732,6 +744,9 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe Draw(resources, frameIndex, graphResources, commandList, drawParams); }; + params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); + params.drawCallDataSize = sizeof(DrawCallData); + params.numCascades = numCascades; params.biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index d678053..7f1d14e 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -370,6 +370,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR u32 occlusionEnabled; u32 bitMaskBufferSizePerView; u32 currentBitmaskIndex; + u32 debugDrawView; }; vec2 viewportSize = _renderer->GetRenderSize(); @@ -382,6 +383,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR const u32 cellCount = static_cast(_cellDatas.Count()); cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); cullConstants->currentBitmaskIndex = frameIndex; + cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); diff --git a/Source/Shaders/Shaders/Terrain/Culling.cs.slang b/Source/Shaders/Shaders/Terrain/Culling.cs.slang index ed90270..5844d90 100644 --- a/Source/Shaders/Shaders/Terrain/Culling.cs.slang +++ b/Source/Shaders/Shaders/Terrain/Culling.cs.slang @@ -15,6 +15,7 @@ struct Constants uint occlusionCull; uint bitMaskBufferSizePerView; uint currentBitmaskIndex; + uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off }; struct HeightRange @@ -100,7 +101,8 @@ struct BitmaskInput void CullForCamera(DrawInput drawInput, Camera camera, - BitmaskInput bitmaskInput) + BitmaskInput bitmaskInput, + bool debugDrawResults) { bool isVisible = true; @@ -131,19 +133,17 @@ void CullForCamera(DrawInput drawInput, } } - // Debug draw AABB boxes - /*float3 cameraPos = _cameras[0].eyePosition.xyz; - float3 aabbPos = (drawInput.aabb.min + drawInput.aabb.max) / 2.0f; - float distanceToCamera = distance(cameraPos, aabbPos); - - if (isVisible) + if (debugDrawResults) { - DrawAABB3D(drawInput.aabb, DebugColor::GREEN); + if (isVisible) + { + DrawAABB3D(drawInput.aabb, DebugColor::GREEN); + } + else + { + DrawAABB3D(drawInput.aabb, DebugColor::RED); + } } - else - { - DrawAABB3D(drawInput.aabb, DebugColor::RED); - }*/ } [shader("compute")] @@ -177,7 +177,7 @@ void main(CSInput input) // Main camera view { - CullForCamera(drawInput, _cameras[0], bitmaskInput); + CullForCamera(drawInput, _cameras[0], bitmaskInput, _constants.debugDrawView == 0); } // Shadow cascades @@ -189,6 +189,7 @@ void main(CSInput input) CullForCamera(drawInput, _cameras[i], - bitmaskInput); + bitmaskInput, + _constants.debugDrawView == i); } } \ No newline at end of file diff --git a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang index 1bb72dc..9f26493 100644 --- a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang +++ b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang @@ -21,6 +21,9 @@ struct Constants uint cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... uint debugDrawColliders; uint currentBitmaskIndex; + uint numCascades; + uint bitMaskBufferUintsPerView; + uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off }; struct InstanceRef @@ -193,6 +196,34 @@ struct CullOutput RWStructuredBuffer instanceLookupTable; }; +#if USE_BITMASKS +// Shadow cascades only need the visibility bitmask, the per-view draw sets are filled from it later in the geometry pass +void CullForCascade(DrawInput drawInput, Camera camera, uint bitmaskOffset, bool debugDrawResults) +{ + bool isVisible = drawInput.instanceCount > 0 && IsSphereInsideFrustum(camera, drawInput.sphere); + + uint bitMask = WaveActiveBallot(isVisible).x; + + // The first thread writes the bitmask + if (drawInput.csInput.groupThreadID.x == 0) + { + WriteCurrentBitmask(drawInput.csInput.groupID.x + bitmaskOffset, bitMask); + } + + if (debugDrawResults) + { + if (isVisible) + { + DrawAABB3D(drawInput.aabb, DebugColor::GREEN); + } + else + { + DrawAABB3D(drawInput.aabb, DebugColor::RED); + } + } +} +#endif + void CullForCamera(DrawInput drawInput, Camera camera, #if USE_BITMASKS @@ -383,4 +414,13 @@ void main(CSInput input) #endif cullOutput); } + +#if USE_BITMASKS + // Shadow cascades + for (uint i = 1; i < _constants.numCascades + 1; i++) + { + bool debugDrawResults = (_constants.debugDrawView == i); + CullForCascade(drawInput, _cameras[i], _constants.bitMaskBufferUintsPerView * i, debugDrawResults); + } +#endif } \ No newline at end of file diff --git a/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang b/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang index 5dffa65..1699b07 100644 --- a/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang +++ b/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang @@ -12,6 +12,7 @@ struct Constants uint baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored uint drawCallDataSize; uint currentBitmaskIndex; + uint bitmaskOffset; // Selects which view's bitmask slice to fill from }; struct InstanceRef @@ -64,11 +65,11 @@ void main(CSInput input) uint bitMask; if (_constants.currentBitmaskIndex == 0) { - bitMask = _culledDrawCallsBitMask0[input.groupID.x]; + bitMask = _culledDrawCallsBitMask0[input.groupID.x + _constants.bitmaskOffset]; } else { - bitMask = _culledDrawCallsBitMask1[input.groupID.x]; + bitMask = _culledDrawCallsBitMask1[input.groupID.x + _constants.bitmaskOffset]; } uint bitIndex = input.groupThreadID.x; From 7639ab4af7da6c57146381030921429aefb4e210 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:05:20 +0200 Subject: [PATCH 04/24] Fix cascade selection, far fade, PCF noise, PCSS reversed-Z and wire filter cvars --- .../Rendering/Material/MaterialRenderer.cpp | 5 ++++ .../Rendering/Shadow/ShadowRenderer.cpp | 4 +-- .../Shaders/Include/Lighting.inc.slang | 13 +++++++--- .../Shaders/Shaders/Include/Shadows.inc.slang | 26 ++++++++++--------- .../Shaders/Material/MaterialPass.cs.slang | 11 ++++---- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index 7e492a0..70b6bd0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -204,6 +204,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Color patchEdgeColor; Color vertexColor; Color brushColor; + vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, zw = UNUSED }; Constants* constants = graphResources.FrameNew(); @@ -236,6 +237,10 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende constants->vertexColor = terrainTools->GetVertexColor(); constants->brushColor = terrainTools->GetBrushColor(); + f32 shadowFilterSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterSize")); + f32 shadowFilterPenumbraSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterPenumbraSize")); + constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, 0.0f, 0.0f); + commandList.PushConstant(constants, 0, sizeof(Constants)); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index c93bdbe..0552ee0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -131,8 +131,8 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) Renderer::SamplerDesc samplerDesc; samplerDesc.enabled = true; samplerDesc.filter = Renderer::SamplerFilter::MIN_MAG_MIP_LINEAR; - samplerDesc.addressU = Renderer::TextureAddressMode::WRAP; - samplerDesc.addressV = Renderer::TextureAddressMode::WRAP; + samplerDesc.addressU = Renderer::TextureAddressMode::CLAMP; + samplerDesc.addressV = Renderer::TextureAddressMode::CLAMP; samplerDesc.addressW = Renderer::TextureAddressMode::CLAMP; samplerDesc.shaderVisibility = Renderer::ShaderVisibility::PIXEL; samplerDesc.comparisonEnabled = true; diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index 591166c..d279af2 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -54,14 +54,21 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte lightColor *= light.color.a; // Intensity in alpha channel // Directional Light Shadows - if (shadowSettings.enableShadows) + if (shadowSettings.enableShadows && numCascades > 0) { shadowSettings.cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); Camera cascadeCamera = _cameras[shadowSettings.cascadeIndex + 1]; // +1 because the first camera is the main camera - + float4 shadowPosition = mul(float4(pixelVertexData.worldPos, 1.0f), cascadeCamera.worldToClip); - + float shadowFactor = GetShadowFactor(uv, shadowPosition, shadowSettings); + + // Fade to unshadowed over the last 15% of the last cascade, beyond it there is no shadow data + float lastSplitDepth = _cameras[numCascades].eyePosition.w; + float fadeStartDepth = 0.85f * lastSplitDepth; + float fade = saturate((pixelVertexData.viewPos.z - fadeStartDepth) / (lastSplitDepth - fadeStartDepth)); + shadowFactor = lerp(shadowFactor, 1.0f, fade); + lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); } diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index 3763f4b..c5913be 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -57,12 +57,13 @@ float FilterPCF(float2 screenUV, float4 shadowCoord, ShadowSettings shadowSettin float dx = scale * (1.0f / texDim.x); float dy = scale * (1.0f / texDim.y); - // Calculate directions for the filtering - uint4 u = uint4(screenUV, uint(screenUV.x) ^ uint(screenUV.y), uint(screenUV.x) + uint(screenUV.y)); - float3 rand = normalize(PCG3DHash(u.xyz)); + // Calculate a random per-pixel rotation for the filtering + uint2 pixelPos = uint2(screenUV * texDim); + uint3 hash = PCG3DHash(uint3(pixelPos, pixelPos.x ^ pixelPos.y)); + float2 rand = float2(hash.xy & 0xFFFFu) / 65535.0f; - float2 dirA = normalize(rand.xy); - float2 dirB = normalize(float2(-dirA.y, dirA.x)); + float2 dirA = normalize(rand * 2.0f - 1.0f); + float2 dirB = float2(-dirA.y, dirA.x); dirA *= dx; dirB *= dy; @@ -110,7 +111,8 @@ float2 VogelDiskSample(int sampleIndex, int samplesCount, float phi) // This should be tuned for the visuals we want float AvgBlockersDepthToPenumbra(float shadowMapViewZ, float avgBlockersDepth) { - float penumbra = (shadowMapViewZ - avgBlockersDepth) / avgBlockersDepth; + // Reversed Z, greater depth = closer to the light, so blocker - receiver is the occlusion gap + float penumbra = (avgBlockersDepth - shadowMapViewZ) / max(1.0f - avgBlockersDepth, 1e-4f); penumbra *= penumbra; return saturate(80.0f * penumbra); } @@ -125,10 +127,10 @@ float Penumbra(float gradientNoise, float2 shadowMapUV, float shadowMapViewZ, fl float2 sampleUV = VogelDiskSample(i, samplesCount, gradientNoise); sampleUV = shadowMapUV + penumbraFilterSize * sampleUV; - // _shadowCascadeRTs[shadowCascadeIndex].SampleCmpLevelZero(_shadowSampler, sc.xy, sc.z); float sampleDepth = _shadowCascadeRTs[shadowCascadeIndex].SampleLevel(_shadowPointClampSampler, sampleUV, 0).x; - if (sampleDepth < shadowMapViewZ) + // Reversed Z, blockers are closer to the light than the receiver and thus have greater depth + if (sampleDepth > shadowMapViewZ) { avgBlockersDepth += sampleDepth; blockersCount += 1.0f; @@ -153,13 +155,12 @@ float FilterPCSS(float2 screenUV, float4 P, ShadowSettings shadowSettings) float4 shadowCoord = P / P.w; shadowCoord.xy = shadowCoord.xy * 0.5f + 0.5f; - shadowCoord.y = -shadowCoord.y; + shadowCoord.y = 1.0f - shadowCoord.y; // Same Y flip as TextureProj, keeps all UVs in [0, 1] const float tau = 6.28318; float gradientNoise = tau * InterleavedGradientNoise(screenUV * texDim); float shadow = 0.0f; - //if (shadowCoord.z > -1.0f && shadowCoord.z < 1.0f) { float penumbra = 1.0f - Penumbra(gradientNoise, shadowCoord.xy, shadowCoord.z, shadowSettings.penumbraFilterSize, 16, shadowSettings.cascadeIndex); @@ -195,15 +196,16 @@ float GetShadowFactor(float2 screenUV, float4 shadowCoord, ShadowSettings shadow #endif } +// Returns a 0-based cascade index, clamped to the last cascade for depths beyond its split uint GetShadowCascadeIndexFromDepth(float depth, uint numCascades) { uint cascadeIndex = 0; - for (int i = numCascades; i > 0; i--) + for (uint i = 1; i < numCascades; i++) { + // _cameras[i].eyePosition.w holds the split depth of cascade i - 1 if (depth > _cameras[i].eyePosition.w) { cascadeIndex = i; - break; } } return cascadeIndex; diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 68864f1..850e058 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -28,6 +28,7 @@ struct Constants float4 patchEdgeColor; float4 vertexColor; float4 brushColor; + float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, zw = UNUSED }; [[vk::push_constant]] Constants _constants; @@ -136,11 +137,10 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit // Apply vertex color color.rgb *= pixelVertexData.color; - // TODO: Don't hardcode this ShadowSettings shadowSettings; shadowSettings.enableShadows = _constants.lightInfo.w == 1; - shadowSettings.filterSize = 3.0f; - shadowSettings.penumbraFilterSize = 3.0f; + shadowSettings.filterSize = _constants.shadowFilterSettings.x; + shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); @@ -282,11 +282,10 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB color = BlendModel(blendingMode, color, shadedColor); // Or on this } - // TODO: Don't hardcode this ShadowSettings shadowSettings; shadowSettings.enableShadows = _constants.lightInfo.w == 1; - shadowSettings.filterSize = 3.0f; - shadowSettings.penumbraFilterSize = 3.0f; + shadowSettings.filterSize = _constants.shadowFilterSettings.x; + shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); From 14db80303acc26762b8bc55baf3aa610808ac06a Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:06:33 +0200 Subject: [PATCH 05/24] Enable shadows by default with 4 cascades --- .../Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp | 2 +- Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp index 16f8547..9ae796a 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp @@ -19,7 +19,7 @@ AutoCVar_Int CVAR_ShadowsStable(CVarCategory::Client | CVarCategory::Rendering, "shadowStable", "stable shadows", 1, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_ShadowsFreezeCascades(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades", "freeze cascade cameras and culling planes for debugging", 0, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 0); +AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 4); AutoCVar_Float CVAR_ShadowCascadeSplitLambda(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda", "split lambda for cascades, between 0.0f and 1.0f", 0.8f); AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side), only applies to cascades created after it is set", 4096); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 0552ee0..d5197af 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -13,7 +13,7 @@ #include -AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 1, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_ShadowDebugMatrices(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatrices", "debug shadow matrices by applying them to the camera", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_ShadowDebugMatrixIndex(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatricesIndex", "index of the cascade to debug", 0); From a9db10ff89637118b4ef38d8fcd0f161f130d256 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:17:47 +0200 Subject: [PATCH 06/24] Update Engine submodule --- Submodules/Engine | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submodules/Engine b/Submodules/Engine index 898c994..55d000c 160000 --- a/Submodules/Engine +++ b/Submodules/Engine @@ -1 +1 @@ -Subproject commit 898c99412c70ecc24cb9fbc5a73457924997ada7 +Subproject commit 55d000c5de19d504568eb2492907ac54ead8ae3b From 1666e9426a68b905a59f3987e290e7a6876a916d Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:31:42 +0200 Subject: [PATCH 07/24] Clock editor exponential speed steps, add sunDebugFullRotation cvar --- .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 38 ++++++++++++------- .../Resources/Scripts/Editor/ClockEditor.luau | 18 ++++++--- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index c700d2e..0f7bba0 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -17,6 +17,9 @@ #include #include +#include + +AutoCVar_Int CVAR_SunDebugFullRotation(CVarCategory::Client | CVarCategory::Rendering, "sunDebugFullRotation", "debug: sun does a full rotation per day instead of the authored wobble", 0, CVarFlags::EditCheckbox); namespace ECS::Systems { @@ -342,24 +345,33 @@ namespace ECS::Systems }; f32 progressDayAndNight = timeOfDay / 86400.0f; - u32 currentPhiIndex = static_cast(progressDayAndNight / 0.25f); - u32 nextPhiIndex = 0; - - if (currentPhiIndex < 3) - nextPhiIndex = currentPhiIndex + 1; - // Lerp between the current value of phi and the next value of phi + if (CVAR_SunDebugFullRotation.Get()) + { + // Full rotation per day, midnight (progress 0) puts the sun straight down, noon straight up + phiValue = glm::pi() + progressDayAndNight * glm::two_pi(); + } + else { - f32 currentTimestamp = currentPhiIndex * 0.25f; - f32 nextTimestamp = nextPhiIndex * 0.25f; + u32 currentPhiIndex = static_cast(progressDayAndNight / 0.25f); + u32 nextPhiIndex = 0; - f32 transitionTime = 0.25f; - f32 transitionProgress = (progressDayAndNight / 0.25f) - currentPhiIndex; + if (currentPhiIndex < 3) + nextPhiIndex = currentPhiIndex + 1; - f32 currentPhiValue = phiTable[currentPhiIndex]; - f32 nextPhiValue = phiTable[nextPhiIndex]; + // Lerp between the current value of phi and the next value of phi + { + f32 currentTimestamp = currentPhiIndex * 0.25f; + f32 nextTimestamp = nextPhiIndex * 0.25f; + + f32 transitionTime = 0.25f; + f32 transitionProgress = (progressDayAndNight / 0.25f) - currentPhiIndex; - phiValue = glm::mix(currentPhiValue, nextPhiValue, transitionProgress); + f32 currentPhiValue = phiTable[currentPhiIndex]; + f32 nextPhiValue = phiTable[nextPhiIndex]; + + phiValue = glm::mix(currentPhiValue, nextPhiValue, transitionProgress); + } } // Convert from Spherical Position to Cartesian coordinates diff --git a/Source/Resources/Scripts/Editor/ClockEditor.luau b/Source/Resources/Scripts/Editor/ClockEditor.luau index 3ed676e..d4e1216 100644 --- a/Source/Resources/Scripts/Editor/ClockEditor.luau +++ b/Source/Resources/Scripts/Editor/ClockEditor.luau @@ -5,6 +5,9 @@ Layer 1100 sits above the dev top bar (1000) so the window draws over the bar and open dropdowns. Speed control is +/- step buttons rather than a slider: the Slider widget's inner-panel anchoring doesn't compose with non-default anchors on the slider itself, follow-up once NumericInput exists. + +Speed steps are x10 per click: the day is 86400 real seconds, so additive +steps around 1x are imperceptible. 0x (paused) sits below 1x on the way down. ]] local EditorButton = require("@src/API/UI/EditorButton") @@ -20,7 +23,7 @@ local ROW_H = 20 local SIDE_PAD = 6 local ROW3_PAD = 5 local BTN_TEXT_TPL = "DevToolsText" -local SPEED_STEP = 0.25 +local SPEED_FACTOR = 10.0 local LAYER = 1100 local WINDOW_BG_COLOR = vector.create(0.10, 0.10, 0.12) @@ -53,7 +56,7 @@ local function FormatTime(seconds) end local function FormatSpeed(speed) - return string.format("Speed: %.2fx", speed) + return string.format("Speed: %gx", speed) end local canvas = UI.GetCanvas("ClockEditor", 0, 0, 1920, 1080, false) @@ -123,7 +126,7 @@ speedUpBtn.text:SetPosX(1) -- Row 3: Speed readout (left) and HH:MM:SS readout (right). local row3Y = row2Y - ROW_H - ROW3_PAD -local speedText = windowPanel:NewText("Speed: 1.00x", SIDE_PAD, row3Y, 1, BTN_TEXT_TPL) +local speedText = windowPanel:NewText("Speed: 1x", SIDE_PAD, row3Y, 1, BTN_TEXT_TPL) speedText:SetAnchor(0.0, 1.0) speedText:SetRelativePoint(0.0, 1.0) @@ -144,11 +147,16 @@ resetSpeedBtn:SetOnMouseUp(function() RefreshSpeedText() end) speedDownBtn:SetOnMouseUp(function() - Time.SetSpeedModifier(math.max(0.0, Time.GetSpeedModifier() - SPEED_STEP)) + local speed = Time.GetSpeedModifier() / SPEED_FACTOR + if speed < 1.0 then + speed = 0.0 -- Pause instead of crawling below 1x + end + Time.SetSpeedModifier(speed) RefreshSpeedText() end) speedUpBtn:SetOnMouseUp(function() - Time.SetSpeedModifier(Time.GetSpeedModifier() + SPEED_STEP) + local speed = math.max(1.0, Time.GetSpeedModifier() * SPEED_FACTOR) + Time.SetSpeedModifier(math.min(speed, Time.SecondsPerDay)) -- Cap at a full day per real second RefreshSpeedText() end) From f5db236597f40551a241b8199269e17ce2ef014f Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:37:39 +0200 Subject: [PATCH 08/24] Draw sun disc in skybox at the directional light position --- .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 1 + .../Rendering/Skybox/SkyboxRenderer.cpp | 11 ++++++++++ .../Rendering/Skybox/SkyboxRenderer.h | 2 ++ Source/Shaders/Shaders/Skybox/Skybox.ps.slang | 22 +++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index 0f7bba0..d627322 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -326,6 +326,7 @@ namespace ECS::Systems SkyboxRenderer* skyboxRenderer = ServiceLocator::GetGameRenderer()->GetSkyboxRenderer(); skyboxRenderer->SetSkybandColors(areaLightInfo.finalColorData.skybandTopColor, areaLightInfo.finalColorData.skybandMiddleColor, areaLightInfo.finalColorData.skybandBottomColor, areaLightInfo.finalColorData.skybandAboveHorizonColor, areaLightInfo.finalColorData.skybandHorizonColor); + skyboxRenderer->SetSunDirection(-direction); // direction is the direction light travels, the sun sits opposite *CVarSystem::Get()->GetVecFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogColor"_h) = vec4(areaLightInfo.finalColorData.fogColor, 1.0f); *CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogBlendBegin"_h) = areaLightInfo.finalColorData.fogEnd * areaLightInfo.finalColorData.fogScaler; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp index b641a02..8cca132 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp @@ -6,11 +6,15 @@ #include "Game-Lib/Util/ServiceLocator.h" #include "Game-Lib/Application/EnttRegistries.h" +#include + #include #include #include +AutoCVar_Int CVAR_SkyboxDrawSun(CVarCategory::Client | CVarCategory::Rendering, "skyboxDrawSun", "draw a sun disc in the skybox at the directional light's position", 1, CVarFlags::EditCheckbox); + SkyboxRenderer::SkyboxRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer) : _renderer(renderer) , _gameRenderer(gameRenderer) @@ -73,6 +77,7 @@ void SkyboxRenderer::AddSkyboxPass(Renderer::RenderGraph* renderGraph, RenderRes commandList.BindDescriptorSet(data.globalSet, frameIndex); // Skyband Color Push Constant + _skybandColors.sunDirection.w = static_cast(CVAR_SkyboxDrawSun.Get()); commandList.PushConstant(&_skybandColors, 0, sizeof(SkybandColors)); // NumVertices hardcoded as we use a Fullscreen Triangle (Check FullscreenTriangle.vs for more information) @@ -92,6 +97,12 @@ void SkyboxRenderer::SetSkybandColors(const vec3& skyTopColor, const vec3& skyMi _skybandColors.horizon = vec4(skyHorizonColor, 0.0f); } +void SkyboxRenderer::SetSunDirection(const vec3& directionToSun) +{ + f32 enabled = _skybandColors.sunDirection.w; + _skybandColors.sunDirection = vec4(glm::normalize(directionToSun), enabled); +} + void SkyboxRenderer::CreatePermanentResources() { Renderer::GraphicsPipelineDesc pipelineDesc; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h index 453a81a..31b0611 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h @@ -24,6 +24,7 @@ class SkyboxRenderer vec4 bottom = vec4(0.60f, 0.86f, 0.96f, 0.0f); vec4 aboveHorizon = vec4(0.69f, 0.85f, 0.88f, 0.0f); vec4 horizon = vec4(0.71f, 0.71f, 0.71f, 0.0f); + vec4 sunDirection = vec4(0.0f, 1.0f, 0.0f, 0.0f); // xyz = direction to the sun, w = enabled }; public: @@ -35,6 +36,7 @@ class SkyboxRenderer void AddSkyboxPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void SetSkybandColors(const vec3& skyTopColor, const vec3& skyMiddleColor, const vec3& skyBottomColor, const vec3& skyAboveHorizonColor, const vec3& skyHorizonColor); + void SetSunDirection(const vec3& directionToSun); private: void CreatePermanentResources(); diff --git a/Source/Shaders/Shaders/Skybox/Skybox.ps.slang b/Source/Shaders/Shaders/Skybox/Skybox.ps.slang index 0b8a932..9e3edb6 100644 --- a/Source/Shaders/Shaders/Skybox/Skybox.ps.slang +++ b/Source/Shaders/Shaders/Skybox/Skybox.ps.slang @@ -10,6 +10,7 @@ struct SkybandColors float4 bottom; float4 aboveHorizon; float4 horizon; + float4 sunDirection; // xyz = direction to the sun, w = enabled }; [[vk::push_constant]] SkybandColors _skybandColors; @@ -62,6 +63,27 @@ PSOutput main(VertexOutput input) : SV_Target color = lerp(_skybandColors.middle, _skybandColors.top, blendFactor); } + // Sun disc where the view ray aligns with the directional light + if (_skybandColors.sunDirection.w > 0.0f) + { + // Same NDC convention as ScreenToView in Culling.inc.slang + float2 ndc = float2(input.uv.x, 1.0f - input.uv.y) * 2.0f - 1.0f; + float4 nearPoint = mul(float4(ndc, 1.0f, 1.0f), _cameras[0].clipToWorld); // Reversed Z, depth 1 is the near plane + float3 viewDir = normalize((nearPoint.xyz / nearPoint.w) - _cameras[0].eyePosition.xyz); + + float cosSun = dot(viewDir, _skybandColors.sunDirection.xyz); + + const float discEdge = 0.9993f; // ~2.1 degree angular radius + const float glowEdge = 0.997f; + + float disc = smoothstep(discEdge, discEdge + 0.0003f, cosSun); + float glow = saturate((cosSun - glowEdge) / (1.0f - glowEdge)); + glow = glow * glow * 0.3f; + + const float3 sunColor = float3(1.0f, 0.95f, 0.8f); + color.rgb = lerp(color.rgb, sunColor, saturate(disc + glow)); + } + PSOutput output; output.color = float4(color.rgb, 1.0f); return output; From 3a302483e69e943cbe0a69773d99aa640bac0232 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 11:40:50 +0200 Subject: [PATCH 09/24] Build skybox sun ray in view space to avoid world-space precision noise --- Source/Shaders/Shaders/Skybox/Skybox.ps.slang | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Shaders/Shaders/Skybox/Skybox.ps.slang b/Source/Shaders/Shaders/Skybox/Skybox.ps.slang index 9e3edb6..e54d0ac 100644 --- a/Source/Shaders/Shaders/Skybox/Skybox.ps.slang +++ b/Source/Shaders/Shaders/Skybox/Skybox.ps.slang @@ -68,8 +68,13 @@ PSOutput main(VertexOutput input) : SV_Target { // Same NDC convention as ScreenToView in Culling.inc.slang float2 ndc = float2(input.uv.x, 1.0f - input.uv.y) * 2.0f - 1.0f; - float4 nearPoint = mul(float4(ndc, 1.0f, 1.0f), _cameras[0].clipToWorld); // Reversed Z, depth 1 is the near plane - float3 viewDir = normalize((nearPoint.xyz / nearPoint.w) - _cameras[0].eyePosition.xyz); + + // Build the ray in view space (camera at origin) and rotate it to world as a direction, + // going through clipToWorld instead subtracts two huge world positions and the + // cancellation noise makes the disc shimmer and facet + float4 viewPos = mul(float4(ndc, 1.0f, 1.0f), _cameras[0].clipToView); // Reversed Z, depth 1 is the near plane + float3 viewRay = normalize(viewPos.xyz / viewPos.w); + float3 viewDir = normalize(mul(float4(viewRay, 0.0f), _cameras[0].viewToWorld).xyz); float cosSun = dot(viewDir, _skybandColors.sunDirection.xyz); From c7fda12087bcf48db482aacab4daf415661a143d Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 12:48:26 +0200 Subject: [PATCH 10/24] Standardize light direction, fade shadows at night, clamp cascade distance --- .../Systems/CalculateShadowCameraMatrices.cpp | 23 +++++++++++++------ .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 13 +++++++---- .../Game-Lib/Rendering/CulledRenderer.cpp | 4 ++++ .../Rendering/Material/MaterialRenderer.cpp | 7 +++--- .../Rendering/Shadow/ShadowRenderer.cpp | 1 + .../Shaders/Include/Lighting.inc.slang | 3 +++ .../Shaders/Shaders/Include/Shadows.inc.slang | 1 + .../Shaders/Material/MaterialPass.cs.slang | 4 +++- 8 files changed, 41 insertions(+), 15 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp index 9ae796a..f146790 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp @@ -21,6 +21,7 @@ AutoCVar_Int CVAR_ShadowsFreezeCascades(CVarCategory::Client | CVarCategory::Ren AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 4); AutoCVar_Float CVAR_ShadowCascadeSplitLambda(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda", "split lambda for cascades, between 0.0f and 1.0f", 0.8f); +AutoCVar_Float CVAR_ShadowMaxDistance(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance", "distance the cascades are distributed over, shadows fade out at the end of it", 1000.0f); AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side), only applies to cascades created after it is set", 4096); @@ -101,8 +102,11 @@ namespace ECS::Systems f32 clipRange = farClip - nearClip; - f32 minZ = nearClip; - f32 maxZ = nearClip + clipRange; + // Distribute the cascades up to shadowMaxDistance instead of the full view distance. + // The log split is computed from at least 1.0, a tiny near plane makes the log terms + // vanish and degenerates the distribution to its uniform part + f32 minZ = Math::Max(nearClip, 1.0f); + f32 maxZ = Math::Min(nearClip + clipRange, CVAR_ShadowMaxDistance.GetFloat()); f32 range = maxZ - minZ; f32 ratio = maxZ / minZ; @@ -170,8 +174,13 @@ namespace ECS::Systems vec3 maxExtents; if (stableShadows) { - // This needs to be constant for it to be stable + // This needs to be constant for it to be stable, fall back to Z when the light is + // near vertical or lookAt degenerates (sun straight above at noon) upDir = vec3(0.0f, 1.0f, 0.0f); + if (glm::abs(lightDirection.y) > 0.99f) + { + upDir = vec3(0.0f, 0.0f, 1.0f); + } // Calculate the radius of a bounding sphere surrounding the frustum corners f32 sphereRadius = 0.0f; @@ -188,9 +197,9 @@ namespace ECS::Systems } else { - // Create a temporary view matrix for the light + // Create a temporary view matrix for the light, looking along the direction the light travels vec3 lightCameraPos = frustumCenter; - vec3 lookAt = frustumCenter - lightDirection; + vec3 lookAt = frustumCenter + lightDirection; mat4x4 lightView = glm::lookAt(lightCameraPos, lookAt, upDir); // Calculate an AABB around the frustum corners @@ -217,8 +226,8 @@ namespace ECS::Systems maxExtents.y *= scale; } - // Get postion of the shadow camera - vec3 shadowCameraPos = frustumCenter + lightDirection * -minExtents.z; + // Get postion of the shadow camera, minExtents.z is negative so this places it on the sun side + vec3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; // Store the far and near planes f32 farPlane = maxExtents.z; diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index d627322..b497f66 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -328,7 +328,12 @@ namespace ECS::Systems skyboxRenderer->SetSkybandColors(areaLightInfo.finalColorData.skybandTopColor, areaLightInfo.finalColorData.skybandMiddleColor, areaLightInfo.finalColorData.skybandBottomColor, areaLightInfo.finalColorData.skybandAboveHorizonColor, areaLightInfo.finalColorData.skybandHorizonColor); skyboxRenderer->SetSunDirection(-direction); // direction is the direction light travels, the sun sits opposite - *CVarSystem::Get()->GetVecFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogColor"_h) = vec4(areaLightInfo.finalColorData.fogColor, 1.0f); + // Fade shadows out as the sun approaches the horizon, below it the cascades would project the underside of the world + f32 sunElevationSin = -direction.y; // Positive while the sun is above the horizon + f32 shadowStrength = glm::clamp(sunElevationSin / 0.1f, 0.0f, 1.0f); + *CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength"_h) = shadowStrength; + + *CVarSystem::Get()->GetVecFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogColor"_h) = vec4(areaLightInfo.finalColorData.fogColor, 1.0f); *CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogBlendBegin"_h) = areaLightInfo.finalColorData.fogEnd * areaLightInfo.finalColorData.fogScaler; *CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogBlendEnd"_h) = areaLightInfo.finalColorData.fogEnd; } @@ -350,7 +355,7 @@ namespace ECS::Systems if (CVAR_SunDebugFullRotation.Get()) { // Full rotation per day, midnight (progress 0) puts the sun straight down, noon straight up - phiValue = glm::pi() + progressDayAndNight * glm::two_pi(); + phiValue = progressDayAndNight * glm::two_pi(); } else { @@ -386,7 +391,7 @@ namespace ECS::Systems f32 lightDirZ = sinPhi * sinTheta; f32 lightDirY = cosPhi; - // Can also try (X, Z, -Y) - return -vec3(lightDirX, lightDirY, lightDirZ); + // Direction the light travels (sun to ground), the authored phi table points below the horizon during the day + return vec3(lightDirX, lightDirY, lightDirZ); } } \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index d12fa92..8bda06a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -90,7 +90,11 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) const bool debugOrdered = false; params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); + params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); // CreateIndirect accumulates the surviving counts, reset or the readback stats carry over from last frame + params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); if (params.disableTwoStepCulling) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index 70b6bd0..affc719 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -204,7 +204,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Color patchEdgeColor; Color vertexColor; Color brushColor; - vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, zw = UNUSED + vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = UNUSED }; Constants* constants = graphResources.FrameNew(); @@ -214,7 +214,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende CVarSystem* cvarSystem = CVarSystem::Get(); const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); - const u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")); + const f32 shadowStrength = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength")); + const u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")) && shadowStrength > 0.0f; constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), 0, numCascades, shadowEnabled); constants->tileInfo = uvec4(_lightRenderer->CalculateNumTiles2D(outputSize), 0, 0); @@ -239,7 +240,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende f32 shadowFilterSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterSize")); f32 shadowFilterPenumbraSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterPenumbraSize")); - constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, 0.0f, 0.0f); + constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, shadowStrength, 0.0f); commandList.PushConstant(constants, 0, sizeof(Constants)); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index d5197af..53b2fdc 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -14,6 +14,7 @@ #include AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 1, CVarFlags::EditCheckbox); +AutoCVar_Float CVAR_ShadowStrength(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength", "directional shadow strength, overwritten each frame from the sun elevation", 1.0f); AutoCVar_Int CVAR_ShadowDebugMatrices(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatrices", "debug shadow matrices by applying them to the camera", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_ShadowDebugMatrixIndex(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatricesIndex", "index of the cascade to debug", 0); diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index d279af2..012d932 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -69,6 +69,9 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte float fade = saturate((pixelVertexData.viewPos.z - fadeStartDepth) / (lastSplitDepth - fadeStartDepth)); shadowFactor = lerp(shadowFactor, 1.0f, fade); + // Sun elevation strength, fades shadows out around dawn/dusk + shadowFactor = lerp(1.0f, shadowFactor, shadowSettings.strength); + lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); } diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index c5913be..93e7332 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -9,6 +9,7 @@ struct ShadowSettings bool enableShadows; float filterSize; float penumbraFilterSize; + float strength; // Driven by sun elevation, fades shadows out around dawn/dusk uint cascadeIndex; // Filled in by ApplyLighting }; diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 850e058..6fec0bf 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -28,7 +28,7 @@ struct Constants float4 patchEdgeColor; float4 vertexColor; float4 brushColor; - float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, zw = UNUSED + float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = UNUSED }; [[vk::push_constant]] Constants _constants; @@ -141,6 +141,7 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit shadowSettings.enableShadows = _constants.lightInfo.w == 1; shadowSettings.filterSize = _constants.shadowFilterSettings.x; shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; + shadowSettings.strength = _constants.shadowFilterSettings.z; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); @@ -286,6 +287,7 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB shadowSettings.enableShadows = _constants.lightInfo.w == 1; shadowSettings.filterSize = _constants.shadowFilterSettings.x; shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; + shadowSettings.strength = _constants.shadowFilterSettings.z; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); From b4265fe1a8e68d20d51a0833140d93abdcd41fc0 Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 9 Jul 2026 23:22:23 +0200 Subject: [PATCH 11/24] Add same-frame GPU SDSM shadow cascade fitting --- .../Systems/CalculateShadowCameraMatrices.cpp | 48 ++- .../Systems/CalculateShadowCameraMatrices.h | 3 + .../Editor/PerformanceDiagnostics.cpp | 30 +- .../Game-Lib/Rendering/CulledRenderer.cpp | 79 +++- .../Game-Lib/Rendering/CulledRenderer.h | 3 + .../Game-Lib/Rendering/GameRenderer.cpp | 11 +- .../Game-Lib/Rendering/GameRenderer.h | 1 + .../Rendering/Material/MaterialRenderer.cpp | 5 +- .../Rendering/Model/ModelRenderer.cpp | 255 +++++++++++- .../Game-Lib/Rendering/Model/ModelRenderer.h | 2 + .../Rendering/Shadow/ShadowRenderer.cpp | 342 +++++++++++++++- .../Rendering/Shadow/ShadowRenderer.h | 28 ++ .../Rendering/Terrain/TerrainRenderer.cpp | 267 ++++++++++++- .../Rendering/Terrain/TerrainRenderer.h | 2 + .../Shaders/Include/Lighting.inc.slang | 9 +- .../Shaders/Shaders/Include/Shadows.inc.slang | 1 + .../Shaders/Material/MaterialPass.cs.slang | 4 +- .../Shaders/Shadows/CascadeFit.cs.slang | 371 ++++++++++++++++++ .../Shaders/Shadows/DepthMinMax.cs.slang | 60 +++ .../Shaders/Shaders/Terrain/Culling.cs.slang | 2 + .../Shaders/Utils/CullingInstanced.cs.slang | 2 + 21 files changed, 1484 insertions(+), 41 deletions(-) create mode 100644 Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp index f146790..ef4dcf8 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp @@ -22,11 +22,24 @@ AutoCVar_Int CVAR_ShadowsFreezeCascades(CVarCategory::Client | CVarCategory::Ren AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 4); AutoCVar_Float CVAR_ShadowCascadeSplitLambda(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda", "split lambda for cascades, between 0.0f and 1.0f", 0.8f); AutoCVar_Float CVAR_ShadowMaxDistance(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance", "distance the cascades are distributed over, shadows fade out at the end of it", 1000.0f); +AutoCVar_Float CVAR_ShadowCasterMargin(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin", "extends cascade culling toward the sun so far-away casters with long shadows are not culled, depth clamp pancakes them onto the near plane", 2500.0f); +AutoCVar_Float CVAR_ShadowSunUpdateInterval(CVarCategory::Client | CVarCategory::Rendering, "shadowSunUpdateInterval", "game seconds between shadow sun direction updates, a continuously rotating sun re-renders every shadow texel each frame and shimmers", 120.0f); AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side), only applies to cascades created after it is set", 4096); namespace ECS::Systems { + // The shadow sun steps in discrete intervals, the visual sun stays smooth. A continuously + // rotating light invalidates the whole texel grid every frame, which snapping cannot hide + f32 GetShadowTimeOfDay(f32 timeOfDay) + { + f32 interval = CVAR_ShadowSunUpdateInterval.GetFloat(); + if (interval <= 0.0f) + return timeOfDay; + + return glm::floor(timeOfDay / interval) * interval; + } + // Converts a Gribb-Hartmann clip plane row (inside if dot(n, p) + d >= 0) to the // encoding the culling shaders expect: vec4(n, dot(n, pointOnPlane)) with normalized inward n inline vec4 ExtractPlane(const vec4& row) @@ -74,14 +87,36 @@ namespace ECS::Systems renderResources.shadowDepthCascades.push_back(cascadeDepthImage); } + CVarSystem* cvarSystem = CVarSystem::Get(); + + // Master toggle, no shadow work at all while disabled (resource growth above stays so enabling works at runtime) + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + const bool useSDSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowUseSDSM"_h) != 0; + const bool validateParity = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity"_h) != 0; + + // The GPU fit pass owns the cascade cameras while SDSM is on, the CPU copies go stale. + // On toggle-off force a full re-upload (the loop below would normally re-dirty every frame, + // but not while frozen) + static bool previousUseSDSM = useSDSM; + if (previousUseSDSM && !useSDSM) + { + renderResources.cameras.SetDirtyElements(1, numCascades); + } + previousUseSDSM = useSDSM; + + if (useSDSM && !validateParity) + return; + if (CVAR_ShadowsFreezeCascades.Get()) return; entt::registry::context& ctx = registry.ctx(); auto& dayNightCycle = ctx.get(); - // Get light settings - vec3 lightDirection = UpdateAreaLights::GetLightDirection(dayNightCycle.GetTimeInSecondsF32()); + // Get light settings, the shadow sun steps in discrete intervals + vec3 lightDirection = UpdateAreaLights::GetLightDirection(GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32())); // Get active render camera auto& activeCamera = ctx.get(); @@ -283,6 +318,10 @@ namespace ECS::Systems cascadeCamera.frustum[(size_t)FrustumPlane::Near] = ExtractPlane(row3 - row2); // Reversed Z, depth 1 is near cascadeCamera.frustum[(size_t)FrustumPlane::Far] = ExtractPlane(row2); + // The near plane sits at the shadow camera on the sun side. Casters beyond it still render + // correctly (depth clamp pancakes them), so culling must not reject them + cascadeCamera.frustum[(size_t)FrustumPlane::Near].w -= CVAR_ShadowCasterMargin.GetFloat(); + #if NC_DEBUG for (u32 j = 0; j < 6; j++) { @@ -291,7 +330,10 @@ namespace ECS::Systems NC_ASSERT(distance > 0.0f, "CalculateShadowCameraMatrices : Cascade frustum plane {0} does not contain the frustum center", j); } #endif - renderResources.cameras.SetDirtyElement(i+1); + if (!useSDSM) // In parity-validation mode the CPU result stays local for comparison, the GPU fit owns the upload + { + renderResources.cameras.SetDirtyElement(i+1); + } lastSplitDist = cascadeSplits[i]; } diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h index b036e20..15e5613 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h @@ -4,6 +4,9 @@ namespace ECS::Systems { + // Quantizes the time of day used for the shadow sun direction, see shadowSunUpdateInterval + f32 GetShadowTimeOfDay(f32 timeOfDay); + class CalculateShadowCameraMatrices { public: diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index a468f2b..60c8532 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,31 @@ namespace Editor u32 numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + // SDSM reduced depth bounds + { + GameRenderer* gameRenderer = ServiceLocator::GetGameRenderer(); + ShadowRenderer* shadowRenderer = gameRenderer->GetShadowRenderer(); + + f32 minDistance = 0.0f; + f32 maxDistance = 0.0f; + if (shadowRenderer && shadowRenderer->GetDepthBoundsViewDistances(gameRenderer->GetRenderResources(), minDistance, maxDistance)) + { + f32 shadowMaxDistance = static_cast(*CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance"_h)); + ImGui::Text("Visible Depth: %.1f - %.1f (Shadow Max: %.0f)", minDistance, maxDistance, shadowMaxDistance); + + f32 effectiveMin = 0.0f; + f32 effectiveMax = 0.0f; + if (shadowRenderer->GetEffectiveShadowRange(effectiveMin, effectiveMax)) + { + ImGui::Text("Shadow Range: %.1f - %.1f (used by cascades)", effectiveMin, effectiveMax); + } + } + else + { + ImGui::Text("Visible Depth: - (no valid samples)"); + } + } + ImGui::Spacing(); if (_showSurvivingDrawCalls || _showSurvivingTriangle) @@ -598,7 +624,7 @@ namespace Editor u32 viewDrawCalls = 0; u32 viewDrawCallsSurvived = 0; - bool viewSupportsTerrainOcclusionCulling = true; + bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Cascades draw their full surviving set in one phase, no occluders bool viewSupportsModelsOcclusionCulling = viewID == 0; bool viewRendersTerrainCulling = true; @@ -707,7 +733,7 @@ namespace Editor u32 viewTriangles = 0; u32 viewTrianglesSurvived = 0; - bool viewSupportsTerrainOcclusionCulling = true; + bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Cascades draw their full surviving set in one phase, no occluders bool viewSupportsModelsOcclusionCulling = viewID == 0; bool viewRendersTerrainCulling = true; // Only main view supports terrain culling so far diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index 8bda06a..6afbfe9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -393,6 +393,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) u32 numCascades; u32 bitMaskBufferUintsPerView; u32 debugDrawView; + u32 cullMainView; }; CullConstants* cullConstants = params.graphResources->FrameNew(); cullConstants->viewportSizeX = u32(viewportSize.x); @@ -414,6 +415,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) cullConstants->numCascades = params.numCascades; cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); + cullConstants->cullMainView = params.cullMainView; params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); params.cullingDescriptorSet.Bind("_depthPyramid"_h, params.depthPyramid); @@ -561,13 +563,88 @@ void CulledRenderer::CullingPass(CullingPassParams& params) } } +void CulledRenderer::CascadeCullingPass(CullingPassParams& params) +{ + NC_ASSERT(params.drawCallDataSize > 0, "CulledRenderer : CascadeCullingPass params provided an invalid drawCallDataSize"); + NC_ASSERT(params.cullingResources->IsInstanced(), "CulledRenderer : CascadeCullingPass only supports instanced culling resources"); + + const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); + u32 numInstances = params.cullingResources->GetNumInstances(); + + if (numDrawCalls == 0 || numInstances == 0 || params.numCascades == 0) + return; + + // Frustum-only cull of the cascade views into their bitmask slices. No counter resets (the + // per-cascade fill in the geometry pass resets and rebuilds the shared draw sets), no occlusion + std::string debugName = params.passName + " Cascade Culling"; + params.commandList->PushMarker(debugName, Color::Yellow); + + Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[1]; // Cascades require the bitmask permutation + params.commandList->BeginPipeline(pipeline); + + vec2 viewportSize = _renderer->GetRenderSize(); + + struct CullConstants + { + u32 viewportSizeX; + u32 viewportSizeY; + u32 numTotalInstances; + u32 occlusionCull; + u32 instanceCountOffset; + u32 drawCallSize; + u32 baseInstanceLookupOffset; + u32 modelIDOffset; + u32 drawCallDataSize; + u32 cullingDataIsWorldspace; + u32 debugDrawColliders; + u32 currentBitmaskIndex; + u32 numCascades; + u32 bitMaskBufferUintsPerView; + u32 debugDrawView; + u32 cullMainView; + }; + CullConstants* cullConstants = params.graphResources->FrameNew(); + cullConstants->viewportSizeX = u32(viewportSize.x); + cullConstants->viewportSizeY = u32(viewportSize.y); + cullConstants->numTotalInstances = numInstances; + cullConstants->occlusionCull = false; + + u32 instanceCountOffset = params.cullingResources->IsIndexed() ? offsetof(Renderer::IndexedIndirectDraw, Renderer::IndexedIndirectDraw::instanceCount) : offsetof(Renderer::IndirectDraw, Renderer::IndirectDraw::instanceCount); + cullConstants->instanceCountOffset = instanceCountOffset; + cullConstants->drawCallSize = params.cullingResources->IsIndexed() ? sizeof(Renderer::IndexedIndirectDraw) : sizeof(Renderer::IndirectDraw); + + cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + cullConstants->modelIDOffset = params.modelIDOffset; + cullConstants->drawCallDataSize = params.drawCallDataSize; + + cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; + cullConstants->debugDrawColliders = false; + cullConstants->currentBitmaskIndex = params.frameIndex; + cullConstants->numCascades = params.numCascades; + cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); + cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); + cullConstants->cullMainView = false; + params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); + + // _depthPyramid stays bound from the main culling pass, rebinding here would rewrite an already-bound set + + params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex); + params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); + params.commandList->BindDescriptorSet(params.cullingDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); +} + void CulledRenderer::GeometryPass(GeometryPassParams& params) { NC_ASSERT(params.drawCallback != nullptr, "CulledRenderer : GeometryPass got params with invalid drawCallback"); const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); - for (u32 i = 0; i < params.numCascades + 1; i++) + for (u32 i = params.firstViewIndex; i < params.numCascades + 1; i++) { std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); params.commandList->PushMarker(markerName, Color::PastelYellow); diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index b587b04..8146352 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -225,6 +225,7 @@ class CulledRenderer Renderer::DescriptorSetResource createIndirectAfterCullSet; u32 numCascades = 0; + bool cullMainView = true; bool occlusionCull = true; bool disableTwoStepCulling = false; @@ -308,6 +309,7 @@ class CulledRenderer u32 baseInstanceLookupOffset = 0; // Instanced only u32 drawCallDataSize = 0; // Instanced only + u32 firstViewIndex = 0; // 0 = main view first, 1 = cascades only u32 numCascades = 0; f32 biasConstantFactor = 0.0f; @@ -318,6 +320,7 @@ class CulledRenderer bool cullingEnabled = false; }; void GeometryPass(GeometryPassParams& params); + void CascadeCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets void SyncToGPU(); void BindCullingResource(CullingResourcesBase& resources); diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index 5406731..2148c75 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -341,8 +341,6 @@ f32 GameRenderer::Render() _skyboxRenderer->AddSkyboxPass(&renderGraph, _resources, _frameIndex); _modelRenderer->AddSkyboxPass(&renderGraph, _resources, _frameIndex); - _shadowRenderer->AddShadowPass(&renderGraph, _resources, _frameIndex); - // Occluder passes _terrainRenderer->AddOccluderPass(&renderGraph, _resources, _frameIndex); _modelRenderer->AddOccluderPass(&renderGraph, _resources, _frameIndex); @@ -409,6 +407,15 @@ f32 GameRenderer::Render() _liquidRenderer->AddCullingPass(&renderGraph, _resources, _frameIndex); _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); + // Cascade block, runs after the main depth is complete so cascades can be fitted, culled and drawn the same frame + _shadowRenderer->AddDepthMinMaxPass(&renderGraph, _resources, _frameIndex); + _shadowRenderer->AddCascadeFitPass(&renderGraph, _resources, _frameIndex); + _shadowRenderer->AddShadowPass(&renderGraph, _resources, _frameIndex); + _terrainRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); + _modelRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); + _terrainRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); + _modelRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); + _lightRenderer->AddClassificationPass(&renderGraph, _resources, _frameIndex); _materialRenderer->AddPreEffectsPass(&renderGraph, _resources, _frameIndex); diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h index ec0bd9f..bd38e87 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h @@ -74,6 +74,7 @@ class GameRenderer LiquidRenderer* GetLiquidRenderer() { return _liquidRenderer; } MaterialRenderer* GetMaterialRenderer() { return _materialRenderer; } ModelRenderer* GetModelRenderer() { return _modelRenderer; } + ShadowRenderer* GetShadowRenderer() { return _shadowRenderer; } TerrainRenderer* GetTerrainRenderer() { return _terrainRenderer; } TextureRenderer* GetTextureRenderer() { return _textureRenderer; } SkyboxRenderer* GetSkyboxRenderer() { return _skyboxRenderer; } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index affc719..a8ba300 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -204,7 +204,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Color patchEdgeColor; Color vertexColor; Color brushColor; - vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = UNUSED + vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias }; Constants* constants = graphResources.FrameNew(); @@ -240,7 +240,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende f32 shadowFilterSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterSize")); f32 shadowFilterPenumbraSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterPenumbraSize")); - constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, shadowStrength, 0.0f); + f32 shadowNormalOffsetBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias")); + constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, shadowStrength, shadowNormalOffsetBias); commandList.PushConstant(constants, 0, sizeof(Constants)); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 3fbb64a..88e18a5 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -15,6 +15,7 @@ #include "Game-Lib/Rendering/CullUtils.h" #include "Game-Lib/Rendering/RenderUtils.h" #include "Game-Lib/Rendering/GameRenderer.h" +#include "Game-Lib/Rendering/Shadow/ShadowRenderer.h" #include "Game-Lib/Rendering/RenderResources.h" #include "Game-Lib/Rendering/Debug/DebugRenderer.h" #include "Game-Lib/Rendering/Model/ModelLoader.h" @@ -401,11 +402,7 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe CVarSystem* cvarSystem = CVarSystem::Get(); - u32 numCascades = 0; - if (CVAR_ModelsCastShadow.Get() == 1) - { - numCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"); - } + const u32 numCascades = 0; // Occluders are main view only, cascades render in their own block late in the frame struct Data { @@ -603,11 +600,8 @@ void ModelRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderRes params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.createIndirectAfterCullSet; - params.numCascades = 0; - if (CVAR_ModelsCastShadow.Get() == 1) - { - params.numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); - } + params.numCascades = 0; // Main view only, cascades are culled in their own block late in the frame + params.cullMainView = true; params.occlusionCull = CVAR_ModelOcclusionCullingEnabled.Get(); params.disableTwoStepCulling = CVAR_ModelDisableTwoStepCulling.Get(); @@ -635,11 +629,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe CVarSystem* cvarSystem = CVarSystem::Get(); const bool cullingEnabled = CVAR_ModelCullingEnabled.Get(); - u32 numCascades = 0; - if (CVAR_ModelsCastShadow.Get() == 1) - { - numCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"); - } + const u32 numCascades = 0; // Main view only, cascades render in their own block late in the frame struct Data { @@ -760,6 +750,241 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe }); } +void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + ZoneScoped; + + if (!CVAR_ModelRendererEnabled.Get()) + return; + + if (!CVAR_ModelCullingEnabled.Get()) + return; + + if (*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_ModelsCastShadow.Get() != 1) + return; + + if (_opaqueCullingResources.GetDrawCalls().Count() == 0) + return; + + u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + if (numCascades == 0) + return; + + struct Data + { + Renderer::ImageResource depthPyramid; + + Renderer::DescriptorSetResource debugSet; + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource cullingSet; + }; + + renderGraph->AddPass("Model (O) Cascade Culling", + [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) // Setup + { + using BufferUsage = Renderer::BufferPassUsage; + + data.depthPyramid = builder.Read(resources.depthPyramid, Renderer::PipelineType::COMPUTE); + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_cullingDatas.GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_instanceMatrices.GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_opaqueCullingResources.GetDrawCalls().GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_opaqueCullingResources.GetInstanceRefs().GetBuffer(), BufferUsage::COMPUTE); + + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::COMPUTE); + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::COMPUTE); // Both bitmask bindings are RW in the shader + + // Not written by the cascade dispatch (cullMainView == 0), but bound RW in the culling set + builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::COMPUTE); + builder.Write(_opaqueCullingResources.GetCulledInstanceLookupTableBuffer(), BufferUsage::COMPUTE); + + data.debugSet = builder.Use(_debugRenderer->GetDebugDescriptorSet()); + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.cullingSet = builder.Use(_opaqueCullingResources.GetCullingDescriptorSet()); + + _debugRenderer->RegisterCullingPassBufferUsage(builder); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, numCascades](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + { + GPU_SCOPED_PROFILER_ZONE(commandList, ModelCascadeCulling); + + CulledRenderer::CullingPassParams params; + params.passName = "Opaque"; + params.graphResources = &graphResources; + params.commandList = &commandList; + params.cullingResources = &_opaqueCullingResources; + params.frameIndex = frameIndex; + + params.depthPyramid = data.depthPyramid; + + params.debugDescriptorSet = data.debugSet; + params.globalDescriptorSet = data.globalSet; + params.cullingDescriptorSet = data.cullingSet; + + params.numCascades = numCascades; + params.cullMainView = false; + params.occlusionCull = false; + + params.cullingDataIsWorldspace = false; + + params.baseInstanceLookupOffset = offsetof(DrawCallData, baseInstanceLookupOffset); + params.modelIDOffset = offsetof(DrawCallData, modelID); + params.drawCallDataSize = sizeof(DrawCallData); + + CascadeCullingPass(params); + }); +} + +void ModelRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + ZoneScoped; + + if (!CVAR_ModelRendererEnabled.Get()) + return; + + if (!CVAR_ModelCullingEnabled.Get()) // Cascades are driven by the culled bitmask slices + return; + + CVarSystem* cvarSystem = CVarSystem::Get(); + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_ModelsCastShadow.Get() != 1) + return; + + if (_opaqueCullingResources.GetDrawCalls().Count() == 0) + return; + + u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + if (numCascades == 0) + return; + + struct Data + { + Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + + Renderer::BufferMutableResource drawCallsBuffer; + Renderer::BufferMutableResource culledDrawCallsBuffer; + Renderer::BufferMutableResource culledDrawCallCountBuffer; + + Renderer::BufferMutableResource culledInstanceCountsBuffer; + + Renderer::BufferMutableResource drawCountBuffer; + Renderer::BufferMutableResource triangleCountBuffer; + Renderer::BufferMutableResource drawCountReadBackBuffer; + Renderer::BufferMutableResource triangleCountReadBackBuffer; + + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource modelSet; + Renderer::DescriptorSetResource fillSet; + Renderer::DescriptorSetResource createIndirectSet; + Renderer::DescriptorSetResource drawSet; + }; + + renderGraph->AddPass("Model (O) Cascade Geometry", + [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + for (u32 i = 1; i < numCascades + 1; i++) + { + data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + } + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_indices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_textureDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_textureUnits.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceMatrices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_boneMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Read(_textureTransformMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + builder.Write(_animatedVertices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + GeometryPassSetup(data, builder, &_opaqueCullingResources, frameIndex); + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + data.culledInstanceCountsBuffer = builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::TRANSFER | BufferUsage::COMPUTE); + + builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.modelSet = builder.Use(resources.modelDescriptorSet); + data.fillSet = builder.Use(_opaqueCullingResources.GetGeometryFillDescriptorSet()); + data.createIndirectSet = builder.Use(_opaqueCullingResources.GetCreateIndirectAfterCullDescriptorSet()); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, &resources, frameIndex, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, ModelCascadeGeometry); + + CulledRenderer::GeometryPassParams params; + params.passName = "Opaque"; + params.graphResources = &graphResources; + params.commandList = &commandList; + params.cullingResources = &_opaqueCullingResources; + + params.frameIndex = frameIndex; + for (u32 i = 1; i < numCascades + 1; i++) + { + params.depth[i] = data.depth[i]; + } + + params.drawCallsBuffer = data.drawCallsBuffer; + params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; + params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; + params.culledInstanceCountsBuffer = data.culledInstanceCountsBuffer; + + params.drawCountBuffer = data.drawCountBuffer; + params.triangleCountBuffer = data.triangleCountBuffer; + params.drawCountReadBackBuffer = data.drawCountReadBackBuffer; + params.triangleCountReadBackBuffer = data.triangleCountReadBackBuffer; + + params.globalDescriptorSet = data.globalSet; + params.fillDescriptorSet = data.fillSet; + params.createIndirectDescriptorSet = data.createIndirectSet; + params.drawDescriptorSet = data.drawSet; + + params.drawCallback = [&](DrawParams& drawParams) + { + drawParams.descriptorSets = { + &data.globalSet, + &data.modelSet + }; + Draw(resources, frameIndex, graphResources, commandList, drawParams); + }; + + params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); + params.drawCallDataSize = sizeof(DrawCallData); + + params.firstViewIndex = 1; + params.numCascades = numCascades; + + params.biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); + params.biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); + params.biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); + + params.enableDrawing = CVAR_ModelDrawGeometry.Get(); + params.cullingEnabled = true; + + GeometryPass(params); + }); +} + void ModelRenderer::AddTransparencyCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { ZoneScoped; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index a07177b..8d2fb4c 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -347,6 +347,8 @@ class ModelRenderer : CulledRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddTransparencyCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddTransparencyGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 53b2fdc..1ab17ab 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -1,10 +1,18 @@ #include "ShadowRenderer.h" +#include #include +#include +#include +#include #include +#include #include #include #include #include +#include + +#include #include @@ -28,6 +36,13 @@ AutoCVar_Float CVAR_ShadowFilterPenumbraSize(CVarCategory::Client | CVarCategory AutoCVar_Float CVAR_ShadowDepthBiasConstantFactor(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant", "constant factor of depth bias to prevent shadow acne", -2.0f); AutoCVar_Float CVAR_ShadowDepthBiasClamp(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp", "clamp of depth bias to prevent shadow acne", 0.0f); AutoCVar_Float CVAR_ShadowDepthBiasSlopeFactor(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope", "slope factor of depth bias to prevent shadow acne", -5.0f); +AutoCVar_Float CVAR_ShadowNormalOffsetBias(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias", "receiver offset along the surface normal in cascade texels, fights acne on hard angles", 1.0f); + +AutoCVar_Int CVAR_ShadowUseSDSM(CVarCategory::Client | CVarCategory::Rendering, "shadowUseSDSM", "fit cascade cameras on the GPU instead of the legacy CPU path", 1, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_ShadowSDSMUseDepthBounds(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMUseDepthBounds", "distribute cascades over the visible depth range instead of the full shadow range", 1, CVarFlags::EditCheckbox); +AutoCVar_Float CVAR_ShadowSDSMQuantizeStep(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMQuantizeStep", "SDSM bounds are quantized outward to this step to keep stable snapping effective", 8.0f); +AutoCVar_Float CVAR_ShadowSDSMShrinkDelay(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMShrinkDelay", "seconds the visible range must stay smaller before the SDSM bounds shrink to it in one jump, expansion is instant", 2.5f); +AutoCVar_Int CVAR_ShadowSDSMValidateParity(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity", "compare GPU-fitted cascade cameras against the CPU math and log the max delta", 0, CVarFlags::EditCheckbox); ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) @@ -35,6 +50,8 @@ ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameR , _debugRenderer(debugRenderer) , _terrainRenderer(terrainRenderer) , _modelRenderer(modelRenderer) + , _depthMinMaxDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _cascadeFitDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) { CreatePermanentResources(resources); } @@ -47,14 +64,77 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) { ZoneScoped; + // Read back last frame's reduced depth bounds for diagnostics + { + u32* readBackData = static_cast(_renderer->MapBuffer(_depthMinMaxReadBackBuffer)); + if (readBackData != nullptr) + { + _depthMinMaxReadBack[0] = readBackData[0]; + _depthMinMaxReadBack[1] = readBackData[1]; + } + _renderer->UnmapBuffer(_depthMinMaxReadBackBuffer); + } + + _lastDeltaTime = deltaTime; + CVarSystem* cvarSystem = CVarSystem::Get(); const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); + const bool useSDSM = CVAR_ShadowUseSDSM.Get(); + + // With SDSM the cascade cameras live on the GPU, the debug tools read the (one frame old) readback copies + if (useSDSM) + { + Camera* readBackData = static_cast(_renderer->MapBuffer(_cascadeCamerasReadBackBuffer)); + if (readBackData != nullptr) + { + memcpy(_readBackCascadeCameras, readBackData, sizeof(Camera) * Renderer::Settings::MAX_SHADOW_CASCADES); + } + _renderer->UnmapBuffer(_cascadeCamerasReadBackBuffer); + + f32* stateData = static_cast(_renderer->MapBuffer(_sdsmStateReadBackBuffer)); + if (stateData != nullptr) + { + memcpy(_sdsmStateReadBack, stateData, sizeof(f32) * 8); + } + _renderer->UnmapBuffer(_sdsmStateReadBackBuffer); + } + + auto GetCascadeCamera = [&](u32 cascadeIndex) -> const Camera& + { + return useSDSM ? _readBackCascadeCameras[cascadeIndex] : resources.cameras[cascadeIndex + 1]; + }; + + if (CVAR_ShadowSDSMValidateParity.Get() && useSDSM) + { + // The CPU mirror holds the legacy math result while validation is on (computed but not uploaded). + // Only meaningful with a static camera, the readback is one frame old + f32 maxDelta = 0.0f; + for (u32 i = 0; i < numCascades; i++) + { + const mat4x4& gpuMatrix = _readBackCascadeCameras[i].worldToClip; + const mat4x4& cpuMatrix = resources.cameras[i + 1].worldToClip; + + for (u32 col = 0; col < 4; col++) + { + for (u32 row = 0; row < 4; row++) + { + maxDelta = Math::Max(maxDelta, glm::abs(gpuMatrix[col][row] - cpuMatrix[col][row])); + } + } + } + + static u32 parityLogCounter = 0; + if (parityLogCounter++ % 60 == 0) + { + NC_LOG_INFO("SDSM Parity: max worldToClip delta {0} across {1} cascades (stand still, readback is one frame old)", maxDelta, numCascades); + } + } const bool debugMatrices = CVAR_ShadowDebugMatrices.Get(); const i32 debugMatrixIndex = CVAR_ShadowDebugMatrixIndex.Get(); if (debugMatrices && debugMatrixIndex >= 0 && debugMatrixIndex < static_cast(numCascades)) { - const Camera& debugCascadeCamera = resources.cameras[debugMatrixIndex + 1]; // +1 because the first camera is the main camera + const Camera& debugCascadeCamera = GetCascadeCamera(debugMatrixIndex); Camera& mainCamera = resources.cameras[0]; @@ -64,7 +144,7 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) if (CVAR_ShadowDrawMatrices.Get()) { - Color colors[] = + Color colors[] = { Color::Red, Color::Green, @@ -78,12 +158,195 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) for (u32 i = 0; i < numCascades; i++) { - const Camera& debugCascadeCamera = resources.cameras[i + 1]; // +1 because the first camera is the main camera + const Camera& debugCascadeCamera = GetCascadeCamera(i); _debugRenderer->DrawFrustum(debugCascadeCamera.worldToClip, colors[i]); } } } +void ShadowRenderer::AddDepthMinMaxPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + struct Data + { + Renderer::DepthImageResource depth; + + Renderer::BufferMutableResource depthMinMaxBuffer; + Renderer::BufferMutableResource depthMinMaxReadBackBuffer; + + Renderer::DescriptorSetResource passSet; + }; + + CVarSystem* cvarSystem = CVarSystem::Get(); + const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); + const bool dispatchEnabled = CVAR_ShadowEnabled.Get() && numCascades > 0; + + renderGraph->AddPass("Shadow Depth MinMax", + [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); + + data.depthMinMaxBuffer = builder.Write(_depthMinMaxBuffer, BufferUsage::TRANSFER | BufferUsage::COMPUTE); + data.depthMinMaxReadBackBuffer = builder.Write(_depthMinMaxReadBackBuffer, BufferUsage::TRANSFER); + + data.passSet = builder.Use(_depthMinMaxDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, dispatchEnabled](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, ShadowDepthMinMax); + + // Reset to the sentinel, if the dispatch is skipped or every pixel is sky it survives + // and the consumers fall back to the full range + commandList.FillBuffer(data.depthMinMaxBuffer, 0, sizeof(u32), 0xFFFFFFFF); + commandList.FillBuffer(data.depthMinMaxBuffer, sizeof(u32), sizeof(u32), 0); + commandList.BufferBarrier(data.depthMinMaxBuffer, Renderer::BufferPassUsage::TRANSFER); + + if (dispatchEnabled) + { + commandList.BeginPipeline(_depthMinMaxPipeline); + + data.passSet.Bind("_depth"_h, data.depth); + commandList.BindDescriptorSet(data.passSet, frameIndex); + + uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); + commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); + + commandList.EndPipeline(_depthMinMaxPipeline); + + commandList.BufferBarrier(data.depthMinMaxBuffer, Renderer::BufferPassUsage::COMPUTE); + } + + commandList.CopyBuffer(data.depthMinMaxReadBackBuffer, 0, data.depthMinMaxBuffer, 0, sizeof(u32) * 2); + }); +} + +void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + struct Data + { + Renderer::BufferMutableResource cameras; + Renderer::BufferMutableResource sdsmStateBuffer; + Renderer::BufferMutableResource sdsmStateReadBackBuffer; + Renderer::BufferMutableResource cascadeCamerasReadBackBuffer; + + Renderer::DescriptorSetResource passSet; + }; + + CVarSystem* cvarSystem = CVarSystem::Get(); + u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + + const bool freezeCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades") != 0; + const bool dispatchEnabled = CVAR_ShadowUseSDSM.Get() && CVAR_ShadowEnabled.Get() && numCascades > 0 && !freezeCascades; + + if (!dispatchEnabled) + return; + + // Record-time inputs for the push constants, the shadow sun steps in discrete intervals + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + auto& dayNightCycle = registry->ctx().get(); + const f32 shadowTimeOfDay = ECS::Systems::GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32()); + const vec3 lightDirection = ECS::Systems::UpdateAreaLights::GetLightDirection(shadowTimeOfDay); + + renderGraph->AddPass("Shadow Cascade Fit", + [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + data.cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::TRANSFER); + builder.Read(_depthMinMaxBuffer, BufferUsage::COMPUTE); + data.sdsmStateBuffer = builder.Write(_sdsmStateBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + data.sdsmStateReadBackBuffer = builder.Write(_sdsmStateReadBackBuffer, BufferUsage::TRANSFER); + data.cascadeCamerasReadBackBuffer = builder.Write(_cascadeCamerasReadBackBuffer, BufferUsage::TRANSFER); + + data.passSet = builder.Use(_cascadeFitDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, numCascades, lightDirection, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, ShadowCascadeFit); + + struct CascadeFitConstants + { + vec4 lightDirection; + u32 numCascades; + f32 cascadeSplitLambda; + f32 cascadeTextureSize; + u32 stableShadows; + f32 shadowMaxDistance; + f32 quantizeStep; + f32 shrinkDelay; + f32 deltaTime; + u32 useDepthBounds; + f32 casterMargin; + }; + + CascadeFitConstants* constants = graphResources.FrameNew(); + constants->lightDirection = vec4(lightDirection, 0.0f); + constants->numCascades = numCascades; + constants->cascadeSplitLambda = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda")); + constants->cascadeTextureSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize")); + constants->stableShadows = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStable") != 0; + constants->shadowMaxDistance = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance")); + constants->quantizeStep = CVAR_ShadowSDSMQuantizeStep.GetFloat(); + constants->shrinkDelay = CVAR_ShadowSDSMShrinkDelay.GetFloat(); + constants->deltaTime = _lastDeltaTime; + constants->useDepthBounds = CVAR_ShadowSDSMUseDepthBounds.Get(); + constants->casterMargin = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin")); + + commandList.BeginPipeline(_cascadeFitPipeline); + + commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); + + data.passSet.Bind("_rwCameras"_h, data.cameras); + commandList.BindDescriptorSet(data.passSet, frameIndex); + + commandList.Dispatch(1, 1, 1); + + commandList.EndPipeline(_cascadeFitPipeline); + + commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.sdsmStateBuffer, Renderer::BufferPassUsage::COMPUTE); + + // Readbacks for the debug tooling, element 0 of the cameras buffer is the main camera + commandList.CopyBuffer(data.cascadeCamerasReadBackBuffer, 0, data.cameras, sizeof(Camera), sizeof(Camera) * numCascades); + commandList.CopyBuffer(data.sdsmStateReadBackBuffer, 0, data.sdsmStateBuffer, 0, sizeof(f32) * 8); + }); +} + +bool ShadowRenderer::GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const +{ + f32 usedMin = _sdsmStateReadBack[2]; + f32 usedMax = _sdsmStateReadBack[3]; + if (usedMax <= usedMin) // Not fitted yet + return false; + + outMinDistance = usedMin; + outMaxDistance = usedMax; + return true; +} + +bool ShadowRenderer::GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const +{ + if (_depthMinMaxReadBack[1] == 0) // Sentinel, no valid depth samples reduced yet + return false; + + const vec4& nearFar = resources.cameras[0].nearFar; + f32 nearClip = nearFar.x; + f32 farClip = nearFar.y; + + // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane + auto Linearize = [nearClip, farClip](f32 depth) { return (nearClip * farClip) / (nearClip + depth * (farClip - nearClip)); }; + + outMinDistance = Linearize(glm::uintBitsToFloat(_depthMinMaxReadBack[1])); // Max depth bits = nearest sample + outMaxDistance = Linearize(glm::uintBitsToFloat(_depthMinMaxReadBack[0])); // Min depth bits = farthest sample + return true; +} + void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { struct ShadowPassData @@ -149,4 +412,77 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) _shadowPointClampSampler = _renderer->CreateSampler(samplerDesc); resources.lightDescriptorSet.Bind("_shadowPointClampSampler"_h, _shadowPointClampSampler); + + // Depth min/max reduction for SDSM cascade fitting + { + Renderer::ComputePipelineDesc pipelineDesc; + pipelineDesc.debugName = "Shadow Depth MinMax"; + + Renderer::ComputeShaderDesc shaderDesc; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/DepthMinMax.cs"_h, "Shadows/DepthMinMax.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + + _depthMinMaxPipeline = _renderer->CreatePipeline(pipelineDesc); + + _depthMinMaxDescriptorSet.RegisterPipeline(_renderer, _depthMinMaxPipeline); + _depthMinMaxDescriptorSet.Init(_renderer); + + Renderer::BufferDesc bufferDesc; + bufferDesc.name = "ShadowDepthMinMax"; + bufferDesc.size = sizeof(u32) * 2; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; + + _depthMinMaxBuffer = _renderer->CreateAndFillBuffer(_depthMinMaxBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + values[0] = 0xFFFFFFFF; // Min depth bits sentinel + values[1] = 0; // Max depth bits sentinel + }); + _depthMinMaxDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); + + bufferDesc.name = "ShadowDepthMinMaxReadBack"; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; + _depthMinMaxReadBackBuffer = _renderer->CreateBuffer(_depthMinMaxReadBackBuffer, bufferDesc); + } + + // SDSM cascade fitting + { + Renderer::ComputePipelineDesc pipelineDesc; + pipelineDesc.debugName = "Shadow Cascade Fit"; + + Renderer::ComputeShaderDesc shaderDesc; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFit.cs"_h, "Shadows/CascadeFit.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + + _cascadeFitPipeline = _renderer->CreatePipeline(pipelineDesc); + + _cascadeFitDescriptorSet.RegisterPipeline(_renderer, _cascadeFitPipeline); + _cascadeFitDescriptorSet.Init(_renderer); + + Renderer::BufferDesc bufferDesc; + bufferDesc.name = "ShadowSDSMState"; + bufferDesc.size = sizeof(f32) * 8; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; + + _sdsmStateBuffer = _renderer->CreateAndFillBuffer(_sdsmStateBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); // smoothedMax <= smoothedMin marks the state uninitialized + }); + + _cascadeFitDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); + _cascadeFitDescriptorSet.Bind("_sdsmState"_h, _sdsmStateBuffer); + // _rwCameras is bound per frame in AddCascadeFitPass, the cameras buffer can be recreated on growth + + bufferDesc.name = "ShadowSDSMStateReadBack"; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; + _sdsmStateReadBackBuffer = _renderer->CreateBuffer(_sdsmStateReadBackBuffer, bufferDesc); + + bufferDesc.name = "ShadowCascadeCamerasReadBack"; + bufferDesc.size = sizeof(Camera) * Renderer::Settings::MAX_SHADOW_CASCADES; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; + _cascadeCamerasReadBackBuffer = _renderer->CreateBuffer(_cascadeCamerasReadBackBuffer, bufferDesc); + } } \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 390c487..19af512 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -1,9 +1,13 @@ #pragma once +#include "Game-Lib/Rendering/Camera.h" + #include #include #include #include +#include +#include #include #include @@ -27,8 +31,16 @@ class ShadowRenderer void Update(f32 deltaTime, RenderResources& resources); + void AddDepthMinMaxPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeFitPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + // Reduced scene depth bounds as view distances, false while no valid depth has been reduced yet + bool GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const; + + // Effective cascade range after hysteresis and quantization, false while SDSM has not fitted yet + bool GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const; + private: void CreatePermanentResources(RenderResources& resources); @@ -41,4 +53,20 @@ class ShadowRenderer Renderer::SamplerID _shadowCmpSampler; Renderer::SamplerID _shadowPointClampSampler; + + Renderer::ComputePipelineID _depthMinMaxPipeline; + Renderer::DescriptorSet _depthMinMaxDescriptorSet; + Renderer::BufferID _depthMinMaxBuffer; + Renderer::BufferID _depthMinMaxReadBackBuffer; + u32 _depthMinMaxReadBack[2] = { 0xFFFFFFFF, 0 }; + + Renderer::ComputePipelineID _cascadeFitPipeline; + Renderer::DescriptorSet _cascadeFitDescriptorSet; + Renderer::BufferID _sdsmStateBuffer; + Renderer::BufferID _sdsmStateReadBackBuffer; + Renderer::BufferID _cascadeCamerasReadBackBuffer; + Camera _readBackCascadeCameras[Renderer::Settings::MAX_SHADOW_CASCADES]; + f32 _sdsmStateReadBack[8] = { 0.0f }; + + f32 _lastDeltaTime = 0.0f; }; \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index 7f1d14e..aa690a1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -3,6 +3,7 @@ #include "Game-Lib/ECS/Singletons/Database/TextureSingleton.h" #include "Game-Lib/Rendering/RenderUtils.h" #include "Game-Lib/Rendering/GameRenderer.h" +#include "Game-Lib/Rendering/Shadow/ShadowRenderer.h" #include "Game-Lib/Rendering/RenderResources.h" #include "Game-Lib/Rendering/Debug/DebugRenderer.h" #include "Game-Lib/Util/ServiceLocator.h" @@ -127,11 +128,7 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render CVarSystem* cvarSystem = CVarSystem::Get(); - u32 numCascades = 0; - if (CVAR_TerrainCastShadow.Get() == 1) - { - numCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"); - } + const u32 numCascades = 0; // Occluders are main view only, cascades render in their own block late in the frame struct Data { @@ -282,7 +279,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR if (_instanceDatas.Count() == 0) return; - u32 numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + const u32 numCascades = 0; // Main view only, cascades are culled in their own block late in the frame struct Data { @@ -371,6 +368,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR u32 bitMaskBufferSizePerView; u32 currentBitmaskIndex; u32 debugDrawView; + u32 cullMainView; }; vec2 viewportSize = _renderer->GetRenderSize(); @@ -384,6 +382,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); cullConstants->currentBitmaskIndex = frameIndex; cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); + cullConstants->cullMainView = true; commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); @@ -414,11 +413,7 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render const bool cullingEnabled = true;//CVAR_TerrainCullingEnabled.Get(); CVarSystem* cvarSystem = CVarSystem::Get(); - u32 numCascades = 0; - if (CVAR_TerrainCastShadow.Get() == 1) - { - numCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"); - } + const u32 numCascades = 0; // Main view only, cascades render in their own block late in the frame struct Data { @@ -551,6 +546,256 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render }); } +void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + ZoneScoped; + + if (!CVAR_TerrainRendererEnabled.Get()) + return; + + if (_instanceDatas.Count() == 0) + return; + + if (*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_TerrainCastShadow.Get() != 1) + return; + + u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + if (numCascades == 0) + return; + + struct Data + { + Renderer::ImageResource depthPyramid; + + Renderer::BufferMutableResource currentInstanceBitMaskBuffer; + + Renderer::DescriptorSetResource debugSet; + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource cullingSet; + }; + + renderGraph->AddPass("Terrain Cascade Culling", + [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) // Setup + { + using BufferUsage = Renderer::BufferPassUsage; + + data.depthPyramid = builder.Read(resources.depthPyramid, Renderer::PipelineType::COMPUTE); + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_instanceDatas.GetBuffer(), BufferUsage::COMPUTE); + builder.Read(_cellHeightRanges.GetBuffer(), BufferUsage::COMPUTE); + + data.currentInstanceBitMaskBuffer = builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE); + builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE); // Both bitmask bindings are RW in the shader + builder.Write(_culledInstanceBuffer, BufferUsage::COMPUTE); // Not written by the cascade dispatch, but bound RW in the culling set + + data.debugSet = builder.Use(_debugRenderer->GetDebugDescriptorSet()); + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.cullingSet = builder.Use(_cullingPassDescriptorSet); + + _debugRenderer->RegisterCullingPassBufferUsage(builder); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, numCascades](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + { + GPU_SCOPED_PROFILER_ZONE(commandList, TerrainCascadeCulling); + + // Frustum-only cull of the cascade views into their bitmask slices, no occlusion culling for cascades + Renderer::ComputePipelineID pipeline = _cullingPipeline; + commandList.BeginPipeline(pipeline); + + struct CullConstants + { + u32 viewportSizeX; + u32 viewportSizeY; + u32 numCascades; + u32 occlusionEnabled; + u32 bitMaskBufferSizePerView; + u32 currentBitmaskIndex; + u32 debugDrawView; + u32 cullMainView; + }; + + vec2 viewportSize = _renderer->GetRenderSize(); + + const u32 cellCount = static_cast(_cellDatas.Count()); + + CullConstants* cullConstants = graphResources.FrameNew(); + cullConstants->viewportSizeX = u32(viewportSize.x); + cullConstants->viewportSizeY = u32(viewportSize.y); + cullConstants->numCascades = numCascades; + cullConstants->occlusionEnabled = false; + cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); + cullConstants->currentBitmaskIndex = frameIndex; + cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); + cullConstants->cullMainView = false; + + commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); + + // _depthPyramid stays bound from the main culling pass, rebinding here would rewrite an already-bound set + + commandList.BindDescriptorSet(data.debugSet, frameIndex); + commandList.BindDescriptorSet(data.globalSet, frameIndex); + commandList.BindDescriptorSet(data.cullingSet, frameIndex); + + commandList.Dispatch((cellCount + 31) / 32, 1, 1); + + commandList.EndPipeline(pipeline); + }); +} + +void TerrainRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + ZoneScoped; + + if (!CVAR_TerrainRendererEnabled.Get()) + return; + + if (_instanceDatas.Count() == 0) + return; + + CVarSystem* cvarSystem = CVarSystem::Get(); + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_TerrainCastShadow.Get() != 1) + return; + + u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + if (numCascades == 0) + return; + + struct Data + { + Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + + Renderer::BufferMutableResource culledInstanceBuffer; + + Renderer::BufferMutableResource argumentBuffer; + Renderer::BufferMutableResource drawCountReadBackBuffer; + + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource fillSet; + Renderer::DescriptorSetResource geometryPassSet; + }; + + renderGraph->AddPass("Terrain Cascade Geometry", + [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) // Setup + { + using BufferUsage = Renderer::BufferPassUsage; + + for (u32 i = 1; i < numCascades + 1; i++) + { + data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + } + + builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); + builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); + data.culledInstanceBuffer = builder.Write(_culledInstanceBuffer, BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_cellDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_chunkDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceDatas.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::GRAPHICS); + + data.argumentBuffer = builder.Write(_argumentBuffer, BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + data.drawCountReadBackBuffer = builder.Write(_drawCountReadBackBuffer, BufferUsage::TRANSFER); + + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.fillSet = builder.Use(_geometryFillPassDescriptorSet); + data.geometryPassSet = builder.Use(resources.terrainDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, &resources, frameIndex, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, TerrainCascadeGeometry); + + const u32 cellCount = static_cast(_cellDatas.Count()); + + for (u32 i = 1; i < numCascades + 1; i++) + { + std::string markerName = "Cascade " + std::to_string(i - 1); + commandList.PushMarker(markerName, Color::White); + + // Reset the counters + { + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + } + + if (CVAR_TerrainGeometryEnabled.Get()) + { + // Cascades draw their full surviving set in one phase, occluders no longer pre-draw into them + FillDrawCallsParams fillParams; + fillParams.passName = "Cascade Geometry"; + fillParams.cellCount = cellCount; + fillParams.viewIndex = i; + fillParams.diffAgainstPrev = false; + fillParams.currentBitmaskIndex = frameIndex; + fillParams.fillSet = data.fillSet; + + FillDrawCalls(frameIndex, graphResources, commandList, fillParams); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); + + if (i == 1) + { + uvec2 shadowDepthDimensions = _renderer->GetImageDimensions(resources.shadowDepthCascades[0]); + + commandList.SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); + commandList.SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); + + f32 biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); + f32 biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); + f32 biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); + commandList.SetDepthBias(biasConstantFactor, biasClamp, biasSlopeFactor); + } + + commandList.PushMarker("Draw", Color::White); + + DrawParams drawParams; + drawParams.shadowPass = true; + drawParams.viewIndex = i; + drawParams.cullingEnabled = true; + drawParams.depth = data.depth[i]; + drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); + drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); + + drawParams.globalDescriptorSet = data.globalSet; + drawParams.drawDescriptorSet = data.geometryPassSet; + + Draw(resources, frameIndex, graphResources, commandList, drawParams); + + commandList.PopMarker(); + } + + // Copy drawn count + { + u32 dstOffset = i * sizeof(u32); + commandList.CopyBuffer(data.drawCountReadBackBuffer, dstOffset, data.argumentBuffer, 4, 4); + } + + commandList.PopMarker(); + } + + // Finish by resetting the viewport, scissor and depth bias + vec2 renderSize = _renderer->GetRenderSize(); + commandList.SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); + commandList.SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); + commandList.SetDepthBias(0, 0, 0); + }); +} + void TerrainRenderer::Clear() { ZoneScoped; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index c250cb8..218c333 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -48,6 +48,8 @@ class TerrainRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void Clear(); void Reserve(u32 numChunks); diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index 012d932..83199d9 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -59,7 +59,14 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte shadowSettings.cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); Camera cascadeCamera = _cameras[shadowSettings.cascadeIndex + 1]; // +1 because the first camera is the main camera - float4 shadowPosition = mul(float4(pixelVertexData.worldPos, 1.0f), cascadeCamera.worldToClip); + // Normal-offset bias: push the receiver along its surface normal by a fraction of the + // cascade texel size, robust against acne on hard angles where slope bias fails + float2 shadowTexDim; + _shadowCascadeRTs[shadowSettings.cascadeIndex].GetDimensions(shadowTexDim.x, shadowTexDim.y); + float texelWorldSize = 2.0f / (cascadeCamera.viewToClip[0][0] * shadowTexDim.x); + float3 offsetWorldPos = pixelVertexData.worldPos + pixelVertexData.worldNormal * (texelWorldSize * shadowSettings.normalOffset); + + float4 shadowPosition = mul(float4(offsetWorldPos, 1.0f), cascadeCamera.worldToClip); float shadowFactor = GetShadowFactor(uv, shadowPosition, shadowSettings); diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index 93e7332..aa38856 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -10,6 +10,7 @@ struct ShadowSettings float filterSize; float penumbraFilterSize; float strength; // Driven by sun elevation, fades shadows out around dawn/dusk + float normalOffset; // Receiver offset along the surface normal in cascade texels uint cascadeIndex; // Filled in by ApplyLighting }; diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 6fec0bf..0fd62e9 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -28,7 +28,7 @@ struct Constants float4 patchEdgeColor; float4 vertexColor; float4 brushColor; - float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = UNUSED + float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias }; [[vk::push_constant]] Constants _constants; @@ -142,6 +142,7 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit shadowSettings.filterSize = _constants.shadowFilterSettings.x; shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; shadowSettings.strength = _constants.shadowFilterSettings.z; + shadowSettings.normalOffset = _constants.shadowFilterSettings.w; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); @@ -288,6 +289,7 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB shadowSettings.filterSize = _constants.shadowFilterSettings.x; shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; shadowSettings.strength = _constants.shadowFilterSettings.z; + shadowSettings.normalOffset = _constants.shadowFilterSettings.w; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); diff --git a/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang new file mode 100644 index 0000000..5e2c99d --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang @@ -0,0 +1,371 @@ +#include "Include/Camera.inc.slang" + +// GPU port of the cascade camera fitting in CalculateShadowCameraMatrices.cpp. +// Writes cascade cameras [1..numCascades] into the shared cameras buffer that culling, +// drawing and the material resolve read. With useDepthBounds the cascades distribute over +// the visible depth range reduced by DepthMinMax.cs (SDSM), otherwise over the full +// [max(near, 1), shadowMaxDistance] range like the legacy CPU path (parity mode). +// +// Matrix layout rule (load-bearing): shaders consume glm column-major uploads with +// mul(rowVector, M), so a Slang float4x4 row i must hold what glm stores in column i. +// glm composition C = A * B therefore becomes C_s = mul(B_s, A_s) here. + +struct Constants +{ + float4 lightDirection; // xyz = direction the light travels + uint numCascades; + float cascadeSplitLambda; + float cascadeTextureSize; + uint stableShadows; + float shadowMaxDistance; + float quantizeStep; + float shrinkDelay; + float deltaTime; + uint useDepthBounds; + float casterMargin; +}; +[[vk::push_constant]] Constants _constants; + +struct SDSMState +{ + float smoothedMin; + float smoothedMax; + float usedMin; + float usedMax; + float windowLowestMin; // Widest bounds seen during the current shrink window + float windowHighestMax; + float windowTimer; + float padding; +}; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera +[[vk::binding(1, PER_PASS)]] StructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _sdsmState; + +// Matches glm::lookAtLH memory layout (GLM_FORCE_LEFT_HANDED) +float4x4 BuildLookAt(float3 eye, float3 center, float3 up) +{ + float3 f = normalize(center - eye); + float3 s = normalize(cross(up, f)); + float3 u = cross(f, s); + + float4x4 m; + m[0] = float4(s.x, u.x, f.x, 0.0f); + m[1] = float4(s.y, u.y, f.y, 0.0f); + m[2] = float4(s.z, u.z, f.z, 0.0f); + m[3] = float4(-dot(s, eye), -dot(u, eye), -dot(f, eye), 1.0f); + return m; +} + +// Analytic inverse of a BuildLookAt matrix (rigid transform) +float4x4 InverseLookAt(float4x4 view, float3 eye) +{ + float3 s = float3(view[0].x, view[1].x, view[2].x); + float3 u = float3(view[0].y, view[1].y, view[2].y); + float3 f = float3(view[0].z, view[1].z, view[2].z); + + float4x4 m; + m[0] = float4(s, 0.0f); + m[1] = float4(u, 0.0f); + m[2] = float4(f, 0.0f); + m[3] = float4(eye, 1.0f); + return m; +} + +// Matches glm::orthoLH_ZO memory layout (GLM_FORCE_DEPTH_ZERO_TO_ONE) +float4x4 BuildOrtho(float left, float right, float bottom, float top, float zNear, float zFar) +{ + float4x4 m; + m[0] = float4(2.0f / (right - left), 0.0f, 0.0f, 0.0f); + m[1] = float4(0.0f, 2.0f / (top - bottom), 0.0f, 0.0f); + m[2] = float4(0.0f, 0.0f, 1.0f / (zFar - zNear), 0.0f); + m[3] = float4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -zNear / (zFar - zNear), 1.0f); + return m; +} + +// Analytic inverse of a BuildOrtho matrix, uses the post-snap projection values +float4x4 InverseOrtho(float4x4 proj) +{ + float3 scale = float3(proj[0].x, proj[1].y, proj[2].z); + float3 translation = proj[3].xyz; + + float4x4 m; + m[0] = float4(1.0f / scale.x, 0.0f, 0.0f, 0.0f); + m[1] = float4(0.0f, 1.0f / scale.y, 0.0f, 0.0f); + m[2] = float4(0.0f, 0.0f, 1.0f / scale.z, 0.0f); + m[3] = float4(-translation / scale, 1.0f); + return m; +} + +// glm row i of M, planes are extracted from mathematical rows +float4 MatrixRow(float4x4 m, uint i) +{ + return float4(m[0][i], m[1][i], m[2][i], m[3][i]); +} + +// Gribb-Hartmann row (inside if dot(n, p) + d >= 0) to the culling encoding vec4(n, dot(n, pointOnPlane)) +float4 ExtractPlane(float4 row) +{ + float normalLength = length(row.xyz); + return float4(row.xyz / normalLength, -row.w / normalLength); +} + +[shader("compute")] +[numthreads(1, 1, 1)] +void main() +{ + Camera mainCamera = _rwCameras[0]; + + float nearClip = mainCamera.nearFar.x; + float farClip = mainCamera.nearFar.y; + float clipRange = farClip - nearClip; + + // Working range for the cascade distribution + float rangeMin = max(nearClip, 1.0f); + float rangeMax = min(farClip, _constants.shadowMaxDistance); + + SDSMState state = _sdsmState[0]; + + if (_constants.useDepthBounds && _depthMinMax[1] != 0) + { + // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane + float minDepth = asfloat(_depthMinMax[1]); // Max depth bits = nearest sample + float maxDepth = asfloat(_depthMinMax[0]); // Min depth bits = farthest sample + + float rawMin = (nearClip * farClip) / (nearClip + minDepth * (farClip - nearClip)); + float rawMax = (nearClip * farClip) / (nearClip + maxDepth * (farClip - nearClip)); + + rawMin = clamp(rawMin, rangeMin, _constants.shadowMaxDistance - 1.0f); + rawMax = clamp(rawMax, rawMin + 1.0f, _constants.shadowMaxDistance); + + // Hysteresis: expand instantly, shrink in a single jump after the range has been smaller + // for a whole window. A continuous shrink rescales the cascades every step and the texel + // grid realignment reads as shimmer, one jump per window is one realignment + if (state.smoothedMax <= state.smoothedMin) // Uninitialized state + { + state.smoothedMin = rawMin; + state.smoothedMax = rawMax; + state.windowLowestMin = rawMin; + state.windowHighestMax = rawMax; + state.windowTimer = 0.0f; + } + else + { + state.smoothedMin = min(state.smoothedMin, rawMin); + state.smoothedMax = max(state.smoothedMax, rawMax); + + state.windowLowestMin = min(state.windowLowestMin, rawMin); + state.windowHighestMax = max(state.windowHighestMax, rawMax); + state.windowTimer += _constants.deltaTime; + + if (state.windowTimer >= _constants.shrinkDelay) + { + // Shrink to the widest bounds the window actually needed + state.smoothedMin = max(state.smoothedMin, state.windowLowestMin); + state.smoothedMax = min(state.smoothedMax, state.windowHighestMax); + + state.windowLowestMin = rawMin; + state.windowHighestMax = rawMax; + state.windowTimer = 0.0f; + } + } + + // Quantize outward AFTER smoothing so the working bounds are piecewise-constant + // and the stable texel snapping stays effective + float step = max(_constants.quantizeStep, 0.01f); + rangeMin = max(floor(state.smoothedMin / step) * step, max(nearClip, 1.0f)); + rangeMax = min(ceil(state.smoothedMax / step) * step, _constants.shadowMaxDistance); + rangeMax = max(rangeMax, rangeMin + 1.0f); + } + else + { + // Parity mode / no valid depth: keep the state seeded so enabling SDSM starts sane + state.smoothedMin = rangeMin; + state.smoothedMax = rangeMax; + state.windowLowestMin = rangeMin; + state.windowHighestMax = rangeMax; + state.windowTimer = 0.0f; + } + + state.usedMin = rangeMin; + state.usedMax = rangeMax; + _sdsmState[0] = state; + + // Split distances, same log/uniform lambda mix as the CPU path. + // The split fractions stay relative to the FULL camera clip range so the frustum + // corner slicing and the eyePosition.w = distance - near semantics hold. + float cascadeSplits[8]; + { + float ratio = rangeMax / rangeMin; + float range = rangeMax - rangeMin; + for (uint i = 0; i < _constants.numCascades; i++) + { + float p = (i + 1) / float(_constants.numCascades); + float logSplit = rangeMin * pow(ratio, p); + float uniformSplit = rangeMin + range * p; + float d = _constants.cascadeSplitLambda * (logSplit - uniformSplit) + uniformSplit; + + cascadeSplits[i] = 1.0f - ((d - nearClip) / clipRange); + } + } + + // Unproject the NDC cube corners, z = 0 is the far plane with reversed Z + const float3 ndcCorners[8] = + { + float3(-1.0f, 1.0f, 0.0f), + float3( 1.0f, 1.0f, 0.0f), + float3( 1.0f, -1.0f, 0.0f), + float3(-1.0f, -1.0f, 0.0f), + float3(-1.0f, 1.0f, 1.0f), + float3( 1.0f, 1.0f, 1.0f), + float3( 1.0f, -1.0f, 1.0f), + float3(-1.0f, -1.0f, 1.0f) + }; + + float3 baseCorners[8]; + for (uint j = 0; j < 8; j++) + { + float4 corner = mul(float4(ndcCorners[j], 1.0f), mainCamera.clipToWorld); + baseCorners[j] = corner.xyz / corner.w; + } + + float3 lightDirection = _constants.lightDirection.xyz; + + float lastSplitDist = 1.0f; + for (uint cascadeIndex = 0; cascadeIndex < _constants.numCascades; cascadeIndex++) + { + float splitDist = cascadeSplits[cascadeIndex]; + + // Slice the frustum, corners 0-3 are on the far plane, the ray points from far to near + float3 frustumCorners[8]; + for (uint j = 0; j < 4; j++) + { + float3 cornerRay = baseCorners[j + 4] - baseCorners[j]; + frustumCorners[j + 4] = baseCorners[j] + cornerRay * splitDist; + frustumCorners[j] = baseCorners[j] + cornerRay * lastSplitDist; + } + + float3 frustumCenter = float3(0.0f, 0.0f, 0.0f); + for (uint j = 0; j < 8; j++) + { + frustumCenter += frustumCorners[j]; + } + frustumCenter /= 8.0f; + + // Non-stable mode uses the camera right axis, world X basis of viewToWorld + float3 upDir = -float3(mainCamera.viewToWorld[0].xyz); + + float3 minExtents; + float3 maxExtents; + if (_constants.stableShadows) + { + // Constant up for stability, fall back to Z when the light is near vertical + upDir = float3(0.0f, 1.0f, 0.0f); + if (abs(lightDirection.y) > 0.99f) + { + upDir = float3(0.0f, 0.0f, 1.0f); + } + + float sphereRadius = 0.0f; + for (uint j = 0; j < 8; j++) + { + float dist = length(frustumCorners[j] - frustumCenter); + sphereRadius = max(sphereRadius, dist); + } + + sphereRadius = ceil(sphereRadius * 16.0f) / 16.0f; + + if (_constants.useDepthBounds) + { + // Quantize the radius into multiplicative buckets: within a bucket the texel scale is + // constant so bound changes are pure translation, which the texel snap absorbs. Only a + // bucket crossing rescales the grid, and only for that cascade + const float bucketRatio = 1.25f; + sphereRadius = pow(bucketRatio, ceil(log(sphereRadius) / log(bucketRatio))); + } + + maxExtents = float3(sphereRadius, sphereRadius, sphereRadius); + minExtents = -maxExtents; + } + else + { + // AABB of the slice in light view space + float4x4 lightView = BuildLookAt(frustumCenter, frustumCenter + lightDirection, upDir); + + float3 mins = float3(3.402823466e+38f, 3.402823466e+38f, 3.402823466e+38f); + float3 maxes = -mins; + for (uint j = 0; j < 8; j++) + { + float4 corner = mul(float4(frustumCorners[j], 1.0f), lightView); + mins = min(mins, corner.xyz / corner.w); + maxes = max(maxes, corner.xyz / corner.w); + } + + minExtents = mins; + maxExtents = maxes; + + // Adjust the min/max to accommodate the filtering size + const float fixedFilterKernelSize = 3.0f; + float scale = (_constants.cascadeTextureSize + fixedFilterKernelSize) / _constants.cascadeTextureSize; + minExtents.xy *= scale; + maxExtents.xy *= scale; + } + + // minExtents.z is negative, this places the shadow camera on the sun side + float3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; + + float farPlane = maxExtents.z; + float nearPlane = minExtents.z; + + float4x4 proj = BuildOrtho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, farPlane - nearPlane, 0.0f); + float4x4 view = BuildLookAt(shadowCameraPos, frustumCenter, upDir); + + if (_constants.stableShadows) + { + // Texel snap: round the projected world origin to texel increments + float4x4 shadowMatrix = mul(view, proj); + float4 shadowOrigin = shadowMatrix[3]; // mul(float4(0,0,0,1), M) = row 3 = glm translation column + shadowOrigin *= _constants.cascadeTextureSize / 2.0f; + + float4 roundedOrigin = round(shadowOrigin); + float4 roundOffset = (roundedOrigin - shadowOrigin) * (2.0f / _constants.cascadeTextureSize); + + proj[3].xy += roundOffset.xy; + } + + Camera cascadeCamera = (Camera)0; + + cascadeCamera.worldToView = view; + cascadeCamera.viewToClip = proj; + cascadeCamera.viewToWorld = InverseLookAt(view, shadowCameraPos); + cascadeCamera.clipToView = InverseOrtho(proj); + cascadeCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView + cascadeCamera.clipToWorld = mul(cascadeCamera.clipToView, cascadeCamera.viewToWorld); // glm viewToWorld * clipToView + + float splitDepth = farClip - (nearClip + splitDist * clipRange); // = split view distance - nearClip + cascadeCamera.eyePosition = float4(shadowCameraPos, splitDepth); + cascadeCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); + cascadeCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); + + // Frustum planes, slot order matches the FrustumPlane enum: Left, Right, Bottom, Top, Near, Far + float4 row0 = MatrixRow(cascadeCamera.worldToClip, 0); + float4 row1 = MatrixRow(cascadeCamera.worldToClip, 1); + float4 row2 = MatrixRow(cascadeCamera.worldToClip, 2); + float4 row3 = MatrixRow(cascadeCamera.worldToClip, 3); + + cascadeCamera.frustum[0] = ExtractPlane(row3 + row0); // Left + cascadeCamera.frustum[1] = ExtractPlane(row3 - row0); // Right + cascadeCamera.frustum[2] = ExtractPlane(row3 + row1); // Bottom + cascadeCamera.frustum[3] = ExtractPlane(row3 - row1); // Top + cascadeCamera.frustum[4] = ExtractPlane(row3 - row2); // Near, reversed Z so depth 1 is near + cascadeCamera.frustum[5] = ExtractPlane(row2); // Far + + // The near plane sits at the shadow camera on the sun side. Casters beyond it still render + // correctly (depth clamp pancakes them), so culling must not reject them + cascadeCamera.frustum[4].w -= _constants.casterMargin; + + _rwCameras[1 + cascadeIndex] = cascadeCamera; + + lastSplitDist = splitDist; + } +} diff --git a/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang b/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang new file mode 100644 index 0000000..6bfe9ec --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang @@ -0,0 +1,60 @@ +#include "DescriptorSet/Global.inc.slang" + +#include "Include/Common.inc.slang" + +// Reduces the scene depth buffer to global min/max depth for SDSM cascade fitting. +// Reversed Z: max depth bits = nearest sample, min depth bits = farthest sample. +// The buffer is reset to the sentinel { 0xFFFFFFFF, 0 } before dispatch, uint ordering +// matches float ordering for depth in [0, 1]. + +[[vk::binding(0, PER_PASS)]] Texture2D _depth; +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) + +groupshared uint gMinDepth; +groupshared uint gMaxDepth; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(16, 16, 1)] +void main(CSInput input) +{ + if (input.groupIndex == 0) + { + gMinDepth = 0xFFFFFFFFu; + gMaxDepth = 0u; + } + GroupMemoryBarrierWithGroupSync(); + + uint2 dim; + _depth.GetDimensions(dim.x, dim.y); + + uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); + float depth = _depth[pixel]; + + // Cleared depth is exactly 0.0 (reversed Z far plane), those pixels are sky and must not drive the bounds + bool isValid = depth != 0.0f; + float minCandidate = isValid ? depth : 1.0f; + float maxCandidate = isValid ? depth : 0.0f; + + float waveMin = WaveActiveMin(minCandidate); + float waveMax = WaveActiveMax(maxCandidate); + + if (WaveIsFirstLane()) + { + InterlockedMin(gMinDepth, asuint(waveMin)); + InterlockedMax(gMaxDepth, asuint(waveMax)); + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex == 0) + { + InterlockedMin(_depthMinMax[0], gMinDepth); + InterlockedMax(_depthMinMax[1], gMaxDepth); + } +} diff --git a/Source/Shaders/Shaders/Terrain/Culling.cs.slang b/Source/Shaders/Shaders/Terrain/Culling.cs.slang index 5844d90..6b389ff 100644 --- a/Source/Shaders/Shaders/Terrain/Culling.cs.slang +++ b/Source/Shaders/Shaders/Terrain/Culling.cs.slang @@ -16,6 +16,7 @@ struct Constants uint bitMaskBufferSizePerView; uint currentBitmaskIndex; uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off + uint cullMainView; // The late cascade dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice }; struct HeightRange @@ -176,6 +177,7 @@ void main(CSInput input) bitmaskInput.bitmaskOffset = 0; // Main camera view + if (_constants.cullMainView) { CullForCamera(drawInput, _cameras[0], bitmaskInput, _constants.debugDrawView == 0); } diff --git a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang index 9f26493..249ca4a 100644 --- a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang +++ b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang @@ -24,6 +24,7 @@ struct Constants uint numCascades; uint bitMaskBufferUintsPerView; uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off + uint cullMainView; // The late cascade dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice and the instance lookup }; struct InstanceRef @@ -406,6 +407,7 @@ void main(CSInput input) cullOutput.instanceLookupTable = _culledInstanceLookupTable; // Cull Main Camera + if (_constants.cullMainView) { CullForCamera(drawInput, _cameras[0], From 5e318aa561f8e32c7080619b44b717b5ef7b85ea Mon Sep 17 00:00:00 2001 From: Pursche Date: Fri, 10 Jul 2026 00:27:00 +0200 Subject: [PATCH 12/24] Fit SDSM cascades to per-cascade light-space bounds of visible samples --- .../Editor/PerformanceDiagnostics.cpp | 17 + .../Game-Lib/Rendering/GameRenderer.cpp | 3 +- .../Rendering/Shadow/ShadowRenderer.cpp | 169 ++++++-- .../Rendering/Shadow/ShadowRenderer.h | 20 +- .../Shaders/Shadows/CascadeFit.cs.slang | 371 ------------------ .../Shadows/CascadeFitCameras.cs.slang | 283 +++++++++++++ .../Shaders/Shadows/CascadeFitRange.cs.slang | 113 ++++++ .../Shaders/Shadows/CascadeXYReduce.cs.slang | 120 ++++++ Source/Shaders/Shaders/Shadows/SDSM.inc.slang | 154 ++++++++ 9 files changed, 841 insertions(+), 409 deletions(-) delete mode 100644 Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SDSM.inc.slang diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index 60c8532..6b125ec 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -100,6 +100,23 @@ namespace Editor { ImGui::Text("Shadow Range: %.1f - %.1f (used by cascades)", effectiveMin, effectiveMax); } + + for (u32 i = 0; i < numCascades; i++) + { + vec3 extents = vec3(0.0f); + bool valid = false; + if (!shadowRenderer->GetCascadeFittedBounds(i, extents, valid)) + break; + + if (valid) + { + ImGui::Text("C%u: %.1f x %.1f x %.1f", i, extents.x, extents.y, extents.z); + } + else + { + ImGui::Text("C%u: empty", i); + } + } } else { diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index 2148c75..c307cef 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -407,7 +407,8 @@ f32 GameRenderer::Render() _liquidRenderer->AddCullingPass(&renderGraph, _resources, _frameIndex); _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); - // Cascade block, runs after the main depth is complete so cascades can be fitted, culled and drawn the same frame + // Cascade block, runs after the main depth is complete so the cascades can be fitted to the + // visible samples (SDSM: depth range + per-cascade light-space bounds), culled and drawn the same frame _shadowRenderer->AddDepthMinMaxPass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddCascadeFitPass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddShadowPass(&renderGraph, _resources, _frameIndex); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 1ab17ab..ba0eeb8 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -43,6 +43,8 @@ AutoCVar_Int CVAR_ShadowSDSMUseDepthBounds(CVarCategory::Client | CVarCategory:: AutoCVar_Float CVAR_ShadowSDSMQuantizeStep(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMQuantizeStep", "SDSM bounds are quantized outward to this step to keep stable snapping effective", 8.0f); AutoCVar_Float CVAR_ShadowSDSMShrinkDelay(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMShrinkDelay", "seconds the visible range must stay smaller before the SDSM bounds shrink to it in one jump, expansion is instant", 2.5f); AutoCVar_Int CVAR_ShadowSDSMValidateParity(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity", "compare GPU-fitted cascade cameras against the CPU math and log the max delta", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_ShadowSDSMUseXYBounds(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMUseXYBounds", "fit cascades to the light-space footprint of visible samples: 0 = off, 1 = diagnostics only, 2 = drive the cameras", 2); +AutoCVar_Float CVAR_ShadowSDSMXYMarginTexels(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMXYMarginTexels", "filter-kernel margin in texels added to the fitted XY bounds, the normal offset bias is added on top", 4.0f); ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) @@ -51,7 +53,9 @@ ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameR , _terrainRenderer(terrainRenderer) , _modelRenderer(modelRenderer) , _depthMinMaxDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) - , _cascadeFitDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _cascadeFitRangeDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _cascadeXYReduceDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _cascadeFitCamerasDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) { CreatePermanentResources(resources); } @@ -91,12 +95,12 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } _renderer->UnmapBuffer(_cascadeCamerasReadBackBuffer); - f32* stateData = static_cast(_renderer->MapBuffer(_sdsmStateReadBackBuffer)); - if (stateData != nullptr) + f32* sdsmData = static_cast(_renderer->MapBuffer(_sdsmDataReadBackBuffer)); + if (sdsmData != nullptr) { - memcpy(_sdsmStateReadBack, stateData, sizeof(f32) * 8); + memcpy(_sdsmDataReadBack, sdsmData, sizeof(f32) * SDSM_DATA_FLOAT_COUNT); } - _renderer->UnmapBuffer(_sdsmStateReadBackBuffer); + _renderer->UnmapBuffer(_sdsmDataReadBackBuffer); } auto GetCascadeCamera = [&](u32 cascadeIndex) -> const Camera& @@ -227,12 +231,17 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende { struct Data { + Renderer::DepthImageResource depth; + Renderer::BufferMutableResource cameras; - Renderer::BufferMutableResource sdsmStateBuffer; - Renderer::BufferMutableResource sdsmStateReadBackBuffer; + Renderer::BufferMutableResource sdsmDataBuffer; + Renderer::BufferMutableResource cascadeBoundsBuffer; + Renderer::BufferMutableResource sdsmDataReadBackBuffer; Renderer::BufferMutableResource cascadeCamerasReadBackBuffer; - Renderer::DescriptorSetResource passSet; + Renderer::DescriptorSetResource rangeSet; + Renderer::DescriptorSetResource reduceSet; + Renderer::DescriptorSetResource camerasSet; }; CVarSystem* cvarSystem = CVarSystem::Get(); @@ -256,13 +265,18 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende { using BufferUsage = Renderer::BufferPassUsage; + data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); + data.cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::TRANSFER); builder.Read(_depthMinMaxBuffer, BufferUsage::COMPUTE); - data.sdsmStateBuffer = builder.Write(_sdsmStateBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); - data.sdsmStateReadBackBuffer = builder.Write(_sdsmStateReadBackBuffer, BufferUsage::TRANSFER); + data.sdsmDataBuffer = builder.Write(_sdsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + data.cascadeBoundsBuffer = builder.Write(_cascadeBoundsBuffer, BufferUsage::TRANSFER | BufferUsage::COMPUTE); + data.sdsmDataReadBackBuffer = builder.Write(_sdsmDataReadBackBuffer, BufferUsage::TRANSFER); data.cascadeCamerasReadBackBuffer = builder.Write(_cascadeCamerasReadBackBuffer, BufferUsage::TRANSFER); - data.passSet = builder.Use(_cascadeFitDescriptorSet); + data.rangeSet = builder.Use(_cascadeFitRangeDescriptorSet); + data.reduceSet = builder.Use(_cascadeXYReduceDescriptorSet); + data.camerasSet = builder.Use(_cascadeFitCamerasDescriptorSet); return true; // Return true from setup to enable this pass, return false to disable it }, @@ -283,6 +297,8 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende f32 deltaTime; u32 useDepthBounds; f32 casterMargin; + u32 useXYBounds; + f32 xyMarginTexels; }; CascadeFitConstants* constants = graphResources.FrameNew(); @@ -298,30 +314,74 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende constants->useDepthBounds = CVAR_ShadowSDSMUseDepthBounds.Get(); constants->casterMargin = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin")); - commandList.BeginPipeline(_cascadeFitPipeline); + // The XY path presumes the stable up rule and depth bounds, and parity compares + // against the legacy CPU math which it intentionally diverges from + u32 useXYBounds = static_cast(Math::Max(CVAR_ShadowSDSMUseXYBounds.Get(), 0)); + if (CVAR_ShadowSDSMValidateParity.Get() || !constants->useDepthBounds || !constants->stableShadows) + { + useXYBounds = Math::Min(useXYBounds, 1u); + } + constants->useXYBounds = useXYBounds; + constants->xyMarginTexels = CVAR_ShadowSDSMXYMarginTexels.GetFloat() + CVAR_ShadowNormalOffsetBias.GetFloat(); + + // Reset the light-space bounds to the encoded sentinels + commandList.FillBuffer(data.cascadeBoundsBuffer, 0, sizeof(u32) * 24, 0xFFFFFFFF); + commandList.FillBuffer(data.cascadeBoundsBuffer, sizeof(u32) * 24, sizeof(u32) * 24, 0); + commandList.BufferBarrier(data.cascadeBoundsBuffer, Renderer::BufferPassUsage::TRANSFER); + // Range: reduced depth bounds -> working range + split distances + commandList.BeginPipeline(_cascadeFitRangePipeline); commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - data.passSet.Bind("_rwCameras"_h, data.cameras); - commandList.BindDescriptorSet(data.passSet, frameIndex); + data.rangeSet.Bind("_srcCameras"_h, data.cameras); + commandList.BindDescriptorSet(data.rangeSet, frameIndex); commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(_cascadeFitRangePipeline); + + commandList.BufferBarrier(data.sdsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + + // XY reduce: light-space AABB of the visible samples per cascade + if (useXYBounds >= 1) + { + commandList.BeginPipeline(_cascadeXYReducePipeline); + commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); + + data.reduceSet.Bind("_depth"_h, data.depth); + data.reduceSet.Bind("_srcCameras"_h, data.cameras); + commandList.BindDescriptorSet(data.reduceSet, frameIndex); + + uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); + commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); + + commandList.EndPipeline(_cascadeXYReducePipeline); + + commandList.BufferBarrier(data.cascadeBoundsBuffer, Renderer::BufferPassUsage::COMPUTE); + } + + // Cameras: build the cascade cameras from the splits + commandList.BeginPipeline(_cascadeFitCamerasPipeline); + commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - commandList.EndPipeline(_cascadeFitPipeline); + data.camerasSet.Bind("_rwCameras"_h, data.cameras); + commandList.BindDescriptorSet(data.camerasSet, frameIndex); + + commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(_cascadeFitCamerasPipeline); commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.sdsmStateBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.sdsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); // Readbacks for the debug tooling, element 0 of the cameras buffer is the main camera commandList.CopyBuffer(data.cascadeCamerasReadBackBuffer, 0, data.cameras, sizeof(Camera), sizeof(Camera) * numCascades); - commandList.CopyBuffer(data.sdsmStateReadBackBuffer, 0, data.sdsmStateBuffer, 0, sizeof(f32) * 8); + commandList.CopyBuffer(data.sdsmDataReadBackBuffer, 0, data.sdsmDataBuffer, 0, sizeof(f32) * SDSM_DATA_FLOAT_COUNT); }); } bool ShadowRenderer::GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const { - f32 usedMin = _sdsmStateReadBack[2]; - f32 usedMax = _sdsmStateReadBack[3]; + f32 usedMin = _sdsmDataReadBack[2]; + f32 usedMax = _sdsmDataReadBack[3]; if (usedMax <= usedMin) // Not fitted yet return false; @@ -330,6 +390,18 @@ bool ShadowRenderer::GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDis return true; } +bool ShadowRenderer::GetCascadeFittedBounds(u32 cascadeIndex, vec3& outExtents, bool& outValid) const +{ + if (CVAR_ShadowSDSMUseXYBounds.Get() < 1 || cascadeIndex >= Renderer::Settings::MAX_SHADOW_CASCADES) + return false; + + // cascadeDiag lives after SDSMState (8) + splitDist (8) + splitDepth (8) + cascadeStable (32) + const f32* diag = &_sdsmDataReadBack[56 + cascadeIndex * 4]; + outExtents = vec3(diag[0], diag[1], diag[2]); + outValid = diag[3] > 0.5f; + return true; +} + bool ShadowRenderer::GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const { if (_depthMinMaxReadBack[1] == 0) // Sentinel, no valid depth samples reduced yet @@ -449,35 +521,68 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) // SDSM cascade fitting { Renderer::ComputePipelineDesc pipelineDesc; - pipelineDesc.debugName = "Shadow Cascade Fit"; + pipelineDesc.debugName = "Shadow Cascade Fit Range"; Renderer::ComputeShaderDesc shaderDesc; - shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFit.cs"_h, "Shadows/CascadeFit.cs"); + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFitRange.cs"_h, "Shadows/CascadeFitRange.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + + _cascadeFitRangePipeline = _renderer->CreatePipeline(pipelineDesc); + + _cascadeFitRangeDescriptorSet.RegisterPipeline(_renderer, _cascadeFitRangePipeline); + _cascadeFitRangeDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "Shadow Cascade XY Reduce"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeXYReduce.cs"_h, "Shadows/CascadeXYReduce.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + + _cascadeXYReducePipeline = _renderer->CreatePipeline(pipelineDesc); + + _cascadeXYReduceDescriptorSet.RegisterPipeline(_renderer, _cascadeXYReducePipeline); + _cascadeXYReduceDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "Shadow Cascade Fit Cameras"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFitCameras.cs"_h, "Shadows/CascadeFitCameras.cs"); pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - _cascadeFitPipeline = _renderer->CreatePipeline(pipelineDesc); + _cascadeFitCamerasPipeline = _renderer->CreatePipeline(pipelineDesc); - _cascadeFitDescriptorSet.RegisterPipeline(_renderer, _cascadeFitPipeline); - _cascadeFitDescriptorSet.Init(_renderer); + _cascadeFitCamerasDescriptorSet.RegisterPipeline(_renderer, _cascadeFitCamerasPipeline); + _cascadeFitCamerasDescriptorSet.Init(_renderer); Renderer::BufferDesc bufferDesc; - bufferDesc.name = "ShadowSDSMState"; - bufferDesc.size = sizeof(f32) * 8; + bufferDesc.name = "ShadowSDSMData"; + bufferDesc.size = sizeof(f32) * SDSM_DATA_FLOAT_COUNT; bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; - _sdsmStateBuffer = _renderer->CreateAndFillBuffer(_sdsmStateBuffer, bufferDesc, [](void* mappedMemory, size_t size) + _sdsmDataBuffer = _renderer->CreateAndFillBuffer(_sdsmDataBuffer, bufferDesc, [](void* mappedMemory, size_t size) { memset(mappedMemory, 0, size); // smoothedMax <= smoothedMin marks the state uninitialized }); - _cascadeFitDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); - _cascadeFitDescriptorSet.Bind("_sdsmState"_h, _sdsmStateBuffer); - // _rwCameras is bound per frame in AddCascadeFitPass, the cameras buffer can be recreated on growth + Renderer::BufferDesc boundsDesc; + boundsDesc.name = "ShadowCascadeBounds"; + boundsDesc.size = sizeof(u32) * 48; // Encoded light-space mins [0..23] and maxs [24..47], 3 axes per cascade + boundsDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + _cascadeBoundsBuffer = _renderer->CreateAndFillBuffer(_cascadeBoundsBuffer, boundsDesc, [](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + for (u32 i = 0; i < 24; i++) values[i] = 0xFFFFFFFF; + for (u32 i = 24; i < 48; i++) values[i] = 0; + }); + + _cascadeFitRangeDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); + _cascadeFitRangeDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); + _cascadeXYReduceDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); + _cascadeXYReduceDescriptorSet.Bind("_cascadeBounds"_h, _cascadeBoundsBuffer); + _cascadeFitCamerasDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); + _cascadeFitCamerasDescriptorSet.Bind("_cascadeBounds"_h, _cascadeBoundsBuffer); + // _srcCameras / _rwCameras / _depth are bound per frame in AddCascadeFitPass, those resources can be recreated - bufferDesc.name = "ShadowSDSMStateReadBack"; + bufferDesc.name = "ShadowSDSMDataReadBack"; bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; - _sdsmStateReadBackBuffer = _renderer->CreateBuffer(_sdsmStateReadBackBuffer, bufferDesc); + _sdsmDataReadBackBuffer = _renderer->CreateBuffer(_sdsmDataReadBackBuffer, bufferDesc); bufferDesc.name = "ShadowCascadeCamerasReadBack"; bufferDesc.size = sizeof(Camera) * Renderer::Settings::MAX_SHADOW_CASCADES; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 19af512..3f00297 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -41,6 +41,9 @@ class ShadowRenderer // Effective cascade range after hysteresis and quantization, false while SDSM has not fitted yet bool GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const; + // Fitted light-space extents per cascade, false while the XY reduction is not running + bool GetCascadeFittedBounds(u32 cascadeIndex, vec3& outExtents, bool& outValid) const; + private: void CreatePermanentResources(RenderResources& resources); @@ -60,13 +63,20 @@ class ShadowRenderer Renderer::BufferID _depthMinMaxReadBackBuffer; u32 _depthMinMaxReadBack[2] = { 0xFFFFFFFF, 0 }; - Renderer::ComputePipelineID _cascadeFitPipeline; - Renderer::DescriptorSet _cascadeFitDescriptorSet; - Renderer::BufferID _sdsmStateBuffer; - Renderer::BufferID _sdsmStateReadBackBuffer; + static constexpr u32 SDSM_DATA_FLOAT_COUNT = 8 + 8 + 8 + 4 * Renderer::Settings::MAX_SHADOW_CASCADES * 2; // SDSMState + splitDist + splitDepth + cascadeStable + cascadeDiag + + Renderer::ComputePipelineID _cascadeFitRangePipeline; + Renderer::ComputePipelineID _cascadeXYReducePipeline; + Renderer::ComputePipelineID _cascadeFitCamerasPipeline; + Renderer::DescriptorSet _cascadeFitRangeDescriptorSet; + Renderer::DescriptorSet _cascadeXYReduceDescriptorSet; + Renderer::DescriptorSet _cascadeFitCamerasDescriptorSet; + Renderer::BufferID _sdsmDataBuffer; + Renderer::BufferID _cascadeBoundsBuffer; + Renderer::BufferID _sdsmDataReadBackBuffer; Renderer::BufferID _cascadeCamerasReadBackBuffer; Camera _readBackCascadeCameras[Renderer::Settings::MAX_SHADOW_CASCADES]; - f32 _sdsmStateReadBack[8] = { 0.0f }; + f32 _sdsmDataReadBack[SDSM_DATA_FLOAT_COUNT] = { 0.0f }; f32 _lastDeltaTime = 0.0f; }; \ No newline at end of file diff --git a/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang deleted file mode 100644 index 5e2c99d..0000000 --- a/Source/Shaders/Shaders/Shadows/CascadeFit.cs.slang +++ /dev/null @@ -1,371 +0,0 @@ -#include "Include/Camera.inc.slang" - -// GPU port of the cascade camera fitting in CalculateShadowCameraMatrices.cpp. -// Writes cascade cameras [1..numCascades] into the shared cameras buffer that culling, -// drawing and the material resolve read. With useDepthBounds the cascades distribute over -// the visible depth range reduced by DepthMinMax.cs (SDSM), otherwise over the full -// [max(near, 1), shadowMaxDistance] range like the legacy CPU path (parity mode). -// -// Matrix layout rule (load-bearing): shaders consume glm column-major uploads with -// mul(rowVector, M), so a Slang float4x4 row i must hold what glm stores in column i. -// glm composition C = A * B therefore becomes C_s = mul(B_s, A_s) here. - -struct Constants -{ - float4 lightDirection; // xyz = direction the light travels - uint numCascades; - float cascadeSplitLambda; - float cascadeTextureSize; - uint stableShadows; - float shadowMaxDistance; - float quantizeStep; - float shrinkDelay; - float deltaTime; - uint useDepthBounds; - float casterMargin; -}; -[[vk::push_constant]] Constants _constants; - -struct SDSMState -{ - float smoothedMin; - float smoothedMax; - float usedMin; - float usedMax; - float windowLowestMin; // Widest bounds seen during the current shrink window - float windowHighestMax; - float windowTimer; - float padding; -}; - -[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera -[[vk::binding(1, PER_PASS)]] StructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) -[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _sdsmState; - -// Matches glm::lookAtLH memory layout (GLM_FORCE_LEFT_HANDED) -float4x4 BuildLookAt(float3 eye, float3 center, float3 up) -{ - float3 f = normalize(center - eye); - float3 s = normalize(cross(up, f)); - float3 u = cross(f, s); - - float4x4 m; - m[0] = float4(s.x, u.x, f.x, 0.0f); - m[1] = float4(s.y, u.y, f.y, 0.0f); - m[2] = float4(s.z, u.z, f.z, 0.0f); - m[3] = float4(-dot(s, eye), -dot(u, eye), -dot(f, eye), 1.0f); - return m; -} - -// Analytic inverse of a BuildLookAt matrix (rigid transform) -float4x4 InverseLookAt(float4x4 view, float3 eye) -{ - float3 s = float3(view[0].x, view[1].x, view[2].x); - float3 u = float3(view[0].y, view[1].y, view[2].y); - float3 f = float3(view[0].z, view[1].z, view[2].z); - - float4x4 m; - m[0] = float4(s, 0.0f); - m[1] = float4(u, 0.0f); - m[2] = float4(f, 0.0f); - m[3] = float4(eye, 1.0f); - return m; -} - -// Matches glm::orthoLH_ZO memory layout (GLM_FORCE_DEPTH_ZERO_TO_ONE) -float4x4 BuildOrtho(float left, float right, float bottom, float top, float zNear, float zFar) -{ - float4x4 m; - m[0] = float4(2.0f / (right - left), 0.0f, 0.0f, 0.0f); - m[1] = float4(0.0f, 2.0f / (top - bottom), 0.0f, 0.0f); - m[2] = float4(0.0f, 0.0f, 1.0f / (zFar - zNear), 0.0f); - m[3] = float4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -zNear / (zFar - zNear), 1.0f); - return m; -} - -// Analytic inverse of a BuildOrtho matrix, uses the post-snap projection values -float4x4 InverseOrtho(float4x4 proj) -{ - float3 scale = float3(proj[0].x, proj[1].y, proj[2].z); - float3 translation = proj[3].xyz; - - float4x4 m; - m[0] = float4(1.0f / scale.x, 0.0f, 0.0f, 0.0f); - m[1] = float4(0.0f, 1.0f / scale.y, 0.0f, 0.0f); - m[2] = float4(0.0f, 0.0f, 1.0f / scale.z, 0.0f); - m[3] = float4(-translation / scale, 1.0f); - return m; -} - -// glm row i of M, planes are extracted from mathematical rows -float4 MatrixRow(float4x4 m, uint i) -{ - return float4(m[0][i], m[1][i], m[2][i], m[3][i]); -} - -// Gribb-Hartmann row (inside if dot(n, p) + d >= 0) to the culling encoding vec4(n, dot(n, pointOnPlane)) -float4 ExtractPlane(float4 row) -{ - float normalLength = length(row.xyz); - return float4(row.xyz / normalLength, -row.w / normalLength); -} - -[shader("compute")] -[numthreads(1, 1, 1)] -void main() -{ - Camera mainCamera = _rwCameras[0]; - - float nearClip = mainCamera.nearFar.x; - float farClip = mainCamera.nearFar.y; - float clipRange = farClip - nearClip; - - // Working range for the cascade distribution - float rangeMin = max(nearClip, 1.0f); - float rangeMax = min(farClip, _constants.shadowMaxDistance); - - SDSMState state = _sdsmState[0]; - - if (_constants.useDepthBounds && _depthMinMax[1] != 0) - { - // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane - float minDepth = asfloat(_depthMinMax[1]); // Max depth bits = nearest sample - float maxDepth = asfloat(_depthMinMax[0]); // Min depth bits = farthest sample - - float rawMin = (nearClip * farClip) / (nearClip + minDepth * (farClip - nearClip)); - float rawMax = (nearClip * farClip) / (nearClip + maxDepth * (farClip - nearClip)); - - rawMin = clamp(rawMin, rangeMin, _constants.shadowMaxDistance - 1.0f); - rawMax = clamp(rawMax, rawMin + 1.0f, _constants.shadowMaxDistance); - - // Hysteresis: expand instantly, shrink in a single jump after the range has been smaller - // for a whole window. A continuous shrink rescales the cascades every step and the texel - // grid realignment reads as shimmer, one jump per window is one realignment - if (state.smoothedMax <= state.smoothedMin) // Uninitialized state - { - state.smoothedMin = rawMin; - state.smoothedMax = rawMax; - state.windowLowestMin = rawMin; - state.windowHighestMax = rawMax; - state.windowTimer = 0.0f; - } - else - { - state.smoothedMin = min(state.smoothedMin, rawMin); - state.smoothedMax = max(state.smoothedMax, rawMax); - - state.windowLowestMin = min(state.windowLowestMin, rawMin); - state.windowHighestMax = max(state.windowHighestMax, rawMax); - state.windowTimer += _constants.deltaTime; - - if (state.windowTimer >= _constants.shrinkDelay) - { - // Shrink to the widest bounds the window actually needed - state.smoothedMin = max(state.smoothedMin, state.windowLowestMin); - state.smoothedMax = min(state.smoothedMax, state.windowHighestMax); - - state.windowLowestMin = rawMin; - state.windowHighestMax = rawMax; - state.windowTimer = 0.0f; - } - } - - // Quantize outward AFTER smoothing so the working bounds are piecewise-constant - // and the stable texel snapping stays effective - float step = max(_constants.quantizeStep, 0.01f); - rangeMin = max(floor(state.smoothedMin / step) * step, max(nearClip, 1.0f)); - rangeMax = min(ceil(state.smoothedMax / step) * step, _constants.shadowMaxDistance); - rangeMax = max(rangeMax, rangeMin + 1.0f); - } - else - { - // Parity mode / no valid depth: keep the state seeded so enabling SDSM starts sane - state.smoothedMin = rangeMin; - state.smoothedMax = rangeMax; - state.windowLowestMin = rangeMin; - state.windowHighestMax = rangeMax; - state.windowTimer = 0.0f; - } - - state.usedMin = rangeMin; - state.usedMax = rangeMax; - _sdsmState[0] = state; - - // Split distances, same log/uniform lambda mix as the CPU path. - // The split fractions stay relative to the FULL camera clip range so the frustum - // corner slicing and the eyePosition.w = distance - near semantics hold. - float cascadeSplits[8]; - { - float ratio = rangeMax / rangeMin; - float range = rangeMax - rangeMin; - for (uint i = 0; i < _constants.numCascades; i++) - { - float p = (i + 1) / float(_constants.numCascades); - float logSplit = rangeMin * pow(ratio, p); - float uniformSplit = rangeMin + range * p; - float d = _constants.cascadeSplitLambda * (logSplit - uniformSplit) + uniformSplit; - - cascadeSplits[i] = 1.0f - ((d - nearClip) / clipRange); - } - } - - // Unproject the NDC cube corners, z = 0 is the far plane with reversed Z - const float3 ndcCorners[8] = - { - float3(-1.0f, 1.0f, 0.0f), - float3( 1.0f, 1.0f, 0.0f), - float3( 1.0f, -1.0f, 0.0f), - float3(-1.0f, -1.0f, 0.0f), - float3(-1.0f, 1.0f, 1.0f), - float3( 1.0f, 1.0f, 1.0f), - float3( 1.0f, -1.0f, 1.0f), - float3(-1.0f, -1.0f, 1.0f) - }; - - float3 baseCorners[8]; - for (uint j = 0; j < 8; j++) - { - float4 corner = mul(float4(ndcCorners[j], 1.0f), mainCamera.clipToWorld); - baseCorners[j] = corner.xyz / corner.w; - } - - float3 lightDirection = _constants.lightDirection.xyz; - - float lastSplitDist = 1.0f; - for (uint cascadeIndex = 0; cascadeIndex < _constants.numCascades; cascadeIndex++) - { - float splitDist = cascadeSplits[cascadeIndex]; - - // Slice the frustum, corners 0-3 are on the far plane, the ray points from far to near - float3 frustumCorners[8]; - for (uint j = 0; j < 4; j++) - { - float3 cornerRay = baseCorners[j + 4] - baseCorners[j]; - frustumCorners[j + 4] = baseCorners[j] + cornerRay * splitDist; - frustumCorners[j] = baseCorners[j] + cornerRay * lastSplitDist; - } - - float3 frustumCenter = float3(0.0f, 0.0f, 0.0f); - for (uint j = 0; j < 8; j++) - { - frustumCenter += frustumCorners[j]; - } - frustumCenter /= 8.0f; - - // Non-stable mode uses the camera right axis, world X basis of viewToWorld - float3 upDir = -float3(mainCamera.viewToWorld[0].xyz); - - float3 minExtents; - float3 maxExtents; - if (_constants.stableShadows) - { - // Constant up for stability, fall back to Z when the light is near vertical - upDir = float3(0.0f, 1.0f, 0.0f); - if (abs(lightDirection.y) > 0.99f) - { - upDir = float3(0.0f, 0.0f, 1.0f); - } - - float sphereRadius = 0.0f; - for (uint j = 0; j < 8; j++) - { - float dist = length(frustumCorners[j] - frustumCenter); - sphereRadius = max(sphereRadius, dist); - } - - sphereRadius = ceil(sphereRadius * 16.0f) / 16.0f; - - if (_constants.useDepthBounds) - { - // Quantize the radius into multiplicative buckets: within a bucket the texel scale is - // constant so bound changes are pure translation, which the texel snap absorbs. Only a - // bucket crossing rescales the grid, and only for that cascade - const float bucketRatio = 1.25f; - sphereRadius = pow(bucketRatio, ceil(log(sphereRadius) / log(bucketRatio))); - } - - maxExtents = float3(sphereRadius, sphereRadius, sphereRadius); - minExtents = -maxExtents; - } - else - { - // AABB of the slice in light view space - float4x4 lightView = BuildLookAt(frustumCenter, frustumCenter + lightDirection, upDir); - - float3 mins = float3(3.402823466e+38f, 3.402823466e+38f, 3.402823466e+38f); - float3 maxes = -mins; - for (uint j = 0; j < 8; j++) - { - float4 corner = mul(float4(frustumCorners[j], 1.0f), lightView); - mins = min(mins, corner.xyz / corner.w); - maxes = max(maxes, corner.xyz / corner.w); - } - - minExtents = mins; - maxExtents = maxes; - - // Adjust the min/max to accommodate the filtering size - const float fixedFilterKernelSize = 3.0f; - float scale = (_constants.cascadeTextureSize + fixedFilterKernelSize) / _constants.cascadeTextureSize; - minExtents.xy *= scale; - maxExtents.xy *= scale; - } - - // minExtents.z is negative, this places the shadow camera on the sun side - float3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; - - float farPlane = maxExtents.z; - float nearPlane = minExtents.z; - - float4x4 proj = BuildOrtho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, farPlane - nearPlane, 0.0f); - float4x4 view = BuildLookAt(shadowCameraPos, frustumCenter, upDir); - - if (_constants.stableShadows) - { - // Texel snap: round the projected world origin to texel increments - float4x4 shadowMatrix = mul(view, proj); - float4 shadowOrigin = shadowMatrix[3]; // mul(float4(0,0,0,1), M) = row 3 = glm translation column - shadowOrigin *= _constants.cascadeTextureSize / 2.0f; - - float4 roundedOrigin = round(shadowOrigin); - float4 roundOffset = (roundedOrigin - shadowOrigin) * (2.0f / _constants.cascadeTextureSize); - - proj[3].xy += roundOffset.xy; - } - - Camera cascadeCamera = (Camera)0; - - cascadeCamera.worldToView = view; - cascadeCamera.viewToClip = proj; - cascadeCamera.viewToWorld = InverseLookAt(view, shadowCameraPos); - cascadeCamera.clipToView = InverseOrtho(proj); - cascadeCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView - cascadeCamera.clipToWorld = mul(cascadeCamera.clipToView, cascadeCamera.viewToWorld); // glm viewToWorld * clipToView - - float splitDepth = farClip - (nearClip + splitDist * clipRange); // = split view distance - nearClip - cascadeCamera.eyePosition = float4(shadowCameraPos, splitDepth); - cascadeCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); - cascadeCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); - - // Frustum planes, slot order matches the FrustumPlane enum: Left, Right, Bottom, Top, Near, Far - float4 row0 = MatrixRow(cascadeCamera.worldToClip, 0); - float4 row1 = MatrixRow(cascadeCamera.worldToClip, 1); - float4 row2 = MatrixRow(cascadeCamera.worldToClip, 2); - float4 row3 = MatrixRow(cascadeCamera.worldToClip, 3); - - cascadeCamera.frustum[0] = ExtractPlane(row3 + row0); // Left - cascadeCamera.frustum[1] = ExtractPlane(row3 - row0); // Right - cascadeCamera.frustum[2] = ExtractPlane(row3 + row1); // Bottom - cascadeCamera.frustum[3] = ExtractPlane(row3 - row1); // Top - cascadeCamera.frustum[4] = ExtractPlane(row3 - row2); // Near, reversed Z so depth 1 is near - cascadeCamera.frustum[5] = ExtractPlane(row2); // Far - - // The near plane sits at the shadow camera on the sun side. Casters beyond it still render - // correctly (depth clamp pancakes them), so culling must not reject them - cascadeCamera.frustum[4].w -= _constants.casterMargin; - - _rwCameras[1 + cascadeIndex] = cascadeCamera; - - lastSplitDist = splitDist; - } -} diff --git a/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang new file mode 100644 index 0000000..28de0dc --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang @@ -0,0 +1,283 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SDSM.inc.slang" + +// Final fit dispatch: builds the cascade cameras and writes them into the shared cameras +// buffer that culling, drawing and the material resolve read. Splits come from +// CascadeFitRange via the SDSM data buffer. +// Single thread: the work is a handful of tiny ortho fits with a sequential +// lastSplitDist dependency, cross-lane sync would cost more than it saves. + +[[vk::push_constant]] SDSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _sdsmData; +[[vk::binding(2, PER_PASS)]] StructuredBuffer _cascadeBounds; // Written by CascadeXYReduce + +// 1.25x multiplicative buckets with 5% down-hysteresis: within a bucket the texel scale is +// constant so bound changes are pure translation, which the texel snap absorbs +float BucketHalfExtent(float halfExtent, float previousBucket) +{ + const float bucketRatio = 1.25f; + float target = pow(bucketRatio, ceil(log(halfExtent) / log(bucketRatio))); + target = max(target, 2.0f); // Keep degenerate fits (single-texel receivers) sane + + if (previousBucket > 0.0f && target < previousBucket) + { + // Only shrink when the padded extent clearly fits the smaller bucket + if (halfExtent >= 0.76f * previousBucket) + { + return previousBucket; + } + } + return target; +} + +// Every world position maps to clip (0, 0, 10, 1): TextureProj's z window rejects it and the +// material pass renders lit without sampling. Culling rejects everything via the frustum planes +Camera BuildEmptyCascadeCamera(float3 mainCameraPos, float splitDepth) +{ + Camera camera = (Camera)0; + camera.worldToClip[3] = float4(0.0f, 0.0f, 10.0f, 1.0f); + camera.viewToClip = BuildOrtho(-0.5f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f); // Keeps the normal-offset texel math finite + camera.eyePosition = float4(mainCameraPos, splitDepth); // Split depth must stay valid for neighbor selection + + for (uint i = 0; i < 6; i++) + { + camera.frustum[i] = float4(0.0f, 1.0f, 0.0f, 3.0e38f); // dot(n, p) - w is always far below -radius + } + return camera; +} + +[shader("compute")] +[numthreads(1, 1, 1)] +void main() +{ + Camera mainCamera = _rwCameras[0]; + + float nearClip = mainCamera.nearFar.x; + float farClip = mainCamera.nearFar.y; + float clipRange = farClip - nearClip; + + // Unproject the NDC cube corners, z = 0 is the far plane with reversed Z + const float3 ndcCorners[8] = + { + float3(-1.0f, 1.0f, 0.0f), + float3( 1.0f, 1.0f, 0.0f), + float3( 1.0f, -1.0f, 0.0f), + float3(-1.0f, -1.0f, 0.0f), + float3(-1.0f, 1.0f, 1.0f), + float3( 1.0f, 1.0f, 1.0f), + float3( 1.0f, -1.0f, 1.0f), + float3(-1.0f, -1.0f, 1.0f) + }; + + float3 baseCorners[8]; + for (uint j = 0; j < 8; j++) + { + float4 corner = mul(float4(ndcCorners[j], 1.0f), mainCamera.clipToWorld); + baseCorners[j] = corner.xyz / corner.w; + } + + float3 lightDirection = _constants.lightDirection.xyz; + + float lastSplitDist = 1.0f; + for (uint cascadeIndex = 0; cascadeIndex < _constants.numCascades; cascadeIndex++) + { + float splitDist = _sdsmData[0].splitDist[cascadeIndex]; + + // Decode the reduced light-space bounds for this cascade, sentinel decodes as min > max + bool xyValid = false; + float3 boundsMin = float3(0.0f, 0.0f, 0.0f); + float3 boundsMax = float3(0.0f, 0.0f, 0.0f); + if (_constants.useXYBounds >= 1) + { + uint3 encodedMin = uint3(_cascadeBounds[cascadeIndex * 3 + 0], _cascadeBounds[cascadeIndex * 3 + 1], _cascadeBounds[cascadeIndex * 3 + 2]); + uint3 encodedMax = uint3(_cascadeBounds[24 + cascadeIndex * 3 + 0], _cascadeBounds[24 + cascadeIndex * 3 + 1], _cascadeBounds[24 + cascadeIndex * 3 + 2]); + + xyValid = all(encodedMin <= encodedMax); + if (xyValid) + { + boundsMin = float3(DecodeLightSpaceCoord(encodedMin.x), DecodeLightSpaceCoord(encodedMin.y), DecodeLightSpaceCoord(encodedMin.z)); + boundsMax = float3(DecodeLightSpaceCoord(encodedMax.x), DecodeLightSpaceCoord(encodedMax.y), DecodeLightSpaceCoord(encodedMax.z)); + } + + _sdsmData[0].cascadeDiag[cascadeIndex] = float4(xyValid ? boundsMax - boundsMin : float3(0.0f, 0.0f, 0.0f), xyValid ? 1.0f : 0.0f); + } + + if (_constants.useXYBounds == 2 && !xyValid) + { + // No visible receivers in this cascade's depth range: nothing can sample it + _rwCameras[1 + cascadeIndex] = BuildEmptyCascadeCamera(mainCamera.eyePosition.xyz, _sdsmData[0].splitDepth[cascadeIndex]); + _sdsmData[0].cascadeStable[cascadeIndex] = float4(0.0f, 0.0f, 0.0f, 0.0f); // Buckets reseed when the cascade refills + lastSplitDist = splitDist; + continue; + } + + float3 frustumCenter; + float3 upDir; + float3 minExtents; + float3 maxExtents; + + if (_constants.useXYBounds == 2) + { + upDir = StableUp(lightDirection); + + float3 centerLS = (boundsMin + boundsMax) * 0.5f; + float3 halfLS = (boundsMax - boundsMin) * 0.5f; + + // Filter + normal-offset margin, applied before bucketing so the bucket absorbs it + float marginScale = (_constants.cascadeTextureSize + 2.0f * _constants.xyMarginTexels) / _constants.cascadeTextureSize; + halfLS.xy *= marginScale; + + float previousBucketX = _sdsmData[0].cascadeStable[cascadeIndex].x; + float previousBucketY = _sdsmData[0].cascadeStable[cascadeIndex].y; + float bucketHalfX = BucketHalfExtent(halfLS.x, previousBucketX); + float bucketHalfY = BucketHalfExtent(halfLS.y, previousBucketY); + _sdsmData[0].cascadeStable[cascadeIndex] = float4(bucketHalfX, bucketHalfY, 0.0f, 0.0f); + + // Z: pad by the margin in world units and quantize outward, Z only affects the bias + // distribution so piecewise-constant is enough + float texelWorldSize = (2.0f * bucketHalfX) / _constants.cascadeTextureSize; + float zPadding = _constants.xyMarginTexels * texelWorldSize; + float zStep = max(_constants.quantizeStep, 1.0f); + float quantizedMinZ = floor((boundsMin.z - zPadding) / zStep) * zStep; + float quantizedMaxZ = ceil((boundsMax.z + zPadding) / zStep) * zStep; + quantizedMaxZ = max(quantizedMaxZ, quantizedMinZ + 1.0f); + + // The reduced bounds are camera-relative, rotate the center back and re-anchor on the camera + float3x3 lightRotation = BuildLightRotation(lightDirection); + frustumCenter = mainCamera.eyePosition.xyz + mul(centerLS, transpose(lightRotation)); + + minExtents = float3(-bucketHalfX, -bucketHalfY, quantizedMinZ - centerLS.z); + maxExtents = float3(bucketHalfX, bucketHalfY, quantizedMaxZ - centerLS.z); + } + else + { + + // Slice the frustum, corners 0-3 are on the far plane, the ray points from far to near + float3 frustumCorners[8]; + for (uint j = 0; j < 4; j++) + { + float3 cornerRay = baseCorners[j + 4] - baseCorners[j]; + frustumCorners[j + 4] = baseCorners[j] + cornerRay * splitDist; + frustumCorners[j] = baseCorners[j] + cornerRay * lastSplitDist; + } + + frustumCenter = float3(0.0f, 0.0f, 0.0f); + for (uint j = 0; j < 8; j++) + { + frustumCenter += frustumCorners[j]; + } + frustumCenter /= 8.0f; + + // Non-stable mode uses the camera right axis, world X basis of viewToWorld + upDir = -float3(mainCamera.viewToWorld[0].xyz); + + if (_constants.stableShadows) + { + upDir = StableUp(lightDirection); + + float sphereRadius = 0.0f; + for (uint j = 0; j < 8; j++) + { + float dist = length(frustumCorners[j] - frustumCenter); + sphereRadius = max(sphereRadius, dist); + } + + sphereRadius = ceil(sphereRadius * 16.0f) / 16.0f; + + if (_constants.useDepthBounds) + { + // Quantize the radius into multiplicative buckets: within a bucket the texel scale is + // constant so bound changes are pure translation, which the texel snap absorbs. Only a + // bucket crossing rescales the grid, and only for that cascade + const float bucketRatio = 1.25f; + sphereRadius = pow(bucketRatio, ceil(log(sphereRadius) / log(bucketRatio))); + } + + maxExtents = float3(sphereRadius, sphereRadius, sphereRadius); + minExtents = -maxExtents; + } + else + { + // AABB of the slice in light view space + float4x4 lightView = BuildLookAt(frustumCenter, frustumCenter + lightDirection, upDir); + + float3 mins = float3(3.402823466e+38f, 3.402823466e+38f, 3.402823466e+38f); + float3 maxes = -mins; + for (uint j = 0; j < 8; j++) + { + float4 corner = mul(float4(frustumCorners[j], 1.0f), lightView); + mins = min(mins, corner.xyz / corner.w); + maxes = max(maxes, corner.xyz / corner.w); + } + + minExtents = mins; + maxExtents = maxes; + + // Adjust the min/max to accommodate the filtering size + const float fixedFilterKernelSize = 3.0f; + float scale = (_constants.cascadeTextureSize + fixedFilterKernelSize) / _constants.cascadeTextureSize; + minExtents.xy *= scale; + maxExtents.xy *= scale; + } + + } + + // minExtents.z is negative, this places the shadow camera on the sun side + float3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; + + float farPlane = maxExtents.z; + float nearPlane = minExtents.z; + + float4x4 proj = BuildOrtho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, farPlane - nearPlane, 0.0f); + float4x4 view = BuildLookAt(shadowCameraPos, frustumCenter, upDir); + + if (_constants.stableShadows) + { + // Texel snap: round the projected world origin to texel increments + float4x4 shadowMatrix = mul(view, proj); + float4 shadowOrigin = shadowMatrix[3]; // mul(float4(0,0,0,1), M) = row 3 = glm translation column + shadowOrigin *= _constants.cascadeTextureSize / 2.0f; + + float4 roundedOrigin = round(shadowOrigin); + float4 roundOffset = (roundedOrigin - shadowOrigin) * (2.0f / _constants.cascadeTextureSize); + + proj[3].xy += roundOffset.xy; + } + + Camera cascadeCamera = (Camera)0; + + cascadeCamera.worldToView = view; + cascadeCamera.viewToClip = proj; + cascadeCamera.viewToWorld = InverseLookAt(view, shadowCameraPos); + cascadeCamera.clipToView = InverseOrtho(proj); + cascadeCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView + cascadeCamera.clipToWorld = mul(cascadeCamera.clipToView, cascadeCamera.viewToWorld); // glm viewToWorld * clipToView + + cascadeCamera.eyePosition = float4(shadowCameraPos, _sdsmData[0].splitDepth[cascadeIndex]); + cascadeCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); + cascadeCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); + + // Frustum planes, slot order matches the FrustumPlane enum: Left, Right, Bottom, Top, Near, Far + float4 row0 = MatrixRow(cascadeCamera.worldToClip, 0); + float4 row1 = MatrixRow(cascadeCamera.worldToClip, 1); + float4 row2 = MatrixRow(cascadeCamera.worldToClip, 2); + float4 row3 = MatrixRow(cascadeCamera.worldToClip, 3); + + cascadeCamera.frustum[0] = ExtractPlane(row3 + row0); // Left + cascadeCamera.frustum[1] = ExtractPlane(row3 - row0); // Right + cascadeCamera.frustum[2] = ExtractPlane(row3 + row1); // Bottom + cascadeCamera.frustum[3] = ExtractPlane(row3 - row1); // Top + cascadeCamera.frustum[4] = ExtractPlane(row3 - row2); // Near, reversed Z so depth 1 is near + cascadeCamera.frustum[5] = ExtractPlane(row2); // Far + + // The near plane sits at the shadow camera on the sun side. Casters beyond it still render + // correctly (depth clamp pancakes them), so culling must not reject them + cascadeCamera.frustum[4].w -= _constants.casterMargin; + + _rwCameras[1 + cascadeIndex] = cascadeCamera; + + lastSplitDist = splitDist; + } +} diff --git a/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang new file mode 100644 index 0000000..445f3f0 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang @@ -0,0 +1,113 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SDSM.inc.slang" + +// First fit dispatch: turns the reduced depth min/max into the working cascade range +// (expand-instant, windowed-jump shrink, outward quantization) and the split distances. +// CascadeXYReduce assigns pixels to cascades from splitDepth, CascadeFitCameras builds +// the cameras from splitDist. Both forms are stored, not recomputed, to keep bit parity. + +[[vk::push_constant]] SDSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _sdsmData; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) +[[vk::binding(2, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera + +[shader("compute")] +[numthreads(1, 1, 1)] +void main() +{ + Camera mainCamera = _srcCameras[0]; + + float nearClip = mainCamera.nearFar.x; + float farClip = mainCamera.nearFar.y; + float clipRange = farClip - nearClip; + + // Working range for the cascade distribution + float rangeMin = max(nearClip, 1.0f); + float rangeMax = min(farClip, _constants.shadowMaxDistance); + + SDSMState state = _sdsmData[0].state; + + if (_constants.useDepthBounds && _depthMinMax[1] != 0) + { + // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane + float minDepth = asfloat(_depthMinMax[1]); // Max depth bits = nearest sample + float maxDepth = asfloat(_depthMinMax[0]); // Min depth bits = farthest sample + + float rawMin = (nearClip * farClip) / (nearClip + minDepth * (farClip - nearClip)); + float rawMax = (nearClip * farClip) / (nearClip + maxDepth * (farClip - nearClip)); + + rawMin = clamp(rawMin, rangeMin, _constants.shadowMaxDistance - 1.0f); + rawMax = clamp(rawMax, rawMin + 1.0f, _constants.shadowMaxDistance); + + // Hysteresis: expand instantly, shrink in a single jump after the range has been smaller + // for a whole window. A continuous shrink rescales the cascades every step and the texel + // grid realignment reads as shimmer, one jump per window is one realignment + if (state.smoothedMax <= state.smoothedMin) // Uninitialized state + { + state.smoothedMin = rawMin; + state.smoothedMax = rawMax; + state.windowLowestMin = rawMin; + state.windowHighestMax = rawMax; + state.windowTimer = 0.0f; + } + else + { + state.smoothedMin = min(state.smoothedMin, rawMin); + state.smoothedMax = max(state.smoothedMax, rawMax); + + state.windowLowestMin = min(state.windowLowestMin, rawMin); + state.windowHighestMax = max(state.windowHighestMax, rawMax); + state.windowTimer += _constants.deltaTime; + + if (state.windowTimer >= _constants.shrinkDelay) + { + // Shrink to the widest bounds the window actually needed + state.smoothedMin = max(state.smoothedMin, state.windowLowestMin); + state.smoothedMax = min(state.smoothedMax, state.windowHighestMax); + + state.windowLowestMin = rawMin; + state.windowHighestMax = rawMax; + state.windowTimer = 0.0f; + } + } + + // Quantize outward AFTER smoothing so the working bounds are piecewise-constant + // and the stable texel snapping stays effective + float step = max(_constants.quantizeStep, 0.01f); + rangeMin = max(floor(state.smoothedMin / step) * step, max(nearClip, 1.0f)); + rangeMax = min(ceil(state.smoothedMax / step) * step, _constants.shadowMaxDistance); + rangeMax = max(rangeMax, rangeMin + 1.0f); + } + else + { + // Parity mode / no valid depth: keep the state seeded so enabling SDSM starts sane + state.smoothedMin = rangeMin; + state.smoothedMax = rangeMax; + state.windowLowestMin = rangeMin; + state.windowHighestMax = rangeMax; + state.windowTimer = 0.0f; + } + + state.usedMin = rangeMin; + state.usedMax = rangeMax; + _sdsmData[0].state = state; + + // Split distances, same log/uniform lambda mix as the legacy CPU path. + // The split fractions stay relative to the FULL camera clip range so the frustum + // corner slicing and the eyePosition.w = distance - near semantics hold + { + float ratio = rangeMax / rangeMin; + float range = rangeMax - rangeMin; + for (uint i = 0; i < _constants.numCascades; i++) + { + float p = (i + 1) / float(_constants.numCascades); + float logSplit = rangeMin * pow(ratio, p); + float uniformSplit = rangeMin + range * p; + float d = _constants.cascadeSplitLambda * (logSplit - uniformSplit) + uniformSplit; + + _sdsmData[0].splitDist[i] = 1.0f - ((d - nearClip) / clipRange); + _sdsmData[0].splitDepth[i] = d - nearClip; + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang new file mode 100644 index 0000000..1fea56f --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang @@ -0,0 +1,120 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SDSM.inc.slang" + +// Second fit dispatch: reduces the light-space AABB of the visible depth samples per cascade. +// Positions are reconstructed camera-relative (clipToView + rotate-only viewToWorld), going +// through clipToWorld at world-scale coordinates loses precision. Coordinates are encoded +// with the order-preserving signed-float transform so uint atomics work on them. + +[[vk::push_constant]] SDSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] Texture2D _depth; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera +[[vk::binding(2, PER_PASS)]] StructuredBuffer _sdsmData; +[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _cascadeBounds; // [cascade * 3 + axis] encoded mins, [24 + cascade * 3 + axis] encoded maxs + +groupshared uint gMins[24]; +groupshared uint gMaxs[24]; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +void Contribute(uint cascadeIndex, uint3 encoded) +{ + uint base = cascadeIndex * 3; + InterlockedMin(gMins[base + 0], encoded.x); + InterlockedMin(gMins[base + 1], encoded.y); + InterlockedMin(gMins[base + 2], encoded.z); + InterlockedMax(gMaxs[base + 0], encoded.x); + InterlockedMax(gMaxs[base + 1], encoded.y); + InterlockedMax(gMaxs[base + 2], encoded.z); +} + +[shader("compute")] +[numthreads(16, 16, 1)] +void main(CSInput input) +{ + if (input.groupIndex < 24) + { + gMins[input.groupIndex] = 0xFFFFFFFFu; + gMaxs[input.groupIndex] = 0u; + } + GroupMemoryBarrierWithGroupSync(); + + uint2 dim; + _depth.GetDimensions(dim.x, dim.y); + + uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); + float depth = _depth[pixel]; + + // Cleared depth is exactly 0.0 (reversed Z far plane), sky must not drive the bounds + if (depth != 0.0f) + { + Camera mainCamera = _srcCameras[0]; + + float2 uv = (float2(pixel) + 0.5f) / float2(dim); + float2 ndc = float2(uv.x, 1.0f - uv.y) * 2.0f - 1.0f; + + float4 viewPos = mul(float4(ndc, depth, 1.0f), mainCamera.clipToView); + viewPos.xyz /= viewPos.w; + + float viewZ = viewPos.z; + uint numCascades = _constants.numCascades; + float lastSplitDepth = _sdsmData[0].splitDepth[numCascades - 1]; + + // Beyond the last split the material pass is fully faded, those pixels never use cascade data + if (viewZ <= lastSplitDepth * 1.005f) + { + // Mirrors GetShadowCascadeIndexFromDepth, _cameras[i].eyePosition.w == splitDepth[i - 1] + uint cascadeIndex = 0; + for (uint i = 1; i < numCascades; i++) + { + if (viewZ > _sdsmData[0].splitDepth[i - 1]) + { + cascadeIndex = i; + } + } + + // Camera-relative world position, rotation only + float3 relativePos = mul(float4(viewPos.xyz, 0.0f), mainCamera.viewToWorld).xyz; + float3 lightSpacePos = mul(relativePos, BuildLightRotation(_constants.lightDirection.xyz)); + + uint3 encoded = uint3( + EncodeLightSpaceCoord(lightSpacePos.x), + EncodeLightSpaceCoord(lightSpacePos.y), + EncodeLightSpaceCoord(lightSpacePos.z)); + + Contribute(cascadeIndex, encoded); + + // Pixels near a split also contribute to the neighbor: the material pass reconstructs + // viewZ through a different float path and must never select a cascade this reduce + // considered empty + float epsilon = 0.005f * max(_sdsmData[0].splitDepth[cascadeIndex], 1.0f); + if (cascadeIndex > 0 && (viewZ - _sdsmData[0].splitDepth[cascadeIndex - 1]) < epsilon) + { + Contribute(cascadeIndex - 1, encoded); + } + if (cascadeIndex + 1 < numCascades && (_sdsmData[0].splitDepth[cascadeIndex] - viewZ) < epsilon) + { + Contribute(cascadeIndex + 1, encoded); + } + } + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex < 24) + { + if (gMins[input.groupIndex] != 0xFFFFFFFFu) + { + InterlockedMin(_cascadeBounds[input.groupIndex], gMins[input.groupIndex]); + } + if (gMaxs[input.groupIndex] != 0u) + { + InterlockedMax(_cascadeBounds[24 + input.groupIndex], gMaxs[input.groupIndex]); + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/SDSM.inc.slang b/Source/Shaders/Shaders/Shadows/SDSM.inc.slang new file mode 100644 index 0000000..8747429 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SDSM.inc.slang @@ -0,0 +1,154 @@ +#ifndef SDSM_INCLUDED +#define SDSM_INCLUDED + +// Shared types and helpers for the SDSM cascade fitting chain: +// DepthMinMax.cs -> CascadeFitRange.cs -> CascadeXYReduce.cs -> CascadeFitCameras.cs +// +// Matrix layout rule (load-bearing): shaders consume glm column-major uploads with +// mul(rowVector, M), so a Slang float4x4 row i must hold what glm stores in column i. +// glm composition C = A * B therefore becomes C_s = mul(B_s, A_s) here. + +// Push constants shared by all fit dispatches, identical bytes pushed to each pipeline +struct SDSMConstants +{ + float4 lightDirection; // xyz = direction the light travels + uint numCascades; + float cascadeSplitLambda; + float cascadeTextureSize; + uint stableShadows; + float shadowMaxDistance; + float quantizeStep; + float shrinkDelay; + float deltaTime; + uint useDepthBounds; + float casterMargin; + uint useXYBounds; // 0 = sphere fit, 1 = reduce runs for diagnostics only, 2 = XY bounds drive the cameras + float xyMarginTexels; +}; + +struct SDSMState +{ + float smoothedMin; + float smoothedMax; + float usedMin; + float usedMax; + float windowLowestMin; // Widest bounds seen during the current shrink window + float windowHighestMax; + float windowTimer; + float padding; +}; + +struct SDSMData +{ + SDSMState state; // Persistent Z-range hysteresis, float offsets [0..7] are read back by the CPU + float splitDist[8]; // Split as an NDC-corner lerp fraction, consumed by the slice corner math + float splitDepth[8]; // Split as view distance - nearClip, mirrors _cameras[i].eyePosition.w semantics + float4 cascadeStable[8]; // Persistent per cascade: x = bucketHalfX, y = bucketHalfY + float4 cascadeDiag[8]; // Per frame diagnostics: xyz = fitted extents, w = valid +}; + +// Constant up for the stable path, falls back to Z when the light is near vertical +float3 StableUp(float3 lightDirection) +{ + return abs(lightDirection.y) > 0.99f ? float3(0.0f, 0.0f, 1.0f) : float3(0.0f, 1.0f, 0.0f); +} + +// Matches glm::lookAtLH memory layout (GLM_FORCE_LEFT_HANDED) +float4x4 BuildLookAt(float3 eye, float3 center, float3 up) +{ + float3 f = normalize(center - eye); + float3 s = normalize(cross(up, f)); + float3 u = cross(f, s); + + float4x4 m; + m[0] = float4(s.x, u.x, f.x, 0.0f); + m[1] = float4(s.y, u.y, f.y, 0.0f); + m[2] = float4(s.z, u.z, f.z, 0.0f); + m[3] = float4(-dot(s, eye), -dot(u, eye), -dot(f, eye), 1.0f); + return m; +} + +// Rotation-only light basis, lightSpace = mul(cameraRelativeWorld, R). The rows mirror +// BuildLookAt(0, lightDirection, StableUp) term for term so CascadeXYReduce coordinates +// and the CascadeFitCameras view matrix agree bit for bit +float3x3 BuildLightRotation(float3 lightDirection) +{ + float3 f = normalize(lightDirection); + float3 s = normalize(cross(StableUp(lightDirection), f)); + float3 u = cross(f, s); + + float3x3 m; + m[0] = float3(s.x, u.x, f.x); + m[1] = float3(s.y, u.y, f.y); + m[2] = float3(s.z, u.z, f.z); + return m; +} + +// Analytic inverse of a BuildLookAt matrix (rigid transform) +float4x4 InverseLookAt(float4x4 view, float3 eye) +{ + float3 s = float3(view[0].x, view[1].x, view[2].x); + float3 u = float3(view[0].y, view[1].y, view[2].y); + float3 f = float3(view[0].z, view[1].z, view[2].z); + + float4x4 m; + m[0] = float4(s, 0.0f); + m[1] = float4(u, 0.0f); + m[2] = float4(f, 0.0f); + m[3] = float4(eye, 1.0f); + return m; +} + +// Matches glm::orthoLH_ZO memory layout (GLM_FORCE_DEPTH_ZERO_TO_ONE) +float4x4 BuildOrtho(float left, float right, float bottom, float top, float zNear, float zFar) +{ + float4x4 m; + m[0] = float4(2.0f / (right - left), 0.0f, 0.0f, 0.0f); + m[1] = float4(0.0f, 2.0f / (top - bottom), 0.0f, 0.0f); + m[2] = float4(0.0f, 0.0f, 1.0f / (zFar - zNear), 0.0f); + m[3] = float4(-(right + left) / (right - left), -(top + bottom) / (top - bottom), -zNear / (zFar - zNear), 1.0f); + return m; +} + +// Analytic inverse of a BuildOrtho matrix, uses the post-snap projection values +float4x4 InverseOrtho(float4x4 proj) +{ + float3 scale = float3(proj[0].x, proj[1].y, proj[2].z); + float3 translation = proj[3].xyz; + + float4x4 m; + m[0] = float4(1.0f / scale.x, 0.0f, 0.0f, 0.0f); + m[1] = float4(0.0f, 1.0f / scale.y, 0.0f, 0.0f); + m[2] = float4(0.0f, 0.0f, 1.0f / scale.z, 0.0f); + m[3] = float4(-translation / scale, 1.0f); + return m; +} + +// glm row i of M, planes are extracted from mathematical rows +float4 MatrixRow(float4x4 m, uint i) +{ + return float4(m[0][i], m[1][i], m[2][i], m[3][i]); +} + +// Gribb-Hartmann row (inside if dot(n, p) + d >= 0) to the culling encoding vec4(n, dot(n, pointOnPlane)) +float4 ExtractPlane(float4 row) +{ + float normalLength = length(row.xyz); + return float4(row.xyz / normalLength, -row.w / normalLength); +} + +// Order-preserving float <-> uint for atomic min/max on signed light-space coordinates. +// Branches on the stored sign bit so -0.0 orders below +0.0. Sentinels: 0xFFFFFFFF orders +// above all real keys, 0x00000000 below, so an untouched slot decodes as min > max (invalid) +uint EncodeLightSpaceCoord(float f) +{ + uint u = asuint(f); + return (u & 0x80000000u) != 0 ? ~u : (u | 0x80000000u); +} + +float DecodeLightSpaceCoord(uint u) +{ + return asfloat((u & 0x80000000u) != 0 ? (u ^ 0x80000000u) : ~u); +} + +#endif // SDSM_INCLUDED From dc33327cad7bbd12e03dbc3cbfb0680d4ff7e636 Mon Sep 17 00:00:00 2001 From: Pursche Date: Wed, 15 Jul 2026 00:44:11 +0200 Subject: [PATCH 13/24] Add Sparse Virtual Shadow Maps as shadowTechnique 1, A/B alongside CSM --- .../Systems/CalculateShadowCameraMatrices.cpp | 34 +- .../Editor/PerformanceDiagnostics.cpp | 110 +- .../Game-Lib/Rendering/CulledRenderer.cpp | 286 +++-- .../Game-Lib/Rendering/CulledRenderer.h | 17 +- .../Game-Lib/Rendering/GameRenderer.cpp | 18 +- .../Rendering/Material/MaterialRenderer.cpp | 72 +- .../Rendering/Model/ModelRenderer.cpp | 552 ++++++++- .../Game-Lib/Rendering/Model/ModelRenderer.h | 50 + .../Rendering/Shadow/ShadowRenderer.cpp | 1068 ++++++++++++++++- .../Rendering/Shadow/ShadowRenderer.h | 125 ++ .../Rendering/Terrain/TerrainRenderer.cpp | 253 +++- .../Rendering/Terrain/TerrainRenderer.h | 11 + .../Shaders/DescriptorSet/Light.inc.slang | 9 + .../Shaders/Include/Lighting.inc.slang | 14 +- .../Shaders/Shaders/Include/Shadows.inc.slang | 396 ++++++ .../Shaders/Material/MaterialPass.cs.slang | 10 +- .../Shaders/Shaders/Model/DrawSVSM.ps.slang | 75 ++ Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 228 ++++ .../Shaders/Shadows/SVSMDynamicMark.cs.slang | 78 ++ .../Shadows/SVSMDynamicUpdate.cs.slang | 129 ++ .../Shaders/Shadows/SVSMFinalize.cs.slang | 106 ++ .../Shadows/SVSMInvalidateAABBs.cs.slang | 75 ++ .../Shaders/Shadows/SVSMPageClear.cs.slang | 34 + .../Shaders/Shadows/SVSMPageMark.cs.slang | 129 ++ .../Shadows/SVSMPageTableDebug.cs.slang | 88 ++ .../Shaders/Shadows/SVSMPageUpdateA.cs.slang | 131 ++ .../Shaders/Shadows/SVSMPageUpdateB.cs.slang | 149 +++ .../Shaders/Shadows/SVSMPoolDebug.cs.slang | 49 + .../Shaders/Shadows/SVSMPrepare.cs.slang | 131 ++ .../Shaders/Shaders/Terrain/DrawSVSM.ps.slang | 31 + ...FillInstancedDrawCallsFromBitmask.cs.slang | 13 + 31 files changed, 4324 insertions(+), 147 deletions(-) create mode 100644 Source/Shaders/Shaders/Model/DrawSVSM.ps.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSM.inc.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMDynamicUpdate.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPageClear.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPageTableDebug.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPageUpdateA.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPoolDebug.cs.slang create mode 100644 Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang create mode 100644 Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp index ef4dcf8..1e40983 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp @@ -61,9 +61,20 @@ namespace ECS::Systems i32 cascadeTextureSize = CVAR_ShadowCascadeTextureSize.Get(); bool stableShadows = CVAR_ShadowsStable.Get() == 1; + CVarSystem* cvarSystem = CVarSystem::Get(); + const bool useSVSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + // Initialize any new shadow cascades - // Cascade count can shrink at runtime, we keep the excess cameras and depth images around unused since neither can shrink - u32 numCamerasNeeded = numCascades + 1; // Main camera + cascades + // Cascade count can shrink at runtime, we keep the excess cameras and depth images around unused since neither can shrink. + // SVSM clipmaps reuse the same camera slots but need no cascade depth images + u32 numViews = numCascades; + if (useSVSM) + { + u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + numViews = glm::max(numCascades, numClipmaps); + } + + u32 numCamerasNeeded = numViews + 1; // Main camera + cascades/clipmaps u32 numCameras = renderResources.cameras.Count(); if (numCamerasNeeded > numCameras) { @@ -87,8 +98,6 @@ namespace ECS::Systems renderResources.shadowDepthCascades.push_back(cascadeDepthImage); } - CVarSystem* cvarSystem = CVarSystem::Get(); - // Master toggle, no shadow work at all while disabled (resource growth above stays so enabling works at runtime) if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) return; @@ -96,17 +105,18 @@ namespace ECS::Systems const bool useSDSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowUseSDSM"_h) != 0; const bool validateParity = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity"_h) != 0; - // The GPU fit pass owns the cascade cameras while SDSM is on, the CPU copies go stale. - // On toggle-off force a full re-upload (the loop below would normally re-dirty every frame, - // but not while frozen) - static bool previousUseSDSM = useSDSM; - if (previousUseSDSM && !useSDSM) + // A GPU pass owns the cascade camera slots while SDSM or SVSM is on, the CPU copies go + // stale. On toggle back to CPU ownership force a full re-upload (the loop below would + // normally re-dirty every frame, but not while frozen) + const bool gpuOwnsCameras = useSVSM || (useSDSM && !validateParity); + static bool previousGpuOwnsCameras = gpuOwnsCameras; + if (previousGpuOwnsCameras && !gpuOwnsCameras) { - renderResources.cameras.SetDirtyElements(1, numCascades); + renderResources.cameras.SetDirtyElements(1, renderResources.cameras.Count() - 1); } - previousUseSDSM = useSDSM; + previousGpuOwnsCameras = gpuOwnsCameras; - if (useSDSM && !validateParity) + if (gpuOwnsCameras) return; if (CVAR_ShadowsFreezeCascades.Get()) diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index 6b125ec..736a530 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -80,7 +80,10 @@ namespace Editor const std::string rightHeaderText = "Survived / Total (%)"; static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit /*| ImGuiTableFlags_BordersOuter*/ | ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersH | ImGuiTableFlags_ContextMenuInBody; - u32 numCascades = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + // Under SVSM the shadow views are clipmaps instead of cascades + const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + u32 numCascades = useSVSM ? *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h) + : *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); // SDSM reduced depth bounds { @@ -122,6 +125,80 @@ namespace Editor { ImGui::Text("Visible Depth: - (no valid samples)"); } + + // SVSM page table state, one frame old + if (*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1 && shadowRenderer) + { + u32 freePages = 0; + u32 totalPages = 0; + u32 overflow = 0; + u32 invalidationCause = 0; + shadowRenderer->GetSVSMGlobalStats(freePages, totalPages, overflow, invalidationCause); + + ImGui::Spacing(); + ImGui::Text("SVSM Pool: %u / %u pages used", totalPages - glm::min(freePages, totalPages), totalPages); + if (overflow > 0) + { + ImGui::Text("SVSM Overflow: %u page requests dropped", overflow); + } + + u32 dynamicLive = 0; + u32 dynamicTotal = 0; + u32 dynamicOverflow = 0; + shadowRenderer->GetSVSMDynamicStats(dynamicLive, dynamicTotal, dynamicOverflow); + + u32 dynamicCasters = 0; + u32 animatedCasters = 0; + u32 droppedAABBs = 0; + shadowRenderer->GetSVSMCasterStats(dynamicCasters, animatedCasters, droppedAABBs); + if (dynamicLive > 0 || dynamicOverflow > 0 || dynamicCasters > 0 || animatedCasters > 0) + { + ImGui::Text("SVSM Dynamic: %u / %u pages live%s | casters: %u dynamic + %u animated", dynamicLive, dynamicTotal, dynamicOverflow > 0 ? " (overflowing!)" : "", dynamicCasters, animatedCasters); + if (droppedAABBs > 0) + { + ImGui::Text("SVSM Dynamic: %u caster AABBs dropped!", droppedAABBs); + } + + // Per-clipmap surviving dynamic instances of the SVSM dynamic fills, one + // frame old. A bouncing count here = the fill/cull chain loses casters + if (ModelRenderer* svsmModelRenderer = gameRenderer->GetModelRenderer()) + { + u32 numClipmapViews = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); + std::string dynDraws = "SVSM Dyn instances:"; + for (u32 view = 1; view <= numClipmapViews && view < Renderer::Settings::MAX_VIEWS; view++) + { + dynDraws += " " + std::to_string(svsmModelRenderer->GetNumSVSMDynamicInstances(view)); + } + ImGui::Text("%s", dynDraws.c_str()); + } + } + + i32 renderBudget = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmRenderBudget"_h); + if (renderBudget > 0) + { + ImGui::Text("SVSM Budget: %u requested / %d allowed", shadowRenderer->GetSVSMBudgetUsed(), renderBudget); + } + + if (invalidationCause != 0) + { + std::string causes; + if (invalidationCause & 1) causes += "sun step "; + if (invalidationCause & 2) causes += "z range "; + if (invalidationCause & 4) causes += "manual "; + if (invalidationCause & 8) causes += "aabb overflow "; + ImGui::Text("SVSM Invalidation: %s", causes.c_str()); + } + + u32 numClipmaps = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); + for (u32 i = 0; i < numClipmaps; i++) + { + ShadowRenderer::SVSMClipmapStats stats; + if (!shadowRenderer->GetSVSMClipmapStats(i, stats)) + break; + + ImGui::Text("P%u (%.0fm): %u marked, %u resident, %u dirty, %u inv, %u evict, %u dyn, %u defer", i, stats.extent, stats.marked, stats.resident, stats.dirty, stats.invalidated, stats.evicted, stats.dynamicLive, stats.deferred); + } + } } ImGui::Spacing(); @@ -523,6 +600,29 @@ namespace Editor if (frameTimeQueries.size() > 0) { ImGui::Text("Render Passes (GPU)"); + + // Copy the pass list as "name;ms" lines. Semicolon separator so a comma decimal + // separator can never collide with it + const char* copyLabel = "Copy CSV"; + ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - ImGui::CalcTextSize(copyLabel).x - ImGui::GetStyle().FramePadding.x * 2.0f); + if (ImGui::SmallButton(copyLabel)) + { + std::string csv = "Pass;Milliseconds\n"; + for (u32 i = 0; i < frameTimeQueries.size(); i++) + { + const std::string& name = renderer->GetTimeQueryName(frameTimeQueries[i]); + + f32 averageMS = 0.0f; + if (stats.AverageNamed(name, 240, averageMS)) + { + char line[256]; + snprintf(line, sizeof(line), "%s;%.3f\n", name.c_str(), averageMS); + csv += line; + } + } + ImGui::SetClipboardText(csv.c_str()); + } + if (ImGui::BeginTable("passtimes", 2, flags)) { for (u32 i = 0; i < frameTimeQueries.size(); i++) @@ -617,7 +717,8 @@ namespace Editor std::string viewName = "Main View Instances"; if (viewID > 0) { - viewName = "Shadow Cascade " + std::to_string(viewID - 1) + " Drawcalls"; + const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + viewName = (useSVSM ? "Clipmap " : "Shadow Cascade ") + std::to_string(viewID - 1) + " Drawcalls"; } if (!_drawCallStatsOnlyForMainView) @@ -637,7 +738,7 @@ namespace Editor ImGui::Text("%s", rightHeaderText.c_str()); ImGui::Separator(); } - + u32 viewDrawCalls = 0; u32 viewDrawCallsSurvived = 0; @@ -726,7 +827,8 @@ namespace Editor std::string viewName = "Main View Triangles"; if (viewID > 0) { - viewName = "Shadow Cascade " + std::to_string(viewID - 1) + " Triangles"; + const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + viewName = (useSVSM ? "Clipmap " : "Shadow Cascade ") + std::to_string(viewID - 1) + " Triangles"; } if (!_drawCallStatsOnlyForMainView) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index 6afbfe9..8a267ae 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -10,6 +10,7 @@ AutoCVar_Int CVAR_ShadowDebugCullingView(CVarCategory::Client | CVarCategory::Re bool CulledRenderer::_pipelinesCreated = false; Renderer::ComputePipelineID CulledRenderer::_fillInstancedDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed +Renderer::ComputePipelineID CulledRenderer::_fillInstancedDrawCallsFilteredPipeline[2]; // Same, with the SVSM dynamic-mask filter Renderer::ComputePipelineID CulledRenderer::_fillDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed Renderer::ComputePipelineID CulledRenderer::_createIndirectAfterCullingPipeline[2]; // [0] = non-indexed, [1] = indexed Renderer::ComputePipelineID CulledRenderer::_createIndirectAfterCullingOrderedPipeline[2]; // [0] = non-indexed, [1] = indexed @@ -40,6 +41,11 @@ void CulledRenderer::InitCullingResources(CullingResourcesBase& cullingResources Renderer::DescriptorSet& geometryFillDescriptorSet = cullingResources.GetGeometryFillDescriptorSet(); geometryFillDescriptorSet.RegisterPipeline(_renderer, fillPipeline); + if (isInstanced) + { + // SVSM caster-split fills, only ever dispatched for resources that bind the dynamic mask + geometryFillDescriptorSet.RegisterPipeline(_renderer, _fillInstancedDrawCallsFilteredPipeline[isIndexed]); + } geometryFillDescriptorSet.Init(_renderer); } @@ -638,25 +644,148 @@ void CulledRenderer::CascadeCullingPass(CullingPassParams& params) params.commandList->PopMarker(); } +// Rebuilds the shared instance counts/lookup/argument buffers from a view's bitmask slice. +// filtered selects the SVSM caster-split fill variant, keepDynamic its instance class +void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic) +{ + const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); + const u32 numInstances = params.cullingResources->GetNumInstances(); + + // Reset the counters and per-drawcall instance counts + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); + params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); + params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + + // Fill the instances visible in this view + { + std::string debugName = params.passName + " Instanced Geometry Fill"; + params.commandList->PushMarker(debugName, Color::White); + + Renderer::ComputePipelineID pipeline = filtered ? _fillInstancedDrawCallsFilteredPipeline[params.cullingResources->IsIndexed()] + : _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct FillDrawCallConstants + { + u32 numTotalInstances; + u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored + u32 drawCallDataSize; + u32 currentBitmaskIndex; + u32 bitmaskOffset; + u32 keepDynamic; + }; + + FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); + fillConstants->numTotalInstances = numInstances; + fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + fillConstants->drawCallDataSize = params.drawCallDataSize; + fillConstants->currentBitmaskIndex = params.frameIndex; + fillConstants->bitmaskOffset = viewIndex * params.cullingResources->GetBitMaskBufferUintsPerView(); + fillConstants->keepDynamic = keepDynamic; + params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); + + params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); + } + + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); + + params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); + params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); + + // Create indirect argument buffer + { + std::string debugName = params.passName + " Create Indirect"; + params.commandList->PushMarker(debugName, Color::Yellow); + + Renderer::ComputePipelineID pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct CreateIndirectConstants + { + u32 numTotalDrawCalls; + u32 baseInstanceLookupOffset; + u32 drawCallDataSize; + }; + CreateIndirectConstants* createIndirectConstants = params.graphResources->FrameNew(); + + createIndirectConstants->numTotalDrawCalls = numDrawCalls; + createIndirectConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + createIndirectConstants->drawCallDataSize = params.drawCallDataSize; + params.commandList->PushConstant(createIndirectConstants, 0, sizeof(CreateIndirectConstants)); + + params.commandList->BindDescriptorSet(params.createIndirectDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); + } + + params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); +} + void CulledRenderer::GeometryPass(GeometryPassParams& params) { NC_ASSERT(params.drawCallback != nullptr, "CulledRenderer : GeometryPass got params with invalid drawCallback"); const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); + // svsmProfileGeometry: per-view fill/draw GPU timings, they surface in the perf editor's + // render pass list. Off by default, queries around tiny dispatches serialize slightly + const bool profileSVSM = params.svsmPass && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry"_h) != 0; + auto Profiled = [&](const char* stageName, u32 viewIndex, auto&& work) + { + if (!profileSVSM) + { + work(); + return; + } + + Renderer::TimeQueryDesc timeQueryDesc; + timeQueryDesc.name = "SVSM " + std::string(stageName) + " v" + std::to_string(viewIndex); + Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); + params.commandList->BeginTimeQuery(timeQuery); + work(); + params.commandList->EndTimeQuery(timeQuery); + }; + for (u32 i = params.firstViewIndex; i < params.numCascades + 1; i++) { - std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); + std::string markerName = (i == 0) ? "Main" : (params.svsmPass ? "Clipmap " : "Cascade ") + std::to_string(i - 1); params.commandList->PushMarker(markerName, Color::PastelYellow); if (i == 1) { - uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); + if (params.svsmPass) + { + // The viewport spans the virtual texture so SV_Position.xy is the virtual texel. + // No depth bias: there is no depth attachment for it to apply to + params.commandList->SetViewport(0, 0, static_cast(params.svsmExtent.x), static_cast(params.svsmExtent.y), 0.0f, 1.0f); + params.commandList->SetScissorRect(0, params.svsmExtent.x, 0, params.svsmExtent.y); + } + else + { + uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); - params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); + params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); + params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); + params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); + } } // Reset the counters @@ -708,90 +837,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) { // The main view (i == 0) consumes the culling pass's output directly, cascades rebuild the // shared instance counts/lookup/argument buffers from their bitmask slice before drawing - const u32 numInstances = params.cullingResources->GetNumInstances(); - - // Reset the counters and per-drawcall instance counts - params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); - params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); - params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); - params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); - - // Fill the instances visible in this cascade - { - std::string debugName = params.passName + " Instanced Geometry Fill"; - params.commandList->PushMarker(debugName, Color::White); - - Renderer::ComputePipelineID pipeline = _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); - - struct FillDrawCallConstants - { - u32 numTotalInstances; - u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored - u32 drawCallDataSize; - u32 currentBitmaskIndex; - u32 bitmaskOffset; - }; - - FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); - fillConstants->numTotalInstances = numInstances; - fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - fillConstants->drawCallDataSize = params.drawCallDataSize; - fillConstants->currentBitmaskIndex = params.frameIndex; - fillConstants->bitmaskOffset = i * params.cullingResources->GetBitMaskBufferUintsPerView(); - params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); - - params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); - - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } - - params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); - - params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); - params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); - - // Create indirect argument buffer - { - std::string debugName = params.passName + " Create Indirect"; - params.commandList->PushMarker(debugName, Color::Yellow); - - Renderer::ComputePipelineID pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); - - struct CreateIndirectConstants - { - u32 numTotalDrawCalls; - u32 baseInstanceLookupOffset; - u32 drawCallDataSize; - }; - CreateIndirectConstants* createIndirectConstants = params.graphResources->FrameNew(); - - createIndirectConstants->numTotalDrawCalls = numDrawCalls; - createIndirectConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - createIndirectConstants->drawCallDataSize = params.drawCallDataSize; - params.commandList->PushConstant(createIndirectConstants, 0, sizeof(CreateIndirectConstants)); - - params.commandList->BindDescriptorSet(params.createIndirectDescriptorSet, params.frameIndex); - - params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } - - params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); - params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); + Profiled("Fill", i, [&] { RunInstancedGeometryFill(params, i, params.svsmSplitFills, false); }); } if (!params.cullingEnabled) @@ -811,6 +857,8 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) DrawParams drawParams; drawParams.cullingEnabled = params.cullingEnabled; drawParams.shadowPass = i > 0; + drawParams.svsmPass = params.svsmPass; + drawParams.svsmExtent = params.svsmExtent; drawParams.viewIndex = i; drawParams.cullingResources = params.cullingResources; drawParams.rt0 = params.rt0; @@ -848,7 +896,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) } - params.drawCallback(drawParams); + Profiled("Draw", i, [&] { params.drawCallback(drawParams); }); } // Copy from our draw count buffer to the readback buffer @@ -860,6 +908,37 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) params.commandList->PopMarker(); } + // SVSM caster split: rebuild the shared buffers with only dynamic instances and draw them + // into the dynamic pool. Runs after the static readback copies so the perf stats show the + // static (dominant) counts. Views without recent live dynamic pages skip the whole phase + if (params.svsmSplitFills && params.enableDrawing && i > 0 && params.svsmDynamicViewActive[i]) + { + params.commandList->PushMarker("Dynamic", Color::PastelOrange); + + Profiled("DynFill", i, [&] { RunInstancedGeometryFill(params, i, true, true); }); + + DrawParams drawParams; + drawParams.cullingEnabled = params.cullingEnabled; + drawParams.shadowPass = true; + drawParams.svsmPass = params.svsmPass; + drawParams.svsmDynamicPass = true; + drawParams.svsmExtent = params.svsmExtent; + drawParams.viewIndex = i; + drawParams.cullingResources = params.cullingResources; + drawParams.argumentBuffer = params.culledDrawCallsBuffer; + drawParams.drawCountBuffer = params.culledDrawCallCountBuffer; + drawParams.drawCountIndex = 0; + drawParams.numMaxDrawCalls = params.cullingResources->GetDrawCallCount(); + + Profiled("DynDraw", i, [&] { params.drawCallbackDynamic(drawParams); }); + + // Dynamic surviving-count readback: the fill wrote this view's dynamic instance count + // to drawCountBuffer, snapshot it so missing dynamic draws are visible in the perf editor + params.commandList->CopyBuffer(params.svsmDynamicDrawCountReadBackBuffer, sizeof(u32) * i, params.drawCountBuffer, 0, sizeof(u32)); + + params.commandList->PopMarker(); + } + params.commandList->PopMarker(); } @@ -910,18 +989,29 @@ void CulledRenderer::CreatePipelines() for (u32 i = 0; i < 2; i++) { - std::vector permutationFields = + for (u32 filtered = 0; filtered < 2; filtered++) { - { "IS_INDEXED", std::to_string(i) } - }; - u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Utils/FillInstancedDrawCallsFromBitmask.cs", permutationFields); + std::vector permutationFields = + { + { "IS_INDEXED", std::to_string(i) }, + { "DYNAMIC_FILTER", std::to_string(filtered) } + }; + u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Utils/FillInstancedDrawCallsFromBitmask.cs", permutationFields); - Renderer::ComputeShaderDesc shaderDesc; - shaderDesc.shaderEntry = shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Utils/FillInstancedDrawCallsFromBitmask.cs"); + Renderer::ComputeShaderDesc shaderDesc; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Utils/FillInstancedDrawCallsFromBitmask.cs"); - pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - _fillInstancedDrawCallsFromBitmaskPipeline[i] = _renderer->CreatePipeline(pipelineDesc); + if (filtered == 0) + { + _fillInstancedDrawCallsFromBitmaskPipeline[i] = _renderer->CreatePipeline(pipelineDesc); + } + else + { + _fillInstancedDrawCallsFilteredPipeline[i] = _renderer->CreatePipeline(pipelineDesc); + } + } } } { diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index 8146352..d995553 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -38,13 +38,16 @@ class CulledRenderer public: bool cullingEnabled = false; bool shadowPass = false; + bool svsmPass = false; // Attachment-less page render, needs an explicit render area + bool svsmDynamicPass = false; // Caster-split dynamic instances into the dynamic pool u32 viewIndex = 0; CullingResourcesBase* cullingResources; - + Renderer::ImageMutableResource rt0; Renderer::ImageMutableResource rt1; Renderer::DepthImageMutableResource depth; + uvec2 svsmExtent = uvec2(0, 0); Renderer::BufferMutableResource argumentBuffer; Renderer::BufferMutableResource drawCountBuffer; @@ -318,8 +321,19 @@ class CulledRenderer bool enableDrawing = false; // Allows us to do everything but the actual drawcall, for debugging bool cullingEnabled = false; + + bool svsmPass = false; // SVSM page render: no depth targets, attachment-less draws into svsmExtent + uvec2 svsmExtent = uvec2(0, 0); + + // SVSM caster split: per view, fill+draw static instances (drawCallback) then dynamic + // instances (drawCallbackDynamic), filtered by the dynamic instance mask + bool svsmSplitFills = false; + std::function drawCallbackDynamic; + Renderer::BufferMutableResource svsmDynamicDrawCountReadBackBuffer; // Per-view dynamic surviving counts for the perf editor + bool svsmDynamicViewActive[Renderer::Settings::MAX_VIEWS] = { false }; // Views without recent live dynamic pages skip their dynamic fill+draw }; void GeometryPass(GeometryPassParams& params); + void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic); // Shared-buffer rebuild from a view's bitmask slice void CascadeCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets void SyncToGPU(); @@ -338,6 +352,7 @@ class CulledRenderer static bool _pipelinesCreated; static Renderer::ComputePipelineID _fillInstancedDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed + static Renderer::ComputePipelineID _fillInstancedDrawCallsFilteredPipeline[2]; // Same, with the SVSM dynamic-mask filter static Renderer::ComputePipelineID _fillDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed static Renderer::ComputePipelineID _createIndirectAfterCullingPipeline[2]; // [0] = non-indexed, [1] = indexed static Renderer::ComputePipelineID _createIndirectAfterCullingOrderedPipeline[2]; // [0] = non-indexed, [1] = indexed diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index c307cef..e3c407a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -268,7 +268,10 @@ f32 GameRenderer::Render() if (_resources.cameras.SyncToGPU(_renderer)) { + // Should never fire: the vector is prefilled to MAX_VIEWS at init. Kept as a safety net, + // note the rebinds only reach the shaders a frame-cycle later _resources.globalDescriptorSet.Bind("_cameras", _resources.cameras.GetBuffer()); + _shadowRenderer->BindCameraBuffers(_resources); } // Create rendergraph @@ -408,14 +411,19 @@ f32 GameRenderer::Render() _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); // Cascade block, runs after the main depth is complete so the cascades can be fitted to the - // visible samples (SDSM: depth range + per-cascade light-space bounds), culled and drawn the same frame + // visible samples (SDSM: depth range + per-cascade light-space bounds), culled and drawn the same frame. + // The SVSM update (shadowTechnique 1) analyzes the same depth to mark and allocate virtual shadow + // pages, it runs alongside the CSM path until the SVSM rendering and sampling stages land _shadowRenderer->AddDepthMinMaxPass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddCascadeFitPass(&renderGraph, _resources, _frameIndex); + _shadowRenderer->AddSVSMUpdatePass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddShadowPass(&renderGraph, _resources, _frameIndex); _terrainRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); _modelRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); _terrainRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); _modelRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); + _terrainRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); + _modelRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); _lightRenderer->AddClassificationPass(&renderGraph, _resources, _frameIndex); @@ -433,6 +441,7 @@ f32 GameRenderer::Render() _debugRenderer->Add2DPass(&renderGraph, _resources, _frameIndex); _lightRenderer->AddDebugPass(&renderGraph, _resources, _frameIndex); + _shadowRenderer->AddSVSMDebugOverlayPass(&renderGraph, _resources, _frameIndex); Renderer::ImageID finalTarget = isEditorMode ? _resources.finalColor : _resources.sceneColor; _uiRenderer->AddImguiPass(&renderGraph, _resources, _frameIndex, finalTarget); @@ -597,6 +606,13 @@ void GameRenderer::CreatePermanentResources() _resources.cameras.SetDebugName("Cameras"); _resources.cameras.SetUsage(Renderer::BufferUsage::STORAGE_BUFFER); _resources.cameras.Add(Camera()); + + // Prefill to the runtime cap and create the buffer now: descriptor binds of it happen at init + // (mid-frame buffer binds only reach the canonical descriptor copies at a later FlipFrame), + // so the buffer must exist before the renderers bind it and must never resize + _resources.cameras.AddCount(Renderer::Settings::MAX_VIEWS - 1); + _resources.cameras.SyncToGPU(_renderer); + _resources.globalDescriptorSet.Bind("_cameras", _resources.cameras.GetBuffer()); } void GameRenderer::CreateRenderTargets() diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index a8ba300..ff1fc02 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -8,6 +8,7 @@ #include "Game-Lib/Rendering/Light/LightRenderer.h" #include "Game-Lib/Rendering/Model/ModelRenderer.h" #include "Game-Lib/Rendering/RenderResources.h" +#include "Game-Lib/Rendering/Shadow/ShadowRenderer.h" #include "Game-Lib/Rendering/Terrain/TerrainRenderer.h" #include "Game-Lib/Util/PhysicsUtil.h" #include "Game-Lib/Util/ServiceLocator.h" @@ -19,7 +20,7 @@ #include -AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - ShadowCascade", 0); +AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - ShadowCascade, 5 - SVSM Compare Margin", 0); AutoCVar_ShowFlag CVAR_DrawTerrainWireframe(CVarCategory::Client | CVarCategory::Rendering, "drawTerrainWireframe", "Draw terrain wireframe", ShowFlag::DISABLED); AutoCVar_ShowFlag CVAR_EnableFog(CVarCategory::Client | CVarCategory::Rendering, "enableFog", "Toggle fog", ShowFlag::DISABLED); AutoCVar_VecFloat CVAR_FogColor(CVarCategory::Client | CVarCategory::Rendering, "fogColor", "Change fog color", vec4(0.33f, 0.2f, 0.38f, 1.0f), CVarFlags::None); @@ -128,6 +129,9 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Renderer::ImageResource ambientOcclusion; + Renderer::ImageResource svsmPagePool; + Renderer::ImageResource svsmDynamicPagePool; + Renderer::DescriptorSetResource debugSet; Renderer::DescriptorSetResource globalSet; Renderer::DescriptorSetResource lightSet; @@ -136,7 +140,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Renderer::DescriptorSetResource materialSet; }; - const i32 visibilityBufferDebugID = Math::Clamp(CVAR_VisibilityBufferDebugID.Get(), 0, 4); + const i32 visibilityBufferDebugID = Math::Clamp(CVAR_VisibilityBufferDebugID.Get(), 0, 5); renderGraph->AddPass("Material Pass", [this, &resources](MaterialPassData& data, Renderer::RenderGraphBuilder& builder) // Setup @@ -152,6 +156,16 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende builder.Read(resources.cameras.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); builder.Read(_directionalLights.GetBuffer(), Renderer::BufferPassUsage::COMPUTE); + // SVSM sampling reads the page pools the geometry passes wrote this frame, the graph + // needs the dependencies declared for the barriers. The pool bindings must always be + // valid, a placeholder stands in until the real pools are lazily created + ShadowRenderer* shadowRenderer = _gameRenderer->GetShadowRenderer(); + data.svsmPagePool = builder.Read(shadowRenderer->GetSVSMPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); + data.svsmDynamicPagePool = builder.Read(shadowRenderer->GetSVSMDynamicPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); + builder.Read(shadowRenderer->GetSVSMDataBuffer(), Renderer::BufferPassUsage::COMPUTE); + builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), Renderer::BufferPassUsage::COMPUTE); + builder.Read(shadowRenderer->GetSVSMDynamicPageTableBuffer(), Renderer::BufferPassUsage::COMPUTE); + data.debugSet = builder.Use(resources.debugDescriptorSet); data.globalSet = builder.Use(resources.globalDescriptorSet); data.lightSet = builder.Use(resources.lightDescriptorSet); @@ -180,13 +194,43 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende data.materialSet.Bind("_ambientOcclusion", data.ambientOcclusion); - // Bind descriptorset - commandList.BindDescriptorSet(data.debugSet, frameIndex); - commandList.BindDescriptorSet(data.globalSet, frameIndex); - commandList.BindDescriptorSet(data.lightSet, frameIndex); - commandList.BindDescriptorSet(data.terrainSet, frameIndex); - commandList.BindDescriptorSet(data.modelSet, frameIndex); - commandList.BindDescriptorSet(data.materialSet, frameIndex); + data.lightSet.Bind("_svsmPagePool"_h, data.svsmPagePool); + data.lightSet.Bind("_svsmDynamicPagePool"_h, data.svsmDynamicPagePool); + + // The debug permutations dead-strip descriptor set usage, so each variant binds + // exactly what it statically references or the set-usage validation fires + switch (visibilityBufferDebugID) + { + case 1: // TypeID, pure math on the visibility buffer + case 3: // TriangleID + commandList.BindDescriptorSet(data.materialSet, frameIndex); + break; + case 2: // ObjectID reads terrain instance data + commandList.BindDescriptorSet(data.terrainSet, frameIndex); + commandList.BindDescriptorSet(data.materialSet, frameIndex); + break; + case 4: // CascadeID reconstructs vertex data and reads the cascade cameras + commandList.BindDescriptorSet(data.globalSet, frameIndex); + commandList.BindDescriptorSet(data.terrainSet, frameIndex); + commandList.BindDescriptorSet(data.modelSet, frameIndex); + commandList.BindDescriptorSet(data.materialSet, frameIndex); + break; + case 5: // SVSM compare margin reconstructs vertex data and samples the virtual shadow map + commandList.BindDescriptorSet(data.globalSet, frameIndex); + commandList.BindDescriptorSet(data.lightSet, frameIndex); + commandList.BindDescriptorSet(data.terrainSet, frameIndex); + commandList.BindDescriptorSet(data.modelSet, frameIndex); + commandList.BindDescriptorSet(data.materialSet, frameIndex); + break; + default: // Full shading + commandList.BindDescriptorSet(data.debugSet, frameIndex); + commandList.BindDescriptorSet(data.globalSet, frameIndex); + commandList.BindDescriptorSet(data.lightSet, frameIndex); + commandList.BindDescriptorSet(data.terrainSet, frameIndex); + commandList.BindDescriptorSet(data.modelSet, frameIndex); + commandList.BindDescriptorSet(data.materialSet, frameIndex); + break; + } //if (CVAR_DrawTerrainWireframe.Get() == ShowFlag::ENABLED) { @@ -205,6 +249,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Color vertexColor; Color brushColor; vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias + vec4 svsmSettings; // x = Constant Bias (world meters) }; Constants* constants = graphResources.FrameNew(); @@ -215,8 +260,12 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende CVarSystem* cvarSystem = CVarSystem::Get(); const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); const f32 shadowStrength = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength")); + // lightInfo.y selects the shadow technique: 0 = cascades, 1 = sparse virtual shadow maps + ShadowRenderer* shadowRenderer = _gameRenderer->GetShadowRenderer(); + const bool useSVSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique") == 1 + && shadowRenderer != nullptr && shadowRenderer->GetSVSMPagePool() != Renderer::ImageID::Invalid(); const u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")) && shadowStrength > 0.0f; - constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), 0, numCascades, shadowEnabled); + constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), useSVSM ? 1 : 0, numCascades, shadowEnabled); constants->tileInfo = uvec4(_lightRenderer->CalculateNumTiles2D(outputSize), 0, 0); @@ -243,6 +292,9 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende f32 shadowNormalOffsetBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias")); constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, shadowStrength, shadowNormalOffsetBias); + f32 svsmConstantBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias")); + constants->svsmSettings = vec4(svsmConstantBias, 0.0f, 0.0f, 0.0f); + commandList.PushConstant(constants, 0, sizeof(Constants)); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 88e18a5..20855ab 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -3,6 +3,7 @@ #include "Game-Lib/Application/EnttRegistries.h" #include "Game-Lib/ECS/Components/DisplayInfo.h" #include "Game-Lib/ECS/Components/Model.h" +#include "Game-Lib/ECS/Singletons/ActiveCamera.h" #include "Game-Lib/ECS/Components/Tags.h" #include "Game-Lib/ECS/Components/UnitCustomization.h" #include "Game-Lib/ECS/Singletons/Database/ClientDBSingleton.h" @@ -59,7 +60,12 @@ ModelRenderer::ModelRenderer(Renderer::Renderer* renderer, GameRenderer* gameRen , _renderer(renderer) , _gameRenderer(gameRenderer) , _debugRenderer(debugRenderer) + , _svsmDrawDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmDynamicDrawDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) { + _dynamicInstanceMask.SetDebugName("ModelDynamicInstanceMask"); + _dynamicInstanceMask.SetUsage(Renderer::BufferUsage::STORAGE_BUFFER); + CreatePermanentResources(); if (CVAR_ModelValidateTransfers.Get()) @@ -109,9 +115,132 @@ void ModelRenderer::Update(f32 deltaTime) matrix = transform.GetMatrix(); _instanceMatrices.SetDirtyElement(instanceID); + + // Moved this frame -> dynamic shadow caster this frame + _dynamicInstanceQueue.enqueue(instanceID); }); } + // Animated doodads (static position, moving geometry) within svsmAnimatedCasterRange of the + // camera are classified as dynamic shadow casters so their shadows move every frame; beyond + // the range they stay static with their pose baked, where coarse rings can't resolve the + // motion anyway. Range transitions re-bake the static pages under the instance + { + ZoneScopedN("Animated Shadow Casters"); + + _animatedCasterFrame++; + _animatedCasterAABBs.clear(); + + CVarSystem* cvarSystem = CVarSystem::Get(); + const bool svsmActive = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + const f32 range = svsmActive ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; + const bool split = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1; + + u32 modelID; + while (_uninstancedAnimatedModelQueue.try_dequeue(modelID)) + { + _animatedModelLastPushFrame[modelID] = _animatedCasterFrame; + } + + vec3 cameraPos = vec3(0.0f); + bool hasCamera = false; + if (auto* activeCamera = gameRegistry->ctx().find()) + { + if (activeCamera->entity != entt::null && gameRegistry->all_of(activeCamera->entity)) + { + cameraPos = gameRegistry->get(activeCamera->entity).GetWorldPosition(); + hasCamera = true; + } + } + + robin_hood::unordered_set newSet; + if (range > 0.0f && hasCamera) + { + // A model stays "animated" for a grace period after its last bone push so a brief + // animation pause doesn't mass-invalidate and re-invalidate everything in range + constexpr u64 animatedGraceFrames = 30; + const f32 enterDistSq = range * range; + const f32 leaveDist = range * 1.15f; // Hysteresis against camera jitter at the boundary + const f32 leaveDistSq = leaveDist * leaveDist; + + for (auto it = _animatedModelLastPushFrame.begin(); it != _animatedModelLastPushFrame.end();) + { + if (_animatedCasterFrame - it->second > animatedGraceFrames) + { + it = _animatedModelLastPushFrame.erase(it); + continue; + } + + u32 animatedModelID = it->first; + std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]); + for (u32 instanceID : _modelManifests[animatedModelID].instances) + { + vec3 toCamera = vec3(_instanceMatrices[instanceID][3]) - cameraPos; + f32 distSq = glm::dot(toCamera, toCamera); + f32 limitSq = _animatedCasterInstances.contains(instanceID) ? leaveDistSq : enterDistSq; + if (distSq < limitSq) + { + newSet.insert(instanceID); + } + } + ++it; + } + } + + // Entering instances: their baked pose must come out of the static pages + for (u32 instanceID : newSet) + { + if (!_animatedCasterInstances.contains(instanceID)) + { + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + } + } + + // Leaving instances (or animation stopped, or range/technique turned off): their final + // pose must bake back in. Instances despawned while in the set already invalidated via + // RemoveInstance; a recycled ID here costs one spurious page refresh at worst + for (u32 instanceID : _animatedCasterInstances) + { + if (!newSet.contains(instanceID)) + { + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + } + } + + for (u32 instanceID : newSet) + { + if (split) + { + // Dynamic-class: excluded from static pages via the instance mask, rendered into + // the transient dynamic pool over the AABBs collected here + _dynamicInstanceQueue.enqueue(instanceID); + + vec3 aabbMin, aabbMax; + ComputeInstanceShadowAABB(instanceID, _instanceMatrices[instanceID], aabbMin, aabbMax); + _animatedCasterAABBs.push_back(vec4(aabbMin, 0.0f)); + _animatedCasterAABBs.push_back(vec4(aabbMax, 0.0f)); + } + else + { + // Split off: v1-style per-frame static invalidation, range-bounded + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + } + } + + _animatedCasterInstances = std::move(newSet); + } + + { + ZoneScopedN("SVSM Dynamic Draw Count ReadBack"); + + u32* counts = static_cast(_renderer->MapBuffer(_svsmDynamicDrawCountReadBackBuffer)); + if (counts != nullptr) + { + memcpy(_numSvsmDynamicInstances, counts, sizeof(u32) * Renderer::Settings::MAX_VIEWS); + } + _renderer->UnmapBuffer(_svsmDynamicDrawCountReadBackBuffer); + } + { ZoneScopedN("Update Culling Resources"); @@ -382,6 +511,13 @@ void ModelRenderer::Clear() while (_textureLoadRequests.try_dequeue(textureLoadRequest)) {} _dirtyTextureUnitOffsets.clear(); + // Queued modelIDs/instanceIDs are invalid after a clear + u32 drainedID; + while (_uninstancedAnimatedModelQueue.try_dequeue(drainedID)) {} + _animatedModelLastPushFrame.clear(); + _animatedCasterInstances.clear(); + _animatedCasterAABBs.clear(); + _renderer->UnloadTexturesInArray(_textures, 1); SyncToGPU(); @@ -769,8 +905,19 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re if (_opaqueCullingResources.GetDrawCalls().Count() == 0) return; - u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + // Under SVSM the same per-view culling runs against the clipmap cameras, which need no + // cascade depth images + const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + u32 numCascades; + if (useSVSM) + { + numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + } + else + { + numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + } if (numCascades == 0) return; @@ -861,6 +1008,10 @@ void ModelRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, R if (CVAR_ModelsCastShadow.Get() != 1) return; + // SVSM renders pages through AddSVSMGeometryPass instead + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1) + return; + if (_opaqueCullingResources.GetDrawCalls().Count() == 0) return; @@ -985,6 +1136,200 @@ void ModelRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, R }); } +void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer) +{ + ZoneScoped; + + if (!CVAR_ModelRendererEnabled.Get()) + return; + + if (!CVAR_ModelCullingEnabled.Get()) // Clipmaps are driven by the culled bitmask slices + return; + + CVarSystem* cvarSystem = CVarSystem::Get(); + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_ModelsCastShadow.Get() != 1) + return; + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) != 1) + return; + + if (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid()) + return; + + if (_opaqueCullingResources.GetDrawCalls().Count() == 0) + return; + + const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 virtualSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize"_h)); + + // No dynamic casters this frame -> skip the dynamic fills/draws outright. The instance mask + // is all zero then, so the unfiltered static fill is equivalent to the filtered one + const bool splitFills = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1 + && shadowRenderer->GetSVSMDynamicPagePool() != Renderer::ImageID::Invalid() + && shadowRenderer->HasSVSMDynamicCasters(); + + struct Data + { + Renderer::ImageMutableResource pagePool; + Renderer::ImageMutableResource dynamicPagePool; + Renderer::BufferResource svsmData; + Renderer::BufferResource pageTable; + Renderer::BufferResource dynamicPageTable; + + Renderer::BufferMutableResource drawCallsBuffer; + Renderer::BufferMutableResource culledDrawCallsBuffer; + Renderer::BufferMutableResource culledDrawCallCountBuffer; + + Renderer::BufferMutableResource culledInstanceCountsBuffer; + + Renderer::BufferMutableResource drawCountBuffer; + Renderer::BufferMutableResource triangleCountBuffer; + Renderer::BufferMutableResource drawCountReadBackBuffer; + Renderer::BufferMutableResource triangleCountReadBackBuffer; + + Renderer::BufferMutableResource svsmDynamicDrawCountReadBackBuffer; + + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource modelSet; + Renderer::DescriptorSetResource fillSet; + Renderer::DescriptorSetResource createIndirectSet; + Renderer::DescriptorSetResource drawSet; + Renderer::DescriptorSetResource svsmSet; + Renderer::DescriptorSetResource svsmDynamicSet; + }; + + renderGraph->AddPass("Model (O) SVSM Geometry", + [this, &resources, frameIndex, shadowRenderer, splitFills](Data& data, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); + data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); + if (splitFills) + { + data.dynamicPagePool = builder.Write(shadowRenderer->GetSVSMDynamicPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + data.dynamicPageTable = builder.Read(shadowRenderer->GetSVSMDynamicPageTableBuffer(), BufferUsage::GRAPHICS); + builder.Read(_dynamicInstanceMask.GetBuffer(), BufferUsage::COMPUTE); + data.svsmDynamicDrawCountReadBackBuffer = builder.Write(_svsmDynamicDrawCountReadBackBuffer, BufferUsage::TRANSFER); + data.svsmDynamicSet = builder.Use(_svsmDynamicDrawDescriptorSet); + } + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_indices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_textureDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_textureUnits.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceMatrices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_boneMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Read(_textureTransformMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + builder.Write(_animatedVertices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + GeometryPassSetup(data, builder, &_opaqueCullingResources, frameIndex); + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + data.culledInstanceCountsBuffer = builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::TRANSFER | BufferUsage::COMPUTE); + + builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.modelSet = builder.Use(resources.modelDescriptorSet); + data.fillSet = builder.Use(_opaqueCullingResources.GetGeometryFillDescriptorSet()); + data.createIndirectSet = builder.Use(_opaqueCullingResources.GetCreateIndirectAfterCullDescriptorSet()); + data.svsmSet = builder.Use(_svsmDrawDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, &resources, frameIndex, numClipmaps, virtualSize, splitFills, shadowRenderer](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, ModelSVSMGeometry); + + // _svsmData and the page tables are bound once at init through BindSVSMBuffers, only + // the lazily created pools bind per frame (image binds write the descriptor immediately) + data.svsmSet.Bind("_pagePool"_h, data.pagePool); + if (splitFills) + { + data.svsmDynamicSet.Bind("_pagePool"_h, data.dynamicPagePool); + } + + CulledRenderer::GeometryPassParams params; + params.passName = "Opaque"; + params.graphResources = &graphResources; + params.commandList = &commandList; + params.cullingResources = &_opaqueCullingResources; + + params.frameIndex = frameIndex; + + params.drawCallsBuffer = data.drawCallsBuffer; + params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; + params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; + params.culledInstanceCountsBuffer = data.culledInstanceCountsBuffer; + + params.drawCountBuffer = data.drawCountBuffer; + params.triangleCountBuffer = data.triangleCountBuffer; + params.drawCountReadBackBuffer = data.drawCountReadBackBuffer; + params.triangleCountReadBackBuffer = data.triangleCountReadBackBuffer; + + params.globalDescriptorSet = data.globalSet; + params.fillDescriptorSet = data.fillSet; + params.createIndirectDescriptorSet = data.createIndirectSet; + params.drawDescriptorSet = data.drawSet; + + params.drawCallback = [&](DrawParams& drawParams) + { + drawParams.descriptorSets = { + &data.globalSet, + &data.modelSet, + &data.svsmSet + }; + Draw(resources, frameIndex, graphResources, commandList, drawParams); + }; + + params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); + params.drawCallDataSize = sizeof(DrawCallData); + + params.firstViewIndex = 1; + params.numCascades = numClipmaps; + + params.svsmPass = true; + params.svsmExtent = uvec2(virtualSize, virtualSize); + + params.svsmSplitFills = splitFills; + if (splitFills) + { + // Rings with no live dynamic pages the last two frames skip their dynamic phase, + // a caster entering a fresh ring renders at most one frame late + for (u32 view = 1; view <= numClipmaps; view++) + { + params.svsmDynamicViewActive[view] = shadowRenderer->GetSVSMDynamicViewActive(view - 1); + } + + params.svsmDynamicDrawCountReadBackBuffer = data.svsmDynamicDrawCountReadBackBuffer; + params.drawCallbackDynamic = [&](DrawParams& drawParams) + { + drawParams.descriptorSets = { + &data.globalSet, + &data.modelSet, + &data.svsmDynamicSet + }; + Draw(resources, frameIndex, graphResources, commandList, drawParams); + }; + } + + params.enableDrawing = CVAR_ModelDrawGeometry.Get(); + params.cullingEnabled = true; + + GeometryPass(params); + }); +} + void ModelRenderer::AddTransparencyCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { ZoneScoped; @@ -1792,6 +2137,59 @@ u32 ModelRenderer::AddPlacementInstance(entt::entity entityID, u32 modelID, u32 return instanceIndex; } +void ModelRenderer::ComputeInstanceShadowAABB(u32 instanceID, const mat4x4& transformMatrix, vec3& outMin, vec3& outMax) +{ + const InstanceData& instanceData = _instanceDatas[instanceID]; + const Model::ComplexModel::CullingData& cullingData = _cullingDatas[instanceData.modelID]; + + vec3 center = vec3(transformMatrix * vec4(vec3(cullingData.center), 1.0f)); + mat3x3 absRotation = mat3x3(transformMatrix); + absRotation[0] = glm::abs(absRotation[0]); + absRotation[1] = glm::abs(absRotation[1]); + absRotation[2] = glm::abs(absRotation[2]); + vec3 extents = absRotation * vec3(cullingData.extents); + + outMin = center - extents; + outMax = center + extents; +} + +void ModelRenderer::QueueShadowInvalidation(u32 instanceID, const mat4x4& transformMatrix) +{ + vec3 aabbMin, aabbMax; + ComputeInstanceShadowAABB(instanceID, transformMatrix, aabbMin, aabbMax); + + ShadowInvalidation invalidation; + invalidation.min = vec4(aabbMin, 0.0f); + invalidation.max = vec4(aabbMax, 0.0f); + _shadowInvalidationQueue.enqueue(invalidation); +} + +void ModelRenderer::BindSVSMBuffers(Renderer::BufferID svsmDataBuffer, Renderer::BufferID pageTableBuffer, Renderer::BufferID dynamicPageTableBuffer) +{ + _svsmDrawDescriptorSet.Bind("_svsmData"_h, svsmDataBuffer); + _svsmDrawDescriptorSet.Bind("_pageTable"_h, pageTableBuffer); + _svsmDynamicDrawDescriptorSet.Bind("_svsmData"_h, svsmDataBuffer); + _svsmDynamicDrawDescriptorSet.Bind("_pageTable"_h, dynamicPageTableBuffer); +} + +u32 ModelRenderer::DrainShadowInvalidations(std::vector& outMinMaxPairs, u32 maxPairs) +{ + u32 numDrained = 0; + + ShadowInvalidation invalidation; + while (_shadowInvalidationQueue.try_dequeue(invalidation)) + { + if (numDrained < maxPairs) + { + outMinMaxPairs.push_back(invalidation.min); + outMinMaxPairs.push_back(invalidation.max); + } + numDrained++; // Keeps draining past the cap so the queue can't grow unbounded + } + + return numDrained; +} + u32 ModelRenderer::AddInstance(entt::entity entityID, u32 modelID, Model::ComplexModel* model, const mat4x4& transformMatrix, u64 displayInfoPacked) { InstanceOffsets instanceOffsets; @@ -1847,6 +2245,9 @@ u32 ModelRenderer::AddInstance(entt::entity entityID, u32 modelID, Model::Comple RequestChangeSkybox(instanceOffsets.instanceIndex, true); } + // A spawned static caster must invalidate the cached static shadow pages under it + QueueShadowInvalidation(instanceOffsets.instanceIndex, transformMatrix); + _instancesDirty = true; return instanceOffsets.instanceIndex; @@ -1857,6 +2258,9 @@ void ModelRenderer::RemoveInstance(u32 instanceID) InstanceData& instanceData = _instanceDatas[instanceID]; ModelManifest& manifest = _modelManifests[instanceData.modelID]; + // Capture the shadow footprint before the instance data dies, its baked static shadow must go + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + // TODO: We need to change _animatedVerticesIndex so we can free up between instanceData.animatedVertexOffset + manifest.numVertices // Remove Instance from ModelManifest @@ -1893,6 +2297,10 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo { InstanceData& instanceData = _instanceDatas[instanceID]; + // The old shadow footprint's cached static pages must refresh, the new one queues below once + // the model ID is swapped + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + u32 oldModelID = instanceData.modelID; ModelManifest& oldManifest = _modelManifests[oldModelID]; @@ -1941,6 +2349,9 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo instanceMatrix = transformMatrix; } + // New model + transform in place, refresh the new shadow footprint too + QueueShadowInvalidation(instanceID, transformMatrix); + InstanceManifest& instanceManifest = _instanceManifests[instanceID]; instanceManifest.modelID = modelID; @@ -2526,6 +2937,10 @@ bool ModelRenderer::SetUninstancedBoneMatricesAsDirty(u32 modelID, u32 boneMatri if (endGlobalBoneIndex > boneMatrixOffset + modelManifest.numBones) return false; + // This model's shared animation advanced, Update classifies its in-range placements as + // dynamic shadow casters + _uninstancedAnimatedModelQueue.enqueue(modelID); + if (count == 1) { _boneMatrices[globalBoneIndex] = *boneMatrixArray; @@ -2652,6 +3067,10 @@ bool ModelRenderer::SetBoneMatricesAsDirty(u32 instanceID, u32 localBoneIndex, u _boneMatrices.SetDirtyElements(globalBoneIndex, count); } + // Pushed bone matrices this frame -> dynamic shadow caster this frame. The static-baked + // animation path (SetUninstancedBoneMatricesAsDirty) intentionally does not do this + _dynamicInstanceQueue.enqueue(instanceID); + return true; } @@ -2700,6 +3119,13 @@ void ModelRenderer::CreatePermanentResources() RenderResources& resources = _gameRenderer->GetRenderResources(); + Renderer::BufferDesc svsmDynamicCountDesc; + svsmDynamicCountDesc.name = "ModelSVSMDynamicDrawCountRBBuffer"; + svsmDynamicCountDesc.size = sizeof(u32) * Renderer::Settings::MAX_VIEWS; + svsmDynamicCountDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + svsmDynamicCountDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; + _svsmDynamicDrawCountReadBackBuffer = _renderer->CreateBuffer(_svsmDynamicDrawCountReadBackBuffer, svsmDynamicCountDesc); + Renderer::TextureArrayDesc textureArrayDesc; textureArrayDesc.size = Renderer::Settings::MAX_TEXTURES; @@ -2931,6 +3357,50 @@ void ModelRenderer::CreateModelPipelines() // Draw _drawShadowPipeline = _renderer->CreatePipeline(pipelineDesc); } + // SVSM pages: attachment-less, the pixel shader keeps the alpha-key discard and writes depth + // into the page pool with image atomics. Depth clamp stays on so pancaked casters rasterize + { + Renderer::GraphicsPipelineDesc pipelineDesc; + pipelineDesc.debugName = "Model Draw SVSM"; + + // Rasterizer state, no depth or color attachments + pipelineDesc.states.rasterizerState.cullMode = Renderer::CullMode::NONE; + pipelineDesc.states.rasterizerState.depthClampEnabled = true; + + Renderer::VertexShaderDesc vertexShaderDesc; + std::vector vertexPermutationFields = + { + { "EDITOR_PASS", "0" }, + { "SHADOW_PASS", "1"} + }; + u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); + vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.vs"); + pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); + + for (u32 dynamic = 0; dynamic < 2; dynamic++) + { + std::vector pixelPermutationFields = + { + { "SVSM_DYNAMIC", std::to_string(dynamic) } + }; + u32 pixelShaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/DrawSVSM.ps", pixelPermutationFields); + + Renderer::PixelShaderDesc pixelShaderDesc; + pixelShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(pixelShaderEntryNameHash, "Model/DrawSVSM.ps"); + pipelineDesc.states.pixelShader = _renderer->LoadShader(pixelShaderDesc); + + if (dynamic == 0) + { + pipelineDesc.debugName = "Model Draw SVSM"; + _drawSVSMPipeline = _renderer->CreatePipeline(pipelineDesc); + } + else + { + pipelineDesc.debugName = "Model Draw SVSM Dynamic"; + _drawSVSMDynamicPipeline = _renderer->CreatePipeline(pipelineDesc); + } + } + } // Transparencies { Renderer::GraphicsPipelineDesc pipelineDesc; @@ -3092,6 +3562,15 @@ void ModelRenderer::InitDescriptorSets() geometryPassDescriptorSet.RegisterPipeline(_renderer, _drawSkyboxTransparentPipeline); geometryPassDescriptorSet.Init(_renderer); } + + // SVSM page render + { + _svsmDrawDescriptorSet.RegisterPipeline(_renderer, _drawSVSMPipeline); + _svsmDrawDescriptorSet.Init(_renderer); + + _svsmDynamicDrawDescriptorSet.RegisterPipeline(_renderer, _drawSVSMDynamicPipeline); + _svsmDynamicDrawDescriptorSet.Init(_renderer); + } } void ModelRenderer::AllocateModel(const Model::ComplexModel& model, ModelOffsets& offsets) @@ -3610,6 +4089,53 @@ void ModelRenderer::SyncToGPU() BindCullingResource(_transparentCullingResources); BindCullingResource(_opaqueSkyboxCullingResources); BindCullingResource(_transparentSkyboxCullingResources); + + // Rebuild the SVSM dynamic instance mask from this frame's producers (moved + bone-pushed). + // Runs after every ECS system, so ordering between the producers doesn't matter + { + ZoneScopedN("Sync Dynamic Instance Mask"); + + u32 wordsNeeded = (static_cast(_instanceDatas.Capacity()) + 31) / 32; + u32 wordCount = static_cast(_dynamicInstanceMask.Count()); + if (wordsNeeded > wordCount) + { + _dynamicInstanceMask.AddCount(wordsNeeded - wordCount); + for (u32 i = wordCount; i < wordsNeeded; i++) + { + _dynamicInstanceMask[i] = 0; + } + } + + // Clear last frame's bits, then set this frame's. The bit test doubles as dedupe + for (u32 instanceID : _dynamicInstanceIDs) + { + u32 word = instanceID / 32; + _dynamicInstanceMask[word] &= ~(1u << (instanceID & 31)); + _dynamicInstanceMask.SetDirtyElement(word); + } + _dynamicInstanceIDs.clear(); + + u32 instanceID; + while (_dynamicInstanceQueue.try_dequeue(instanceID)) + { + u32 word = instanceID / 32; + if (word >= wordsNeeded) + continue; + + u32 bit = 1u << (instanceID & 31); + if ((_dynamicInstanceMask[word] & bit) == 0) + { + _dynamicInstanceMask[word] |= bit; + _dynamicInstanceMask.SetDirtyElement(word); + _dynamicInstanceIDs.push_back(instanceID); + } + } + + if (_dynamicInstanceMask.SyncToGPU(_renderer)) + { + _opaqueCullingResources.GetGeometryFillDescriptorSet().Bind("_dynamicInstanceMask"_h, _dynamicInstanceMask.GetBuffer()); + } + } } void ModelRenderer::Draw(const RenderResources& resources, u8 frameIndex, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, const DrawParams& params) @@ -3618,18 +4144,28 @@ void ModelRenderer::Draw(const RenderResources& resources, u8 frameIndex, Render graphResources.InitializeRenderPassDesc(renderPassDesc); // Render targets - if (!params.shadowPass) + if (params.svsmPass) { - renderPassDesc.renderTargets[0] = params.rt0; - if (params.rt1 != Renderer::ImageMutableResource::Invalid()) + // Attachment-less: with no render targets BeginRenderPass has no extent fallback, + // the render area must be explicit or it collapses to zero + renderPassDesc.extent = params.svsmExtent; + } + else + { + if (!params.shadowPass) { - renderPassDesc.renderTargets[1] = params.rt1; + renderPassDesc.renderTargets[0] = params.rt0; + if (params.rt1 != Renderer::ImageMutableResource::Invalid()) + { + renderPassDesc.renderTargets[1] = params.rt1; + } } + renderPassDesc.depthStencil = params.depth; } - renderPassDesc.depthStencil = params.depth; commandList.BeginRenderPass(renderPassDesc); - Renderer::GraphicsPipelineID pipeline = params.shadowPass ? _drawShadowPipeline : _drawPipeline; + Renderer::GraphicsPipelineID pipeline = params.svsmPass ? (params.svsmDynamicPass ? _drawSVSMDynamicPipeline : _drawSVSMPipeline) + : (params.shadowPass ? _drawShadowPipeline : _drawPipeline); commandList.BeginPipeline(pipeline); struct PushConstants diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index 8d2fb4c..fa5ab6a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -19,6 +19,7 @@ class DebugRenderer; class GameRenderer; +class ShadowRenderer; struct RenderResources; namespace Renderer @@ -349,6 +350,11 @@ class ModelRenderer : CulledRenderer void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); + + // Called once by ShadowRenderer at init, buffer binds must happen before the first frame + // (they only reach the canonical descriptor copies at a later FlipFrame) + void BindSVSMBuffers(Renderer::BufferID svsmDataBuffer, Renderer::BufferID pageTableBuffer, Renderer::BufferID dynamicPageTableBuffer); void AddTransparencyCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddTransparencyGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); @@ -358,6 +364,12 @@ class ModelRenderer : CulledRenderer Renderer::GPUVector& GetInstanceMatrices() { return _instanceMatrices; } const std::vector& GetModelManifests() { return _modelManifests; } + // SVSM: appends world (min, max) pairs of spawned/despawned instances, returns pairs appended + u32 DrainShadowInvalidations(std::vector& outMinMaxPairs, u32 maxPairs); + + // SVSM: world (min, max) pairs of in-range animated shadow casters, rebuilt each Update + const std::vector& GetAnimatedCasterAABBs() const { return _animatedCasterAABBs; } + CullingResourcesIndexed& GetOpaqueCullingResources() { return _opaqueCullingResources; } CullingResourcesIndexed& GetTransparentCullingResources() { return _transparentCullingResources; } @@ -365,6 +377,7 @@ class ModelRenderer : CulledRenderer u32 GetNumDrawCalls() { return 0; } u32 GetNumOccluderDrawCalls() { return _numOccluderDrawCalls; } u32 GetNumSurvivingDrawCalls(u32 viewID) { return _numSurvivingDrawCalls[viewID]; } + u32 GetNumSVSMDynamicInstances(u32 viewID) { return _numSvsmDynamicInstances[viewID]; } // One frame old, view 0 unused // Triangle stats u32 GetNumTriangles() { return 0; } @@ -388,6 +401,9 @@ class ModelRenderer : CulledRenderer void MakeInstanceTransparent(u32 instanceID, InstanceManifest& instanceManifest, ModelManifest& modelManifest); void MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanceManifest, bool skybox); + void ComputeInstanceShadowAABB(u32 instanceID, const mat4x4& transformMatrix, vec3& outMin, vec3& outMax); + void QueueShadowInvalidation(u32 instanceID, const mat4x4& transformMatrix); + void CompactInstanceRefs(); void SyncToGPU(); @@ -441,10 +457,44 @@ class ModelRenderer : CulledRenderer Renderer::GraphicsPipelineID _drawPipeline; Renderer::GraphicsPipelineID _drawShadowPipeline; + Renderer::GraphicsPipelineID _drawSVSMPipeline; + Renderer::GraphicsPipelineID _drawSVSMDynamicPipeline; Renderer::GraphicsPipelineID _drawTransparentPipeline; Renderer::GraphicsPipelineID _drawSkyboxOpaquePipeline; Renderer::GraphicsPipelineID _drawSkyboxTransparentPipeline; + Renderer::DescriptorSet _svsmDrawDescriptorSet; + Renderer::DescriptorSet _svsmDynamicDrawDescriptorSet; + + // SVSM static/dynamic caster split: instances that moved or pushed bone matrices this frame. + // Producers enqueue from any thread, SyncToGPU rebuilds the bit mask once per frame + moodycamel::ConcurrentQueue _dynamicInstanceQueue; + std::vector _dynamicInstanceIDs; // Last frame's live set, backs the incremental bit updates + Renderer::GPUVector _dynamicInstanceMask; + + // Spawned/despawned/re-modeled instances must invalidate the cached static shadow pages under + // them, ShadowRenderer drains this each frame + struct ShadowInvalidation + { + vec4 min; + vec4 max; + }; + moodycamel::ConcurrentQueue _shadowInvalidationQueue; + + // Per-view surviving dynamic-class instance counts of the SVSM dynamic fills, the instrument + // for diagnosing missing dynamic shadow draws + Renderer::BufferID _svsmDynamicDrawCountReadBackBuffer; + u32 _numSvsmDynamicInstances[Renderer::Settings::MAX_VIEWS] = { 0 }; + + // Animated doodads (windmills, flags, swaying vegetation) share one uninstanced bone stream + // per model; the queue signals which models advanced their animation this frame so Update can + // classify their placements within svsmAnimatedCasterRange as dynamic shadow casters + moodycamel::ConcurrentQueue _uninstancedAnimatedModelQueue; + robin_hood::unordered_map _animatedModelLastPushFrame; // modelID -> last frame it pushed bones + robin_hood::unordered_set _animatedCasterInstances; // In-range animated instances, persists as last frame's set + std::vector _animatedCasterAABBs; // World (min, max) pairs of the set, rebuilt each frame + u64 _animatedCasterFrame = 0; + // GPU-only workbuffers Renderer::BufferID _occluderArgumentBuffer; Renderer::BufferID _argumentBuffer; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index ba0eeb8..8bb39d7 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -1,7 +1,11 @@ #include "ShadowRenderer.h" #include +#include +#include #include +#include #include +#include #include #include #include @@ -46,6 +50,54 @@ AutoCVar_Int CVAR_ShadowSDSMValidateParity(CVarCategory::Client | CVarCategory:: AutoCVar_Int CVAR_ShadowSDSMUseXYBounds(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMUseXYBounds", "fit cascades to the light-space footprint of visible samples: 0 = off, 1 = diagnostics only, 2 = drive the cameras", 2); AutoCVar_Float CVAR_ShadowSDSMXYMarginTexels(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMXYMarginTexels", "filter-kernel margin in texels added to the fitted XY bounds, the normal offset bias is added on top", 4.0f); +AutoCVar_Int CVAR_ShadowTechnique(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique", "0 = cascaded shadow maps (SDSM), 1 = sparse virtual shadow maps", 0); +AutoCVar_Int CVAR_SVSMNumClipmaps(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps", "number of SVSM clipmap rings, each ring doubles the covered area", 6); +AutoCVar_Float CVAR_SVSMClipmap0Extent(CVarCategory::Client | CVarCategory::Rendering, "svsmClipmap0Extent", "world extent of the finest SVSM clipmap window in meters, finer rings multiply the near-field page demand", 64.0f); +AutoCVar_Int CVAR_SVSMVirtualSize(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize", "virtual texture resolution per clipmap", 8192); +AutoCVar_Int CVAR_SVSMPageSize(CVarCategory::Client | CVarCategory::Rendering, "svsmPageSize", "texels per page", 128); +AutoCVar_Int CVAR_SVSMPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmPoolSize", "physical page pool texture resolution, page count is fixed at startup", 8192); +AutoCVar_Int CVAR_SVSMPageEvictAge(CVarCategory::Client | CVarCategory::Rendering, "svsmPageEvictAge", "frames without a visible sample before a cached page returns to the pool, the age counter caps at 254", 240); +AutoCVar_Float CVAR_SVSMMarkBorderTexels(CVarCategory::Client | CVarCategory::Rendering, "svsmMarkBorderTexels", "filter footprint margin in texels, samples near a page border also mark the neighbor page", 4.0f); +AutoCVar_Float CVAR_SVSMResolutionScale(CVarCategory::Client | CVarCategory::Rendering, "svsmResolutionScale", "eye-distance clipmap floor: skip rings finer than the sample's screen footprint times this, 0 disables, lower = sharper distant shadows for more pages. Below ~0.25 the pool pressure-evicts and churns", 1.0f); +AutoCVar_Int CVAR_SVSMDynamicSplit(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit", "static/dynamic caster split: animated and moving casters render into a transient dynamic pool instead of churning the static cache, 0 reverts to v1 behavior", 1, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMDynamicPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicPoolSize", "dynamic page pool texture resolution, page count is fixed at startup", 2048); +AutoCVar_Int CVAR_SVSMRenderBudget(CVarCategory::Client | CVarCategory::Rendering, "svsmRenderBudget", "static pages rendered per frame, 0 = unlimited; overflow refines over following frames coarse-to-fine, the coarsest two rings are exempt", 0); +AutoCVar_Float CVAR_SVSMAnimatedCasterRange(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange", "camera range in meters within which animated doodads (windmills, flags) cast dynamic shadows, beyond it their pose bakes static, 0 disables", 128.0f); +AutoCVar_Int CVAR_SVSMFreeze(CVarCategory::Client | CVarCategory::Rendering, "svsmFreeze", "freeze SVSM page marking and lifecycle to inspect the cached state", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMInvalidateAll(CVarCategory::Client | CVarCategory::Rendering, "svsmInvalidateAll", "one shot, invalidate every cached SVSM page", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMValidateDynamic(CVarCategory::Client | CVarCategory::Rendering, "svsmValidateDynamic", "one shot, read back the dynamic page table and free list and validate pool invariants (aliasing, leaks)", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMDebugClipmap(CVarCategory::Client | CVarCategory::Rendering, "svsmDebugClipmap", "draw this clipmap's page table as an overlay, -1 disables", -1); +AutoCVar_Int CVAR_SVSMDebugShowPool(CVarCategory::Client | CVarCategory::Rendering, "svsmDebugShowPool", "draw a downsampled view of a physical page pool as an overlay: 1 = static pool, 2 = dynamic pool", 0); +AutoCVar_Float CVAR_SVSMZHalfRange(CVarCategory::Client | CVarCategory::Rendering, "svsmZHalfRange", "half depth range of the clipmap windows around the camera in light space, changes invalidate all pages", 2048.0f); +AutoCVar_Float CVAR_SVSMConstantBias(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias", "SVSM compare bias toward the sun in world meters, the software depth path has no hardware bias", 0.15f); +AutoCVar_Int CVAR_SVSMProfileGeometry(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry", "debug: per-view fill/draw GPU time queries in the SVSM geometry passes, shown in the render pass list", 0, CVarFlags::EditCheckbox); + +// u32 indices into the flat SVSMData readback, mirrors Shadows/SVSM.inc.slang +namespace SVSMDataOffsets +{ + constexpr u32 Extent = 56; + constexpr u32 StatsMarked = 112; + constexpr u32 StatsResident = 120; + constexpr u32 StatsDirty = 128; + constexpr u32 StatsEvicted = 136; + constexpr u32 StatsInvalidated = 144; + constexpr u32 StatsOverflow = 152; + constexpr u32 StatsInvalidationCause = 153; + constexpr u32 StatsFreeListCount = 154; + constexpr u32 StatsDynamicLive = 196; + constexpr u32 StatsDynamicOverflow = 204; + constexpr u32 StatsDynamicTotal = 205; + constexpr u32 StatsDeferred = 208; + constexpr u32 StatsBudgetUsed = 216; +} + +// SVSM invalidation cause bits, shared with Shadows/SVSM.inc.slang +namespace SVSMCause +{ + constexpr u32 Manual = 4; + constexpr u32 AABBOverflow = 8; +} + ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) , _gameRenderer(gameRenderer) @@ -56,6 +108,18 @@ ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameR , _cascadeFitRangeDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _cascadeXYReduceDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _cascadeFitCamerasDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPrepareDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmInvalidateAABBsDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPageUpdateADescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPageMarkDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPageUpdateBDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmDynamicMarkDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmDynamicUpdateDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmFinalizeDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPageClearDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmDynamicPageClearDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPageTableDebugDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmPoolDebugDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) { CreatePermanentResources(resources); } @@ -103,6 +167,253 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) _renderer->UnmapBuffer(_sdsmDataReadBackBuffer); } + // SVSM: last frame's page table stats plus this frame's caster bounds. With the caster split, + // the current dynamic set (moved + animated) feeds dynamic page marking, and only + // classification transitions and spawns/despawns invalidate cached static content. With the + // split off, everything feeds static invalidation like v1 + _svsmDirtyAABBs.clear(); + _svsmDynamicAABBs.clear(); + _svsmDirtyAABBOverflow = false; + _svsmNumDynamicCasters = 0; + _svsmNumAnimatedCasters = 0; + _svsmDynamicAABBsDropped = 0; + if (CVAR_ShadowTechnique.Get() == 1 && CVAR_ShadowEnabled.Get()) + { + // The pools are the SVSM VRAM cost, only allocated once the technique is actually used + if (_svsmPagePool == Renderer::ImageID::Invalid()) + { + Renderer::ImageDesc poolDesc; + poolDesc.debugName = "SVSMPagePool"; + poolDesc.dimensions = vec2(CVAR_SVSMPoolSize.Get(), CVAR_SVSMPoolSize.Get()); + poolDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_ABSOLUTE; + poolDesc.format = Renderer::ImageFormat::R32_UINT; + poolDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; + poolDesc.clearUInts = uvec4(0, 0, 0, 0); + + _svsmPagePool = _renderer->CreateImage(poolDesc); + + poolDesc.debugName = "SVSMDynamicPagePool"; + poolDesc.dimensions = vec2(CVAR_SVSMDynamicPoolSize.Get(), CVAR_SVSMDynamicPoolSize.Get()); + _svsmDynamicPagePool = _renderer->CreateImage(poolDesc); + + _svsmPoolNeedsClear = true; // Fresh VRAM is garbage that must never be sampled, zero both once + } + + u32* svsmData = static_cast(_renderer->MapBuffer(_svsmDataReadBackBuffer)); + if (svsmData != nullptr) + { + memcpy(_svsmDynamicLivePrev, &_svsmDataReadBack[196], sizeof(_svsmDynamicLivePrev)); // SVSMDataOffsets::StatsDynamicLive + memcpy(_svsmDataReadBack, svsmData, sizeof(u32) * SVSM_DATA_UINT_COUNT); + } + _renderer->UnmapBuffer(_svsmDataReadBackBuffer); + + // svsmValidateDynamic one-shot: check the snapshotted dynamic pool invariants — every + // physical page referenced by at most one resident entry, free-list entries unique and + // unreferenced, resident + free == pool. Aliasing here is the ghost-shadow failure mode + if (_svsmValidatePending) + { + const u32 tableUints = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + const u32* validateData = static_cast(_renderer->MapBuffer(_svsmDynamicValidateReadBackBuffer)); + if (validateData != nullptr) + { + const u32* freeListData = validateData + tableUints; + const u32 poolPages = _svsmDynamicPoolPages; + const u32 rawFreeCount = freeListData[0]; + const u32 freeCount = glm::min(rawFreeCount, SVSM_MAX_POOL_PAGES); + + std::vector physRefCounts(poolPages, 0); + u32 numResident = 0; + u32 numAliasedRefs = 0; + u32 numBadPhys = 0; + for (u32 i = 0; i < tableUints; i++) + { + u32 entry = validateData[i]; + if (entry == 0 || (entry & (1u << 25)) == 0) // SVSM_PAGE_RESIDENT + continue; + + numResident++; + u32 physicalPage = entry & 0xFFFu; // SVSM_PAGE_PHYS_MASK + if (physicalPage >= poolPages) + { + numBadPhys++; + continue; + } + if (++physRefCounts[physicalPage] > 1) + { + numAliasedRefs++; + } + } + + std::vector freeSeen(poolPages, 0); + u32 numBadFree = 0; + u32 numFreeDuplicates = 0; + u32 numFreeButReferenced = 0; + for (u32 i = 0; i < freeCount; i++) + { + u32 physicalPage = freeListData[4 + i]; + if (physicalPage >= poolPages) + { + numBadFree++; + continue; + } + if (freeSeen[physicalPage]++ != 0) + { + numFreeDuplicates++; + } + else if (physRefCounts[physicalPage] != 0) + { + numFreeButReferenced++; + } + } + + bool leaked = numResident + rawFreeCount != poolPages; + if (numAliasedRefs != 0 || numBadPhys != 0 || numBadFree != 0 || numFreeDuplicates != 0 || numFreeButReferenced != 0 || leaked) + { + NC_LOG_ERROR("SVSM dynamic pool validation FAILED: {0} resident + {1} free of {2} pages | {3} aliased refs, {4} bad phys, {5} free dups, {6} free-but-referenced, {7} bad free entries", numResident, rawFreeCount, poolPages, numAliasedRefs, numBadPhys, numFreeDuplicates, numFreeButReferenced, numBadFree); + } + else + { + NC_LOG_INFO("SVSM dynamic pool validation OK: {0} resident + {1} free = {2} pages", numResident, rawFreeCount, poolPages); + } + } + _renderer->UnmapBuffer(_svsmDynamicValidateReadBackBuffer); + _svsmValidatePending = false; + CVAR_SVSMValidateDynamic.Set(0); + } + + entt::registry* gameRegistry = ServiceLocator::GetEnttRegistries()->gameRegistry; + const bool split = CVAR_SVSMDynamicSplit.Get() == 1; + + auto AppendStaticAABB = [this](const vec4& min, const vec4& max) + { + if (_svsmDirtyAABBs.size() >= SVSM_MAX_DIRTY_AABBS * 2) + { + _svsmDirtyAABBOverflow = true; // Falls back to a full static invalidation + return; + } + + _svsmDirtyAABBs.push_back(min); + _svsmDirtyAABBs.push_back(max); + }; + + // Dynamic list overflow must NOT full-invalidate the static cache: the dropped casters' + // shadows just freeze for the frame, visible in the perf editor + auto AppendDynamicAABB = [this](const vec4& min, const vec4& max) + { + if (_svsmDynamicAABBs.size() >= SVSM_MAX_DYNAMIC_AABBS * 2) + { + _svsmDynamicAABBsDropped++; + return; + } + + _svsmDynamicAABBs.push_back(min); + _svsmDynamicAABBs.push_back(max); + }; + + // Spawned/despawned/re-modeled instances always invalidate cached static pages + if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, SVSM_MAX_DIRTY_AABBS) * 2 > SVSM_MAX_DIRTY_AABBS * 2) + { + _svsmDirtyAABBOverflow = true; + } + + // The current dynamic caster set: moved this frame or actively bone-simulated + robin_hood::unordered_set currentDynamicEntities; + + auto CollectDynamic = [&](entt::entity entity, const ECS::Components::Model& model, const ECS::Components::WorldAABB& worldAABB) + { + if (model.instanceID == std::numeric_limits::max()) + return; + + u32 entityHandle = static_cast(entity); + if (!currentDynamicEntities.insert(entityHandle).second) + return; // Already collected through the other view + + vec4 aabbMin = vec4(worldAABB.min, 0.0f); + vec4 aabbMax = vec4(worldAABB.max, 0.0f); + if (split) + { + AppendDynamicAABB(aabbMin, aabbMax); + + // Newly dynamic: its shadow is baked into static pages and must come out + if (!_svsmPrevDynamicEntities.contains(entityHandle)) + { + AppendStaticAABB(aabbMin, aabbMax); + } + } + else + { + AppendStaticAABB(aabbMin, aabbMax); // v1 behavior + } + }; + + gameRegistry->view().each([&](entt::entity entity, ECS::Components::Model& model, ECS::Components::WorldAABB& worldAABB, ECS::Components::DirtyTransform&) + { + CollectDynamic(entity, model, worldAABB); + }); + + gameRegistry->view().each([&](entt::entity entity, ECS::Components::Model& model, ECS::Components::WorldAABB& worldAABB, ECS::Components::AnimationData&) + { + CollectDynamic(entity, model, worldAABB); + }); + + // Turned static (or split just toggled off): its last pose must bake back into static pages. + // Destroyed-while-dynamic entities are covered by the despawn queue above + for (u32 entityHandle : _svsmPrevDynamicEntities) + { + if (currentDynamicEntities.contains(entityHandle)) + continue; + + entt::entity entity = static_cast(entityHandle); + if (!gameRegistry->valid(entity) || !gameRegistry->all_of(entity)) + continue; + + const ECS::Components::WorldAABB& worldAABB = gameRegistry->get(entity); + AppendStaticAABB(vec4(worldAABB.min, 0.0f), vec4(worldAABB.max, 0.0f)); + } + + _svsmNumDynamicCasters = static_cast(currentDynamicEntities.size()); + _svsmPrevDynamicEntities = std::move(currentDynamicEntities); + if (!split) + { + _svsmPrevDynamicEntities.clear(); // Re-enabling the split re-transitions everything + } + + // Animated doodads in range, classified and range-diffed by the ModelRenderer. With the + // split off the ModelRenderer feeds them through the static invalidation queue instead + // and this list is empty + const std::vector& animatedAABBs = _modelRenderer->GetAnimatedCasterAABBs(); + _svsmNumAnimatedCasters = static_cast(animatedAABBs.size() / 2); + for (size_t i = 0; i + 1 < animatedAABBs.size(); i += 2) + { + AppendDynamicAABB(animatedAABBs[i], animatedAABBs[i + 1]); + } + + // Upload this frame's AABB lists through the frame-synced staging ring, the render graph + // issues a global upload barrier before any pass executes + if (!_svsmDirtyAABBs.empty()) + { + size_t uploadBytes = sizeof(vec4) * _svsmDirtyAABBs.size(); + auto uploadBuffer = _renderer->CreateUploadBuffer(_svsmDirtyAABBBuffer, 0, uploadBytes); + memcpy(uploadBuffer->mappedMemory, _svsmDirtyAABBs.data(), uploadBytes); + } + if (!_svsmDynamicAABBs.empty()) + { + size_t uploadBytes = sizeof(vec4) * _svsmDynamicAABBs.size(); + auto uploadBuffer = _renderer->CreateUploadBuffer(_svsmDynamicAABBBuffer, 0, uploadBytes); + memcpy(uploadBuffer->mappedMemory, _svsmDynamicAABBs.data(), uploadBytes); + } + } + else + { + // SVSM inactive: spawn/despawn invalidations would accumulate in the queue unboundedly. + // Discard them (maxPairs 0 appends nothing) and re-bake the whole cache when the + // technique comes back instead + if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, 0) > 0) + { + _svsmForceInvalidateAll = true; + } + } + auto GetCascadeCamera = [&](u32 cascadeIndex) -> const Camera& { return useSDSM ? _readBackCascadeCameras[cascadeIndex] : resources.cameras[cascadeIndex + 1]; @@ -249,7 +560,8 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); const bool freezeCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades") != 0; - const bool dispatchEnabled = CVAR_ShadowUseSDSM.Get() && CVAR_ShadowEnabled.Get() && numCascades > 0 && !freezeCascades; + const bool useSVSM = CVAR_ShadowTechnique.Get() == 1; // SVSM Finalize owns the camera slots instead + const bool dispatchEnabled = CVAR_ShadowUseSDSM.Get() && CVAR_ShadowEnabled.Get() && numCascades > 0 && !freezeCascades && !useSVSM; if (!dispatchEnabled) return; @@ -333,7 +645,6 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende commandList.BeginPipeline(_cascadeFitRangePipeline); commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - data.rangeSet.Bind("_srcCameras"_h, data.cameras); commandList.BindDescriptorSet(data.rangeSet, frameIndex); commandList.Dispatch(1, 1, 1); @@ -348,7 +659,6 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); data.reduceSet.Bind("_depth"_h, data.depth); - data.reduceSet.Bind("_srcCameras"_h, data.cameras); commandList.BindDescriptorSet(data.reduceSet, frameIndex); uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); @@ -363,7 +673,6 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende commandList.BeginPipeline(_cascadeFitCamerasPipeline); commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - data.camerasSet.Bind("_rwCameras"_h, data.cameras); commandList.BindDescriptorSet(data.camerasSet, frameIndex); commandList.Dispatch(1, 1, 1); @@ -378,6 +687,453 @@ void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, Rende }); } +void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + struct Data + { + Renderer::DepthImageResource depth; + + Renderer::BufferMutableResource cameras; + Renderer::BufferMutableResource svsmDataBuffer; + Renderer::BufferMutableResource pageTableBuffer; + Renderer::BufferMutableResource freeListBuffer; + Renderer::BufferMutableResource dirtyAABBBuffer; + Renderer::BufferMutableResource clearListBuffer; + Renderer::BufferMutableResource dynamicPageTableBuffer; + Renderer::BufferMutableResource dynamicFreeListBuffer; + Renderer::BufferMutableResource dynamicClearListBuffer; + Renderer::BufferMutableResource dynamicAABBBuffer; + Renderer::BufferMutableResource svsmDataReadBackBuffer; + Renderer::BufferMutableResource dynamicValidateReadBackBuffer; + Renderer::ImageMutableResource pagePool; + Renderer::ImageMutableResource dynamicPagePool; + + Renderer::DescriptorSetResource prepareSet; + Renderer::DescriptorSetResource invalidateSet; + Renderer::DescriptorSetResource updateASet; + Renderer::DescriptorSetResource markSet; + Renderer::DescriptorSetResource updateBSet; + Renderer::DescriptorSetResource dynamicMarkSet; + Renderer::DescriptorSetResource dynamicUpdateSet; + Renderer::DescriptorSetResource finalizeSet; + Renderer::DescriptorSetResource clearSet; + Renderer::DescriptorSetResource dynamicClearSet; + }; + + const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); + const bool enabled = CVAR_ShadowTechnique.Get() == 1 && CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get(); + if (!enabled || _svsmPagePool == Renderer::ImageID::Invalid()) + return; + + // Record-time inputs, the shadow sun steps in discrete intervals so cached pages stay valid + // between steps + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + auto& dayNightCycle = registry->ctx().get(); + const f32 shadowTimeOfDay = ECS::Systems::GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32()); + const vec3 lightDirection = ECS::Systems::UpdateAreaLights::GetLightDirection(shadowTimeOfDay); + + u32 invalidateCause = 0; + if (CVAR_SVSMInvalidateAll.Get() != 0 || _svsmForceInvalidateAll) + { + invalidateCause |= SVSMCause::Manual; + CVAR_SVSMInvalidateAll.Set(0); + _svsmForceInvalidateAll = false; + } + if (_svsmDirtyAABBOverflow) + { + invalidateCause |= SVSMCause::AABBOverflow; + } + + const u32 numDirtyAABBs = static_cast(_svsmDirtyAABBs.size() / 2); + const bool dynamicSplit = CVAR_SVSMDynamicSplit.Get() == 1; + const u32 numDynamicAABBs = dynamicSplit ? static_cast(_svsmDynamicAABBs.size() / 2) : 0; + + renderGraph->AddPass("SVSM Update", + [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); + + data.cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE); + data.svsmDataBuffer = builder.Write(_svsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + data.pageTableBuffer = builder.Write(_svsmPageTableBuffer, BufferUsage::COMPUTE); + data.freeListBuffer = builder.Write(_svsmFreeListBuffer, BufferUsage::COMPUTE); + data.dirtyAABBBuffer = builder.Write(_svsmDirtyAABBBuffer, BufferUsage::COMPUTE); + data.clearListBuffer = builder.Write(_svsmClearListBuffer, BufferUsage::COMPUTE); + data.dynamicPageTableBuffer = builder.Write(_svsmDynamicPageTableBuffer, BufferUsage::COMPUTE); + data.dynamicFreeListBuffer = builder.Write(_svsmDynamicFreeListBuffer, BufferUsage::COMPUTE); + data.dynamicClearListBuffer = builder.Write(_svsmDynamicClearListBuffer, BufferUsage::COMPUTE); + data.dynamicAABBBuffer = builder.Write(_svsmDynamicAABBBuffer, BufferUsage::COMPUTE); + data.svsmDataReadBackBuffer = builder.Write(_svsmDataReadBackBuffer, BufferUsage::TRANSFER); + data.dynamicValidateReadBackBuffer = builder.Write(_svsmDynamicValidateReadBackBuffer, BufferUsage::TRANSFER); + data.pagePool = builder.Write(_svsmPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); + data.dynamicPagePool = builder.Write(_svsmDynamicPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); + + data.prepareSet = builder.Use(_svsmPrepareDescriptorSet); + data.invalidateSet = builder.Use(_svsmInvalidateAABBsDescriptorSet); + data.updateASet = builder.Use(_svsmPageUpdateADescriptorSet); + data.markSet = builder.Use(_svsmPageMarkDescriptorSet); + data.updateBSet = builder.Use(_svsmPageUpdateBDescriptorSet); + data.dynamicMarkSet = builder.Use(_svsmDynamicMarkDescriptorSet); + data.dynamicUpdateSet = builder.Use(_svsmDynamicUpdateDescriptorSet); + data.finalizeSet = builder.Use(_svsmFinalizeDescriptorSet); + data.clearSet = builder.Use(_svsmPageClearDescriptorSet); + data.dynamicClearSet = builder.Use(_svsmDynamicPageClearDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, numClipmaps, lightDirection, invalidateCause, numDirtyAABBs, dynamicSplit, numDynamicAABBs](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, SVSMUpdate); + + struct SVSMConstants + { + vec4 lightDirection; + u32 numClipmaps; + u32 pageTableSize; + u32 pageSize; + u32 poolPagesPerRow; + f32 clipmap0Extent; + f32 markBorderTexels; + u32 evictAge; + u32 invalidateAll; + u32 numDirtyAABBs; + u32 allocClipmap; + f32 casterMargin; + f32 zHalfRange; + f32 padding0; + f32 padding1; + f32 resolutionScale; + u32 dynamicPoolPagesPerRow; + u32 renderBudget; + u32 dynamicPhase; + u32 padding3; + u32 padding4; + }; + + CVarSystem* cvarSystem = CVarSystem::Get(); + const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + const u32 pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / pageSize, 16u, SVSM_MAX_PAGE_TABLE_SIZE); + u32 poolPagesPerRow = static_cast(CVAR_SVSMPoolSize.Get()) / pageSize; + poolPagesPerRow = glm::min(poolPagesPerRow, SVSM_MAX_PAGE_TABLE_SIZE); + u32 dynamicPoolPagesPerRow = static_cast(CVAR_SVSMDynamicPoolSize.Get()) / pageSize; + dynamicPoolPagesPerRow = glm::min(dynamicPoolPagesPerRow, SVSM_MAX_PAGE_TABLE_SIZE); + + SVSMConstants* constants = graphResources.FrameNew(); + constants->lightDirection = vec4(lightDirection, 0.0f); + constants->numClipmaps = numClipmaps; + constants->pageTableSize = pageTableSize; + constants->pageSize = pageSize; + constants->poolPagesPerRow = poolPagesPerRow; + constants->clipmap0Extent = CVAR_SVSMClipmap0Extent.GetFloat(); + constants->markBorderTexels = CVAR_SVSMMarkBorderTexels.GetFloat(); + constants->evictAge = static_cast(glm::clamp(CVAR_SVSMPageEvictAge.Get(), 1, 254)); + constants->invalidateAll = invalidateCause; + constants->numDirtyAABBs = numDirtyAABBs; + constants->allocClipmap = 0; + constants->casterMargin = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin")); + constants->zHalfRange = CVAR_SVSMZHalfRange.GetFloat(); + constants->padding0 = 0.0f; + constants->padding1 = 0.0f; + constants->resolutionScale = CVAR_SVSMResolutionScale.GetFloat(); + constants->dynamicPoolPagesPerRow = dynamicSplit ? dynamicPoolPagesPerRow : 0; + constants->renderBudget = static_cast(glm::max(CVAR_SVSMRenderBudget.Get(), 0)); + constants->dynamicPhase = 0; + + if (_svsmPoolNeedsClear) + { + // One-shot zero of the fresh pools: uninitialized VRAM must never be sampled, and + // depth atomics against garbage would keep the garbage + commandList.Clear(data.pagePool, uvec4(0, 0, 0, 0)); + commandList.ImageBarrier(data.pagePool); + commandList.Clear(data.dynamicPagePool, uvec4(0, 0, 0, 0)); + commandList.ImageBarrier(data.dynamicPagePool); + _svsmPoolNeedsClear = false; + } + + // The AABB lists upload from Update through the frame-synced staging ring, the render + // graph issues a global upload barrier before any pass executes + + // Prepare: invalidation detection, window anchors, per-frame stat reset + commandList.BeginPipeline(_svsmPreparePipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + + commandList.BindDescriptorSet(data.prepareSet, frameIndex); + + commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(_svsmPreparePipeline); + + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + + // Moved and animated instances invalidate the pages under them + if (numDirtyAABBs > 0) + { + commandList.BeginPipeline(_svsmInvalidateAABBsPipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + commandList.BindDescriptorSet(data.invalidateSet, frameIndex); + + commandList.Dispatch((numDirtyAABBs + 63) / 64, numClipmaps, 1); + commandList.EndPipeline(_svsmInvalidateAABBsPipeline); + + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + } + + // Update A: toroidal and global invalidation, aging, eviction. Visits the full table + // capacity so entries orphaned by config changes still age out + const u32 tableCapacity = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + + commandList.BeginPipeline(_svsmPageUpdateAPipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + commandList.BindDescriptorSet(data.updateASet, frameIndex); + + commandList.Dispatch(tableCapacity / 256, 1, 1); + commandList.EndPipeline(_svsmPageUpdateAPipeline); + + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE); + + // Mark: pages touched by visible depth samples + commandList.BeginPipeline(_svsmPageMarkPipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + + data.markSet.Bind("_depth"_h, data.depth); + commandList.BindDescriptorSet(data.markSet, frameIndex); + + uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); + commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); + + commandList.EndPipeline(_svsmPageMarkPipeline); + + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + + // Update B: allocation, re-dirtying, dirty rects. One dispatch per clipmap, finest + // first with a free list barrier between, so pool starvation always lands on the + // coarsest ring where the sampler fallback costs the least + commandList.BeginPipeline(_svsmPageUpdateBPipeline); + commandList.BindDescriptorSet(data.updateBSet, frameIndex); + + for (u32 clipmapIndex = 0; clipmapIndex < numClipmaps; clipmapIndex++) + { + SVSMConstants* allocConstants = graphResources.FrameNew(); + *allocConstants = *constants; + allocConstants->allocClipmap = clipmapIndex; + + commandList.PushConstant(allocConstants, 0, sizeof(SVSMConstants)); + commandList.Dispatch((pageTableSize * pageTableSize) / 256, 1, 1); + + if (clipmapIndex + 1 < numClipmaps) + { + commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE); + } + } + + commandList.EndPipeline(_svsmPageUpdateBPipeline); + + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); + + // Caster split: mark pages under dynamic casters (visible ones only), then run the + // transient dynamic page lifecycle. Must precede Finalize, which unions the rects + if (dynamicSplit) + { + if (numDynamicAABBs > 0) + { + SVSMConstants* dynamicMarkConstants = graphResources.FrameNew(); + *dynamicMarkConstants = *constants; + dynamicMarkConstants->numDirtyAABBs = numDynamicAABBs; + + commandList.BeginPipeline(_svsmDynamicMarkPipeline); + commandList.PushConstant(dynamicMarkConstants, 0, sizeof(SVSMConstants)); + commandList.BindDescriptorSet(data.dynamicMarkSet, frameIndex); + + commandList.Dispatch((numDynamicAABBs + 63) / 64, numClipmaps, 1); + commandList.EndPipeline(_svsmDynamicMarkPipeline); + + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + } + + // Two dispatches: release then acquire. Free-list pushes and pops in one dispatch + // race (a pop can read a slot before the pushed phys index lands), aliasing one + // physical page under two table entries — the ghost-shadow bug + commandList.BeginPipeline(_svsmDynamicUpdatePipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); // dynamicPhase 0 + commandList.BindDescriptorSet(data.dynamicUpdateSet, frameIndex); + commandList.Dispatch(tableCapacity / 256, 1, 1); + + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); + + SVSMConstants* dynamicAcquireConstants = graphResources.FrameNew(); + *dynamicAcquireConstants = *constants; + dynamicAcquireConstants->dynamicPhase = 1; + + commandList.PushConstant(dynamicAcquireConstants, 0, sizeof(SVSMConstants)); + commandList.Dispatch(tableCapacity / 256, 1, 1); + commandList.EndPipeline(_svsmDynamicUpdatePipeline); + + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + } + + // Finalize: clipmap render cameras from the anchors and this frame's dirty rects + commandList.BeginPipeline(_svsmFinalizePipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + + commandList.BindDescriptorSet(data.finalizeSet, frameIndex); + + commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(_svsmFinalizePipeline); + + commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); + + // Clear this frame's dirty pages, one workgroup per page from the list UpdateB built + commandList.BeginPipeline(_svsmPageClearPipeline); + commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + + data.clearSet.Bind("_pagePool"_h, data.pagePool); + commandList.BindDescriptorSet(data.clearSet, frameIndex); + + commandList.DispatchIndirect(data.clearListBuffer, 0); + commandList.EndPipeline(_svsmPageClearPipeline); + + // Caster split: every live dynamic page clears (and later re-renders) each frame. + // Same shader, dynamic list + pool, pool row count overridden + if (dynamicSplit) + { + SVSMConstants* dynamicClearConstants = graphResources.FrameNew(); + *dynamicClearConstants = *constants; + dynamicClearConstants->poolPagesPerRow = constants->dynamicPoolPagesPerRow; + + commandList.BeginPipeline(_svsmPageClearPipeline); + commandList.PushConstant(dynamicClearConstants, 0, sizeof(SVSMConstants)); + + data.dynamicClearSet.Bind("_pagePool"_h, data.dynamicPagePool); + commandList.BindDescriptorSet(data.dynamicClearSet, frameIndex); + + commandList.DispatchIndirect(data.dynamicClearListBuffer, 0); + commandList.EndPipeline(_svsmPageClearPipeline); + } + + commandList.CopyBuffer(data.svsmDataReadBackBuffer, 0, data.svsmDataBuffer, 0, sizeof(u32) * SVSM_DATA_UINT_COUNT); + + // One-shot dynamic pool validation snapshot, Update maps and checks it next frame + if (dynamicSplit && !_svsmValidatePending && CVAR_SVSMValidateDynamic.Get() != 0) + { + const u32 tableUints = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, 0, data.dynamicPageTableBuffer, 0, sizeof(u32) * tableUints); + commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, sizeof(u32) * tableUints, data.dynamicFreeListBuffer, 0, sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES)); + _svsmValidatePending = true; + } + }); +} + +void ShadowRenderer::AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + struct Data + { + Renderer::ImageMutableResource target; + Renderer::ImageResource pagePool; + + Renderer::DescriptorSetResource debugSet; + Renderer::DescriptorSetResource poolDebugSet; + }; + + const i32 debugClipmap = CVAR_SVSMDebugClipmap.Get(); + const i32 poolMode = CVAR_SVSMDebugShowPool.Get(); // 1 = static pool, 2 = dynamic pool + const bool showPool = poolMode != 0 && _svsmPagePool != Renderer::ImageID::Invalid(); + const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); + if (CVAR_ShadowTechnique.Get() != 1 || !CVAR_ShadowEnabled.Get() || (debugClipmap < 0 && !showPool)) + return; + + renderGraph->AddPass("SVSM Debug Overlay", + [this, &resources, showPool, poolMode](Data& data, Renderer::RenderGraphBuilder& builder) + { + data.target = builder.Write(resources.sceneColor, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); + builder.Read(_svsmPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + if (showPool) + { + data.pagePool = builder.Read(poolMode == 2 ? _svsmDynamicPagePool : _svsmPagePool, Renderer::PipelineType::COMPUTE); + } + + data.debugSet = builder.Use(_svsmPageTableDebugDescriptorSet); + data.poolDebugSet = builder.Use(_svsmPoolDebugDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, frameIndex, debugClipmap, numClipmaps, showPool, poolMode](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, SVSMDebugOverlay); + + uvec2 targetDimensions = graphResources.GetImageDimensions(data.target); + + if (debugClipmap >= 0) + { + struct DebugConstants + { + u32 clipmapIndex; + u32 pageTableSize; + u32 cellSize; + u32 padding0; + ivec2 screenOffset; + }; + + const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + const u32 pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / pageSize, 16u, SVSM_MAX_PAGE_TABLE_SIZE); + const u32 cellSize = 8; + const u32 regionSize = pageTableSize * cellSize; + + DebugConstants* constants = graphResources.FrameNew(); + constants->clipmapIndex = glm::min(static_cast(debugClipmap), numClipmaps - 1); + constants->pageTableSize = pageTableSize; + constants->cellSize = cellSize; + constants->screenOffset = ivec2(glm::max(static_cast(targetDimensions.x) - static_cast(regionSize) - 8, 0), 8); + + commandList.BeginPipeline(_svsmPageTableDebugPipeline); + commandList.PushConstant(constants, 0, sizeof(DebugConstants)); + + data.debugSet.Bind("_target"_h, data.target); + commandList.BindDescriptorSet(data.debugSet, frameIndex); + + commandList.Dispatch((regionSize + 15) / 16, (regionSize + 15) / 16, 1); + commandList.EndPipeline(_svsmPageTableDebugPipeline); + } + + if (showPool) + { + struct PoolDebugConstants + { + u32 poolSize; + u32 regionSize; + u32 padding0; + u32 padding1; + ivec2 screenOffset; + }; + + const u32 regionSize = 512; + + PoolDebugConstants* constants = graphResources.FrameNew(); + constants->poolSize = static_cast(poolMode == 2 ? CVAR_SVSMDynamicPoolSize.Get() : CVAR_SVSMPoolSize.Get()); + constants->regionSize = regionSize; + constants->screenOffset = ivec2(glm::max(static_cast(targetDimensions.x) - static_cast(regionSize) - 8, 0), glm::max(static_cast(targetDimensions.y) - static_cast(regionSize) - 8, 0)); + + commandList.BeginPipeline(_svsmPoolDebugPipeline); + commandList.PushConstant(constants, 0, sizeof(PoolDebugConstants)); + + data.poolDebugSet.Bind("_pagePool"_h, data.pagePool); + data.poolDebugSet.Bind("_target"_h, data.target); + commandList.BindDescriptorSet(data.poolDebugSet, frameIndex); + + commandList.Dispatch((regionSize + 15) / 16, (regionSize + 15) / 16, 1); + commandList.EndPipeline(_svsmPoolDebugPipeline); + } + }); +} + bool ShadowRenderer::GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const { f32 usedMin = _sdsmDataReadBack[2]; @@ -419,11 +1175,44 @@ bool ShadowRenderer::GetDepthBoundsViewDistances(const RenderResources& resource return true; } +bool ShadowRenderer::GetSVSMClipmapStats(u32 clipmapIndex, SVSMClipmapStats& outStats) const +{ + if (clipmapIndex >= SVSM_MAX_CLIPMAPS) + return false; + + outStats.marked = _svsmDataReadBack[SVSMDataOffsets::StatsMarked + clipmapIndex]; + outStats.resident = _svsmDataReadBack[SVSMDataOffsets::StatsResident + clipmapIndex]; + outStats.dirty = _svsmDataReadBack[SVSMDataOffsets::StatsDirty + clipmapIndex]; + outStats.evicted = _svsmDataReadBack[SVSMDataOffsets::StatsEvicted + clipmapIndex]; + outStats.invalidated = _svsmDataReadBack[SVSMDataOffsets::StatsInvalidated + clipmapIndex]; + outStats.dynamicLive = _svsmDataReadBack[SVSMDataOffsets::StatsDynamicLive + clipmapIndex]; + outStats.deferred = _svsmDataReadBack[SVSMDataOffsets::StatsDeferred + clipmapIndex]; + outStats.extent = glm::uintBitsToFloat(_svsmDataReadBack[SVSMDataOffsets::Extent + clipmapIndex]); + return true; +} + +void ShadowRenderer::GetSVSMGlobalStats(u32& outFreePages, u32& outTotalPages, u32& outOverflow, u32& outInvalidationCause) const +{ + outFreePages = _svsmDataReadBack[SVSMDataOffsets::StatsFreeListCount]; + outTotalPages = _svsmPoolPages; + outOverflow = _svsmDataReadBack[SVSMDataOffsets::StatsOverflow]; + outInvalidationCause = _svsmDataReadBack[SVSMDataOffsets::StatsInvalidationCause]; +} + +void ShadowRenderer::GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, u32& outOverflow) const +{ + outLivePages = _svsmDataReadBack[SVSMDataOffsets::StatsDynamicTotal]; + outTotalPages = _svsmDynamicPoolPages; + outOverflow = _svsmDataReadBack[SVSMDataOffsets::StatsDynamicOverflow]; +} + void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { struct ShadowPassData { Renderer::DepthImageMutableResource shadowDepthCascades[Renderer::Settings::MAX_SHADOW_CASCADES]; + Renderer::ImageResource svsmPagePool; + Renderer::ImageResource svsmDynamicPagePool; Renderer::DescriptorSetResource lightDescriptorSet; }; @@ -438,11 +1227,17 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes renderGraph->AddPass("Shadow Pass", [=, &resources](ShadowPassData& data, Renderer::RenderGraphBuilder& builder) { + // Under SVSM the cascade RTs are never rendered or sampled, skip the 4x4096 clears but + // keep them bound so the material pass layout stays satisfied + const Renderer::LoadMode cascadeLoadMode = CVAR_ShadowTechnique.Get() == 1 ? Renderer::LoadMode::LOAD : Renderer::LoadMode::CLEAR; for (u32 i = 0; i < numCascades; i++) { - data.shadowDepthCascades[i] = builder.Write(resources.shadowDepthCascades[i], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::CLEAR); + data.shadowDepthCascades[i] = builder.Write(resources.shadowDepthCascades[i], Renderer::PipelineType::GRAPHICS, cascadeLoadMode); } + data.svsmPagePool = builder.Read(GetSVSMPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); + data.svsmDynamicPagePool = builder.Read(GetSVSMDynamicPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); + data.lightDescriptorSet = builder.Use(resources.lightDescriptorSet); return true; // Return true from setup to enable this pass, return false to disable it @@ -459,6 +1254,10 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes data.lightDescriptorSet.BindArray("_shadowCascadeRTs", cascadeDepthResource, i); } + + // Every LIGHT set consumer needs the SVSM pool bindings valid, real pools or placeholder + data.lightDescriptorSet.Bind("_svsmPagePool"_h, data.svsmPagePool); + data.lightDescriptorSet.Bind("_svsmDynamicPagePool"_h, data.svsmDynamicPagePool); }); } @@ -590,4 +1389,263 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; _cascadeCamerasReadBackBuffer = _renderer->CreateBuffer(_cascadeCamerasReadBackBuffer, bufferDesc); } + + // SVSM page table lifecycle + { + Renderer::ComputePipelineDesc pipelineDesc; + Renderer::ComputeShaderDesc shaderDesc; + + pipelineDesc.debugName = "SVSM Prepare"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPrepare.cs"_h, "Shadows/SVSMPrepare.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPreparePipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPrepareDescriptorSet.RegisterPipeline(_renderer, _svsmPreparePipeline); + _svsmPrepareDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Invalidate AABBs"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMInvalidateAABBs.cs"_h, "Shadows/SVSMInvalidateAABBs.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmInvalidateAABBsPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmInvalidateAABBsDescriptorSet.RegisterPipeline(_renderer, _svsmInvalidateAABBsPipeline); + _svsmInvalidateAABBsDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Page Update A"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPageUpdateA.cs"_h, "Shadows/SVSMPageUpdateA.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPageUpdateAPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPageUpdateADescriptorSet.RegisterPipeline(_renderer, _svsmPageUpdateAPipeline); + _svsmPageUpdateADescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Page Mark"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPageMark.cs"_h, "Shadows/SVSMPageMark.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPageMarkPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPageMarkDescriptorSet.RegisterPipeline(_renderer, _svsmPageMarkPipeline); + _svsmPageMarkDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Page Update B"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPageUpdateB.cs"_h, "Shadows/SVSMPageUpdateB.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPageUpdateBPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPageUpdateBDescriptorSet.RegisterPipeline(_renderer, _svsmPageUpdateBPipeline); + _svsmPageUpdateBDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Dynamic Mark"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMDynamicMark.cs"_h, "Shadows/SVSMDynamicMark.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmDynamicMarkPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmDynamicMarkDescriptorSet.RegisterPipeline(_renderer, _svsmDynamicMarkPipeline); + _svsmDynamicMarkDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Dynamic Update"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMDynamicUpdate.cs"_h, "Shadows/SVSMDynamicUpdate.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmDynamicUpdatePipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmDynamicUpdateDescriptorSet.RegisterPipeline(_renderer, _svsmDynamicUpdatePipeline); + _svsmDynamicUpdateDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Finalize"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMFinalize.cs"_h, "Shadows/SVSMFinalize.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmFinalizePipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmFinalizeDescriptorSet.RegisterPipeline(_renderer, _svsmFinalizePipeline); + _svsmFinalizeDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Page Clear"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPageClear.cs"_h, "Shadows/SVSMPageClear.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPageClearPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPageClearDescriptorSet.RegisterPipeline(_renderer, _svsmPageClearPipeline); + _svsmPageClearDescriptorSet.Init(_renderer); + + // Second set instance on the same clear pipeline, bound to the dynamic list + pool + _svsmDynamicPageClearDescriptorSet.RegisterPipeline(_renderer, _svsmPageClearPipeline); + _svsmDynamicPageClearDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Page Table Debug"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPageTableDebug.cs"_h, "Shadows/SVSMPageTableDebug.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPageTableDebugPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPageTableDebugDescriptorSet.RegisterPipeline(_renderer, _svsmPageTableDebugPipeline); + _svsmPageTableDebugDescriptorSet.Init(_renderer); + + pipelineDesc.debugName = "SVSM Pool Debug"; + shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/SVSMPoolDebug.cs"_h, "Shadows/SVSMPoolDebug.cs"); + pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); + _svsmPoolDebugPipeline = _renderer->CreatePipeline(pipelineDesc); + _svsmPoolDebugDescriptorSet.RegisterPipeline(_renderer, _svsmPoolDebugPipeline); + _svsmPoolDebugDescriptorSet.Init(_renderer); + + // The physical page index is 12 bits in the table entry, the pool page counts are fixed at + // startup (svsmPoolSize/svsmDynamicPoolSize changes need a restart) + const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + const u32 poolPagesPerRow = static_cast(CVAR_SVSMPoolSize.Get()) / pageSize; + _svsmPoolPages = glm::min(poolPagesPerRow * poolPagesPerRow, SVSM_MAX_POOL_PAGES); + const u32 dynamicPoolPagesPerRow = static_cast(CVAR_SVSMDynamicPoolSize.Get()) / pageSize; + _svsmDynamicPoolPages = glm::min(dynamicPoolPagesPerRow * dynamicPoolPagesPerRow, SVSM_MAX_POOL_PAGES); + + Renderer::BufferDesc bufferDesc; + bufferDesc.name = "SVSMData"; + bufferDesc.size = sizeof(u32) * SVSM_DATA_UINT_COUNT; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; + _svsmDataBuffer = _renderer->CreateAndFillBuffer(_svsmDataBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); // prevLightDirection.w = 0 marks the state uninitialized + }); + + bufferDesc.name = "SVSMPageTable"; + bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + _svsmPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); // A zeroed entry is a free slot + }); + + bufferDesc.name = "SVSMFreeList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + const u32 poolPages = _svsmPoolPages; + _svsmFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmFreeListBuffer, bufferDesc, [poolPages](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + values[0] = poolPages; // [0] = count, [1..3] padding, entries from [4] + values[1] = 0; + values[2] = 0; + values[3] = 0; + for (u32 i = 0; i < SVSM_MAX_POOL_PAGES; i++) + { + values[4 + i] = i < poolPages ? i : 0; + } + }); + + bufferDesc.name = "SVSMDirtyAABBs"; + bufferDesc.size = sizeof(vec4) * SVSM_MAX_DIRTY_AABBS * 2; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + _svsmDirtyAABBBuffer = _renderer->CreateBuffer(_svsmDirtyAABBBuffer, bufferDesc); + + bufferDesc.name = "SVSMDynamicAABBs"; + bufferDesc.size = sizeof(vec4) * SVSM_MAX_DYNAMIC_AABBS * 2; + _svsmDynamicAABBBuffer = _renderer->CreateBuffer(_svsmDynamicAABBBuffer, bufferDesc); + + // Static/dynamic caster split resources + bufferDesc.name = "SVSMDynamicPageTable"; + bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + _svsmDynamicPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); + }); + + bufferDesc.name = "SVSMDynamicFreeList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + const u32 dynamicPoolPages = _svsmDynamicPoolPages; + _svsmDynamicFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicFreeListBuffer, bufferDesc, [dynamicPoolPages](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + memset(values, 0, size); + values[0] = dynamicPoolPages; + for (u32 i = 0; i < dynamicPoolPages; i++) + { + values[4 + i] = i; + } + }); + + bufferDesc.name = "SVSMDynamicClearList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::INDIRECT_ARGUMENT_BUFFER; + _svsmDynamicClearListBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicClearListBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + memset(values, 0, size); + values[1] = 1; // Indirect group counts y and z + values[2] = 1; + }); + + bufferDesc.name = "SVSMClearList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); // [0..2] indirect dispatch args, physical pages from [4] + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::INDIRECT_ARGUMENT_BUFFER; + _svsmClearListBuffer = _renderer->CreateAndFillBuffer(_svsmClearListBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + memset(values, 0, size); + values[1] = 1; // Indirect group counts y and z + values[2] = 1; + }); + + _svsmPrepareDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPrepareDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmPrepareDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); + _svsmPrepareDescriptorSet.Bind("_dynamicClearList"_h, _svsmDynamicClearListBuffer); + _svsmInvalidateAABBsDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmInvalidateAABBsDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmInvalidateAABBsDescriptorSet.Bind("_dirtyAABBs"_h, _svsmDirtyAABBBuffer); + _svsmPageUpdateADescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageUpdateADescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateADescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmPageMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_dynamicAABBs"_h, _svsmDynamicAABBBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_dynamicFreeList"_h, _svsmDynamicFreeListBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_dynamicClearList"_h, _svsmDynamicClearListBuffer); + _svsmFinalizeDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageClearDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); + _svsmDynamicPageClearDescriptorSet.Bind("_clearList"_h, _svsmDynamicClearListBuffer); + _svsmPageTableDebugDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + // _srcCameras / _depth / _target / _rwCameras / _pagePool are bound per frame, those resources can be recreated + + bufferDesc.name = "SVSMDataReadBack"; + bufferDesc.size = sizeof(u32) * SVSM_DATA_UINT_COUNT; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; + bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; + _svsmDataReadBackBuffer = _renderer->CreateBuffer(_svsmDataReadBackBuffer, bufferDesc); + + bufferDesc.name = "SVSMDynamicValidateReadBack"; + bufferDesc.size = sizeof(u32) * (SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE + 4 + SVSM_MAX_POOL_PAGES); + _svsmDynamicValidateReadBackBuffer = _renderer->CreateBuffer(_svsmDynamicValidateReadBackBuffer, bufferDesc); + + // The material pass samples SVSM through the LIGHT set. The buffers bind here, the pools + // bind per frame in the material pass (real pools once created, a placeholder before) + resources.lightDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + resources.lightDescriptorSet.Bind("_svsmPageTable"_h, _svsmPageTableBuffer); + resources.lightDescriptorSet.Bind("_svsmDynamicPageTable"_h, _svsmDynamicPageTableBuffer); + + Renderer::ImageDesc placeholderDesc; + placeholderDesc.debugName = "SVSMPagePoolPlaceholder"; + placeholderDesc.dimensions = vec2(1, 1); + placeholderDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_ABSOLUTE; + placeholderDesc.format = Renderer::ImageFormat::R32_UINT; + placeholderDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; + placeholderDesc.clearUInts = uvec4(0, 0, 0, 0); + + _svsmPagePoolPlaceholder = _renderer->CreateImage(placeholderDesc); + + // The terrain/model SVSM page-render sets read these permanent buffers, bind them once + // here (the pools stay per-frame, they are created lazily). Same for the cameras buffer + _terrainRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer); + _modelRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer, _svsmDynamicPageTableBuffer); + } + + BindCameraBuffers(resources); +} + +void ShadowRenderer::BindCameraBuffers(RenderResources& resources) +{ + Renderer::BufferID camerasBuffer = resources.cameras.GetBuffer(); + + _cascadeFitRangeDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); + _cascadeXYReduceDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); + _cascadeFitCamerasDescriptorSet.Bind("_rwCameras"_h, camerasBuffer); + _svsmPrepareDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); + _svsmPageMarkDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); + _svsmFinalizeDescriptorSet.Bind("_rwCameras"_h, camerasBuffer); } \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 3f00297..7d84fd5 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -7,7 +7,10 @@ #include #include +#include + #include +#include #include #include @@ -35,6 +38,11 @@ class ShadowRenderer void AddCascadeFitPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + // SVSM (shadowTechnique 1): page marking, page table lifecycle and allocation. Runs alongside + // the CSM path until the SVSM rendering and sampling stages land + void AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + // Reduced scene depth bounds as view distances, false while no valid depth has been reduced yet bool GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const; @@ -44,6 +52,61 @@ class ShadowRenderer // Fitted light-space extents per cascade, false while the XY reduction is not running bool GetCascadeFittedBounds(u32 cascadeIndex, vec3& outExtents, bool& outValid) const; + struct SVSMClipmapStats + { + u32 marked = 0; + u32 resident = 0; + u32 dirty = 0; + u32 evicted = 0; + u32 invalidated = 0; + u32 dynamicLive = 0; + u32 deferred = 0; + f32 extent = 0.0f; + }; + + // One frame old readback values, only meaningful while shadowTechnique is 1 + bool GetSVSMClipmapStats(u32 clipmapIndex, SVSMClipmapStats& outStats) const; + void GetSVSMGlobalStats(u32& outFreePages, u32& outTotalPages, u32& outOverflow, u32& outInvalidationCause) const; + void GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, u32& outOverflow) const; + u32 GetSVSMBudgetUsed() const { return _svsmDataReadBack[216]; } // SVSMDataOffsets::StatsBudgetUsed + + // Binds the cameras buffer into every SDSM/SVSM set that reads or writes it. Called at init + // (buffer binds only reach the canonical descriptor copies at a later FlipFrame, so mid-frame + // binds leave a hole) and again if the cameras buffer is ever recreated + void BindCameraBuffers(RenderResources& resources); + + // Current-frame CPU knowledge: with no dynamic casters at all, the dynamic fills/draws skip entirely + bool HasSVSMDynamicCasters() const { return !_svsmDynamicAABBs.empty(); } + + // True if the clipmap had live dynamic pages in either of the last two frames (readback is one + // frame old, the second frame is hysteresis). Inactive views skip their dynamic fill+draw, + // a caster entering a fresh ring renders at most one frame late + bool GetSVSMDynamicViewActive(u32 clipmapIndex) const + { + return _svsmDataReadBack[196 + clipmapIndex] != 0 || _svsmDynamicLivePrev[clipmapIndex] != 0; // SVSMDataOffsets::StatsDynamicLive + } + + // CPU-side caster classification counts, rebuilt each Update + void GetSVSMCasterStats(u32& outDynamicCasters, u32& outAnimatedCasters, u32& outDroppedAABBs) const + { + outDynamicCasters = _svsmNumDynamicCasters; + outAnimatedCasters = _svsmNumAnimatedCasters; + outDroppedAABBs = _svsmDynamicAABBsDropped; + } + + // SVSM resources for the terrain/model page render passes, the pools are created lazily on + // the first shadowTechnique 1 frame + Renderer::BufferID GetSVSMDataBuffer() const { return _svsmDataBuffer; } + Renderer::BufferID GetSVSMPageTableBuffer() const { return _svsmPageTableBuffer; } + Renderer::ImageID GetSVSMPagePool() const { return _svsmPagePool; } + Renderer::BufferID GetSVSMDynamicPageTableBuffer() const { return _svsmDynamicPageTableBuffer; } + Renderer::ImageID GetSVSMDynamicPagePool() const { return _svsmDynamicPagePool; } + + // For the material pass LIGHT set bindings, which must stay valid before the pools exist. + // Nothing samples the placeholder while the page tables have no resident entries + Renderer::ImageID GetSVSMPagePoolOrPlaceholder() const { return _svsmPagePool != Renderer::ImageID::Invalid() ? _svsmPagePool : _svsmPagePoolPlaceholder; } + Renderer::ImageID GetSVSMDynamicPagePoolOrPlaceholder() const { return _svsmDynamicPagePool != Renderer::ImageID::Invalid() ? _svsmDynamicPagePool : _svsmPagePoolPlaceholder; } + private: void CreatePermanentResources(RenderResources& resources); @@ -78,5 +141,67 @@ class ShadowRenderer Camera _readBackCascadeCameras[Renderer::Settings::MAX_SHADOW_CASCADES]; f32 _sdsmDataReadBack[SDSM_DATA_FLOAT_COUNT] = { 0.0f }; + // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp + static constexpr u32 SVSM_DATA_UINT_COUNT = 220; + static constexpr u32 SVSM_MAX_CLIPMAPS = 8; + static constexpr u32 SVSM_MAX_PAGE_TABLE_SIZE = 64; // Pages per row, buffers are sized for this cap + static constexpr u32 SVSM_MAX_POOL_PAGES = 4096; // Physical page index is 12 bits in the table entry + static constexpr u32 SVSM_MAX_DIRTY_AABBS = 1024; + static constexpr u32 SVSM_MAX_DYNAMIC_AABBS = 4096; // Animated forests are many small casters, overflow drops (never full-invalidates) + + Renderer::ComputePipelineID _svsmPreparePipeline; + Renderer::ComputePipelineID _svsmInvalidateAABBsPipeline; + Renderer::ComputePipelineID _svsmPageUpdateAPipeline; + Renderer::ComputePipelineID _svsmPageMarkPipeline; + Renderer::ComputePipelineID _svsmPageUpdateBPipeline; + Renderer::ComputePipelineID _svsmDynamicMarkPipeline; + Renderer::ComputePipelineID _svsmDynamicUpdatePipeline; + Renderer::ComputePipelineID _svsmFinalizePipeline; + Renderer::ComputePipelineID _svsmPageClearPipeline; + Renderer::ComputePipelineID _svsmPageTableDebugPipeline; + Renderer::ComputePipelineID _svsmPoolDebugPipeline; + Renderer::DescriptorSet _svsmPrepareDescriptorSet; + Renderer::DescriptorSet _svsmInvalidateAABBsDescriptorSet; + Renderer::DescriptorSet _svsmPageUpdateADescriptorSet; + Renderer::DescriptorSet _svsmPageMarkDescriptorSet; + Renderer::DescriptorSet _svsmPageUpdateBDescriptorSet; + Renderer::DescriptorSet _svsmDynamicMarkDescriptorSet; + Renderer::DescriptorSet _svsmDynamicUpdateDescriptorSet; + Renderer::DescriptorSet _svsmFinalizeDescriptorSet; + Renderer::DescriptorSet _svsmPageClearDescriptorSet; + Renderer::DescriptorSet _svsmDynamicPageClearDescriptorSet; + Renderer::DescriptorSet _svsmPageTableDebugDescriptorSet; + Renderer::DescriptorSet _svsmPoolDebugDescriptorSet; + + Renderer::BufferID _svsmDataBuffer; + Renderer::BufferID _svsmPageTableBuffer; + Renderer::BufferID _svsmFreeListBuffer; + Renderer::BufferID _svsmDirtyAABBBuffer; + Renderer::BufferID _svsmClearListBuffer; + Renderer::BufferID _svsmDynamicPageTableBuffer; + Renderer::BufferID _svsmDynamicFreeListBuffer; + Renderer::BufferID _svsmDynamicClearListBuffer; + Renderer::BufferID _svsmDynamicAABBBuffer; + Renderer::BufferID _svsmDataReadBackBuffer; + Renderer::BufferID _svsmDynamicValidateReadBackBuffer; // svsmValidateDynamic one-shot: dynamic table + free list + Renderer::ImageID _svsmPagePool; + Renderer::ImageID _svsmDynamicPagePool; + Renderer::ImageID _svsmPagePoolPlaceholder; + u32 _svsmDataReadBack[SVSM_DATA_UINT_COUNT] = { 0 }; + u32 _svsmDynamicLivePrev[SVSM_MAX_CLIPMAPS] = { 0 }; // The readback generation before the current one, for skip hysteresis + + std::vector _svsmDirtyAABBs; // Static invalidation (min, max) pairs, uploaded by the update pass + std::vector _svsmDynamicAABBs; // This frame's dynamic caster (min, max) pairs + robin_hood::unordered_set _svsmPrevDynamicEntities; // Last frame's dynamic entity handles for transition detection + bool _svsmDirtyAABBOverflow = false; + u32 _svsmNumDynamicCasters = 0; // ECS entities: moved or actively bone-simulated + u32 _svsmNumAnimatedCasters = 0; // In-range animated doodad instances from the ModelRenderer + u32 _svsmDynamicAABBsDropped = 0; // Dynamic list overflow: dropped casters freeze for the frame + bool _svsmForceInvalidateAll = false; // Set when invalidations were discarded while SVSM was inactive + bool _svsmValidatePending = false; // A validation copy was recorded, Update maps and checks it next frame + bool _svsmPoolNeedsClear = false; + u32 _svsmPoolPages = 0; + u32 _svsmDynamicPoolPages = 0; + f32 _lastDeltaTime = 0.0f; }; \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index aa690a1..3f930d9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -43,6 +43,7 @@ TerrainRenderer::TerrainRenderer(Renderer::Renderer* renderer, GameRenderer* gam , _resetIndirectDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _cullingPassDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _geometryFillPassDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) + , _svsmDrawDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) { if (CVAR_TerrainValidateTransfers.Get()) { @@ -562,8 +563,19 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, if (CVAR_TerrainCastShadow.Get() != 1) return; - u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + // Under SVSM the same per-view culling runs against the clipmap cameras, which need no + // cascade depth images + const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; + u32 numCascades; + if (useSVSM) + { + numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + } + else + { + numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); + numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); + } if (numCascades == 0) return; @@ -667,6 +679,10 @@ void TerrainRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, if (CVAR_TerrainCastShadow.Get() != 1) return; + // SVSM renders pages through AddSVSMGeometryPass instead + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1) + return; + u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); if (numCascades == 0) @@ -796,6 +812,183 @@ void TerrainRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, }); } +void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer) +{ + ZoneScoped; + + if (!CVAR_TerrainRendererEnabled.Get()) + return; + + if (_instanceDatas.Count() == 0) + return; + + CVarSystem* cvarSystem = CVarSystem::Get(); + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) + return; + + if (CVAR_TerrainCastShadow.Get() != 1) + return; + + if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) != 1) + return; + + if (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid()) + return; + + const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 virtualSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize"_h)); + + struct Data + { + Renderer::ImageMutableResource pagePool; + Renderer::BufferResource svsmData; + Renderer::BufferResource pageTable; + + Renderer::BufferMutableResource culledInstanceBuffer; + + Renderer::BufferMutableResource argumentBuffer; + Renderer::BufferMutableResource drawCountReadBackBuffer; + + Renderer::DescriptorSetResource globalSet; + Renderer::DescriptorSetResource fillSet; + Renderer::DescriptorSetResource geometryPassSet; + Renderer::DescriptorSetResource svsmSet; + }; + + renderGraph->AddPass("Terrain SVSM Geometry", + [this, &resources, frameIndex, shadowRenderer](Data& data, Renderer::RenderGraphBuilder& builder) // Setup + { + using BufferUsage = Renderer::BufferPassUsage; + + data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); + data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); + + builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); + builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); + data.culledInstanceBuffer = builder.Write(_culledInstanceBuffer, BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + + builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_cellDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_chunkDatas.GetBuffer(), BufferUsage::GRAPHICS); + builder.Read(_instanceDatas.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::GRAPHICS); + + data.argumentBuffer = builder.Write(_argumentBuffer, BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); + data.drawCountReadBackBuffer = builder.Write(_drawCountReadBackBuffer, BufferUsage::TRANSFER); + + data.globalSet = builder.Use(resources.globalDescriptorSet); + data.fillSet = builder.Use(_geometryFillPassDescriptorSet); + data.geometryPassSet = builder.Use(resources.terrainDescriptorSet); + data.svsmSet = builder.Use(_svsmDrawDescriptorSet); + + return true; // Return true from setup to enable this pass, return false to disable it + }, + [this, &resources, frameIndex, numClipmaps, virtualSize](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + GPU_SCOPED_PROFILER_ZONE(commandList, TerrainSVSMGeometry); + + const u32 cellCount = static_cast(_cellDatas.Count()); + + // svsmProfileGeometry: per-view fill/draw GPU timings for the perf editor's render pass list + const bool profileSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry"_h) != 0; + auto Profiled = [&](const char* stageName, u32 viewIndex, auto&& work) + { + if (!profileSVSM) + { + work(); + return; + } + + Renderer::TimeQueryDesc timeQueryDesc; + timeQueryDesc.name = "Terrain SVSM " + std::string(stageName) + " v" + std::to_string(viewIndex); + Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); + commandList.BeginTimeQuery(timeQuery); + work(); + commandList.EndTimeQuery(timeQuery); + }; + + // _svsmData/_pageTable are bound once at init through BindSVSMBuffers, only the lazily + // created pool binds per frame (image binds write the descriptor immediately) + data.svsmSet.Bind("_pagePool"_h, data.pagePool); + + for (u32 i = 1; i < numClipmaps + 1; i++) + { + std::string markerName = "Clipmap " + std::to_string(i - 1); + commandList.PushMarker(markerName, Color::White); + + // Reset the counters + { + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + } + + if (CVAR_TerrainGeometryEnabled.Get()) + { + FillDrawCallsParams fillParams; + fillParams.passName = "SVSM Geometry"; + fillParams.cellCount = cellCount; + fillParams.viewIndex = i; + fillParams.diffAgainstPrev = false; + fillParams.currentBitmaskIndex = frameIndex; + fillParams.fillSet = data.fillSet; + + Profiled("Fill", i, [&] { FillDrawCalls(frameIndex, graphResources, commandList, fillParams); }); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); + + if (i == 1) + { + // The viewport spans the virtual texture so SV_Position.xy is the virtual + // texel. No depth bias: there is no depth attachment for it to apply to + commandList.SetViewport(0, 0, static_cast(virtualSize), static_cast(virtualSize), 0.0f, 1.0f); + commandList.SetScissorRect(0, virtualSize, 0, virtualSize); + } + + commandList.PushMarker("Draw", Color::White); + + DrawParams drawParams; + drawParams.shadowPass = true; + drawParams.svsmPass = true; + drawParams.viewIndex = i; + drawParams.cullingEnabled = true; + drawParams.svsmExtent = uvec2(virtualSize, virtualSize); + drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); + drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); + + drawParams.globalDescriptorSet = data.globalSet; + drawParams.drawDescriptorSet = data.geometryPassSet; + drawParams.svsmDescriptorSet = data.svsmSet; + + Profiled("Draw", i, [&] { Draw(resources, frameIndex, graphResources, commandList, drawParams); }); + + commandList.PopMarker(); + } + + // Copy drawn count + { + u32 dstOffset = i * sizeof(u32); + commandList.CopyBuffer(data.drawCountReadBackBuffer, dstOffset, data.argumentBuffer, 4, 4); + } + + commandList.PopMarker(); + } + + // Finish by resetting the viewport and scissor + vec2 renderSize = _renderer->GetRenderSize(); + commandList.SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); + commandList.SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); + }); +} + +void TerrainRenderer::BindSVSMBuffers(Renderer::BufferID svsmDataBuffer, Renderer::BufferID pageTableBuffer) +{ + _svsmDrawDescriptorSet.Bind("_svsmData"_h, svsmDataBuffer); + _svsmDrawDescriptorSet.Bind("_pageTable"_h, pageTableBuffer); +} + void TerrainRenderer::Clear() { ZoneScoped; @@ -1337,6 +1530,38 @@ void TerrainRenderer::CreatePipelines() _drawShadowPipeline = _renderer->CreatePipeline(pipelineDesc); } + // Draw SVSM pages: attachment-less, the pixel shader writes depth into the page pool with + // image atomics. Depth clamp stays on so pancaked casters rasterize instead of clipping + { + Renderer::GraphicsPipelineDesc pipelineDesc; + pipelineDesc.debugName = "Terrain Draw SVSM"; + + // Shaders + Renderer::VertexShaderDesc vertexShaderDesc; + { + std::vector permutationFields = + { + { "EDITOR_PASS", "0" }, + { "SHADOW_PASS", "1" } + }; + u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); + vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Terrain/Draw.vs"); + pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); + } + + Renderer::PixelShaderDesc pixelShaderDesc; + { + pixelShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Terrain/DrawSVSM.ps"_h, "Terrain/DrawSVSM.ps"); + pipelineDesc.states.pixelShader = _renderer->LoadShader(pixelShaderDesc); + } + + // Rasterizer state, no depth or color attachments + pipelineDesc.states.rasterizerState.cullMode = Renderer::CullMode::NONE; + pipelineDesc.states.rasterizerState.frontFaceMode = Renderer::Settings::FRONT_FACE_STATE; + pipelineDesc.states.rasterizerState.depthClampEnabled = true; + + _drawSVSMPipeline = _renderer->CreatePipeline(pipelineDesc); + } } void TerrainRenderer::InitDescriptorSets() @@ -1352,6 +1577,9 @@ void TerrainRenderer::InitDescriptorSets() _geometryFillPassDescriptorSet.RegisterPipeline(_renderer, _fillDrawCallsPipeline); _geometryFillPassDescriptorSet.Init(_renderer); + + _svsmDrawDescriptorSet.RegisterPipeline(_renderer, _drawSVSMPipeline); + _svsmDrawDescriptorSet.Init(_renderer); } void TerrainRenderer::SyncToGPU() @@ -1438,15 +1666,24 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend graphResources.InitializeRenderPassDesc(renderPassDesc); // Render targets - if (!params.shadowPass) + if (params.svsmPass) + { + // Attachment-less: with no render targets BeginRenderPass has no extent fallback, + // the render area must be explicit or it collapses to zero + renderPassDesc.extent = params.svsmExtent; + } + else { - renderPassDesc.renderTargets[0] = params.visibilityBuffer; + if (!params.shadowPass) + { + renderPassDesc.renderTargets[0] = params.visibilityBuffer; + } + renderPassDesc.depthStencil = params.depth; } - renderPassDesc.depthStencil = params.depth; commandList.BeginRenderPass(renderPassDesc); // Set pipeline - Renderer::GraphicsPipelineID pipeline = params.shadowPass ? _drawShadowPipeline : _drawPipeline; + Renderer::GraphicsPipelineID pipeline = params.svsmPass ? _drawSVSMPipeline : (params.shadowPass ? _drawShadowPipeline : _drawPipeline); commandList.BeginPipeline(pipeline); // Set index buffer @@ -1467,6 +1704,10 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend // Bind descriptorset commandList.BindDescriptorSet(params.globalDescriptorSet, frameIndex); commandList.BindDescriptorSet(params.drawDescriptorSet, frameIndex); + if (params.svsmPass) + { + commandList.BindDescriptorSet(params.svsmDescriptorSet, frameIndex); + } if (params.cullingEnabled) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index 218c333..cc6bf57 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -13,6 +13,7 @@ class DebugRenderer; class GameRenderer; +class ShadowRenderer; struct RenderResources; namespace Renderer @@ -50,6 +51,11 @@ class TerrainRenderer void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); + + // Called once by ShadowRenderer at init, buffer binds must happen before the first frame + // (they only reach the canonical descriptor copies at a later FlipFrame) + void BindSVSMBuffers(Renderer::BufferID svsmDataBuffer, Renderer::BufferID pageTableBuffer); void Clear(); void Reserve(u32 numChunks); @@ -81,17 +87,20 @@ class TerrainRenderer { public: bool shadowPass = false; + bool svsmPass = false; // Attachment-less page render, needs an explicit render area u32 viewIndex = 0; bool cullingEnabled = false; Renderer::ImageMutableResource visibilityBuffer; Renderer::DepthImageMutableResource depth; + uvec2 svsmExtent = uvec2(0, 0); Renderer::BufferResource instanceBuffer; Renderer::BufferResource argumentBuffer; Renderer::DescriptorSetResource globalDescriptorSet; Renderer::DescriptorSetResource drawDescriptorSet; + Renderer::DescriptorSetResource svsmDescriptorSet; u32 argumentsIndex = 0; }; @@ -158,6 +167,7 @@ class TerrainRenderer Renderer::ComputePipelineID _cullingPipeline; Renderer::GraphicsPipelineID _drawPipeline; Renderer::GraphicsPipelineID _drawShadowPipeline; + Renderer::GraphicsPipelineID _drawSVSMPipeline; Renderer::GPUVector _cellIndices; Renderer::GPUVector _vertices; @@ -190,6 +200,7 @@ class TerrainRenderer Renderer::DescriptorSet _resetIndirectDescriptorSet; Renderer::DescriptorSet _cullingPassDescriptorSet; Renderer::DescriptorSet _geometryFillPassDescriptorSet; + Renderer::DescriptorSet _svsmDrawDescriptorSet; std::vector _cellBoundingBoxes; std::vector _chunkBoundingBoxes; diff --git a/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang b/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang index a2e51a3..0732edc 100644 --- a/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang +++ b/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang @@ -5,6 +5,8 @@ #define MAX_SHADOW_CASCADES 8 // Has to be kept in sync with the one in RenderSettings.h #endif +#include "Shadows/SVSM.inc.slang" + // WARNING: For now, when you change this file, make sure you recompile ALL shaders or you might get descriptor set reflection issues // This will be fixed later when we have better dependency tracking @@ -14,4 +16,11 @@ [[vk::binding(3, LIGHT)]] Texture2D _shadowCascadeRTs[MAX_SHADOW_CASCADES]; [[vk::binding(4, LIGHT)]] StructuredBuffer _packedDecals; // All decals in the world +// SVSM (shadowTechnique 1) sampling resources +[[vk::binding(5, LIGHT)]] StructuredBuffer _svsmData; +[[vk::binding(6, LIGHT)]] StructuredBuffer _svsmPageTable; +[[vk::binding(7, LIGHT)]] Texture2D _svsmPagePool; +[[vk::binding(8, LIGHT)]] StructuredBuffer _svsmDynamicPageTable; +[[vk::binding(9, LIGHT)]] Texture2D _svsmDynamicPagePool; + #endif // LIGHT_SET_INCLUDED \ No newline at end of file diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index 83199d9..eccbba6 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -28,6 +28,7 @@ DirectionalLight LoadDirectionalLight(uint index) float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVertexData, uint4 lightInfo, ShadowSettings shadowSettings) { uint numDirectionalLights = lightInfo.x; + uint shadowTechnique = lightInfo.y; // 0 = cascaded shadow maps, 1 = sparse virtual shadow maps uint numCascades = lightInfo.z; float3 ambientColor = float3(0.0f, 0.0f, 0.0f); @@ -54,7 +55,18 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte lightColor *= light.color.a; // Intensity in alpha channel // Directional Light Shadows - if (shadowSettings.enableShadows && numCascades > 0) + if (shadowSettings.enableShadows && shadowTechnique == 1) + { + // Sparse virtual shadow maps: clipmap selection, page walk and far fade all live in + // GetSVSMShadowFactor + float shadowFactor = GetSVSMShadowFactor(pixelVertexData.worldPos, pixelVertexData.worldNormal, shadowSettings); + + // Sun elevation strength, fades shadows out around dawn/dusk + shadowFactor = lerp(1.0f, shadowFactor, shadowSettings.strength); + + lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); + } + else if (shadowSettings.enableShadows && numCascades > 0) { shadowSettings.cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); Camera cascadeCamera = _cameras[shadowSettings.cascadeIndex + 1]; // +1 because the first camera is the main camera diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index aa38856..2707bec 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -12,6 +12,8 @@ struct ShadowSettings float strength; // Driven by sun elevation, fades shadows out around dawn/dusk float normalOffset; // Receiver offset along the surface normal in cascade texels + float svsmConstantBias; // SVSM only: compare bias toward the sun in world meters, no hardware bias in the software depth path + uint cascadeIndex; // Filled in by ApplyLighting }; @@ -212,4 +214,398 @@ uint GetShadowCascadeIndexFromDepth(float depth, uint numCascades) } return cascadeIndex; } + +// ---- SVSM (shadowTechnique 1) ---- + +// One texel from the virtual shadow map through the page table, resident = false when the page +// has no physical backing (the caller falls back to a coarser clipmap) +float SVSMLoadDepth(uint clipmapIndex, int2 virtualTexel, uint pageTableSize, uint pageSize, uint poolPagesPerRow, out bool resident) +{ + int2 localPage = virtualTexel / int(pageSize); + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 slot = WrapPageSlot(anchorPageMin + localPage, pageTableSize); + + uint entry = _svsmPageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + + // INVALID content may predate a Z-range or sun change, i.e. live in a different depth + // mapping — never sample it, the coarser fallback is always in the current mapping + resident = (entry & (SVSM_PAGE_RESIDENT | SVSM_PAGE_INVALID)) == SVSM_PAGE_RESIDENT; + if (!resident) + { + return 0.0f; + } + + uint physicalPage = GetPagePhysical(entry); + uint2 physicalBase = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + uint2 pageTexel = uint2(virtualTexel - localPage * int(pageSize)); + + return asfloat(_svsmPagePool[physicalBase + pageTexel]); +} + +// Dynamic caster depth for the same texel, 0 when no dynamic page exists there. Non-residency is +// the normal state for almost every page and must never drive the coarser-clipmap fallback +float SVSMLoadDynamicDepth(uint clipmapIndex, int2 virtualTexel, uint pageTableSize, uint pageSize, out bool resident) +{ + int2 localPage = virtualTexel / int(pageSize); + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 slot = WrapPageSlot(anchorPageMin + localPage, pageTableSize); + + uint entry = _svsmDynamicPageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + resident = (entry & SVSM_PAGE_RESIDENT) != 0; + if (!resident) + { + return 0.0f; + } + + uint poolPagesPerRow = _svsmData[0].configDynamicPoolPagesPerRow; + uint physicalPage = GetPagePhysical(entry); + uint2 physicalBase = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + uint2 pageTexel = uint2(virtualTexel - localPage * int(pageSize)); + + return asfloat(_svsmDynamicPagePool[physicalBase + pageTexel]); +} + +// Software percentage-closer filtering over the sparse clipmaps: select the finest covering +// clipmap, walk to coarser ones when a touched page is not resident, 3x3 bilinear-weighted taps +// over a shared 4x4 texel block. Returns 1 (lit) when nothing is resident, the marking pass +// guarantees residency for everything the main view actually sees. Static and dynamic resolve +// their rings independently (see the dynamic block below) and combine via min +float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings shadowSettings) +{ + uint numClipmaps = _svsmData[0].configNumClipmaps; + float4 lightDirection = _svsmData[0].prevLightDirection; + if (numClipmaps == 0 || lightDirection.w < 0.5f) + { + return 1.0f; + } + + uint pageTableSize = _svsmData[0].configPageTableSize; + uint pageSize = _svsmData[0].configPageSize; + uint poolPagesPerRow = _svsmData[0].configPoolPagesPerRow; + + float3x3 lightRotation = BuildLightRotation(lightDirection.xyz); + float3 relativePos = worldPos - _cameras[0].eyePosition.xyz; // Camera-relative, world-magnitude wrap math loses texels + float2 lightSpaceRelCam = mul(relativePos, lightRotation).xy; + + // Same margin the marking pass used, so every page the filter can touch was marked. Floor ring + // bias -1: the walk must pass through the ring the marker marked despite float divergence + int selectedClipmap = SelectClipmap(lightSpaceRelCam, length(relativePos), _svsmData, numClipmaps, pageTableSize, pageSize, _svsmData[0].configMarkBorderTexels + 2.0f, _svsmData[0].configResolutionScale, -1); + if (selectedClipmap < 0) + { + return 1.0f; // Outside the coarsest window, beyond all shadow data + } + + float zRangeMin = _svsmData[0].zRangeMin; + float zRangeMax = _svsmData[0].zRangeMax; + float zRangeLength = max(zRangeMax - zRangeMin, 1.0f); + + // Ring-independent scalars hoisted out of the walks, structured-buffer loads don't reliably CSE + float zHalf = zRangeLength * 0.5f; + float camMinusZAnchor = _svsmData[0].configCamMinusZAnchor; + float compareBias = shadowSettings.svsmConstantBias / zRangeLength; + + float staticFactor = 1.0f; + for (uint k = uint(selectedClipmap); k < numClipmaps; k++) + { + float extent = _svsmData[0].extent[k]; + float texelWorld = extent / float(pageTableSize * pageSize); + + // Receiver pushed along its surface normal in clipmap texels, same acne control as the CSM path + float3 lightSpacePos = mul(relativePos + worldNormal * (texelWorld * shadowSettings.normalOffset), lightRotation); + float2 windowPos = lightSpacePos.xy + float2(_svsmData[0].camMinusWindowMinX[k], _svsmData[0].camMinusWindowMinY[k]); + + // Reversed Z like the cascades: 1 = sun side. lightSpacePos is camera-relative, the raster + // maps absolute light-space Z, camMinusZAnchor reconciles the two with small quantities. + // Positive bias moves the receiver toward the sun + float compareDepth = (zHalf - (lightSpacePos.z + camMinusZAnchor)) / zRangeLength + compareBias; + + // 4x4 texel block shared by all 9 bilinear taps + float2 texelPos = windowPos / texelWorld; + float2 blockBaseF = floor(texelPos - 0.5f); + int2 blockBase = int2(blockBaseF) - 1; + float2 fracPart = texelPos - 0.5f - blockBaseF; + + // The block spans at most 2x2 pages: resolve their table entries once, the taps then only + // pay pool loads. The containment margin keeps the whole block inside the window + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[k], _svsmData[0].anchorPageMinY[k]); + int2 basePage = blockBase / int(pageSize); + int2 baseRel = blockBase - basePage * int(pageSize); + bool2 crossesPage = bool2(baseRel.x + 3 >= int(pageSize), baseRel.y + 3 >= int(pageSize)); + + bool pageResident[2][2]; + uint2 pagePhysBase[2][2]; + + [unroll] + for (int py = 0; py < 2; py++) + { + [unroll] + for (int px = 0; px < 2; px++) + { + int2 slot = WrapPageSlot(anchorPageMin + basePage + int2(px, py), pageTableSize); + uint entry = _svsmPageTable[PageEntryIndex(k, slot, pageTableSize)]; + + // INVALID content may live in a dead depth mapping, never sample it + pageResident[py][px] = (entry & (SVSM_PAGE_RESIDENT | SVSM_PAGE_INVALID)) == SVSM_PAGE_RESIDENT; + uint physicalPage = GetPagePhysical(entry); + pagePhysBase[py][px] = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + } + } + + bool allResident = pageResident[0][0] + && (!crossesPage.x || pageResident[0][1]) + && (!crossesPage.y || pageResident[1][0]) + && (!(crossesPage.x && crossesPage.y) || pageResident[1][1]); + if (!allResident) + { + continue; // Fall back to a coarser clipmap, worst case ends lit + } + + float lit[4][4]; + + [unroll] + for (int y = 0; y < 4; y++) + { + [unroll] + for (int x = 0; x < 4; x++) + { + int2 rel = baseRel + int2(x, y); + int2 pageSel = int2(rel.x >= int(pageSize) ? 1 : 0, rel.y >= int(pageSize) ? 1 : 0); + uint2 pageTexel = uint2(rel - pageSel * int(pageSize)); + float storedDepth = asfloat(_svsmPagePool[pagePhysBase[pageSel.y][pageSel.x] + pageTexel]); + lit[y][x] = compareDepth > storedDepth ? 1.0f : 0.0f; + } + } + + float shadowFactor = 0.0f; + + [unroll] + for (int ty = 0; ty < 3; ty++) + { + [unroll] + for (int tx = 0; tx < 3; tx++) + { + float litX0 = lerp(lit[ty + 0][tx + 0], lit[ty + 0][tx + 1], fracPart.x); + float litX1 = lerp(lit[ty + 1][tx + 0], lit[ty + 1][tx + 1], fracPart.x); + shadowFactor += lerp(litX0, litX1, fracPart.y); + } + } + shadowFactor /= 9.0f; + + // Fade to unshadowed over the outer 15% of the coarsest window, beyond it there is no shadow data + if (k == numClipmaps - 1) + { + float halfExtent = extent * 0.5f; + float centerDistance = max(abs(windowPos.x - halfExtent), abs(windowPos.y - halfExtent)); + float fadeStart = 0.85f * halfExtent; + float fade = saturate((centerDistance - fadeStart) / (0.15f * halfExtent)); + shadowFactor = lerp(shadowFactor, 1.0f, fade); + } + + staticFactor = shadowFactor; + break; + } + + // Dynamic casters resolve independently of the static ring: static residency is a long-lived + // cache, dynamic pages exist only at rings marked THIS frame (and marking has per-frame noise + // from animated-geometry pixels). Coupling the two flickers dynamic shadows whenever the + // static resolve lands on a ring whose dynamic page came and went — so walk the dynamic + // table on its own and combine. Depths share the global Z window, min() keeps the darker result + float dynamicFactor = 1.0f; + if (_svsmData[0].statsDynamicTotal != 0) + { + for (uint kd = uint(selectedClipmap); kd < numClipmaps; kd++) + { + if (_svsmData[0].statsDynamicLive[kd] == 0) + { + continue; // No dynamic pages anywhere in this ring this frame + } + + float extent = _svsmData[0].extent[kd]; + float texelWorld = extent / float(pageTableSize * pageSize); + + float3 lightSpacePos = mul(relativePos + worldNormal * (texelWorld * shadowSettings.normalOffset), lightRotation); + float2 windowPos = lightSpacePos.xy + float2(_svsmData[0].camMinusWindowMinX[kd], _svsmData[0].camMinusWindowMinY[kd]); + float2 texelPos = windowPos / texelWorld; + + // Center-texel residency probe: DynamicMark pads the caster footprint by a full page, + // far wider than the filter, so a non-resident center means no dynamic caster here + bool centerResident; + SVSMLoadDynamicDepth(kd, int2(floor(texelPos)), pageTableSize, pageSize, centerResident); + if (!centerResident) + { + continue; + } + + float compareDepth = (zHalf - (lightSpacePos.z + camMinusZAnchor)) / zRangeLength + compareBias; + + float2 blockBaseF = floor(texelPos - 0.5f); + int2 blockBase = int2(blockBaseF) - 1; + float2 fracPart = texelPos - 0.5f - blockBaseF; + + // Same 2x2 page resolve as the static filter. Non-resident pages read depth 0 = lit, + // correct: no dynamic caster covers that texel + uint dynamicPoolPagesPerRow = _svsmData[0].configDynamicPoolPagesPerRow; + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[kd], _svsmData[0].anchorPageMinY[kd]); + int2 basePage = blockBase / int(pageSize); + int2 baseRel = blockBase - basePage * int(pageSize); + + bool pageResident[2][2]; + uint2 pagePhysBase[2][2]; + + [unroll] + for (int py = 0; py < 2; py++) + { + [unroll] + for (int px = 0; px < 2; px++) + { + int2 slot = WrapPageSlot(anchorPageMin + basePage + int2(px, py), pageTableSize); + uint entry = _svsmDynamicPageTable[PageEntryIndex(kd, slot, pageTableSize)]; + pageResident[py][px] = (entry & SVSM_PAGE_RESIDENT) != 0; + uint physicalPage = GetPagePhysical(entry); + pagePhysBase[py][px] = uint2(physicalPage % dynamicPoolPagesPerRow, physicalPage / dynamicPoolPagesPerRow) * pageSize; + } + } + + float lit[4][4]; + + [unroll] + for (int y = 0; y < 4; y++) + { + [unroll] + for (int x = 0; x < 4; x++) + { + int2 rel = baseRel + int2(x, y); + int2 pageSel = int2(rel.x >= int(pageSize) ? 1 : 0, rel.y >= int(pageSize) ? 1 : 0); + uint2 pageTexel = uint2(rel - pageSel * int(pageSize)); + float storedDepth = pageResident[pageSel.y][pageSel.x] ? asfloat(_svsmDynamicPagePool[pagePhysBase[pageSel.y][pageSel.x] + pageTexel]) : 0.0f; + lit[y][x] = compareDepth > storedDepth ? 1.0f : 0.0f; + } + } + + float shadowFactor = 0.0f; + + [unroll] + for (int ty = 0; ty < 3; ty++) + { + [unroll] + for (int tx = 0; tx < 3; tx++) + { + float litX0 = lerp(lit[ty + 0][tx + 0], lit[ty + 0][tx + 1], fracPart.x); + float litX1 = lerp(lit[ty + 1][tx + 0], lit[ty + 1][tx + 1], fracPart.x); + shadowFactor += lerp(litX0, litX1, fracPart.y); + } + } + dynamicFactor = shadowFactor / 9.0f; + break; + } + } + + return min(staticFactor, dynamicFactor); +} + +// Diagnostic view of the SVSM compare: single center-texel sample, classification colors. +// Dark purple = no SVSM state, blue = outside coverage, magenta = page never resident, +// red = shadowed (brightness = margin up to 8m, green channel rises toward 512m -> yellow), +// green = lit (brightness = margin up to 8m, blue channel rises toward 512m -> cyan) +float3 GetSVSMDebugColor(float3 worldPos, float3 worldNormal, float normalOffset, float constantBias) +{ + uint numClipmaps = _svsmData[0].configNumClipmaps; + float4 lightDirection = _svsmData[0].prevLightDirection; + if (numClipmaps == 0 || lightDirection.w < 0.5f) + { + return float3(0.1f, 0.0f, 0.3f); + } + + uint pageTableSize = _svsmData[0].configPageTableSize; + uint pageSize = _svsmData[0].configPageSize; + uint poolPagesPerRow = _svsmData[0].configPoolPagesPerRow; + + float3x3 lightRotation = BuildLightRotation(lightDirection.xyz); + float3 relativePos = worldPos - _cameras[0].eyePosition.xyz; + float2 lightSpaceRelCam = mul(relativePos, lightRotation).xy; + + int selectedClipmap = SelectClipmap(lightSpaceRelCam, length(relativePos), _svsmData, numClipmaps, pageTableSize, pageSize, _svsmData[0].configMarkBorderTexels + 2.0f, _svsmData[0].configResolutionScale, -1); + if (selectedClipmap < 0) + { + return float3(0.0f, 0.3f, 1.0f); + } + + float zRangeMin = _svsmData[0].zRangeMin; + float zRangeMax = _svsmData[0].zRangeMax; + float zRangeLength = max(zRangeMax - zRangeMin, 1.0f); + + // Same independent static/dynamic resolves as GetSVSMShadowFactor, single center tap each. + // The classification compares whichever depth is more occluding against its own receiver + float deltaMeters = 0.0f; + bool anyResident = false; + + for (uint k = uint(selectedClipmap); k < numClipmaps; k++) + { + float extent = _svsmData[0].extent[k]; + float texelWorld = extent / float(pageTableSize * pageSize); + + float3 lightSpacePos = mul(relativePos + worldNormal * (texelWorld * normalOffset), lightRotation); + float2 windowPos = lightSpacePos.xy + float2(_svsmData[0].camMinusWindowMinX[k], _svsmData[0].camMinusWindowMinY[k]); + + float zHalf = zRangeLength * 0.5f; + float receiverDepth = (zHalf - (lightSpacePos.z + _svsmData[0].configCamMinusZAnchor)) / zRangeLength; + float compareDepth = receiverDepth + constantBias / zRangeLength; + + bool resident; + float storedDepth = SVSMLoadDepth(k, int2(windowPos / texelWorld), pageTableSize, pageSize, poolPagesPerRow, resident); + if (!resident) + { + continue; + } + + deltaMeters = (storedDepth - compareDepth) * zRangeLength; + anyResident = true; + break; + } + + if (_svsmData[0].statsDynamicTotal != 0) + { + for (uint kd = uint(selectedClipmap); kd < numClipmaps; kd++) + { + if (_svsmData[0].statsDynamicLive[kd] == 0) + { + continue; + } + + float extent = _svsmData[0].extent[kd]; + float texelWorld = extent / float(pageTableSize * pageSize); + + float3 lightSpacePos = mul(relativePos + worldNormal * (texelWorld * normalOffset), lightRotation); + float2 windowPos = lightSpacePos.xy + float2(_svsmData[0].camMinusWindowMinX[kd], _svsmData[0].camMinusWindowMinY[kd]); + + bool resident; + float dynamicDepth = SVSMLoadDynamicDepth(kd, int2(windowPos / texelWorld), pageTableSize, pageSize, resident); + if (!resident) + { + continue; + } + + float zHalf = zRangeLength * 0.5f; + float receiverDepth = (zHalf - (lightSpacePos.z + _svsmData[0].configCamMinusZAnchor)) / zRangeLength; + float compareDepth = receiverDepth + constantBias / zRangeLength; + + float dynamicDelta = (dynamicDepth - compareDepth) * zRangeLength; + deltaMeters = anyResident ? max(deltaMeters, dynamicDelta) : dynamicDelta; + anyResident = true; + break; + } + } + + if (!anyResident) + { + return float3(1.0f, 0.0f, 1.0f); + } + + if (deltaMeters > 0.0f) + { + return float3(saturate(deltaMeters / 8.0f), saturate(deltaMeters / 512.0f), 0.0f); // Shadowed + } + return float3(0.0f, saturate(-deltaMeters / 8.0f), saturate(-deltaMeters / 512.0f)); // Lit +} + #endif // SHADOWS_INCLUDED \ No newline at end of file diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 0fd62e9..86e1f5f 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -1,4 +1,4 @@ -permutation DEBUG_ID = [0, 1, 2, 3, 4]; +permutation DEBUG_ID = [0, 1, 2, 3, 4, 5]; permutation SHADOW_FILTER_MODE = [0, 1, 2]; // Off, PCF, PCSS permutation EDITOR_MODE = [0, 1]; // Off, Terrain @@ -29,6 +29,7 @@ struct Constants float4 vertexColor; float4 brushColor; float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias + float4 svsmSettings; // x = Constant Bias (world meters) }; [[vk::push_constant]] Constants _constants; @@ -143,6 +144,7 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; shadowSettings.strength = _constants.shadowFilterSettings.z; shadowSettings.normalOffset = _constants.shadowFilterSettings.w; + shadowSettings.svsmConstantBias = _constants.svsmSettings.x; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); @@ -290,6 +292,7 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; shadowSettings.strength = _constants.shadowFilterSettings.z; shadowSettings.normalOffset = _constants.shadowFilterSettings.w; + shadowSettings.svsmConstantBias = _constants.svsmSettings.x; // Apply lighting color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); @@ -344,6 +347,11 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) uint cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); _resolvedColor[pixelPos] = float4(CascadeIDToColor(cascadeIndex), 1); return; +#elif DEBUG_ID == 5 // SVSM compare margin + PixelVertexData pixelVertexData = GetPixelVertexData(pixelPos, vBuffer, 0, _constants.renderInfo.xy); + float3 debugColor = GetSVSMDebugColor(pixelVertexData.worldPos, pixelVertexData.worldNormal, _constants.shadowFilterSettings.w, _constants.svsmSettings.x); + _resolvedColor[pixelPos] = float4(debugColor, 1); + return; #endif float2 pixelUV = pixelPos / _constants.renderInfo.xy; diff --git a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang new file mode 100644 index 0000000..6120d2d --- /dev/null +++ b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang @@ -0,0 +1,75 @@ +permutation SVSM_DYNAMIC = [0, 1]; // 1 = dynamic caster split pass, renders into the dynamic pool + +#define GEOMETRY_PASS 1 + +#include "DescriptorSet/Global.inc.slang" +#include "DescriptorSet/Model.inc.slang" + +#include "Include/Common.inc.slang" +#include "Model/ModelShared.inc.slang" +#include "Shadows/SVSM.inc.slang" + +// SVSM page render for models: keeps the alpha-key discard from the shadow permutation of +// Model/Draw.ps, then writes reversed-Z depth into the physical page pool with image atomics. +// No attachments, viewport spans the full virtual texture so SV_Position.xy is the window-local +// virtual texel. Paired with the SHADOW_PASS=1 vertex permutation. +// +// SVSM_DYNAMIC renders the dynamic-instance fill into the dynamic pool: same bindings, the pass +// binds the dynamic table/pool instead, and dynamic pages render whenever RESIDENT (every frame) + +struct Constants +{ + uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 +}; + +[[vk::push_constant]] Constants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _pageTable; +[[vk::binding(2, PER_PASS)]] RWTexture2D _pagePool; + +struct PSInput +{ + float4 position : SV_Position; + uint4 drawIDInstanceIDTextureDataIDInstanceRefID : TEXCOORD0; + float4 uv01 : TEXCOORD1; +}; + +[shader("fragment")] +void main(PSInput input) +{ + uint textureDataID = input.drawIDInstanceIDTextureDataIDInstanceRefID.z; + + // Alpha-key discard, mirrors the SHADOW_PASS branch of Model/Draw.ps + TextureData textureData = LoadModelTextureData(_packedModelTextureDatas, textureDataID); + + for (uint textureUnitIndex = textureData.textureUnitOffset; textureUnitIndex < textureData.textureUnitOffset + textureData.numTextureUnits; textureUnitIndex++) + { + ModelTextureUnit textureUnit = _modelTextureUnits[textureUnitIndex]; + + uint blendingMode = (textureUnit.data1 >> 11) & 0x7; + + if (blendingMode != 1) // ALPHA KEY + continue; + + uint texture0SamplerIndex = (textureUnit.data1 >> 1) & 0x3; + uint materialType = (textureUnit.data1 >> 16) & 0xFFFF; + + if (materialType == 0x8000) + continue; + + float4 texture1 = _modelTextures[NonUniformResourceIndex(textureUnit.textureIDs[0])].Sample(_samplers[texture0SamplerIndex], input.uv01.xy); + + float minAlpha = texture1.a; + if (minAlpha < (128.0 / 255.0)) + { + discard; + } + } + +#if SVSM_DYNAMIC + SVSMWritePageDepth(input.position.xyz, _constants.viewIndex - 1, SVSM_PAGE_RESIDENT, _svsmData[0].configDynamicPoolPagesPerRow, _svsmData, _pageTable, _pagePool); +#else + SVSMWritePageDepth(input.position.xyz, _constants.viewIndex - 1, SVSM_PAGE_RESIDENT | SVSM_PAGE_DIRTY, _svsmData[0].configPoolPagesPerRow, _svsmData, _pageTable, _pagePool); +#endif +} diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang new file mode 100644 index 0000000..0a7e5dd --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -0,0 +1,228 @@ +#ifndef SVSM_INCLUDED +#define SVSM_INCLUDED + +#include "Shadows/SDSM.inc.slang" + +// Shared types and helpers for the Sparse Virtual Shadow Map chain: +// SVSMPrepare.cs -> SVSMInvalidateAABBs.cs -> SVSMPageUpdateA.cs -> SVSMPageMark.cs -> SVSMPageUpdateB.cs +// +// Clipmap k is a camera-centered light-space window of clipmap0Extent * 2^k meters, backed by a +// pageTableSize^2 page table over a virtual pageTableSize * pageSize texture. Page content is +// world-stable: a physical page holds a fixed absolute light-space page until its table slot is +// reused (toroidal wrap) or its content is invalidated. +// +// Precision rule: no wrap math at world magnitude. World coordinates reach ~17k and the finest +// texels are millimeters, so all addressing is anchor-relative: Prepare stores per clipmap the +// page-snapped window origin as an integer absolute page index (anchorPageMin) plus the camera +// offset inside the window (camMinusWindowMin), and every pass works with +// windowPos = lightSpace(P - camera) + camMinusWindowMin, which is at most one window extent. + +// Push constants shared by all SVSM dispatches, identical bytes pushed to each pipeline +struct SVSMConstants +{ + float4 lightDirection; // xyz = direction the light travels, quantized shadow sun + uint numClipmaps; + uint pageTableSize; // pages per virtual texture row + uint pageSize; // texels per page row + uint poolPagesPerRow; // physical pool pages per row, total pages = poolPagesPerRow^2 + float clipmap0Extent; // world extent of the finest clipmap window + float markBorderTexels; // filter footprint margin, marks neighbor pages near page borders + uint evictAge; // frames without a mark before a resident page returns to the pool + uint invalidateAll; // CPU-requested invalidation cause bits (SVSM_CAUSE_*) + uint numDirtyAABBs; + uint allocClipmap; // UpdateB runs once per clipmap, finest first, so pool starvation always lands on the coarsest ring + float casterMargin; // Sun-side sweep on the near cull plane, depth clamp pancakes those casters + float zHalfRange; // Half depth range of every clipmap window around the quantized Z anchor + float padding0; + float padding1; + float resolutionScale; // Eye-distance clipmap floor scale, <= 0 disables + uint dynamicPoolPagesPerRow; // Caster-split dynamic pool pages per row, 0 while the split is off + uint renderBudget; // Static pages rendered per frame, 0 = unlimited; the coarsest two rings are exempt + uint dynamicPhase; // DynamicUpdate: 0 = release pages, 1 = acquire + bookkeeping. Split so free-list pushes and pops never share a dispatch + uint padding3; + uint padding4; +}; + +// Page table entry bits. A zeroed entry is a free slot +#define SVSM_PAGE_PHYS_MASK 0xFFFu // Physical page index, flat, pool is capped to 4096 pages +#define SVSM_PAGE_AGE_SHIFT 12 +#define SVSM_PAGE_AGE_MASK (0xFFu << SVSM_PAGE_AGE_SHIFT) +#define SVSM_PAGE_INVALID (1u << 24) // Content is stale, re-render before the next sample +#define SVSM_PAGE_RESIDENT (1u << 25) // Backed by a physical page +#define SVSM_PAGE_MARKED (1u << 26) // Touched by a visible sample this frame +#define SVSM_PAGE_DIRTY (1u << 27) // Renders this frame, always consumed the frame it is set + +// Invalidation cause bits, CPU bits come in through SVSMConstants::invalidateAll +#define SVSM_CAUSE_SUN_STEP 1u +#define SVSM_CAUSE_Z_RANGE 2u +#define SVSM_CAUSE_MANUAL 4u +#define SVSM_CAUSE_AABB_OVERFLOW 8u + +// Persistent GPU state + per-frame stats. Scalar arrays only so the std430 layout matches the +// flat u32 readback on the CPU, same pattern as SDSMData +struct SVSMData +{ + float4 prevLightDirection; // w > 0.5 once valid + + float zRangeMin; // Reserved for the stage 2 Z fit + float zRangeMax; + uint frameInvalidationFlags; // Transient: written by Prepare (or AABB overflow), consumed by UpdateA the same frame + uint padding0; + + int anchorPageMinX[8]; // Absolute page index of the window min corner + int anchorPageMinY[8]; + int prevAnchorPageMinX[8]; + int prevAnchorPageMinY[8]; + float camMinusWindowMinX[8]; // Camera light-space XY minus the window min corner, small + float camMinusWindowMinY[8]; + float extent[8]; + float pageWorld[8]; // extent / pageTableSize + uint clipmapInvalidate[8]; // Transient: clipmap config changed, UpdateA invalidates the whole clipmap + + int dirtyRectMinX[8]; // Window-local page coords of this frame's dirty pages + int dirtyRectMinY[8]; + int dirtyRectMaxX[8]; + int dirtyRectMaxY[8]; + + uint statsMarked[8]; + uint statsResident[8]; + uint statsDirty[8]; + uint statsEvicted[8]; + uint statsInvalidated[8]; + uint statsOverflow; + uint statsInvalidationCause; + uint statsFreeListCount; // Snapshot at the start of the frame + + // Config mirror written by Prepare so draw-time shaders only need this buffer plus viewIndex + uint configPageTableSize; + uint configPageSize; + uint configPoolPagesPerRow; + uint configNumClipmaps; + float configMarkBorderTexels; // The sampler mirrors the marking margin through this + float configPadding0; + float configCamMinusZAnchor; // Camera light-space Z minus the Z anchor: the sampler works + // camera-relative for precision, the raster works absolute, this + // small offset reconciles the two depth mappings + float configResolutionScale; // Eye-distance clipmap floor scale for the sampler, <= 0 disables + uint padding3; + + // Static/dynamic caster split: dynamic pages live in their own small pool and re-render every + // frame, so cached static content is never touched by animated/moving casters + int dynamicRectMinX[8]; // Window-local page coords of this frame's live dynamic pages + int dynamicRectMinY[8]; + int dynamicRectMaxX[8]; + int dynamicRectMaxY[8]; + + uint statsDynamicLive[8]; + uint statsDynamicOverflow; + uint statsDynamicTotal; // Live dynamic pages across all clipmaps, gates the sampler's dynamic lookup + uint configDynamicPoolPagesPerRow; + uint padding4; + + // Render budget: pages past the per-frame cap stay MARKED+INVALID and retry next frame + uint statsDeferred[8]; + uint statsBudgetUsed; + uint padding5; + uint padding6; + uint padding7; +}; + +uint GetPagePhysical(uint entry) +{ + return entry & SVSM_PAGE_PHYS_MASK; +} + +uint GetPageAge(uint entry) +{ + return (entry & SVSM_PAGE_AGE_MASK) >> SVSM_PAGE_AGE_SHIFT; +} + +uint PackPageEntry(uint physicalPage, uint age, uint flags) +{ + return (physicalPage & SVSM_PAGE_PHYS_MASK) | (age << SVSM_PAGE_AGE_SHIFT) | flags; +} + +// Table slot of an absolute page index, pageTableSize is a power of two +int2 WrapPageSlot(int2 absolutePage, uint pageTableSize) +{ + return absolutePage & int2(pageTableSize - 1, pageTableSize - 1); +} + +uint PageEntryIndex(uint clipmapIndex, int2 slot, uint pageTableSize) +{ + return clipmapIndex * pageTableSize * pageTableSize + uint(slot.y) * pageTableSize + uint(slot.x); +} + +// Finest clipmap whose window contains the position with margin to spare, -1 if none. +// One shared function for marking and sampling: the margin guarantees the filter footprint and +// the border page marks never leave the selected window. lightSpaceRelCam = mul(P - cam, R).xy +// +// Eye-distance floor: clipmap containment is light-space-centric but resolution needs are +// eye-centric (at grazing sun, distant relief sits close in light-space XY and would burn fine +// texels on content whose screen pixels are meters wide). Rings whose texels are finer than the +// sample's approximate screen footprint are skipped. floorRingBias: the sampler passes -1 so its +// walk always passes through the ring the marker marked, regardless of float divergence between +// their eye distance reconstructions. resolutionScale <= 0 disables the floor +int SelectClipmap(float2 lightSpaceRelCam, float eyeDistance, StructuredBuffer svsmData, uint numClipmaps, uint pageTableSize, uint pageSize, float marginTexels, float resolutionScale, int floorRingBias) +{ + uint startClipmap = 0; + if (resolutionScale > 0.0f) + { + // ~1000 pixels across the FOV as the fixed reference, exactness is irrelevant + float minTexelWorld = eyeDistance * resolutionScale * (1.0f / 1000.0f); + for (uint k = 0; k + 1 < numClipmaps; k++) + { + float texelWorld = svsmData[0].extent[k] / float(pageTableSize * pageSize); + if (texelWorld >= minTexelWorld) + { + break; + } + startClipmap++; + } + startClipmap = uint(max(int(startClipmap) + floorRingBias, 0)); + } + + for (uint k = startClipmap; k < numClipmaps; k++) + { + float extent = svsmData[0].extent[k]; + float texelWorld = extent / float(pageTableSize * pageSize); + float margin = marginTexels * texelWorld; + + float2 windowPos = lightSpaceRelCam + float2(svsmData[0].camMinusWindowMinX[k], svsmData[0].camMinusWindowMinY[k]); + if (all(windowPos > float2(margin, margin)) && all(windowPos < float2(extent - margin, extent - margin))) + { + return int(k); + } + } + return -1; +} + +// Software depth write for the SVSM page render passes. svPosition comes straight from +// SV_Position: xy are window-local virtual texel coordinates (the viewport spans the full virtual +// texture), z is the reversed ortho depth. Fragments outside pages that render this frame exit +// after one table load; depth clamp disables clipping so pancaked casters saturate here instead. +// requiredFlags: static pages render only while DIRTY, dynamic pages render whenever RESIDENT +void SVSMWritePageDepth(float3 svPosition, uint clipmapIndex, uint requiredFlags, uint poolPagesPerRow, StructuredBuffer svsmData, StructuredBuffer pageTable, RWTexture2D pagePool) +{ + uint pageTableSize = svsmData[0].configPageTableSize; + uint pageSize = svsmData[0].configPageSize; + + int2 texel = int2(svPosition.xy); + int2 localPage = texel / int(pageSize); + int2 anchorPageMin = int2(svsmData[0].anchorPageMinX[clipmapIndex], svsmData[0].anchorPageMinY[clipmapIndex]); + int2 slot = WrapPageSlot(anchorPageMin + localPage, pageTableSize); + + uint entry = pageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + if ((entry & requiredFlags) != requiredFlags) + { + return; + } + + uint physicalPage = GetPagePhysical(entry); + uint2 physicalBase = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + uint2 pageTexel = uint2(texel - localPage * int(pageSize)); + + InterlockedMax(pagePool[physicalBase + pageTexel], asuint(saturate(svPosition.z))); +} + +#endif // SVSM_INCLUDED diff --git a/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang new file mode 100644 index 0000000..cc16cee --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang @@ -0,0 +1,78 @@ +#include "Shadows/SVSM.inc.slang" + +// Marks dynamic pages: for each dynamic caster AABB and clipmap, pages that the caster overlaps +// AND that are visible this frame (marked in the static table) get MARKED in the dynamic table. +// Runs after SVSMPageMark so the static visibility marks are current. One thread per +// (AABB, clipmap) pair, same light-space rect math as SVSMInvalidateAABBs — for an ortho light a +// caster's light-space XY equals its shadow's XY, so this covers where the shadow lands. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _pageTable; // Static table, read for visibility +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _dynamicPageTable; +[[vk::binding(3, PER_PASS)]] StructuredBuffer _dynamicAABBs; // Pairs of world (min, max) + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; +}; + +[shader("compute")] +[numthreads(64, 1, 1)] +void main(CSInput input) +{ + uint aabbIndex = input.dispatchThreadID.x; + uint clipmapIndex = input.dispatchThreadID.y; + if (aabbIndex >= _constants.numDirtyAABBs || clipmapIndex >= _constants.numClipmaps) + { + return; + } + + float3 aabbMin = _dynamicAABBs[aabbIndex * 2 + 0].xyz; + float3 aabbMax = _dynamicAABBs[aabbIndex * 2 + 1].xyz; + float3 center = (aabbMin + aabbMax) * 0.5f; + float3 halfExtents = (aabbMax - aabbMin) * 0.5f; + + float3x3 lightRotation = BuildLightRotation(_constants.lightDirection.xyz); + float2 centerLS = mul(center, lightRotation).xy; + float2 halfLS = mul(halfExtents, float3x3(abs(lightRotation[0]), abs(lightRotation[1]), abs(lightRotation[2]))).xy; + + uint pageTableSize = _constants.pageTableSize; + float pageWorld = _svsmData[0].pageWorld[clipmapIndex]; + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + float2 windowMin = float2(anchorPageMin) * pageWorld; + + // One page of pad so filter taps just beyond the caster footprint see dynamic depth too + int2 pageMin = int2(floor((centerLS - halfLS - windowMin) / pageWorld)) - 1; + int2 pageMax = int2(floor((centerLS + halfLS - windowMin) / pageWorld)) + 1; + pageMin = max(pageMin, int2(0, 0)); + pageMax = min(pageMax, int2(pageTableSize - 1, pageTableSize - 1)); + + if (any(pageMax < pageMin)) + { + return; // Outside this clipmap's window + } + + // A huge dynamic mover marks at most this many pages, beyond it the finer clipmaps cover it + int2 span = pageMax - pageMin + int2(1, 1); + if (span.x * span.y > 1024) + { + return; + } + + for (int y = pageMin.y; y <= pageMax.y; y++) + { + for (int x = pageMin.x; x <= pageMax.x; x++) + { + int2 slot = WrapPageSlot(anchorPageMin + int2(x, y), pageTableSize); + uint entryIndex = PageEntryIndex(clipmapIndex, slot, pageTableSize); + + // Only where something visible actually samples this frame + if ((_pageTable[entryIndex] & SVSM_PAGE_MARKED) != 0) + { + InterlockedOr(_dynamicPageTable[entryIndex], SVSM_PAGE_MARKED); + } + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMDynamicUpdate.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMDynamicUpdate.cs.slang new file mode 100644 index 0000000..69fc713 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMDynamicUpdate.cs.slang @@ -0,0 +1,129 @@ +#include "Shadows/SVSM.inc.slang" + +// Dynamic page lifecycle, one thread per entry, runs after SVSMDynamicMark. Dispatched TWICE: +// dynamicPhase 0 releases pages nobody marked, dynamicPhase 1 allocates marked entries from the +// dynamic free list and queues every live page for the per-frame clear+render. The phases must +// not share a dispatch: a pop racing a push can read the list slot before the pushed physical +// index lands, aliasing one physical page under two table entries (ghost shadows). +// Dynamic pages are transient by design — they re-render every frame, so there is no age, no +// DIRTY bookkeeping and no invalidation. Clears MARKED on write so next frame starts clean. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _dynamicPageTable; +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _dynamicFreeList; // [0] = count, entries from [4] +[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _dynamicClearList; // [0..2] = indirect dispatch args, physical pages from [4] + +groupshared uint gLiveCount; +groupshared int gRectMinX; +groupshared int gRectMinY; +groupshared int gRectMaxX; +groupshared int gRectMaxY; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(256, 1, 1)] +void main(CSInput input) +{ + if (input.groupIndex == 0) + { + gLiveCount = 0; + gRectMinX = 0x7FFFFFFF; + gRectMinY = 0x7FFFFFFF; + gRectMaxX = -1; + gRectMaxY = -1; + } + GroupMemoryBarrierWithGroupSync(); + + uint pageTableSize = _constants.pageTableSize; + uint entriesPerClipmap = pageTableSize * pageTableSize; + uint entryIndex = input.dispatchThreadID.x; + uint clipmapIndex = entryIndex / entriesPerClipmap; // Group-uniform, entriesPerClipmap is a multiple of 256 + + uint entry = _dynamicPageTable[entryIndex]; + // No early returns before the group barrier below. Phase 0 only releases; allocation and + // bookkeeping run in phase 1, after the free-list barrier between the two dispatches + if (entry != 0 && _constants.dynamicPhase == 0) + { + bool marked = (entry & SVSM_PAGE_MARKED) != 0; + bool resident = (entry & SVSM_PAGE_RESIDENT) != 0; + if (!marked && resident) + { + // No dynamic caster overlaps this page anymore, return it + uint writeIndex; + InterlockedAdd(_dynamicFreeList[0], 1, writeIndex); + _dynamicFreeList[4 + writeIndex] = GetPagePhysical(entry); + _dynamicPageTable[entryIndex] = 0; + } + } + else if (entry != 0) + { + bool marked = (entry & SVSM_PAGE_MARKED) != 0; + bool resident = (entry & SVSM_PAGE_RESIDENT) != 0; + + if (marked && !resident) + { + uint previousCount; + InterlockedAdd(_dynamicFreeList[0], uint(-1), previousCount); + if (int(previousCount) <= 0) + { + InterlockedAdd(_dynamicFreeList[0], 1); + InterlockedAdd(_svsmData[0].statsDynamicOverflow, 1); + entry = 0; // Stays non-resident, the sampler simply sees no dynamic caster here + } + else + { + uint physicalPage = _dynamicFreeList[4 + previousCount - 1]; + entry = PackPageEntry(physicalPage, 0, SVSM_PAGE_RESIDENT); + resident = true; + } + } + else if (marked && resident) + { + entry = PackPageEntry(GetPagePhysical(entry), 0, SVSM_PAGE_RESIDENT); // Clear MARKED for next frame + } + + if (resident) + { + // Every live dynamic page clears and re-renders this frame + uint clearIndex; + InterlockedAdd(_dynamicClearList[0], 1, clearIndex); + _dynamicClearList[4 + clearIndex] = GetPagePhysical(entry); + + InterlockedAdd(gLiveCount, 1); + + if (clipmapIndex < _constants.numClipmaps) + { + uint slotLinear = entryIndex - clipmapIndex * entriesPerClipmap; + int2 slot = int2(slotLinear & (pageTableSize - 1), slotLinear / pageTableSize); + int2 anchor = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 localPage = WrapPageSlot(slot - anchor, pageTableSize); + + InterlockedMin(gRectMinX, localPage.x); + InterlockedMin(gRectMinY, localPage.y); + InterlockedMax(gRectMaxX, localPage.x); + InterlockedMax(gRectMaxY, localPage.y); + } + } + + _dynamicPageTable[entryIndex] = entry; + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex == 0 && clipmapIndex < 8 && gLiveCount != 0) + { + InterlockedAdd(_svsmData[0].statsDynamicLive[clipmapIndex], gLiveCount); + InterlockedAdd(_svsmData[0].statsDynamicTotal, gLiveCount); + InterlockedMin(_svsmData[0].dynamicRectMinX[clipmapIndex], gRectMinX); + InterlockedMin(_svsmData[0].dynamicRectMinY[clipmapIndex], gRectMinY); + InterlockedMax(_svsmData[0].dynamicRectMaxX[clipmapIndex], gRectMaxX); + InterlockedMax(_svsmData[0].dynamicRectMaxY[clipmapIndex], gRectMaxY); + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang new file mode 100644 index 0000000..c06ef72 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang @@ -0,0 +1,106 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SVSM.inc.slang" + +// Last SVSM dispatch, single thread, runs after UpdateB so the dirty rects are known: writes the +// clipmap render cameras into the shared cameras buffer that culling and the page render passes +// read. Mirrors the CascadeFitCameras tail, minus the texel snap: the window center is aligned to +// the absolute page grid by construction. +// +// The camera MATRICES always map the full clipmap window (the render viewport spans the whole +// virtual texture). The cull PLANES are tightened to this frame's dirty page rect, XY-tight is +// safe for an ortho light (a caster's light-space XY equals its shadow's XY); the near plane is +// swept toward the sun by the caster margin, depth clamp pancakes those casters at render time. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera +[[vk::binding(1, PER_PASS)]] StructuredBuffer _svsmData; + +[shader("compute")] +[numthreads(1, 1, 1)] +void main() +{ + float3 lightDirection = _constants.lightDirection.xyz; + float3x3 lightRotation = BuildLightRotation(lightDirection); + float3 upDir = StableUp(lightDirection); + + float zRangeMin = _svsmData[0].zRangeMin; + float zRangeMax = _svsmData[0].zRangeMax; + float zCenter = (zRangeMin + zRangeMax) * 0.5f; + float zHalf = max((zRangeMax - zRangeMin) * 0.5f, 1.0f); + + for (uint k = 0; k < _constants.numClipmaps; k++) + { + float extent = _svsmData[0].extent[k]; + float pageWorld = _svsmData[0].pageWorld[k]; + float halfExtent = extent * 0.5f; + + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[k], _svsmData[0].anchorPageMinY[k]); + float2 windowMinLS = float2(anchorPageMin) * pageWorld; + float2 centerLS = windowMinLS + float2(halfExtent, halfExtent); + + // Rotate the light-space center back to world, mul(v, transpose(R)) inverts the rotation + float3 worldCenter = mul(float3(centerLS, zCenter), transpose(lightRotation)); + float3 shadowCameraPos = worldCenter - lightDirection * zHalf; // Sun side + + // Bottom/top swapped: the engine's viewport Y-flip would otherwise mirror the page content + // against the marking/sampling texel math, this way SV_Position.xy IS the virtual texel + float4x4 proj = BuildOrtho(-halfExtent, halfExtent, halfExtent, -halfExtent, 2.0f * zHalf, 0.0f); // Reversed Z, sun side = 1 + float4x4 view = BuildLookAt(shadowCameraPos, worldCenter, upDir); + + Camera clipmapCamera = (Camera)0; + clipmapCamera.worldToView = view; + clipmapCamera.viewToClip = proj; + clipmapCamera.viewToWorld = InverseLookAt(view, shadowCameraPos); + clipmapCamera.clipToView = InverseOrtho(proj); + clipmapCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView + clipmapCamera.clipToWorld = mul(clipmapCamera.clipToView, clipmapCamera.viewToWorld); // glm viewToWorld * clipToView + + clipmapCamera.eyePosition = float4(shadowCameraPos, halfExtent); // w = window coverage radius, the far fade reads it + clipmapCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); + clipmapCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); + + // Union of the static dirty rect and the dynamic live rect: one cull volume per view serves + // both fill classes, the class filter and the PS page checks absorb the over-inclusion + int2 dirtyMin = min(int2(_svsmData[0].dirtyRectMinX[k], _svsmData[0].dirtyRectMinY[k]), + int2(_svsmData[0].dynamicRectMinX[k], _svsmData[0].dynamicRectMinY[k])); + int2 dirtyMax = max(int2(_svsmData[0].dirtyRectMaxX[k], _svsmData[0].dirtyRectMaxY[k]), + int2(_svsmData[0].dynamicRectMaxX[k], _svsmData[0].dynamicRectMaxY[k])); + if (any(dirtyMax < dirtyMin)) + { + // Nothing renders into this clipmap this frame, cull everything + for (uint p = 0; p < 6; p++) + { + clipmapCamera.frustum[p] = float4(0.0f, 1.0f, 0.0f, 3.0e38f); // dot(n, p) - w is always far below -radius + } + } + else + { + // Cull volume over the dirty pages only, window-local rect back to center-relative extents + float cullMinX = float(dirtyMin.x) * pageWorld - halfExtent; + float cullMaxX = float(dirtyMax.x + 1) * pageWorld - halfExtent; + float cullMinY = float(dirtyMin.y) * pageWorld - halfExtent; + float cullMaxY = float(dirtyMax.y + 1) * pageWorld - halfExtent; + + float4x4 cullProj = BuildOrtho(cullMinX, cullMaxX, cullMaxY, cullMinY, 2.0f * zHalf, 0.0f); // Same Y mirror as proj, plane extraction is sign-agnostic + float4x4 cullMatrix = mul(view, cullProj); + + float4 row0 = MatrixRow(cullMatrix, 0); + float4 row1 = MatrixRow(cullMatrix, 1); + float4 row2 = MatrixRow(cullMatrix, 2); + float4 row3 = MatrixRow(cullMatrix, 3); + + clipmapCamera.frustum[0] = ExtractPlane(row3 + row0); // Left + clipmapCamera.frustum[1] = ExtractPlane(row3 - row0); // Right + clipmapCamera.frustum[2] = ExtractPlane(row3 + row1); // Bottom + clipmapCamera.frustum[3] = ExtractPlane(row3 - row1); // Top + clipmapCamera.frustum[4] = ExtractPlane(row3 - row2); // Near, reversed Z so depth 1 is near + clipmapCamera.frustum[5] = ExtractPlane(row2); // Far + + // Casters beyond the near plane still render correctly (depth clamp pancakes them) + clipmapCamera.frustum[4].w -= _constants.casterMargin; + } + + _rwCameras[1 + k] = clipmapCamera; + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang new file mode 100644 index 0000000..0001b8c --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang @@ -0,0 +1,75 @@ +#include "Shadows/SVSM.inc.slang" + +// Marks pages under moved or animated instances as INVALID. One thread per (AABB, clipmap) pair, +// each ORs the flag over the covered page rect. Page granularity is coarse (quarter meters at the +// finest clipmap) so plain world-magnitude light-space math is precise enough here. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _pageTable; +[[vk::binding(2, PER_PASS)]] StructuredBuffer _dirtyAABBs; // Pairs of world (min, max) + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; +}; + +[shader("compute")] +[numthreads(64, 1, 1)] +void main(CSInput input) +{ + uint aabbIndex = input.dispatchThreadID.x; + uint clipmapIndex = input.dispatchThreadID.y; + if (aabbIndex >= _constants.numDirtyAABBs || clipmapIndex >= _constants.numClipmaps) + { + return; + } + + float3 aabbMin = _dirtyAABBs[aabbIndex * 2 + 0].xyz; + float3 aabbMax = _dirtyAABBs[aabbIndex * 2 + 1].xyz; + float3 center = (aabbMin + aabbMax) * 0.5f; + float3 halfExtents = (aabbMax - aabbMin) * 0.5f; + + // Light-space XY rect of the world AABB, |R| trick for the extents + float3x3 lightRotation = BuildLightRotation(_constants.lightDirection.xyz); + float2 centerLS = mul(center, lightRotation).xy; + float2 halfLS = mul(halfExtents, float3x3(abs(lightRotation[0]), abs(lightRotation[1]), abs(lightRotation[2]))).xy; + + uint pageTableSize = _constants.pageTableSize; + float pageWorld = _svsmData[0].pageWorld[clipmapIndex]; + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + + // Same reconstruction Prepare used for camMinusWindowMin, deterministic vs the stored anchors + float2 windowMin = float2(anchorPageMin) * pageWorld; + + // One page of pad so filter taps just outside the caster footprint refresh too + int2 pageMin = int2(floor((centerLS - halfLS - windowMin) / pageWorld)) - 1; + int2 pageMax = int2(floor((centerLS + halfLS - windowMin) / pageWorld)) + 1; + pageMin = max(pageMin, int2(0, 0)); + pageMax = min(pageMax, int2(pageTableSize - 1, pageTableSize - 1)); + + if (any(pageMax < pageMin)) + { + return; // Outside this clipmap's window + } + + // A huge mover would loop for thousands of pages from one thread, fall back to a full + // invalidation instead. UpdateA consumes frameInvalidationFlags after this pass's barrier + int2 span = pageMax - pageMin + int2(1, 1); + if (span.x * span.y > 1024) + { + InterlockedOr(_svsmData[0].frameInvalidationFlags, SVSM_CAUSE_AABB_OVERFLOW); + InterlockedOr(_svsmData[0].statsInvalidationCause, SVSM_CAUSE_AABB_OVERFLOW); + return; + } + + for (int y = pageMin.y; y <= pageMax.y; y++) + { + for (int x = pageMin.x; x <= pageMax.x; x++) + { + int2 slot = WrapPageSlot(anchorPageMin + int2(x, y), pageTableSize); + InterlockedOr(_pageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)], SVSM_PAGE_INVALID); + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageClear.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageClear.cs.slang new file mode 100644 index 0000000..1847269 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPageClear.cs.slang @@ -0,0 +1,34 @@ +#include "Shadows/SVSM.inc.slang" + +// Clears this frame's dirty pages in the physical pool, dispatched indirectly from the clear +// list UpdateB filled: one workgroup per dirty page. Cleared depth 0 (reversed Z far) reads as +// fully lit, so a dirty page with no casters is still correct. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _clearList; // [0..2] = indirect dispatch args, physical pages from [4] +[[vk::binding(1, PER_PASS)]] RWTexture2D _pagePool; + +struct CSInput +{ + uint3 groupID : SV_GroupID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(256, 1, 1)] +void main(CSInput input) +{ + uint physicalPage = _clearList[4 + input.groupID.x]; + + uint pageSize = _constants.pageSize; + uint poolPagesPerRow = _constants.poolPagesPerRow; + uint2 physicalBase = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + + uint texelsPerPage = pageSize * pageSize; + for (uint i = input.groupIndex; i < texelsPerPage; i += 256) + { + uint2 texel = uint2(i % pageSize, i / pageSize); + _pagePool[physicalBase + texel] = 0; + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang new file mode 100644 index 0000000..7c38112 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang @@ -0,0 +1,129 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SVSM.inc.slang" + +// Marks the pages touched by visible depth samples, structurally CascadeXYReduce with a page +// table as the write target. Positions are reconstructed camera-relative (clipToView + rotate-only +// viewToWorld) and addressed through the anchor-relative window math from SVSM.inc. +// +// Samples within markBorderTexels of a page border also mark the neighbor page so the filter +// footprint is resident at sampling time; the selection margin guarantees those neighbors exist +// inside the selected clipmap's window. +// +// A 16x16 group usually lands in a handful of distinct pages, so marks are deduplicated through a +// small LDS cache and flushed as a few global ORs instead of 256. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] Texture2D _depth; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera +[[vk::binding(2, PER_PASS)]] StructuredBuffer _svsmData; +[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _pageTable; + +#define MARK_CACHE_SIZE 16 +#define MARK_CACHE_EMPTY 0xFFFFFFFFu + +groupshared uint gMarkCache[MARK_CACHE_SIZE]; + +void MarkPage(uint entryIndex) +{ + // Dedup through the cache, linear probing. Falls back to a direct OR when full + for (uint i = 0; i < MARK_CACHE_SIZE; i++) + { + uint existing; + InterlockedCompareExchange(gMarkCache[i], MARK_CACHE_EMPTY, entryIndex, existing); + if (existing == MARK_CACHE_EMPTY || existing == entryIndex) + { + return; + } + } + InterlockedOr(_pageTable[entryIndex], SVSM_PAGE_MARKED); +} + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(16, 16, 1)] +void main(CSInput input) +{ + if (input.groupIndex < MARK_CACHE_SIZE) + { + gMarkCache[input.groupIndex] = MARK_CACHE_EMPTY; + } + GroupMemoryBarrierWithGroupSync(); + + uint2 dim; + _depth.GetDimensions(dim.x, dim.y); + + uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); + float depth = _depth[pixel]; + + // Cleared depth is exactly 0.0 (reversed Z far plane), sky never needs shadow pages + if (depth != 0.0f) + { + Camera mainCamera = _srcCameras[0]; + + float2 uv = (float2(pixel) + 0.5f) / float2(dim); + float2 ndc = float2(uv.x, 1.0f - uv.y) * 2.0f - 1.0f; + + float4 viewPos = mul(float4(ndc, depth, 1.0f), mainCamera.clipToView); + viewPos.xyz /= viewPos.w; + + // Camera-relative world position, rotation only + float3 relativePos = mul(float4(viewPos.xyz, 0.0f), mainCamera.viewToWorld).xyz; + float2 lightSpaceRelCam = mul(relativePos, BuildLightRotation(_constants.lightDirection.xyz)).xy; + + uint pageTableSize = _constants.pageTableSize; + uint pageSize = _constants.pageSize; + + // +2 texels of slack over the border marks so snap jitter can never select a clipmap whose + // border pages fall outside the window + float eyeDistance = length(viewPos.xyz); + int clipmapIndex = SelectClipmap(lightSpaceRelCam, eyeDistance, _svsmData, _constants.numClipmaps, pageTableSize, pageSize, _constants.markBorderTexels + 2.0f, _constants.resolutionScale, 0); + if (clipmapIndex >= 0) + { + float extent = _svsmData[0].extent[clipmapIndex]; + float texelWorld = extent / float(pageTableSize * pageSize); + float2 windowPos = lightSpaceRelCam + float2(_svsmData[0].camMinusWindowMinX[clipmapIndex], _svsmData[0].camMinusWindowMinY[clipmapIndex]); + + int2 texel = int2(windowPos / texelWorld); + int2 localPage = texel / int(pageSize); + int2 pageTexel = texel - localPage * int(pageSize); + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + + int border = int(_constants.markBorderTexels); + int2 neighborStep = int2(0, 0); + if (pageTexel.x < border) neighborStep.x = -1; + else if (pageTexel.x >= int(pageSize) - border) neighborStep.x = 1; + if (pageTexel.y < border) neighborStep.y = -1; + else if (pageTexel.y >= int(pageSize) - border) neighborStep.y = 1; + + // The containing page plus the bordering neighbors (corner marks all three) + for (int sy = 0; sy < 2; sy++) + { + for (int sx = 0; sx < 2; sx++) + { + int2 pageOffset = int2(sx * neighborStep.x, sy * neighborStep.y); + if ((sx == 1 && pageOffset.x == 0) || (sy == 1 && pageOffset.y == 0)) + { + continue; // No border on this axis, skip the duplicate + } + + int2 page = clamp(localPage + pageOffset, int2(0, 0), int2(pageTableSize - 1, pageTableSize - 1)); + int2 slot = WrapPageSlot(anchorPageMin + page, pageTableSize); + MarkPage(PageEntryIndex(clipmapIndex, slot, pageTableSize)); + } + } + } + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex < MARK_CACHE_SIZE && gMarkCache[input.groupIndex] != MARK_CACHE_EMPTY) + { + InterlockedOr(_pageTable[gMarkCache[input.groupIndex]], SVSM_PAGE_MARKED); + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageTableDebug.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageTableDebug.cs.slang new file mode 100644 index 0000000..35651cf --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPageTableDebug.cs.slang @@ -0,0 +1,88 @@ +#include "Shadows/SVSM.inc.slang" + +// Draws one clipmap's page table as a colored grid straight into the scene color corner. +// Black = free slot, green = resident (darkens with age), orange = resident but invalidated, +// red = dirty (rendered this frame), yellow border tint = marked this frame, magenta = marked but +// not resident (pool overflow). + +struct DebugConstants +{ + uint clipmapIndex; + uint pageTableSize; + uint cellSize; + uint padding0; + int2 screenOffset; +}; + +[[vk::push_constant]] DebugConstants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _pageTable; +[[vk::binding(1, PER_PASS)]] RWTexture2D _target; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; +}; + +[shader("compute")] +[numthreads(16, 16, 1)] +void main(CSInput input) +{ + uint pageTableSize = _constants.pageTableSize; + uint cellSize = _constants.cellSize; + uint regionSize = pageTableSize * cellSize; + if (any(input.dispatchThreadID.xy >= uint2(regionSize, regionSize))) + { + return; + } + + uint2 targetDim; + _target.GetDimensions(targetDim.x, targetDim.y); + int2 targetPixel = _constants.screenOffset + int2(input.dispatchThreadID.xy); + if (any(targetPixel < int2(0, 0)) || any(targetPixel >= int2(targetDim))) + { + return; + } + + uint2 slot = input.dispatchThreadID.xy / cellSize; + uint2 cellPixel = input.dispatchThreadID.xy - slot * cellSize; + + // The table is indexed by wrapped slots, displaying slot space directly shows the torus + uint entry = _pageTable[PageEntryIndex(_constants.clipmapIndex, int2(slot), pageTableSize)]; + + float3 color = float3(0.05f, 0.05f, 0.05f); + if (entry != 0) + { + bool resident = (entry & SVSM_PAGE_RESIDENT) != 0; + bool invalid = (entry & SVSM_PAGE_INVALID) != 0; + bool marked = (entry & SVSM_PAGE_MARKED) != 0; + bool dirty = (entry & SVSM_PAGE_DIRTY) != 0; + + if (dirty) + { + color = float3(0.9f, 0.1f, 0.1f); + } + else if (resident && invalid) + { + color = float3(0.9f, 0.5f, 0.1f); + } + else if (resident) + { + float freshness = 1.0f - float(GetPageAge(entry)) / 255.0f; + color = float3(0.1f, 0.25f + 0.5f * freshness, 0.1f); + } + else if (marked) + { + color = float3(0.9f, 0.1f, 0.9f); // Marked but never became resident, pool overflow + } + + // Marked pages get a bright border ring so the working set pops against cached pages + bool onCellBorder = any(cellPixel == uint2(0, 0)) || any(cellPixel == uint2(cellSize - 1, cellSize - 1)); + if (marked && onCellBorder) + { + color = float3(0.9f, 0.9f, 0.2f); + } + } + + _target[uint2(targetPixel)] = float4(color, 1.0f); +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateA.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateA.cs.slang new file mode 100644 index 0000000..f14bd52 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateA.cs.slang @@ -0,0 +1,131 @@ +#include "Shadows/SVSM.inc.slang" + +// Page table maintenance, one thread per entry, runs before this frame's marking: +// applies toroidal and global invalidation, ages entries, evicts stale pages back to the free +// list and clears last frame's MARKED/DIRTY bits (safe because dirty pages always rendered the +// frame they were marked). Must run before marking: this is the only pass that read-modify-writes +// entries non-atomically. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _pageTable; +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _freeList; // [0] = count, entries from [4] + +groupshared uint gResidentCount; +groupshared uint gInvalidatedCount; +groupshared uint gEvictedCount; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(256, 1, 1)] +void main(CSInput input) +{ + if (input.groupIndex == 0) + { + gResidentCount = 0; + gInvalidatedCount = 0; + gEvictedCount = 0; + } + GroupMemoryBarrierWithGroupSync(); + + uint pageTableSize = _constants.pageTableSize; + uint entriesPerClipmap = pageTableSize * pageTableSize; + uint entryIndex = input.dispatchThreadID.x; + uint clipmapIndex = entryIndex / entriesPerClipmap; // Group-uniform, entriesPerClipmap is a multiple of 256 + + uint entry = _pageTable[entryIndex]; + if (entry != 0) + { + bool resident = (entry & SVSM_PAGE_RESIDENT) != 0; + bool invalid = (entry & SVSM_PAGE_INVALID) != 0; + bool markedLastFrame = (entry & SVSM_PAGE_MARKED) != 0; + uint age = markedLastFrame ? 0 : min(GetPageAge(entry) + 1, 255u); + + bool clipmapActive = clipmapIndex < _constants.numClipmaps; + if (clipmapActive) + { + // Toroidal reuse: valid only if the slot maps to the same absolute page under the old + // and the new window. Handles any slide distance, teleports included + uint slotLinear = entryIndex - clipmapIndex * entriesPerClipmap; + int2 slot = int2(slotLinear & (pageTableSize - 1), slotLinear / pageTableSize); + + int2 anchor = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 prevAnchor = int2(_svsmData[0].prevAnchorPageMinX[clipmapIndex], _svsmData[0].prevAnchorPageMinY[clipmapIndex]); + int2 absNew = anchor + WrapPageSlot(slot - anchor, pageTableSize); + int2 absOld = prevAnchor + WrapPageSlot(slot - prevAnchor, pageTableSize); + + bool stale = any(absNew != absOld); + stale = stale || (_svsmData[0].frameInvalidationFlags != 0); + stale = stale || (_svsmData[0].clipmapInvalidate[clipmapIndex] != 0); + + if (stale && resident && !invalid) + { + InterlockedAdd(gInvalidatedCount, 1); + } + invalid = invalid || stale; + } + + // Pressure eviction: when the pool runs dry the stale cache recycles within frames instead + // of squatting for the full evict age, so the finest clipmaps can always allocate. + // The age field saturates at 255, an evict age at or above that would never trigger + uint effectiveEvictAge = min(_constants.evictAge, 254u); + uint poolPages = _constants.poolPagesPerRow * _constants.poolPagesPerRow; + uint freeCount = _svsmData[0].statsFreeListCount; // Start-of-frame snapshot from Prepare + if (freeCount < poolPages / 8) + { + effectiveEvictAge = min(effectiveEvictAge, 30u); + } + if (freeCount < poolPages / 32) + { + effectiveEvictAge = min(effectiveEvictAge, 2u); + } + + // Invalidated pages that nothing marked are dead weight: their content may live in an old + // depth mapping (Z-range/sun change) which the sampler refuses, so free them immediately + // instead of letting them squat for the full evict age. Marked ones re-render in UpdateB + if (resident && (age > effectiveEvictAge || (invalid && !markedLastFrame))) + { + // Return the physical page and free the slot + uint writeIndex; + InterlockedAdd(_freeList[0], 1, writeIndex); + _freeList[4 + writeIndex] = GetPagePhysical(entry); + InterlockedAdd(gEvictedCount, 1); + entry = 0; + } + else + { + uint flags = (resident ? SVSM_PAGE_RESIDENT : 0u) | ((invalid && resident) ? SVSM_PAGE_INVALID : 0u); + entry = PackPageEntry(GetPagePhysical(entry), age, flags); + if (resident) + { + InterlockedAdd(gResidentCount, 1); + } + } + + _pageTable[entryIndex] = entry; + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex == 0 && clipmapIndex < 8) + { + if (gResidentCount != 0) + { + InterlockedAdd(_svsmData[0].statsResident[clipmapIndex], gResidentCount); + } + if (gInvalidatedCount != 0) + { + InterlockedAdd(_svsmData[0].statsInvalidated[clipmapIndex], gInvalidatedCount); + } + if (gEvictedCount != 0) + { + InterlockedAdd(_svsmData[0].statsEvicted[clipmapIndex], gEvictedCount); + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang new file mode 100644 index 0000000..e922963 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang @@ -0,0 +1,149 @@ +#include "Shadows/SVSM.inc.slang" + +// Allocation, one thread per entry, runs after marking: marked non-resident entries pop a +// physical page from the free list and become RESIDENT|DIRTY, marked invalid entries re-dirty in +// place. Dirty pages accumulate the per-clipmap window-local dirty rect that stage 2 uses to +// tighten the render frusta. +// +// Dispatched once per clipmap (allocClipmap), finest first with a barrier between, so under pool +// pressure the starvation always lands on the coarsest ring where the fallback costs the least. +// +// Lifecycle invariant: DIRTY pages are always rendered the same frame they are set, UpdateA +// clears the bits next frame counting on that. Pool exhaustion leaves the entry non-resident and +// counts an overflow, the sampler falls back to a coarser clipmap. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _pageTable; +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _freeList; // [0] = count, entries from [4] +[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _clearList; // [0..2] = indirect dispatch args, physical pages from [4] + +groupshared uint gMarkedCount; +groupshared uint gDirtyCount; +groupshared uint gDeferredCount; +groupshared int gRectMinX; +groupshared int gRectMinY; +groupshared int gRectMaxX; +groupshared int gRectMaxY; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; + uint groupIndex : SV_GroupIndex; +}; + +[shader("compute")] +[numthreads(256, 1, 1)] +void main(CSInput input) +{ + if (input.groupIndex == 0) + { + gMarkedCount = 0; + gDirtyCount = 0; + gDeferredCount = 0; + gRectMinX = 0x7FFFFFFF; + gRectMinY = 0x7FFFFFFF; + gRectMaxX = -1; + gRectMaxY = -1; + } + GroupMemoryBarrierWithGroupSync(); + + uint pageTableSize = _constants.pageTableSize; + uint entriesPerClipmap = pageTableSize * pageTableSize; + uint clipmapIndex = _constants.allocClipmap; + uint entryIndex = clipmapIndex * entriesPerClipmap + input.dispatchThreadID.x; + + uint entry = input.dispatchThreadID.x < entriesPerClipmap ? _pageTable[entryIndex] : 0; + if ((entry & SVSM_PAGE_MARKED) != 0) + { + InterlockedAdd(gMarkedCount, 1); + + // Render budget: pages past the per-frame cap defer (entry untouched: MARKED keeps it + // alive, INVALID persists, no alloc) and retry next frame. The coarsest two rings are + // exempt, they are the whole-scene fallback and only hold a handful of pages. The + // finest-first dispatch order means the near-camera pages win the budget. + // No early return, the group barrier below must stay uniform + bool wantsRender = (entry & SVSM_PAGE_RESIDENT) == 0 || (entry & SVSM_PAGE_INVALID) != 0; + bool deferred = false; + if (wantsRender && _constants.renderBudget != 0 && clipmapIndex + 2 < _constants.numClipmaps) + { + uint budgetIndex; + InterlockedAdd(_svsmData[0].statsBudgetUsed, 1, budgetIndex); + if (budgetIndex >= _constants.renderBudget) + { + InterlockedAdd(gDeferredCount, 1); + deferred = true; + } + } + + if (!deferred) + { + if ((entry & SVSM_PAGE_RESIDENT) == 0) + { + uint previousCount; + InterlockedAdd(_freeList[0], uint(-1), previousCount); + if (int(previousCount) <= 0) + { + // Pool exhausted, restore and leave the entry non-resident + InterlockedAdd(_freeList[0], 1); + InterlockedAdd(_svsmData[0].statsOverflow, 1); + } + else + { + uint physicalPage = _freeList[4 + previousCount - 1]; + entry = PackPageEntry(physicalPage, 0, SVSM_PAGE_RESIDENT | SVSM_PAGE_MARKED | SVSM_PAGE_DIRTY); + } + } + else if ((entry & SVSM_PAGE_INVALID) != 0) + { + entry = (entry & ~SVSM_PAGE_INVALID) | SVSM_PAGE_DIRTY; + } + + if ((entry & SVSM_PAGE_DIRTY) != 0) + { + InterlockedAdd(gDirtyCount, 1); + + // Queue the physical page for the indirect clear, one workgroup per page + uint clearIndex; + InterlockedAdd(_clearList[0], 1, clearIndex); + _clearList[4 + clearIndex] = GetPagePhysical(entry); + + // Window-local page coords for the dirty rect + uint slotLinear = entryIndex - clipmapIndex * entriesPerClipmap; + int2 slot = int2(slotLinear & (pageTableSize - 1), slotLinear / pageTableSize); + int2 anchor = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 localPage = WrapPageSlot(slot - anchor, pageTableSize); + + InterlockedMin(gRectMinX, localPage.x); + InterlockedMin(gRectMinY, localPage.y); + InterlockedMax(gRectMaxX, localPage.x); + InterlockedMax(gRectMaxY, localPage.y); + } + + _pageTable[entryIndex] = entry; + } + } + + GroupMemoryBarrierWithGroupSync(); + + if (input.groupIndex == 0 && clipmapIndex < 8) + { + if (gMarkedCount != 0) + { + InterlockedAdd(_svsmData[0].statsMarked[clipmapIndex], gMarkedCount); + } + if (gDirtyCount != 0) + { + InterlockedAdd(_svsmData[0].statsDirty[clipmapIndex], gDirtyCount); + InterlockedMin(_svsmData[0].dirtyRectMinX[clipmapIndex], gRectMinX); + InterlockedMin(_svsmData[0].dirtyRectMinY[clipmapIndex], gRectMinY); + InterlockedMax(_svsmData[0].dirtyRectMaxX[clipmapIndex], gRectMaxX); + InterlockedMax(_svsmData[0].dirtyRectMaxY[clipmapIndex], gRectMaxY); + } + if (gDeferredCount != 0) + { + InterlockedAdd(_svsmData[0].statsDeferred[clipmapIndex], gDeferredCount); + } + } +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPoolDebug.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPoolDebug.cs.slang new file mode 100644 index 0000000..78cf1ae --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPoolDebug.cs.slang @@ -0,0 +1,49 @@ +#include "Shadows/SVSM.inc.slang" + +// Draws a downsampled view of the physical page pool straight into the scene color corner: +// grayscale reversed-Z depth (bright = close to the sun), untouched texels stay black. + +struct DebugConstants +{ + uint poolSize; // Pool texture resolution + uint regionSize; // On-screen size of the debug view + uint padding0; + uint padding1; + int2 screenOffset; +}; + +[[vk::push_constant]] DebugConstants _constants; + +[[vk::binding(0, PER_PASS)]] Texture2D _pagePool; +[[vk::binding(1, PER_PASS)]] RWTexture2D _target; + +struct CSInput +{ + uint3 dispatchThreadID : SV_DispatchThreadID; +}; + +[shader("compute")] +[numthreads(16, 16, 1)] +void main(CSInput input) +{ + uint regionSize = _constants.regionSize; + if (any(input.dispatchThreadID.xy >= uint2(regionSize, regionSize))) + { + return; + } + + uint2 targetDim; + _target.GetDimensions(targetDim.x, targetDim.y); + int2 targetPixel = _constants.screenOffset + int2(input.dispatchThreadID.xy); + if (any(targetPixel < int2(0, 0)) || any(targetPixel >= int2(targetDim))) + { + return; + } + + uint2 poolTexel = input.dispatchThreadID.xy * (_constants.poolSize / regionSize); + float depth = asfloat(_pagePool[poolTexel]); + + // sqrt spreads the reversed-Z range so distant depth stays visible + float value = sqrt(saturate(depth)); + _target[uint2(targetPixel)] = float4(value, value, value, 1.0f); +} diff --git a/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang new file mode 100644 index 0000000..b0dbf96 --- /dev/null +++ b/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang @@ -0,0 +1,131 @@ +#include "Include/Camera.inc.slang" +#include "Shadows/SVSM.inc.slang" + +// First SVSM dispatch, single thread: detects invalidation events, snaps the per-clipmap window +// anchors to the absolute page grid and resets the per-frame stats. +// +// The anchors are the precision foundation for every later pass: anchorPageMin is exact integer +// math, and camMinusWindowMin is the only place the world-magnitude cancellation happens, so all +// consumers share the same (sub-texel) absolute placement error instead of diverging. + +[[vk::push_constant]] SVSMConstants _constants; + +[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera +[[vk::binding(2, PER_PASS)]] StructuredBuffer _freeList; // [0] = count, entries from [4] +[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _clearList; // [0..2] = indirect dispatch args, physical pages from [4] +[[vk::binding(4, PER_PASS)]] RWStructuredBuffer _dynamicClearList; // Same layout, for the caster-split dynamic pool + +[shader("compute")] +[numthreads(1, 1, 1)] +void main() +{ + // Start-of-frame snapshot, the free list only changes in UpdateA/UpdateB later in the frame + _svsmData[0].statsFreeListCount = _freeList[0]; + + // Reset the dirty page clear lists, UpdateB / DynamicUpdate append and bump the indirect group counts + _clearList[0] = 0; + _clearList[1] = 1; + _clearList[2] = 1; + _dynamicClearList[0] = 0; + _dynamicClearList[1] = 1; + _dynamicClearList[2] = 1; + + uint cause = _constants.invalidateAll; + + // The quantized shadow sun stepped, every page's light basis changed + float3 lightDirection = _constants.lightDirection.xyz; + float4 prevLightDirection = _svsmData[0].prevLightDirection; + if (prevLightDirection.w > 0.5f && dot(prevLightDirection.xyz, lightDirection) < 0.999999f) + { + cause |= SVSM_CAUSE_SUN_STEP; + } + + float3x3 lightRotation = BuildLightRotation(lightDirection); + float3 cameraLightSpace = mul(_srcCameras[0].eyePosition.xyz, lightRotation); + + // One global Z window for all clipmaps, anchored on the camera in coarse steps: stored page + // depths change meaning when it moves, so it must move rarely and invalidate when it does + const float zAnchorStep = 512.0f; + float zAnchor = floor(cameraLightSpace.z / zAnchorStep + 0.5f) * zAnchorStep; + float zRangeMin = zAnchor - _constants.zHalfRange; + float zRangeMax = zAnchor + _constants.zHalfRange; + if (prevLightDirection.w > 0.5f && (_svsmData[0].zRangeMin != zRangeMin || _svsmData[0].zRangeMax != zRangeMax)) + { + cause |= SVSM_CAUSE_Z_RANGE; + } + _svsmData[0].zRangeMin = zRangeMin; + _svsmData[0].zRangeMax = zRangeMax; + _svsmData[0].prevLightDirection = float4(lightDirection, 1.0f); + + _svsmData[0].frameInvalidationFlags = cause; + _svsmData[0].statsInvalidationCause = cause; + _svsmData[0].statsOverflow = 0; + + uint pageTableSize = _constants.pageTableSize; + + // Config mirror for the draw-time shaders, they only get viewIndex as a push constant + _svsmData[0].configPageTableSize = pageTableSize; + _svsmData[0].configPageSize = _constants.pageSize; + _svsmData[0].configPoolPagesPerRow = _constants.poolPagesPerRow; + _svsmData[0].configNumClipmaps = _constants.numClipmaps; + _svsmData[0].configMarkBorderTexels = _constants.markBorderTexels; + _svsmData[0].configCamMinusZAnchor = cameraLightSpace.z - zAnchor; + _svsmData[0].configResolutionScale = _constants.resolutionScale; + + _svsmData[0].statsDynamicOverflow = 0; + _svsmData[0].statsDynamicTotal = 0; + _svsmData[0].configDynamicPoolPagesPerRow = _constants.dynamicPoolPagesPerRow; + _svsmData[0].statsBudgetUsed = 0; + + for (uint k = 0; k < 8; k++) + { + // Per-frame stats and dirty rect sentinels + _svsmData[0].statsMarked[k] = 0; + _svsmData[0].statsResident[k] = 0; + _svsmData[0].statsDirty[k] = 0; + _svsmData[0].statsEvicted[k] = 0; + _svsmData[0].statsInvalidated[k] = 0; + _svsmData[0].statsDynamicLive[k] = 0; + _svsmData[0].statsDeferred[k] = 0; + _svsmData[0].dirtyRectMinX[k] = int(pageTableSize); + _svsmData[0].dirtyRectMinY[k] = int(pageTableSize); + _svsmData[0].dirtyRectMaxX[k] = -1; + _svsmData[0].dirtyRectMaxY[k] = -1; + _svsmData[0].dynamicRectMinX[k] = int(pageTableSize); + _svsmData[0].dynamicRectMinY[k] = int(pageTableSize); + _svsmData[0].dynamicRectMaxX[k] = -1; + _svsmData[0].dynamicRectMaxY[k] = -1; + + if (k >= _constants.numClipmaps) + { + _svsmData[0].clipmapInvalidate[k] = 0; + continue; + } + + float extent = _constants.clipmap0Extent * exp2(float(k)); + float pageWorld = extent / float(pageTableSize); + + // A window geometry change makes the whole clipmap's content unaddressable + _svsmData[0].clipmapInvalidate[k] = (_svsmData[0].extent[k] != extent || _svsmData[0].pageWorld[k] != pageWorld) ? 1u : 0u; + + // Window min corner snapped to the absolute page grid, camera sits pageTableSize/2 pages in. + // floor can flip by one page when the camera rests exactly on a boundary, that only costs a + // toroidal row invalidation, never correctness + int2 anchorPageMin = int2(floor(cameraLightSpace.xy / pageWorld)) - int(pageTableSize / 2); + + _svsmData[0].prevAnchorPageMinX[k] = _svsmData[0].anchorPageMinX[k]; + _svsmData[0].prevAnchorPageMinY[k] = _svsmData[0].anchorPageMinY[k]; + _svsmData[0].anchorPageMinX[k] = anchorPageMin.x; + _svsmData[0].anchorPageMinY[k] = anchorPageMin.y; + + // The one world-magnitude subtraction, its rounding shifts the whole clipmap by a fraction + // of a texel and is shared by every consumer through this stored value + float2 windowMin = float2(anchorPageMin) * pageWorld; + _svsmData[0].camMinusWindowMinX[k] = cameraLightSpace.x - windowMin.x; + _svsmData[0].camMinusWindowMinY[k] = cameraLightSpace.y - windowMin.y; + + _svsmData[0].extent[k] = extent; + _svsmData[0].pageWorld[k] = pageWorld; + } +} diff --git a/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang new file mode 100644 index 0000000..2c398ed --- /dev/null +++ b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang @@ -0,0 +1,31 @@ +#define GEOMETRY_PASS 1 + +#include "DescriptorSet/Global.inc.slang" + +#include "Shadows/SVSM.inc.slang" + +// SVSM page render: no attachments, the fragment writes reversed-Z depth into the physical page +// pool with image atomics. Paired with the SHADOW_PASS=1 vertex permutation, viewport spans the +// full virtual texture so SV_Position.xy is the window-local virtual texel. + +struct Constants +{ + uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 +}; + +[[vk::push_constant]] Constants _constants; + +[[vk::binding(0, PER_PASS)]] StructuredBuffer _svsmData; +[[vk::binding(1, PER_PASS)]] StructuredBuffer _pageTable; +[[vk::binding(2, PER_PASS)]] RWTexture2D _pagePool; + +struct PSInput +{ + float4 position : SV_Position; +}; + +[shader("fragment")] +void main(PSInput input) +{ + SVSMWritePageDepth(input.position.xyz, _constants.viewIndex - 1, SVSM_PAGE_RESIDENT | SVSM_PAGE_DIRTY, _svsmData[0].configPoolPagesPerRow, _svsmData, _pageTable, _pagePool); +} diff --git a/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang b/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang index 1699b07..3ce897f 100644 --- a/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang +++ b/Source/Shaders/Shaders/Utils/FillInstancedDrawCallsFromBitmask.cs.slang @@ -1,4 +1,5 @@ permutation IS_INDEXED = [0, 1]; +permutation DYNAMIC_FILTER = [0, 1]; // 1 adds the per-instance dynamic mask filter for the SVSM caster split #include "DescriptorSet/Global.inc.slang" @@ -13,6 +14,7 @@ struct Constants uint drawCallDataSize; uint currentBitmaskIndex; uint bitmaskOffset; // Selects which view's bitmask slice to fill from + uint keepDynamic; // DYNAMIC_FILTER only: 0 = keep static instances, 1 = keep dynamic instances }; struct InstanceRef @@ -40,6 +42,10 @@ typedef Draw DrawType; [[vk::binding(3, PER_PASS)]] RWByteAddressBuffer _culledInstanceCounts; // One uint per draw call [[vk::binding(4, PER_PASS)]] RWStructuredBuffer _culledInstanceLookupTable; // One uint per instance, contains instanceRefID of what survives culling, and thus can get reordered +#if DYNAMIC_FILTER +[[vk::binding(6, PER_PASS)]] StructuredBuffer _dynamicInstanceMask; // One bit per instanceID, rebuilt every frame +#endif + struct CSInput { uint3 dispatchThreadId : SV_DispatchThreadID; @@ -77,6 +83,13 @@ void main(CSInput input) { InstanceRef instanceRef = _instanceRefTable[instanceRefID]; +#if DYNAMIC_FILTER + // SVSM caster split: keep only the requested class + bool isDynamic = (_dynamicInstanceMask[instanceRef.instanceID / 32] & (1u << (instanceRef.instanceID & 31))) != 0; + if (isDynamic != (_constants.keepDynamic != 0)) + return; +#endif + uint countByteOffset = instanceRef.drawID * sizeof(uint); uint instanceIndex; From 98f50e1845a4a60ec6e6bf5a0f0c1f469a8be6a7 Mon Sep 17 00:00:00 2001 From: Pursche Date: Wed, 15 Jul 2026 02:28:21 +0200 Subject: [PATCH 14/24] Clip SVSM page draws to classified dirty stripe rects via clip distances --- .../Game-Lib/Rendering/CulledRenderer.cpp | 22 +++++++++- .../Game-Lib/Rendering/CulledRenderer.h | 1 + .../Rendering/Model/ModelRenderer.cpp | 18 +++++--- .../Rendering/Shadow/ShadowRenderer.cpp | 1 + .../Rendering/Shadow/ShadowRenderer.h | 5 ++- .../Rendering/Terrain/TerrainRenderer.cpp | 37 +++++++++++++--- .../Rendering/Terrain/TerrainRenderer.h | 1 + Source/Shaders/Shaders/Model/Draw.vs.slang | 18 ++++++++ .../Shaders/Shaders/Model/DrawSVSM.ps.slang | 3 +- Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 43 +++++++++++++++++++ .../Shaders/Shadows/SVSMPageUpdateB.cs.slang | 38 ++++++++++++++++ .../Shaders/Shadows/SVSMPrepare.cs.slang | 8 ++++ Source/Shaders/Shaders/Terrain/Draw.vs.slang | 22 +++++++++- .../Shaders/Shaders/Terrain/DrawSVSM.ps.slang | 3 +- 14 files changed, 204 insertions(+), 16 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index 8a267ae..9baf594 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -896,7 +896,27 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) } - Profiled("Draw", i, [&] { params.drawCallback(drawParams); }); + const bool svsmClipRects = params.svsmPass && i > 0 + && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; + if (svsmClipRects) + { + // One draw per clip rect (new X columns, new Y rows, other): the vertex shader + // clips fragments to the rect, so an L-shaped toroidal update rasterizes two thin + // stripes instead of its whole-window bounding box + Profiled("Draw", i, [&] + { + for (u32 rectIndex = 0; rectIndex < 3; rectIndex++) + { + drawParams.svsmRectIndex = rectIndex; + params.drawCallback(drawParams); + } + }); + } + else + { + // svsmClipRects 0: single draw, rect index stays at the disabled sentinel + Profiled("Draw", i, [&] { params.drawCallback(drawParams); }); + } } // Copy from our draw count buffer to the readback buffer diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index d995553..c1d8e68 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -40,6 +40,7 @@ class CulledRenderer bool shadowPass = false; bool svsmPass = false; // Attachment-less page render, needs an explicit render area bool svsmDynamicPass = false; // Caster-split dynamic instances into the dynamic pool + u32 svsmRectIndex = 0xFFFFFFFFu; // Clip rect this draw renders (0-2), SVSM_CLIP_RECT_DISABLED = no clipping u32 viewIndex = 0; CullingResourcesBase* cullingResources; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 20855ab..742584c 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -3301,7 +3301,8 @@ void ModelRenderer::CreateModelPipelines() std::vector vertexPermutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "0"} + { "SHADOW_PASS", "0"}, + { "SVSM_PASS", "0"} }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.vs"); @@ -3339,12 +3340,13 @@ void ModelRenderer::CreateModelPipelines() std::vector vertexPermutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1"} + { "SHADOW_PASS", "1"}, + { "SVSM_PASS", "0"} }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.vs"); pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); - + Renderer::PixelShaderDesc pixelShaderDesc; std::vector pixelPermutationFields = { @@ -3371,7 +3373,8 @@ void ModelRenderer::CreateModelPipelines() std::vector vertexPermutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1"} + { "SHADOW_PASS", "1"}, + { "SVSM_PASS", "1"} }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.vs"); @@ -4171,12 +4174,17 @@ void ModelRenderer::Draw(const RenderResources& resources, u8 frameIndex, Render struct PushConstants { u32 viewIndex; + u32 svsmRectIndex; }; PushConstants* constants = graphResources.FrameNew(); constants->viewIndex = params.viewIndex; - commandList.PushConstant(constants, 0, sizeof(PushConstants)); + constants->svsmRectIndex = params.svsmRectIndex; + + // Only the SVSM vertex permutation declares the rect index, the other pipelines' push range + // is a single uint + commandList.PushConstant(constants, 0, params.svsmPass ? sizeof(PushConstants) : sizeof(u32)); for (auto& descriptorSet : params.descriptorSets) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 8bb39d7..ca75c81 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -71,6 +71,7 @@ AutoCVar_Int CVAR_SVSMDebugShowPool(CVarCategory::Client | CVarCategory::Renderi AutoCVar_Float CVAR_SVSMZHalfRange(CVarCategory::Client | CVarCategory::Rendering, "svsmZHalfRange", "half depth range of the clipmap windows around the camera in light space, changes invalidate all pages", 2048.0f); AutoCVar_Float CVAR_SVSMConstantBias(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias", "SVSM compare bias toward the sun in world meters, the software depth path has no hardware bias", 0.15f); AutoCVar_Int CVAR_SVSMProfileGeometry(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry", "debug: per-view fill/draw GPU time queries in the SVSM geometry passes, shown in the render pass list", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMClipRects(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects", "clip the static page draws to the classified dirty rects (3 draws per view), 0 reverts to one unclipped draw for A/B", 1, CVarFlags::EditCheckbox); // u32 indices into the flat SVSMData readback, mirrors Shadows/SVSM.inc.slang namespace SVSMDataOffsets diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 7d84fd5..d3d91c7 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -141,8 +141,9 @@ class ShadowRenderer Camera _readBackCascadeCameras[Renderer::Settings::MAX_SHADOW_CASCADES]; f32 _sdsmDataReadBack[SDSM_DATA_FLOAT_COUNT] = { 0.0f }; - // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp - static constexpr u32 SVSM_DATA_UINT_COUNT = 220; + // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp. + // Tail: clipRect{MinX,MinY,MaxX,MaxY}[24] at 220..315 (3 clip rects x 8 clipmaps) + static constexpr u32 SVSM_DATA_UINT_COUNT = 316; static constexpr u32 SVSM_MAX_CLIPMAPS = 8; static constexpr u32 SVSM_MAX_PAGE_TABLE_SIZE = 64; // Pages per row, buffers are sized for this cap static constexpr u32 SVSM_MAX_POOL_PAGES = 4096; // Physical page index is 12 bits in the table entry diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index 3f930d9..f60c2d6 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -962,7 +962,26 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re drawParams.drawDescriptorSet = data.geometryPassSet; drawParams.svsmDescriptorSet = data.svsmSet; - Profiled("Draw", i, [&] { Draw(resources, frameIndex, graphResources, commandList, drawParams); }); + const bool svsmClipRects = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; + if (svsmClipRects) + { + // One draw per clip rect (new X columns, new Y rows, other): the vertex + // shader clips fragments to the rect, so an L-shaped toroidal update + // rasterizes two thin stripes instead of its whole-window bounding box + Profiled("Draw", i, [&] + { + for (u32 rectIndex = 0; rectIndex < 3; rectIndex++) + { + drawParams.svsmRectIndex = rectIndex; + Draw(resources, frameIndex, graphResources, commandList, drawParams); + } + }); + } + else + { + // svsmClipRects 0: single draw, rect index stays at the disabled sentinel + Profiled("Draw", i, [&] { Draw(resources, frameIndex, graphResources, commandList, drawParams); }); + } commandList.PopMarker(); } @@ -1468,7 +1487,8 @@ void TerrainRenderer::CreatePipelines() std::vector permutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "0" } + { "SHADOW_PASS", "0" }, + { "SVSM_PASS", "0" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Terrain/Draw.vs"); @@ -1507,7 +1527,8 @@ void TerrainRenderer::CreatePipelines() std::vector permutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1" } + { "SHADOW_PASS", "1" }, + { "SVSM_PASS", "0" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Terrain/Draw.vs"); @@ -1542,7 +1563,8 @@ void TerrainRenderer::CreatePipelines() std::vector permutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1" } + { "SHADOW_PASS", "1" }, + { "SVSM_PASS", "1" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Terrain/Draw.vs"); @@ -1692,11 +1714,16 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend struct PushConstants { u32 viewIndex; + u32 svsmRectIndex; }; PushConstants* constants = graphResources.FrameNew(); constants->viewIndex = params.viewIndex; - commandList.PushConstant(constants, 0, sizeof(PushConstants)); + constants->svsmRectIndex = params.svsmRectIndex; + + // Only the SVSM vertex permutation declares the rect index, the other pipelines' push range + // is a single uint + commandList.PushConstant(constants, 0, params.svsmPass ? sizeof(PushConstants) : sizeof(u32)); // Bind descriptors params.drawDescriptorSet.Bind("_culledInstanceDatas"_h, params.instanceBuffer); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index cc6bf57..273c69e 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -88,6 +88,7 @@ class TerrainRenderer public: bool shadowPass = false; bool svsmPass = false; // Attachment-less page render, needs an explicit render area + u32 svsmRectIndex = 0xFFFFFFFFu; // Clip rect this draw renders (0-2), SVSM_CLIP_RECT_DISABLED = no clipping u32 viewIndex = 0; bool cullingEnabled = false; diff --git a/Source/Shaders/Shaders/Model/Draw.vs.slang b/Source/Shaders/Shaders/Model/Draw.vs.slang index fc78081..e454ff7 100644 --- a/Source/Shaders/Shaders/Model/Draw.vs.slang +++ b/Source/Shaders/Shaders/Model/Draw.vs.slang @@ -1,5 +1,6 @@ permutation EDITOR_PASS = [0, 1]; permutation SHADOW_PASS = [0, 1]; +permutation SVSM_PASS = [0, 1]; // 1 adds per-draw clip-rect distances for the SVSM page render #define GEOMETRY_PASS 1 #include "DescriptorSet/Global.inc.slang" @@ -7,14 +8,24 @@ permutation SHADOW_PASS = [0, 1]; #include "Include/Common.inc.slang" #include "Model/ModelShared.inc.slang" +#if SVSM_PASS +#include "Shadows/SVSM.inc.slang" +#endif struct Constants { uint viewIndex; +#if SVSM_PASS + uint svsmRectIndex; // Which clip rect this draw renders, SVSM_CLIP_RECT_DISABLED = no clipping +#endif }; [[vk::push_constant]] Constants _constants; +#if SVSM_PASS +[[vk::binding(0, PER_PASS)]] StructuredBuffer _svsmData; // Same binding the SVSM pixel shader declares +#endif + struct VSInput { uint vertexID : SV_VulkanVertexID; @@ -28,6 +39,9 @@ struct VSOutput nointerpolation uint4 drawIDInstanceIDTextureDataIDInstanceRefID : TEXCOORD0; float4 uv01 : TEXCOORD1; #endif +#if SVSM_PASS + float4 clipDistances : SV_ClipDistance; // Fixed-function culls fragments outside the draw's clip rect +#endif }; [shader("vertex")] @@ -61,6 +75,10 @@ VSOutput main(VSInput input) VSOutput output; output.position = mul(position, _cameras[_constants.viewIndex].worldToClip); +#if SVSM_PASS + output.clipDistances = SVSMRectClipDistances(position.xyz, _constants.viewIndex - 1, _constants.svsmRectIndex, _cameras[0].eyePosition.xyz, _svsmData); +#endif + #if !EDITOR_PASS output.drawIDInstanceIDTextureDataIDInstanceRefID = int4(drawID, instanceID, instanceRef.extraID, instanceRefID); diff --git a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang index 6120d2d..79b5c57 100644 --- a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang +++ b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang @@ -19,7 +19,8 @@ permutation SVSM_DYNAMIC = [0, 1]; // 1 = dynamic caster split pass, renders int struct Constants { - uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 + uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 + uint svsmRectIndex; // Consumed by the vertex shader's clip rect, declared here so the push block matches }; [[vk::push_constant]] Constants _constants; diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang index 0a7e5dd..13f84f6 100644 --- a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -125,8 +125,51 @@ struct SVSMData uint padding5; uint padding6; uint padding7; + + // Fragment clip rects: this frame's dirty pages classified by UpdateB into three window-local + // page rects per clipmap, indexed [rect * 8 + clipmap]. Rect 0 = newly exposed X columns, + // 1 = newly exposed Y rows, 2 = everything else. The SVSM vertex shaders clip each of the + // three static draws to one rect (SV_ClipDistance) so an L-shaped toroidal update rasterizes + // two thin stripes instead of its huge bounding box + int clipRectMinX[24]; + int clipRectMinY[24]; + int clipRectMaxX[24]; + int clipRectMaxY[24]; }; +#define SVSM_CLIP_RECT_DISABLED 0xFFFFFFFFu + +// Clip distances for one static SVSM draw: positive inside the rect, negative outside, so the +// fixed-function clipper culls triangles before rasterization. Same window math as the sampler. +// An empty rect clips everything, a disabled index (dynamic draws) clips nothing +float4 SVSMRectClipDistances(float3 worldPos, uint clipmapIndex, uint rectIndex, float3 cameraPos, StructuredBuffer svsmData) +{ + if (rectIndex == SVSM_CLIP_RECT_DISABLED) + { + return float4(1.0f, 1.0f, 1.0f, 1.0f); + } + + uint rectSlot = rectIndex * 8 + clipmapIndex; + int2 rectMin = int2(svsmData[0].clipRectMinX[rectSlot], svsmData[0].clipRectMinY[rectSlot]); + int2 rectMax = int2(svsmData[0].clipRectMaxX[rectSlot], svsmData[0].clipRectMaxY[rectSlot]); + if (any(rectMax < rectMin)) + { + return float4(-1.0f, -1.0f, -1.0f, -1.0f); + } + + float3x3 lightRotation = BuildLightRotation(svsmData[0].prevLightDirection.xyz); + float2 lightSpaceXY = mul(worldPos - cameraPos, lightRotation).xy; + float2 windowPos = lightSpaceXY + float2(svsmData[0].camMinusWindowMinX[clipmapIndex], svsmData[0].camMinusWindowMinY[clipmapIndex]); + + uint pageSize = svsmData[0].configPageSize; + float texelWorld = svsmData[0].extent[clipmapIndex] / float(svsmData[0].configPageTableSize * pageSize); + float2 texel = windowPos / texelWorld; + + float2 minTexel = float2(rectMin * int(pageSize)); + float2 maxTexel = float2((rectMax + int2(1, 1)) * int(pageSize)); + return float4(texel.x - minTexel.x, maxTexel.x - texel.x, texel.y - minTexel.y, maxTexel.y - texel.y); +} + uint GetPagePhysical(uint entry) { return entry & SVSM_PAGE_PHYS_MASK; diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang index e922963..c482249 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang @@ -27,6 +27,13 @@ groupshared int gRectMinY; groupshared int gRectMaxX; groupshared int gRectMaxY; +// Clip rects: dirty pages classified as newly-exposed X columns (0), Y rows (1) or other (2), +// each of the three static draws clips its fragments to one of these +groupshared int gClipRectMinX[3]; +groupshared int gClipRectMinY[3]; +groupshared int gClipRectMaxX[3]; +groupshared int gClipRectMaxY[3]; + struct CSInput { uint3 dispatchThreadID : SV_DispatchThreadID; @@ -47,6 +54,13 @@ void main(CSInput input) gRectMaxX = -1; gRectMaxY = -1; } + if (input.groupIndex < 3) + { + gClipRectMinX[input.groupIndex] = 0x7FFFFFFF; + gClipRectMinY[input.groupIndex] = 0x7FFFFFFF; + gClipRectMaxX[input.groupIndex] = -1; + gClipRectMaxY[input.groupIndex] = -1; + } GroupMemoryBarrierWithGroupSync(); uint pageTableSize = _constants.pageTableSize; @@ -119,6 +133,19 @@ void main(CSInput input) InterlockedMin(gRectMinY, localPage.y); InterlockedMax(gRectMaxX, localPage.x); InterlockedMax(gRectMaxY, localPage.y); + + // Classify for the clip rects: a toroidal slide dirties an L whose bounding box + // covers most of the window, but its X and Y stripes are thin on their own + int2 absPage = anchor + localPage; + int2 prevAnchor = int2(_svsmData[0].prevAnchorPageMinX[clipmapIndex], _svsmData[0].prevAnchorPageMinY[clipmapIndex]); + bool exposedX = absPage.x < prevAnchor.x || absPage.x >= prevAnchor.x + int(pageTableSize); + bool exposedY = absPage.y < prevAnchor.y || absPage.y >= prevAnchor.y + int(pageTableSize); + uint clipRect = exposedX ? 0 : (exposedY ? 1 : 2); + + InterlockedMin(gClipRectMinX[clipRect], localPage.x); + InterlockedMin(gClipRectMinY[clipRect], localPage.y); + InterlockedMax(gClipRectMaxX[clipRect], localPage.x); + InterlockedMax(gClipRectMaxY[clipRect], localPage.y); } _pageTable[entryIndex] = entry; @@ -140,6 +167,17 @@ void main(CSInput input) InterlockedMin(_svsmData[0].dirtyRectMinY[clipmapIndex], gRectMinY); InterlockedMax(_svsmData[0].dirtyRectMaxX[clipmapIndex], gRectMaxX); InterlockedMax(_svsmData[0].dirtyRectMaxY[clipmapIndex], gRectMaxY); + + for (uint r = 0; r < 3; r++) + { + if (gClipRectMaxX[r] >= 0) + { + InterlockedMin(_svsmData[0].clipRectMinX[r * 8 + clipmapIndex], gClipRectMinX[r]); + InterlockedMin(_svsmData[0].clipRectMinY[r * 8 + clipmapIndex], gClipRectMinY[r]); + InterlockedMax(_svsmData[0].clipRectMaxX[r * 8 + clipmapIndex], gClipRectMaxX[r]); + InterlockedMax(_svsmData[0].clipRectMaxY[r * 8 + clipmapIndex], gClipRectMaxY[r]); + } + } } if (gDeferredCount != 0) { diff --git a/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang index b0dbf96..1a0acb6 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMPrepare.cs.slang @@ -97,6 +97,14 @@ void main() _svsmData[0].dynamicRectMaxX[k] = -1; _svsmData[0].dynamicRectMaxY[k] = -1; + for (uint r = 0; r < 3; r++) + { + _svsmData[0].clipRectMinX[r * 8 + k] = int(pageTableSize); + _svsmData[0].clipRectMinY[r * 8 + k] = int(pageTableSize); + _svsmData[0].clipRectMaxX[r * 8 + k] = -1; + _svsmData[0].clipRectMaxY[r * 8 + k] = -1; + } + if (k >= _constants.numClipmaps) { _svsmData[0].clipmapInvalidate[k] = 0; diff --git a/Source/Shaders/Shaders/Terrain/Draw.vs.slang b/Source/Shaders/Shaders/Terrain/Draw.vs.slang index 10f506f..ffb3b5e 100644 --- a/Source/Shaders/Shaders/Terrain/Draw.vs.slang +++ b/Source/Shaders/Shaders/Terrain/Draw.vs.slang @@ -1,18 +1,29 @@ permutation EDITOR_PASS = [0, 1]; permutation SHADOW_PASS = [0, 1]; +permutation SVSM_PASS = [0, 1]; // 1 adds per-draw clip-rect distances for the SVSM page render #define GEOMETRY_PASS 1 #include "DescriptorSet/Global.inc.slang" #include "Terrain/TerrainShared.inc.slang" +#if SVSM_PASS +#include "Shadows/SVSM.inc.slang" +#endif struct Constants { uint viewIndex; +#if SVSM_PASS + uint svsmRectIndex; // Which clip rect this draw renders, SVSM_CLIP_RECT_DISABLED = no clipping +#endif }; [[vk::push_constant]] Constants _constants; +#if SVSM_PASS +[[vk::binding(0, PER_PASS)]] StructuredBuffer _svsmData; // Same binding the SVSM pixel shader declares +#endif + struct VSInput { uint vertexID : SV_VulkanVertexID; @@ -25,6 +36,9 @@ struct VSOutput #if !EDITOR_PASS && !SHADOW_PASS uint instanceID : TEXCOORD0; #endif +#if SVSM_PASS + float4 clipDistances : SV_ClipDistance; // Fixed-function culls fragments outside the draw's clip rect +#endif }; [shader("vertex")] @@ -39,6 +53,9 @@ VSOutput main(VSInput input) { const float NaN = asfloat(0b01111111100000000000000000000000); output.position = float4(NaN, NaN, NaN, NaN); +#if SVSM_PASS + output.clipDistances = float4(-1.0f, -1.0f, -1.0f, -1.0f); +#endif return output; } @@ -53,6 +70,9 @@ VSOutput main(VSInput input) #if !EDITOR_PASS && !SHADOW_PASS output.instanceID = instanceData.globalCellID; #endif +#if SVSM_PASS + output.clipDistances = SVSMRectClipDistances(vertex.position, _constants.viewIndex - 1, _constants.svsmRectIndex, _cameras[0].eyePosition.xyz, _svsmData); +#endif return output; -} \ No newline at end of file +} diff --git a/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang index 2c398ed..7b44770 100644 --- a/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang +++ b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang @@ -10,7 +10,8 @@ struct Constants { - uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 + uint viewIndex; // 0 = main camera, clipmap k renders as view k + 1 + uint svsmRectIndex; // Consumed by the vertex shader's clip rect, declared here so the push block matches }; [[vk::push_constant]] Constants _constants; From 7548ddf8b88156d96404e8b5d7b0060b0128f05d Mon Sep 17 00:00:00 2001 From: Pursche Date: Fri, 17 Jul 2026 17:00:05 +0200 Subject: [PATCH 15/24] Remove the CSM/SDSM path, SVSM becomes the only shadow technique --- Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp | 2 - .../Systems/CalculateShadowCameraMatrices.cpp | 351 ----------- .../Systems/CalculateShadowCameraMatrices.h | 15 - .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 12 + .../Game-Lib/ECS/Systems/UpdateAreaLights.h | 3 + .../Editor/PerformanceDiagnostics.cpp | 96 +-- .../Game-Lib/Rendering/CulledRenderer.cpp | 42 +- .../Game-Lib/Rendering/CulledRenderer.h | 11 +- .../Game-Lib/Rendering/GameRenderer.cpp | 12 +- .../Rendering/Material/MaterialRenderer.cpp | 33 +- .../Rendering/Model/ModelRenderer.cpp | 246 +------- .../Game-Lib/Rendering/Model/ModelRenderer.h | 4 +- .../Game-Lib/Rendering/RenderResources.h | 2 - .../Rendering/Shadow/ShadowRenderer.cpp | 561 +----------------- .../Rendering/Shadow/ShadowRenderer.h | 48 +- .../Rendering/Terrain/TerrainRenderer.cpp | 444 +++----------- .../Rendering/Terrain/TerrainRenderer.h | 5 +- .../Shaders/DescriptorSet/Light.inc.slang | 23 +- .../Shaders/Shaders/Include/Culling.inc.slang | 2 - .../Shaders/Include/Lighting.inc.slang | 38 +- .../Shaders/Shaders/Include/Shadows.inc.slang | 208 +------ .../Include/VisibilityBuffers.inc.slang | 17 - .../Shaders/Material/MaterialPass.cs.slang | 43 +- Source/Shaders/Shaders/Model/Draw.ps.slang | 13 +- Source/Shaders/Shaders/Model/Draw.vs.slang | 1 - .../Shaders/Shaders/Model/DrawSVSM.ps.slang | 10 +- .../Shadows/CascadeFitCameras.cs.slang | 283 --------- .../Shaders/Shadows/CascadeFitRange.cs.slang | 113 ---- .../Shaders/Shadows/CascadeXYReduce.cs.slang | 120 ---- .../Shaders/Shadows/DepthMinMax.cs.slang | 60 -- .../{SDSM.inc.slang => LightBasis.inc.slang} | 66 +-- Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 4 +- .../Shaders/Shaders/Terrain/Culling.cs.slang | 4 +- Source/Shaders/Shaders/Terrain/Draw.vs.slang | 5 +- .../Shaders/Shaders/Terrain/DrawSVSM.ps.slang | 2 +- Source/Shaders/Shaders/Utils/Culling.cs.slang | 6 +- .../Shaders/Utils/CullingInstanced.cs.slang | 6 +- 37 files changed, 266 insertions(+), 2645 deletions(-) delete mode 100644 Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp delete mode 100644 Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h delete mode 100644 Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang delete mode 100644 Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang delete mode 100644 Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang delete mode 100644 Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang rename Source/Shaders/Shaders/Shadows/{SDSM.inc.slang => LightBasis.inc.slang} (58%) diff --git a/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp b/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp index 3b58ffe..6c96500 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp @@ -13,7 +13,6 @@ #include "Game-Lib/ECS/Systems/Animation.h" #include "Game-Lib/ECS/Systems/UpdateAreaLights.h" #include "Game-Lib/ECS/Systems/CalculateCameraMatrices.h" -#include "Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h" #include "Game-Lib/ECS/Systems/CalculateTransformMatrices.h" #include "Game-Lib/ECS/Systems/UpdateDayNightCycle.h" #include "Game-Lib/ECS/Systems/DrawDebugMesh.h" @@ -104,7 +103,6 @@ namespace ECS Systems::FreeflyingCamera::Update(gameRegistry, clampedDeltaTime); Systems::OrbitalCamera::Update(gameRegistry, clampedDeltaTime); Systems::CalculateCameraMatrices::Update(gameRegistry, clampedDeltaTime); - Systems::CalculateShadowCameraMatrices::Update(gameRegistry, clampedDeltaTime); Systems::UpdateSkyboxes::Update(gameRegistry, clampedDeltaTime); Systems::UpdateAreaLights::Update(gameRegistry, clampedDeltaTime); Systems::Editor::EditorTools::Update(gameRegistry, clampedDeltaTime); diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp deleted file mode 100644 index 1e40983..0000000 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp +++ /dev/null @@ -1,351 +0,0 @@ -#include "CalculateShadowCameraMatrices.h" - -#include "Game-Lib/ECS/Components/Camera.h" -#include "Game-Lib/ECS/Singletons/ActiveCamera.h" -#include "Game-Lib/ECS/Singletons/DayNightCycle.h" -#include "Game-Lib/ECS/Systems/UpdateAreaLights.h" -#include "Game-Lib/ECS/Util/Transforms.h" -#include "Game-Lib/Rendering/GameRenderer.h" -#include "Game-Lib/Util/ServiceLocator.h" - -#include - -#include - -#include -#include -#include - -AutoCVar_Int CVAR_ShadowsStable(CVarCategory::Client | CVarCategory::Rendering, "shadowStable", "stable shadows", 1, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_ShadowsFreezeCascades(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades", "freeze cascade cameras and culling planes for debugging", 0, CVarFlags::EditCheckbox); - -AutoCVar_Int CVAR_ShadowCascadeNum(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum", "number of shadow cascades", 4); -AutoCVar_Float CVAR_ShadowCascadeSplitLambda(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda", "split lambda for cascades, between 0.0f and 1.0f", 0.8f); -AutoCVar_Float CVAR_ShadowMaxDistance(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance", "distance the cascades are distributed over, shadows fade out at the end of it", 1000.0f); -AutoCVar_Float CVAR_ShadowCasterMargin(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin", "extends cascade culling toward the sun so far-away casters with long shadows are not culled, depth clamp pancakes them onto the near plane", 2500.0f); -AutoCVar_Float CVAR_ShadowSunUpdateInterval(CVarCategory::Client | CVarCategory::Rendering, "shadowSunUpdateInterval", "game seconds between shadow sun direction updates, a continuously rotating sun re-renders every shadow texel each frame and shimmers", 120.0f); - -AutoCVar_Int CVAR_ShadowCascadeTextureSize(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize", "size of biggest cascade (per side), only applies to cascades created after it is set", 4096); - -namespace ECS::Systems -{ - // The shadow sun steps in discrete intervals, the visual sun stays smooth. A continuously - // rotating light invalidates the whole texel grid every frame, which snapping cannot hide - f32 GetShadowTimeOfDay(f32 timeOfDay) - { - f32 interval = CVAR_ShadowSunUpdateInterval.GetFloat(); - if (interval <= 0.0f) - return timeOfDay; - - return glm::floor(timeOfDay / interval) * interval; - } - - // Converts a Gribb-Hartmann clip plane row (inside if dot(n, p) + d >= 0) to the - // encoding the culling shaders expect: vec4(n, dot(n, pointOnPlane)) with normalized inward n - inline vec4 ExtractPlane(const vec4& row) - { - f32 normalLength = glm::length(vec3(row)); - return vec4(vec3(row) / normalLength, -row.w / normalLength); - } - - void CalculateShadowCameraMatrices::Update(entt::registry& registry, f32 deltaTime) - { - ZoneScopedN("ECS::CalculateShadowCameraMatrices"); - - GameRenderer* gameRenderer = ServiceLocator::GetGameRenderer(); - Renderer::Renderer* renderer = gameRenderer->GetRenderer(); - - RenderResources& renderResources = gameRenderer->GetRenderResources(); - - u32 numCascades = CVAR_ShadowCascadeNum.GetU32(); - i32 cascadeTextureSize = CVAR_ShadowCascadeTextureSize.Get(); - bool stableShadows = CVAR_ShadowsStable.Get() == 1; - - CVarSystem* cvarSystem = CVarSystem::Get(); - const bool useSVSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - - // Initialize any new shadow cascades - // Cascade count can shrink at runtime, we keep the excess cameras and depth images around unused since neither can shrink. - // SVSM clipmaps reuse the same camera slots but need no cascade depth images - u32 numViews = numCascades; - if (useSVSM) - { - u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); - numViews = glm::max(numCascades, numClipmaps); - } - - u32 numCamerasNeeded = numViews + 1; // Main camera + cascades/clipmaps - u32 numCameras = renderResources.cameras.Count(); - if (numCamerasNeeded > numCameras) - { - renderResources.cameras.AddCount(numCamerasNeeded - numCameras); - } - - while (renderResources.shadowDepthCascades.size() < numCascades) - { - u32 cascadeIndex = static_cast(renderResources.shadowDepthCascades.size()); - - // Shadow depth rendertarget - Renderer::DepthImageDesc shadowDepthDesc; - shadowDepthDesc.dimensions = vec2(cascadeTextureSize, cascadeTextureSize); - shadowDepthDesc.dimensionType = Renderer::ImageDimensionType::DIMENSION_ABSOLUTE; - shadowDepthDesc.format = Renderer::DepthImageFormat::D32_FLOAT; - shadowDepthDesc.sampleCount = Renderer::SampleCount::SAMPLE_COUNT_1; - shadowDepthDesc.depthClearValue = 0.0f; - shadowDepthDesc.debugName = "ShadowDepthCascade" + std::to_string(cascadeIndex); - - Renderer::DepthImageID cascadeDepthImage = renderer->CreateDepthImage(shadowDepthDesc); - renderResources.shadowDepthCascades.push_back(cascadeDepthImage); - } - - // Master toggle, no shadow work at all while disabled (resource growth above stays so enabling works at runtime) - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) - return; - - const bool useSDSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowUseSDSM"_h) != 0; - const bool validateParity = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity"_h) != 0; - - // A GPU pass owns the cascade camera slots while SDSM or SVSM is on, the CPU copies go - // stale. On toggle back to CPU ownership force a full re-upload (the loop below would - // normally re-dirty every frame, but not while frozen) - const bool gpuOwnsCameras = useSVSM || (useSDSM && !validateParity); - static bool previousGpuOwnsCameras = gpuOwnsCameras; - if (previousGpuOwnsCameras && !gpuOwnsCameras) - { - renderResources.cameras.SetDirtyElements(1, renderResources.cameras.Count() - 1); - } - previousGpuOwnsCameras = gpuOwnsCameras; - - if (gpuOwnsCameras) - return; - - if (CVAR_ShadowsFreezeCascades.Get()) - return; - - entt::registry::context& ctx = registry.ctx(); - auto& dayNightCycle = ctx.get(); - - // Get light settings, the shadow sun steps in discrete intervals - vec3 lightDirection = UpdateAreaLights::GetLightDirection(GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32())); - - // Get active render camera - auto& activeCamera = ctx.get(); - - auto& cameraTransform = registry.get(activeCamera.entity); - auto& camera = registry.get(activeCamera.entity); - - vec3 cameraPos = cameraTransform.GetWorldPosition(); - const mat4x4& cameraViewProj = camera.worldToClip; - - // Calculate frustum split depths and matrices for the shadow map cascades - f32 cascadeSplitLambda = CVAR_ShadowCascadeSplitLambda.GetFloat(); - - f32 cascadeSplits[Renderer::Settings::MAX_SHADOW_CASCADES]; - - f32 nearClip = camera.nearClip; - f32 farClip = camera.farClip; - - f32 clipRange = farClip - nearClip; - - // Distribute the cascades up to shadowMaxDistance instead of the full view distance. - // The log split is computed from at least 1.0, a tiny near plane makes the log terms - // vanish and degenerates the distribution to its uniform part - f32 minZ = Math::Max(nearClip, 1.0f); - f32 maxZ = Math::Min(nearClip + clipRange, CVAR_ShadowMaxDistance.GetFloat()); - - f32 range = maxZ - minZ; - f32 ratio = maxZ / minZ; - - // Calculate split depths based on view camera frustum - // Based on method presented in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html - for (u32 i = 0; i < numCascades; i++) - { - f32 p = (i + 1) / static_cast(numCascades); - f32 log = minZ * std::pow(ratio, p); - f32 uniform = minZ + range * p; - f32 d = cascadeSplitLambda * (log - uniform) + uniform; - - cascadeSplits[i] = 1.0f - ((d - nearClip) / clipRange); - } - - // Calculate orthographic projection matrix for each cascade - f32 lastSplitDist = 1.0f; - for (u32 i = 0; i < numCascades; i++) - { - f32 splitDist = cascadeSplits[i]; - - vec3 frustumCorners[8] = - { - vec3(-1.0f, 1.0f, 0.0f), - vec3(1.0f, 1.0f, 0.0f), - vec3(1.0f, -1.0f, 0.0f), - vec3(-1.0f, -1.0f, 0.0f), - vec3(-1.0f, 1.0f, 1.0f), - vec3(1.0f, 1.0f, 1.0f), - vec3(1.0f, -1.0f, 1.0f), - vec3(-1.0f, -1.0f, 1.0f) - }; - - mat4x4 invViewProj = glm::inverse(cameraViewProj); - for (u32 j = 0; j < 8; j++) - { - vec4 invCorner = invViewProj * vec4(frustumCorners[j], 1.0f); - frustumCorners[j] = invCorner / invCorner.w; - } - - // Get the corners of the current cascsade slice of the view frustum - for (u32 j = 0; j < 4; j++) - { - vec3 cornerRay = frustumCorners[j + 4] - frustumCorners[j]; - vec3 nearCornerRay = cornerRay * lastSplitDist; - vec3 farCornerRay = cornerRay * splitDist; - - frustumCorners[j + 4] = frustumCorners[j] + farCornerRay; // TODO this looks sus - frustumCorners[j] = frustumCorners[j] + nearCornerRay; - } - - // Get frustum center - vec3 frustumCenter = vec3(0.0f); - - for (u32 j = 0; j < 8; j++) - { - frustumCenter += frustumCorners[j]; - } - frustumCenter /= 8.0f; - - vec3 upDir = -cameraTransform.GetLocalRight(); // This was GetLeft, unsure about this, double check if there are issues - - vec3 minExtents; - vec3 maxExtents; - if (stableShadows) - { - // This needs to be constant for it to be stable, fall back to Z when the light is - // near vertical or lookAt degenerates (sun straight above at noon) - upDir = vec3(0.0f, 1.0f, 0.0f); - if (glm::abs(lightDirection.y) > 0.99f) - { - upDir = vec3(0.0f, 0.0f, 1.0f); - } - - // Calculate the radius of a bounding sphere surrounding the frustum corners - f32 sphereRadius = 0.0f; - for (u32 j = 0; j < 8; j++) - { - f32 dist = glm::length(frustumCorners[j] - frustumCenter); - sphereRadius = Math::Max(sphereRadius, dist); - } - - sphereRadius = std::ceil(sphereRadius * 16.0f) / 16.0f; - - maxExtents = vec3(sphereRadius, sphereRadius, sphereRadius); - minExtents = -maxExtents; - } - else - { - // Create a temporary view matrix for the light, looking along the direction the light travels - vec3 lightCameraPos = frustumCenter; - vec3 lookAt = frustumCenter + lightDirection; - mat4x4 lightView = glm::lookAt(lightCameraPos, lookAt, upDir); - - // Calculate an AABB around the frustum corners - vec3 mins = vec3(std::numeric_limits::max()); - vec3 maxes = vec3(std::numeric_limits::lowest()); - for (u32 j = 0; j < 8; j++) - { - vec4 corner = lightView * vec4(frustumCorners[j], 1.0f); - - mins = glm::min(mins, vec3(corner / corner.w)); - maxes = glm::max(maxes, vec3(corner / corner.w)); - } - - minExtents = mins; - maxExtents = maxes; - - // Adjust the min/max to accommodate the filtering size - const u32 fixedFilterKernelSize = 3; // TODO: Figure this out from the MJP sample - - f32 scale = (cascadeTextureSize + fixedFilterKernelSize) / static_cast(cascadeTextureSize); - minExtents.x *= scale; - minExtents.y *= scale; - maxExtents.x *= scale; - maxExtents.y *= scale; - } - - // Get postion of the shadow camera, minExtents.z is negative so this places it on the sun side - vec3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; - - // Store the far and near planes - f32 farPlane = maxExtents.z; - f32 nearPlane = minExtents.z; - - // Come up with a new orthographic projection matrix for the shadow caster - mat4x4 projMatrix = glm::ortho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, farPlane - nearPlane, 0.0f); - mat4x4 viewMatrix = glm::lookAt(shadowCameraPos, frustumCenter, upDir); - - if (stableShadows) - { - // Create the rounding matrix, by projecting the world-space origin and determining the fractional offset in texel space - mat4x4 shadowMatrix = projMatrix * viewMatrix; - vec4 shadowOrigin = vec4(0.0f, 0.0f, 0.0f, 1.0f); - shadowOrigin = shadowMatrix * shadowOrigin; - shadowOrigin *= static_cast(cascadeTextureSize) / 2.0f; - - vec4 roundedOrigin = glm::round(shadowOrigin); - vec4 roundOffset = roundedOrigin - shadowOrigin; - roundOffset *= 2.0f / static_cast(cascadeTextureSize); - roundOffset.z = 0.0f; - roundOffset.w = 0.0f; - - projMatrix[3] += roundOffset; - } - - // Store split distance and matrix in cascade camera - Camera& cascadeCamera = renderResources.cameras[i + 1]; // +1 because the first camera is the main camera - - cascadeCamera.worldToView = viewMatrix; - cascadeCamera.viewToClip = projMatrix; - - cascadeCamera.viewToWorld = glm::inverse(cascadeCamera.worldToView); - cascadeCamera.clipToView = glm::inverse(cascadeCamera.viewToClip); - - cascadeCamera.worldToClip = cascadeCamera.viewToClip * cascadeCamera.worldToView; - cascadeCamera.clipToWorld = cascadeCamera.viewToWorld * cascadeCamera.clipToView; - - f32 splitDepth = (farClip - (nearClip + splitDist * clipRange));// *-1.0f; - cascadeCamera.eyePosition = vec4(shadowCameraPos, splitDepth); // w holds split depth - - // Extract world-space frustum planes from the view-projection matrix (Gribb-Hartmann) - const mat4x4& m = cascadeCamera.worldToClip; - vec4 row0 = glm::row(m, 0); - vec4 row1 = glm::row(m, 1); - vec4 row2 = glm::row(m, 2); - vec4 row3 = glm::row(m, 3); - - cascadeCamera.frustum[(size_t)FrustumPlane::Left] = ExtractPlane(row3 + row0); - cascadeCamera.frustum[(size_t)FrustumPlane::Right] = ExtractPlane(row3 - row0); - cascadeCamera.frustum[(size_t)FrustumPlane::Bottom] = ExtractPlane(row3 + row1); - cascadeCamera.frustum[(size_t)FrustumPlane::Top] = ExtractPlane(row3 - row1); - cascadeCamera.frustum[(size_t)FrustumPlane::Near] = ExtractPlane(row3 - row2); // Reversed Z, depth 1 is near - cascadeCamera.frustum[(size_t)FrustumPlane::Far] = ExtractPlane(row2); - - // The near plane sits at the shadow camera on the sun side. Casters beyond it still render - // correctly (depth clamp pancakes them), so culling must not reject them - cascadeCamera.frustum[(size_t)FrustumPlane::Near].w -= CVAR_ShadowCasterMargin.GetFloat(); - -#if NC_DEBUG - for (u32 j = 0; j < 6; j++) - { - const vec4& plane = cascadeCamera.frustum[j]; - f32 distance = glm::dot(vec3(plane), frustumCenter) - plane.w; - NC_ASSERT(distance > 0.0f, "CalculateShadowCameraMatrices : Cascade frustum plane {0} does not contain the frustum center", j); - } -#endif - if (!useSDSM) // In parity-validation mode the CPU result stays local for comparison, the GPU fit owns the upload - { - renderResources.cameras.SetDirtyElement(i+1); - } - - lastSplitDist = cascadeSplits[i]; - } - } -} diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h b/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h deleted file mode 100644 index 15e5613..0000000 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include -#include - -namespace ECS::Systems -{ - // Quantizes the time of day used for the shadow sun direction, see shadowSunUpdateInterval - f32 GetShadowTimeOfDay(f32 timeOfDay); - - class CalculateShadowCameraMatrices - { - public: - static void Update(entt::registry& registry, f32 deltaTime); - }; -} \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index b497f66..5c55d06 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -20,9 +20,21 @@ #include AutoCVar_Int CVAR_SunDebugFullRotation(CVarCategory::Client | CVarCategory::Rendering, "sunDebugFullRotation", "debug: sun does a full rotation per day instead of the authored wobble", 0, CVarFlags::EditCheckbox); +AutoCVar_Float CVAR_ShadowSunUpdateInterval(CVarCategory::Client | CVarCategory::Rendering, "shadowSunUpdateInterval", "game seconds between shadow sun direction updates, a continuously rotating sun re-renders every shadow texel each frame and shimmers", 120.0f); namespace ECS::Systems { + // The shadow sun steps in discrete intervals, the visual sun stays smooth. A continuously + // rotating light invalidates the whole texel grid every frame, which snapping cannot hide + f32 GetShadowTimeOfDay(f32 timeOfDay) + { + f32 interval = CVAR_ShadowSunUpdateInterval.GetFloat(); + if (interval <= 0.0f) + return timeOfDay; + + return glm::floor(timeOfDay / interval) * interval; + } + vec3 UnpackU32BGRToColor(u32 bgr) { vec3 result; diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h index c48f4ec..2985c76 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h @@ -4,6 +4,9 @@ namespace ECS::Systems { + // Quantizes the time of day used for the shadow sun direction, see shadowSunUpdateInterval + f32 GetShadowTimeOfDay(f32 timeOfDay); + class UpdateAreaLights { public: diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index 736a530..1979b40 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -80,54 +80,76 @@ namespace Editor const std::string rightHeaderText = "Survived / Total (%)"; static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit /*| ImGuiTableFlags_BordersOuter*/ | ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersH | ImGuiTableFlags_ContextMenuInBody; - // Under SVSM the shadow views are clipmaps instead of cascades - const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - u32 numCascades = useSVSM ? *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h) - : *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h); + const u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); - // SDSM reduced depth bounds + // SVSM stat block { GameRenderer* gameRenderer = ServiceLocator::GetGameRenderer(); ShadowRenderer* shadowRenderer = gameRenderer->GetShadowRenderer(); - f32 minDistance = 0.0f; - f32 maxDistance = 0.0f; - if (shadowRenderer && shadowRenderer->GetDepthBoundsViewDistances(gameRenderer->GetRenderResources(), minDistance, maxDistance)) + // Copy the shadow stat block (SVSM pool/dynamic/instances, per-clipmap table) as + // semicolon-separated lines, same rule as the render pass CSV + if (shadowRenderer) { - f32 shadowMaxDistance = static_cast(*CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance"_h)); - ImGui::Text("Visible Depth: %.1f - %.1f (Shadow Max: %.0f)", minDistance, maxDistance, shadowMaxDistance); - - f32 effectiveMin = 0.0f; - f32 effectiveMax = 0.0f; - if (shadowRenderer->GetEffectiveShadowRange(effectiveMin, effectiveMax)) - { - ImGui::Text("Shadow Range: %.1f - %.1f (used by cascades)", effectiveMin, effectiveMax); - } - - for (u32 i = 0; i < numCascades; i++) + const char* copyStatsLabel = "Copy Stats"; + ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - ImGui::CalcTextSize(copyStatsLabel).x - ImGui::GetStyle().FramePadding.x * 2.0f); + if (ImGui::SmallButton(copyStatsLabel)) { - vec3 extents = vec3(0.0f); - bool valid = false; - if (!shadowRenderer->GetCascadeFittedBounds(i, extents, valid)) - break; + std::string csv; + char line[512]; - if (valid) - { - ImGui::Text("C%u: %.1f x %.1f x %.1f", i, extents.x, extents.y, extents.z); - } - else { - ImGui::Text("C%u: empty", i); + u32 freePages = 0; + u32 totalPages = 0; + u32 overflow = 0; + u32 invalidationCause = 0; + shadowRenderer->GetSVSMGlobalStats(freePages, totalPages, overflow, invalidationCause); + snprintf(line, sizeof(line), "SVSMPool;used;%u;total;%u;overflow;%u;invalidationCause;%u\n", totalPages - glm::min(freePages, totalPages), totalPages, overflow, invalidationCause); + csv += line; + + u32 dynamicLive = 0; + u32 dynamicTotal = 0; + u32 dynamicOverflow = 0; + shadowRenderer->GetSVSMDynamicStats(dynamicLive, dynamicTotal, dynamicOverflow); + + u32 dynamicCasters = 0; + u32 animatedCasters = 0; + u32 droppedAABBs = 0; + shadowRenderer->GetSVSMCasterStats(dynamicCasters, animatedCasters, droppedAABBs); + snprintf(line, sizeof(line), "SVSMDynamic;live;%u;total;%u;overflow;%u;dynamicCasters;%u;animatedCasters;%u;droppedAABBs;%u\n", dynamicLive, dynamicTotal, dynamicOverflow, dynamicCasters, animatedCasters, droppedAABBs); + csv += line; + + snprintf(line, sizeof(line), "SVSMBudgetUsed;%u\n", shadowRenderer->GetSVSMBudgetUsed()); + csv += line; + + if (ModelRenderer* statsModelRenderer = gameRenderer->GetModelRenderer()) + { + std::string dynInstances = "SVSMDynInstances"; + for (u32 view = 1; view <= numCascades && view < Renderer::Settings::MAX_VIEWS; view++) + { + dynInstances += ";" + std::to_string(statsModelRenderer->GetNumSVSMDynamicInstances(view)); + } + csv += dynInstances + "\n"; + } + + csv += "Clipmap;extent;marked;resident;dirty;invalidated;evicted;dynamic;deferred\n"; + for (u32 i = 0; i < numCascades; i++) + { + ShadowRenderer::SVSMClipmapStats stats; + if (!shadowRenderer->GetSVSMClipmapStats(i, stats)) + break; + + snprintf(line, sizeof(line), "P%u;%.0f;%u;%u;%u;%u;%u;%u;%u\n", i, stats.extent, stats.marked, stats.resident, stats.dirty, stats.invalidated, stats.evicted, stats.dynamicLive, stats.deferred); + csv += line; + } } + + ImGui::SetClipboardText(csv.c_str()); } } - else - { - ImGui::Text("Visible Depth: - (no valid samples)"); - } // SVSM page table state, one frame old - if (*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1 && shadowRenderer) + if (shadowRenderer) { u32 freePages = 0; u32 totalPages = 0; @@ -717,8 +739,7 @@ namespace Editor std::string viewName = "Main View Instances"; if (viewID > 0) { - const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - viewName = (useSVSM ? "Clipmap " : "Shadow Cascade ") + std::to_string(viewID - 1) + " Drawcalls"; + viewName = "Clipmap " + std::to_string(viewID - 1) + " Drawcalls"; } if (!_drawCallStatsOnlyForMainView) @@ -827,8 +848,7 @@ namespace Editor std::string viewName = "Main View Triangles"; if (viewID > 0) { - const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - viewName = (useSVSM ? "Clipmap " : "Shadow Cascade ") + std::to_string(viewID - 1) + " Triangles"; + viewName = "Clipmap " + std::to_string(viewID - 1) + " Triangles"; } if (!_drawCallStatsOnlyForMainView) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index 9baf594..ff97543 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -205,7 +205,6 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) DrawParams drawParams; drawParams.cullingEnabled = true; // The occuder pass only makes sense if culling is enabled - drawParams.shadowPass = false; drawParams.viewIndex = 0; drawParams.rt0 = params.rt0; drawParams.rt1 = params.rt1; @@ -258,16 +257,6 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); } - if (i == 1) - { - uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); - - params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - - params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); - } - // Fill the occluders to draw { std::string debugName = params.passName + " Occlusion Fill"; @@ -313,7 +302,6 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) DrawParams drawParams; drawParams.cullingEnabled = true; // The occuder pass only makes sense if culling is enabled - drawParams.shadowPass = i > 0; drawParams.viewIndex = i; drawParams.rt0 = params.rt0; drawParams.rt1 = params.rt1; @@ -338,11 +326,10 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) params.commandList->PopMarker(); } - // Finish by resetting the viewport, scissor and depth bias + // Finish by resetting the viewport and scissor vec2 renderSize = _renderer->GetRenderSize(); params.commandList->SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); params.commandList->SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); - params.commandList->SetDepthBias(0, 0, 0); } } @@ -765,27 +752,15 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) for (u32 i = params.firstViewIndex; i < params.numCascades + 1; i++) { - std::string markerName = (i == 0) ? "Main" : (params.svsmPass ? "Clipmap " : "Cascade ") + std::to_string(i - 1); + std::string markerName = (i == 0) ? "Main" : "Clipmap " + std::to_string(i - 1); params.commandList->PushMarker(markerName, Color::PastelYellow); if (i == 1) { - if (params.svsmPass) - { - // The viewport spans the virtual texture so SV_Position.xy is the virtual texel. - // No depth bias: there is no depth attachment for it to apply to - params.commandList->SetViewport(0, 0, static_cast(params.svsmExtent.x), static_cast(params.svsmExtent.y), 0.0f, 1.0f); - params.commandList->SetScissorRect(0, params.svsmExtent.x, 0, params.svsmExtent.y); - } - else - { - uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]); - - params.commandList->SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - params.commandList->SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - - params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor); - } + // The viewport spans the virtual texture so SV_Position.xy is the virtual texel. + // No depth bias: there is no depth attachment for it to apply to + params.commandList->SetViewport(0, 0, static_cast(params.svsmExtent.x), static_cast(params.svsmExtent.y), 0.0f, 1.0f); + params.commandList->SetScissorRect(0, params.svsmExtent.x, 0, params.svsmExtent.y); } // Reset the counters @@ -856,7 +831,6 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) DrawParams drawParams; drawParams.cullingEnabled = params.cullingEnabled; - drawParams.shadowPass = i > 0; drawParams.svsmPass = params.svsmPass; drawParams.svsmExtent = params.svsmExtent; drawParams.viewIndex = i; @@ -939,7 +913,6 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) DrawParams drawParams; drawParams.cullingEnabled = params.cullingEnabled; - drawParams.shadowPass = true; drawParams.svsmPass = params.svsmPass; drawParams.svsmDynamicPass = true; drawParams.svsmExtent = params.svsmExtent; @@ -962,11 +935,10 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) params.commandList->PopMarker(); } - // Finish by resetting the viewport, scissor and depth bias + // Finish by resetting the viewport and scissor vec2 renderSize = _renderer->GetRenderSize(); params.commandList->SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); params.commandList->SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); - params.commandList->SetDepthBias(0, 0, 0); } void CulledRenderer::BindCullingResource(CullingResourcesBase& resources) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index c1d8e68..6e23787 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -37,7 +37,6 @@ class CulledRenderer { public: bool cullingEnabled = false; - bool shadowPass = false; bool svsmPass = false; // Attachment-less page render, needs an explicit render area bool svsmDynamicPass = false; // Caster-split dynamic instances into the dynamic pool u32 svsmRectIndex = 0xFFFFFFFFu; // Clip rect this draw renders (0-2), SVSM_CLIP_RECT_DISABLED = no clipping @@ -155,10 +154,6 @@ class CulledRenderer u32 numCascades = 0; - f32 biasConstantFactor = 0.0f; - f32 biasClamp = 0.0f; - f32 biasSlopeFactor = 0.0f; - bool enableDrawing = false; // Allows us to do everything but the actual drawcall, for debugging bool disableTwoStepCulling = false; }; @@ -313,13 +308,9 @@ class CulledRenderer u32 baseInstanceLookupOffset = 0; // Instanced only u32 drawCallDataSize = 0; // Instanced only - u32 firstViewIndex = 0; // 0 = main view first, 1 = cascades only + u32 firstViewIndex = 0; // 0 = main view first, 1 = clipmap views only u32 numCascades = 0; - f32 biasConstantFactor = 0.0f; - f32 biasClamp = 0.0f; - f32 biasSlopeFactor = 0.0f; - bool enableDrawing = false; // Allows us to do everything but the actual drawcall, for debugging bool cullingEnabled = false; diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index e3c407a..0ec05b2 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -410,18 +410,12 @@ f32 GameRenderer::Render() _liquidRenderer->AddCullingPass(&renderGraph, _resources, _frameIndex); _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); - // Cascade block, runs after the main depth is complete so the cascades can be fitted to the - // visible samples (SDSM: depth range + per-cascade light-space bounds), culled and drawn the same frame. - // The SVSM update (shadowTechnique 1) analyzes the same depth to mark and allocate virtual shadow - // pages, it runs alongside the CSM path until the SVSM rendering and sampling stages land - _shadowRenderer->AddDepthMinMaxPass(&renderGraph, _resources, _frameIndex); - _shadowRenderer->AddCascadeFitPass(&renderGraph, _resources, _frameIndex); + // SVSM block, runs after the main depth is complete so page marking can analyze the visible + // samples, then allocates, culls per clipmap view and renders the dirty pages the same frame _shadowRenderer->AddSVSMUpdatePass(&renderGraph, _resources, _frameIndex); - _shadowRenderer->AddShadowPass(&renderGraph, _resources, _frameIndex); + _shadowRenderer->AddSVSMBindPass(&renderGraph, _resources, _frameIndex); _terrainRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); _modelRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); - _terrainRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); - _modelRenderer->AddCascadeGeometryPass(&renderGraph, _resources, _frameIndex); _terrainRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); _modelRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index ff1fc02..72a3ff1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -236,8 +236,8 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende { struct Constants { - vec4 renderInfo; // x = Render Width, y = Render Height, z = 1/Width, w = 1/Height - uvec4 lightInfo; // x = Directional Light Count, Y = Point Light Count, Z = Cascade Count, W = Shadows Enabled + vec4 renderInfo; // x = Render Width, y = Render Height, z = 1/Width, w = 1/Height + uvec4 lightInfo; // x = Directional Light Count, y = Shadows Ready (enabled, strength > 0, pool created), zw = UNUSED uvec4 tileInfo; // xy = Num Tiles, zw = UNUSED vec4 fogColor; vec4 fogSettings; // x = Enabled, y = Begin Fog Blend Dist, z = End Fog Blend Dist, w = UNUSED @@ -248,8 +248,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Color patchEdgeColor; Color vertexColor; Color brushColor; - vec4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias - vec4 svsmSettings; // x = Constant Bias (world meters) + vec4 shadowSettings; // x = Shadow Strength, y = Normal Offset Bias, z = SVSM Constant Bias (world meters), w = UNUSED }; Constants* constants = graphResources.FrameNew(); @@ -258,14 +257,14 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende constants->renderInfo = vec4(outputSize, 1.0f / outputSize); CVarSystem* cvarSystem = CVarSystem::Get(); - const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); const f32 shadowStrength = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength")); - // lightInfo.y selects the shadow technique: 0 = cascades, 1 = sparse virtual shadow maps + // Shadows sample only once the (lazily created) page pool exists, the placeholder + // pool bound before then must never be read ShadowRenderer* shadowRenderer = _gameRenderer->GetShadowRenderer(); - const bool useSVSM = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique") == 1 + const bool shadowsReady = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")) != 0 + && shadowStrength > 0.0f && shadowRenderer != nullptr && shadowRenderer->GetSVSMPagePool() != Renderer::ImageID::Invalid(); - const u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")) && shadowStrength > 0.0f; - constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), useSVSM ? 1 : 0, numCascades, shadowEnabled); + constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), shadowsReady ? 1 : 0, 0, 0); constants->tileInfo = uvec4(_lightRenderer->CalculateNumTiles2D(outputSize), 0, 0); @@ -287,13 +286,9 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende constants->vertexColor = terrainTools->GetVertexColor(); constants->brushColor = terrainTools->GetBrushColor(); - f32 shadowFilterSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterSize")); - f32 shadowFilterPenumbraSize = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterPenumbraSize")); f32 shadowNormalOffsetBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias")); - constants->shadowFilterSettings = vec4(shadowFilterSize, shadowFilterPenumbraSize, shadowStrength, shadowNormalOffsetBias); - f32 svsmConstantBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias")); - constants->svsmSettings = vec4(svsmConstantBias, 0.0f, 0.0f, 0.0f); + constants->shadowSettings = vec4(shadowStrength, shadowNormalOffsetBias, svsmConstantBias, 0.0f); commandList.PushConstant(constants, 0, sizeof(Constants)); } @@ -362,11 +357,6 @@ void MaterialRenderer::CreatePermanentResources() CreateMaterialPipeline(); }); - CVarSystem::Get()->AddOnIntValueChanged(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterMode"_h, [this](const i32& val) - { - CreateMaterialPipeline(); - }); - // Register pipelines with descriptor sets and init _preEffectsPassDescriptorSet.RegisterPipeline(_renderer, _preEffectsPipeline); _preEffectsPassDescriptorSet.Init(_renderer); @@ -407,12 +397,9 @@ void MaterialRenderer::CreatePermanentResources() void MaterialRenderer::CreateMaterialPipeline() { - const i32 shadowFilterMode = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterMode"_h); - - std::vector permutationFields = + std::vector permutationFields = { { "DEBUG_ID", std::to_string(CVAR_VisibilityBufferDebugID.Get()) }, - { "SHADOW_FILTER_MODE", std::to_string(shadowFilterMode) }, { "EDITOR_MODE", CVAR_DrawTerrainWireframe.Get() == ShowFlag::ENABLED ? "1" : "0" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Material/MaterialPass.cs", permutationFields); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 742584c..264ad49 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -132,8 +132,8 @@ void ModelRenderer::Update(f32 deltaTime) _animatedCasterAABBs.clear(); CVarSystem* cvarSystem = CVarSystem::Get(); - const bool svsmActive = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - const f32 range = svsmActive ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; + const bool shadowsEnabled = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 1; + const f32 range = shadowsEnabled ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; const bool split = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1; u32 modelID; @@ -572,10 +572,6 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe data.visibilityBuffer = builder.Write(resources.visibilityBuffer, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.depth[0] = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); @@ -646,10 +642,6 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe params.numCascades = numCascades; - params.biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - params.biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - params.biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - params.enableDrawing = CVAR_ModelDrawOccluders.Get(); params.disableTwoStepCulling = CVAR_ModelDisableTwoStepCulling.Get(); @@ -797,10 +789,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe data.visibilityBuffer = builder.Write(resources.visibilityBuffer, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.depth[0] = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); @@ -875,10 +863,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.numCascades = numCascades; - params.biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - params.biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - params.biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - params.enableDrawing = CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; @@ -905,21 +889,8 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re if (_opaqueCullingResources.GetDrawCalls().Count() == 0) return; - // Under SVSM the same per-view culling runs against the clipmap cameras, which need no - // cascade depth images - const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - u32 numCascades; - if (useSVSM) - { - numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); - } - else - { - numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - } - if (numCascades == 0) - return; + // The same per-view culling the main view uses, run against the clipmap cameras + const u32 numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); struct Data { @@ -990,152 +961,6 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re }); } -void ModelRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) -{ - ZoneScoped; - - if (!CVAR_ModelRendererEnabled.Get()) - return; - - if (!CVAR_ModelCullingEnabled.Get()) // Cascades are driven by the culled bitmask slices - return; - - CVarSystem* cvarSystem = CVarSystem::Get(); - - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) - return; - - if (CVAR_ModelsCastShadow.Get() != 1) - return; - - // SVSM renders pages through AddSVSMGeometryPass instead - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1) - return; - - if (_opaqueCullingResources.GetDrawCalls().Count() == 0) - return; - - u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - if (numCascades == 0) - return; - - struct Data - { - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; - - Renderer::BufferMutableResource drawCallsBuffer; - Renderer::BufferMutableResource culledDrawCallsBuffer; - Renderer::BufferMutableResource culledDrawCallCountBuffer; - - Renderer::BufferMutableResource culledInstanceCountsBuffer; - - Renderer::BufferMutableResource drawCountBuffer; - Renderer::BufferMutableResource triangleCountBuffer; - Renderer::BufferMutableResource drawCountReadBackBuffer; - Renderer::BufferMutableResource triangleCountReadBackBuffer; - - Renderer::DescriptorSetResource globalSet; - Renderer::DescriptorSetResource modelSet; - Renderer::DescriptorSetResource fillSet; - Renderer::DescriptorSetResource createIndirectSet; - Renderer::DescriptorSetResource drawSet; - }; - - renderGraph->AddPass("Model (O) Cascade Geometry", - [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) - { - using BufferUsage = Renderer::BufferPassUsage; - - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } - - builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_indices.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_textureDatas.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_textureUnits.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_instanceDatas.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_instanceMatrices.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_boneMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - builder.Read(_textureTransformMatrices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - - builder.Write(_animatedVertices.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - - GeometryPassSetup(data, builder, &_opaqueCullingResources, frameIndex); - builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - - data.culledInstanceCountsBuffer = builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::TRANSFER | BufferUsage::COMPUTE); - - builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - - data.globalSet = builder.Use(resources.globalDescriptorSet); - data.modelSet = builder.Use(resources.modelDescriptorSet); - data.fillSet = builder.Use(_opaqueCullingResources.GetGeometryFillDescriptorSet()); - data.createIndirectSet = builder.Use(_opaqueCullingResources.GetCreateIndirectAfterCullDescriptorSet()); - - return true; // Return true from setup to enable this pass, return false to disable it - }, - [this, &resources, frameIndex, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) - { - GPU_SCOPED_PROFILER_ZONE(commandList, ModelCascadeGeometry); - - CulledRenderer::GeometryPassParams params; - params.passName = "Opaque"; - params.graphResources = &graphResources; - params.commandList = &commandList; - params.cullingResources = &_opaqueCullingResources; - - params.frameIndex = frameIndex; - for (u32 i = 1; i < numCascades + 1; i++) - { - params.depth[i] = data.depth[i]; - } - - params.drawCallsBuffer = data.drawCallsBuffer; - params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; - params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; - params.culledInstanceCountsBuffer = data.culledInstanceCountsBuffer; - - params.drawCountBuffer = data.drawCountBuffer; - params.triangleCountBuffer = data.triangleCountBuffer; - params.drawCountReadBackBuffer = data.drawCountReadBackBuffer; - params.triangleCountReadBackBuffer = data.triangleCountReadBackBuffer; - - params.globalDescriptorSet = data.globalSet; - params.fillDescriptorSet = data.fillSet; - params.createIndirectDescriptorSet = data.createIndirectSet; - params.drawDescriptorSet = data.drawSet; - - params.drawCallback = [&](DrawParams& drawParams) - { - drawParams.descriptorSets = { - &data.globalSet, - &data.modelSet - }; - Draw(resources, frameIndex, graphResources, commandList, drawParams); - }; - - params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); - params.drawCallDataSize = sizeof(DrawCallData); - - params.firstViewIndex = 1; - params.numCascades = numCascades; - - params.biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - params.biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - params.biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - - params.enableDrawing = CVAR_ModelDrawGeometry.Get(); - params.cullingEnabled = true; - - GeometryPass(params); - }); -} - void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer) { ZoneScoped; @@ -1154,9 +979,6 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend if (CVAR_ModelsCastShadow.Get() != 1) return; - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) != 1) - return; - if (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid()) return; @@ -3301,7 +3123,6 @@ void ModelRenderer::CreateModelPipelines() std::vector vertexPermutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "0"}, { "SVSM_PASS", "0"} }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); @@ -3309,56 +3130,11 @@ void ModelRenderer::CreateModelPipelines() pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); Renderer::PixelShaderDesc pixelShaderDesc; - std::vector pixelPermutationFields = - { - { "SHADOW_PASS", "0" } - }; - shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.ps", pixelPermutationFields); - pixelShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.ps"); + pixelShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Model/Draw.ps"_h, "Model/Draw.ps"); pipelineDesc.states.pixelShader = _renderer->LoadShader(pixelShaderDesc); _drawPipeline = _renderer->CreatePipeline(pipelineDesc); } - // Shadows - { - Renderer::GraphicsPipelineDesc pipelineDesc; - pipelineDesc.debugName = "Model Draw Shadow"; - - // Depth state - pipelineDesc.states.depthStencilState.depthEnable = true; - pipelineDesc.states.depthStencilState.depthWriteEnable = true; - pipelineDesc.states.depthStencilState.depthFunc = Renderer::ComparisonFunc::GREATER; - - // Rasterizer state - pipelineDesc.states.rasterizerState.cullMode = Renderer::CullMode::NONE; - pipelineDesc.states.rasterizerState.depthBiasEnabled = true; - pipelineDesc.states.rasterizerState.depthClampEnabled = true; - - pipelineDesc.states.depthStencilFormat = Renderer::DepthImageFormat::D32_FLOAT; - - Renderer::VertexShaderDesc vertexShaderDesc; - std::vector vertexPermutationFields = - { - { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1"}, - { "SVSM_PASS", "0"} - }; - u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); - vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.vs"); - pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); - - Renderer::PixelShaderDesc pixelShaderDesc; - std::vector pixelPermutationFields = - { - { "SHADOW_PASS", "1" } - }; - shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.ps", pixelPermutationFields); - pixelShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Model/Draw.ps"); - pipelineDesc.states.pixelShader = _renderer->LoadShader(pixelShaderDesc); - - // Draw - _drawShadowPipeline = _renderer->CreatePipeline(pipelineDesc); - } // SVSM pages: attachment-less, the pixel shader keeps the alpha-key discard and writes depth // into the page pool with image atomics. Depth clamp stays on so pancaked casters rasterize { @@ -3373,7 +3149,6 @@ void ModelRenderer::CreateModelPipelines() std::vector vertexPermutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1"}, { "SVSM_PASS", "1"} }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Model/Draw.vs", vertexPermutationFields); @@ -4155,20 +3930,17 @@ void ModelRenderer::Draw(const RenderResources& resources, u8 frameIndex, Render } else { - if (!params.shadowPass) + renderPassDesc.renderTargets[0] = params.rt0; + if (params.rt1 != Renderer::ImageMutableResource::Invalid()) { - renderPassDesc.renderTargets[0] = params.rt0; - if (params.rt1 != Renderer::ImageMutableResource::Invalid()) - { - renderPassDesc.renderTargets[1] = params.rt1; - } + renderPassDesc.renderTargets[1] = params.rt1; } renderPassDesc.depthStencil = params.depth; } commandList.BeginRenderPass(renderPassDesc); Renderer::GraphicsPipelineID pipeline = params.svsmPass ? (params.svsmDynamicPass ? _drawSVSMDynamicPipeline : _drawSVSMPipeline) - : (params.shadowPass ? _drawShadowPipeline : _drawPipeline); + : _drawPipeline; commandList.BeginPipeline(pipeline); struct PushConstants diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index fa5ab6a..f0f63b4 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -348,8 +348,7 @@ class ModelRenderer : CulledRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); // Called once by ShadowRenderer at init, buffer binds must happen before the first frame @@ -456,7 +455,6 @@ class ModelRenderer : CulledRenderer CullingResourcesIndexed _transparentSkyboxCullingResources; Renderer::GraphicsPipelineID _drawPipeline; - Renderer::GraphicsPipelineID _drawShadowPipeline; Renderer::GraphicsPipelineID _drawSVSMPipeline; Renderer::GraphicsPipelineID _drawSVSMDynamicPipeline; Renderer::GraphicsPipelineID _drawTransparentPipeline; diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h b/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h index e425382..58044a9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h @@ -51,8 +51,6 @@ struct RenderResources Renderer::ImageID depthColorCopy; Renderer::DepthImageID skyboxDepth; - std::vector shadowDepthCascades; - Renderer::DepthImageID debugRendererDepth; Renderer::SemaphoreID sceneRenderedSemaphore; // This semaphore tells the present function when the scene is ready to be blitted and presented diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index ca75c81..8468af4 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -6,7 +6,7 @@ #include #include #include -#include + #include #include #include @@ -27,30 +27,8 @@ AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 1, CVarFlags::EditCheckbox); AutoCVar_Float CVAR_ShadowStrength(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength", "directional shadow strength, overwritten each frame from the sun elevation", 1.0f); - -AutoCVar_Int CVAR_ShadowDebugMatrices(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatrices", "debug shadow matrices by applying them to the camera", 0, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_ShadowDebugMatrixIndex(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugMatricesIndex", "index of the cascade to debug", 0); - -AutoCVar_Int CVAR_ShadowDrawMatrices(CVarCategory::Client | CVarCategory::Rendering, "shadowDrawMatrices", "debug shadow matrices by debug drawing them", 0, CVarFlags::EditCheckbox); - -AutoCVar_Int CVAR_ShadowFilterMode(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterMode", "0: No filtering, 1: Percentage Closer Filtering, 2: Percentage Closer Soft Shadows", 1); -AutoCVar_Float CVAR_ShadowFilterSize(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterSize", "size of the filter used for shadow sampling", 3.0f); -AutoCVar_Float CVAR_ShadowFilterPenumbraSize(CVarCategory::Client | CVarCategory::Rendering, "shadowFilterPenumbraSize", "size of the filter used for penumbra sampling", 3.0f); - -AutoCVar_Float CVAR_ShadowDepthBiasConstantFactor(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant", "constant factor of depth bias to prevent shadow acne", -2.0f); -AutoCVar_Float CVAR_ShadowDepthBiasClamp(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp", "clamp of depth bias to prevent shadow acne", 0.0f); -AutoCVar_Float CVAR_ShadowDepthBiasSlopeFactor(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope", "slope factor of depth bias to prevent shadow acne", -5.0f); -AutoCVar_Float CVAR_ShadowNormalOffsetBias(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias", "receiver offset along the surface normal in cascade texels, fights acne on hard angles", 1.0f); - -AutoCVar_Int CVAR_ShadowUseSDSM(CVarCategory::Client | CVarCategory::Rendering, "shadowUseSDSM", "fit cascade cameras on the GPU instead of the legacy CPU path", 1, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_ShadowSDSMUseDepthBounds(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMUseDepthBounds", "distribute cascades over the visible depth range instead of the full shadow range", 1, CVarFlags::EditCheckbox); -AutoCVar_Float CVAR_ShadowSDSMQuantizeStep(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMQuantizeStep", "SDSM bounds are quantized outward to this step to keep stable snapping effective", 8.0f); -AutoCVar_Float CVAR_ShadowSDSMShrinkDelay(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMShrinkDelay", "seconds the visible range must stay smaller before the SDSM bounds shrink to it in one jump, expansion is instant", 2.5f); -AutoCVar_Int CVAR_ShadowSDSMValidateParity(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMValidateParity", "compare GPU-fitted cascade cameras against the CPU math and log the max delta", 0, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_ShadowSDSMUseXYBounds(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMUseXYBounds", "fit cascades to the light-space footprint of visible samples: 0 = off, 1 = diagnostics only, 2 = drive the cameras", 2); -AutoCVar_Float CVAR_ShadowSDSMXYMarginTexels(CVarCategory::Client | CVarCategory::Rendering, "shadowSDSMXYMarginTexels", "filter-kernel margin in texels added to the fitted XY bounds, the normal offset bias is added on top", 4.0f); - -AutoCVar_Int CVAR_ShadowTechnique(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique", "0 = cascaded shadow maps (SDSM), 1 = sparse virtual shadow maps", 0); +AutoCVar_Float CVAR_ShadowNormalOffsetBias(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias", "receiver offset along the surface normal in shadow texels, fights acne on hard angles", 1.0f); +AutoCVar_Float CVAR_ShadowCasterMargin(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin", "extends clipmap culling toward the sun so far-away casters with long shadows are not culled, depth clamp pancakes them onto the near plane", 2500.0f); AutoCVar_Int CVAR_SVSMNumClipmaps(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps", "number of SVSM clipmap rings, each ring doubles the covered area", 6); AutoCVar_Float CVAR_SVSMClipmap0Extent(CVarCategory::Client | CVarCategory::Rendering, "svsmClipmap0Extent", "world extent of the finest SVSM clipmap window in meters, finer rings multiply the near-field page demand", 64.0f); AutoCVar_Int CVAR_SVSMVirtualSize(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize", "virtual texture resolution per clipmap", 8192); @@ -105,10 +83,6 @@ ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameR , _debugRenderer(debugRenderer) , _terrainRenderer(terrainRenderer) , _modelRenderer(modelRenderer) - , _depthMinMaxDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) - , _cascadeFitRangeDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) - , _cascadeXYReduceDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) - , _cascadeFitCamerasDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _svsmPrepareDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _svsmInvalidateAABBsDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) , _svsmPageUpdateADescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) @@ -133,41 +107,8 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) { ZoneScoped; - // Read back last frame's reduced depth bounds for diagnostics - { - u32* readBackData = static_cast(_renderer->MapBuffer(_depthMinMaxReadBackBuffer)); - if (readBackData != nullptr) - { - _depthMinMaxReadBack[0] = readBackData[0]; - _depthMinMaxReadBack[1] = readBackData[1]; - } - _renderer->UnmapBuffer(_depthMinMaxReadBackBuffer); - } - _lastDeltaTime = deltaTime; - CVarSystem* cvarSystem = CVarSystem::Get(); - const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); - const bool useSDSM = CVAR_ShadowUseSDSM.Get(); - - // With SDSM the cascade cameras live on the GPU, the debug tools read the (one frame old) readback copies - if (useSDSM) - { - Camera* readBackData = static_cast(_renderer->MapBuffer(_cascadeCamerasReadBackBuffer)); - if (readBackData != nullptr) - { - memcpy(_readBackCascadeCameras, readBackData, sizeof(Camera) * Renderer::Settings::MAX_SHADOW_CASCADES); - } - _renderer->UnmapBuffer(_cascadeCamerasReadBackBuffer); - - f32* sdsmData = static_cast(_renderer->MapBuffer(_sdsmDataReadBackBuffer)); - if (sdsmData != nullptr) - { - memcpy(_sdsmDataReadBack, sdsmData, sizeof(f32) * SDSM_DATA_FLOAT_COUNT); - } - _renderer->UnmapBuffer(_sdsmDataReadBackBuffer); - } - // SVSM: last frame's page table stats plus this frame's caster bounds. With the caster split, // the current dynamic set (moved + animated) feeds dynamic page marking, and only // classification transitions and spawns/despawns invalidate cached static content. With the @@ -178,9 +119,9 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) _svsmNumDynamicCasters = 0; _svsmNumAnimatedCasters = 0; _svsmDynamicAABBsDropped = 0; - if (CVAR_ShadowTechnique.Get() == 1 && CVAR_ShadowEnabled.Get()) + if (CVAR_ShadowEnabled.Get()) { - // The pools are the SVSM VRAM cost, only allocated once the technique is actually used + // The pools are the SVSM VRAM cost, only allocated once shadows are actually used if (_svsmPagePool == Renderer::ImageID::Invalid()) { Renderer::ImageDesc poolDesc; @@ -406,286 +347,13 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } else { - // SVSM inactive: spawn/despawn invalidations would accumulate in the queue unboundedly. - // Discard them (maxPairs 0 appends nothing) and re-bake the whole cache when the - // technique comes back instead + // Shadows off: spawn/despawn invalidations would accumulate in the queue unboundedly. + // Discard them (maxPairs 0 appends nothing) and re-bake the whole cache on re-enable instead if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, 0) > 0) { _svsmForceInvalidateAll = true; } } - - auto GetCascadeCamera = [&](u32 cascadeIndex) -> const Camera& - { - return useSDSM ? _readBackCascadeCameras[cascadeIndex] : resources.cameras[cascadeIndex + 1]; - }; - - if (CVAR_ShadowSDSMValidateParity.Get() && useSDSM) - { - // The CPU mirror holds the legacy math result while validation is on (computed but not uploaded). - // Only meaningful with a static camera, the readback is one frame old - f32 maxDelta = 0.0f; - for (u32 i = 0; i < numCascades; i++) - { - const mat4x4& gpuMatrix = _readBackCascadeCameras[i].worldToClip; - const mat4x4& cpuMatrix = resources.cameras[i + 1].worldToClip; - - for (u32 col = 0; col < 4; col++) - { - for (u32 row = 0; row < 4; row++) - { - maxDelta = Math::Max(maxDelta, glm::abs(gpuMatrix[col][row] - cpuMatrix[col][row])); - } - } - } - - static u32 parityLogCounter = 0; - if (parityLogCounter++ % 60 == 0) - { - NC_LOG_INFO("SDSM Parity: max worldToClip delta {0} across {1} cascades (stand still, readback is one frame old)", maxDelta, numCascades); - } - } - - const bool debugMatrices = CVAR_ShadowDebugMatrices.Get(); - const i32 debugMatrixIndex = CVAR_ShadowDebugMatrixIndex.Get(); - if (debugMatrices && debugMatrixIndex >= 0 && debugMatrixIndex < static_cast(numCascades)) - { - const Camera& debugCascadeCamera = GetCascadeCamera(debugMatrixIndex); - - Camera& mainCamera = resources.cameras[0]; - - mainCamera = debugCascadeCamera; - resources.cameras.SetDirtyElement(0); - } - - if (CVAR_ShadowDrawMatrices.Get()) - { - Color colors[] = - { - Color::Red, - Color::Green, - Color::Blue, - Color::Yellow, - Color::Magenta, - Color::Cyan, - Color::PastelOrange, - Color::PastelGreen - }; - - for (u32 i = 0; i < numCascades; i++) - { - const Camera& debugCascadeCamera = GetCascadeCamera(i); - _debugRenderer->DrawFrustum(debugCascadeCamera.worldToClip, colors[i]); - } - } -} - -void ShadowRenderer::AddDepthMinMaxPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) -{ - struct Data - { - Renderer::DepthImageResource depth; - - Renderer::BufferMutableResource depthMinMaxBuffer; - Renderer::BufferMutableResource depthMinMaxReadBackBuffer; - - Renderer::DescriptorSetResource passSet; - }; - - CVarSystem* cvarSystem = CVarSystem::Get(); - const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); - const bool dispatchEnabled = CVAR_ShadowEnabled.Get() && numCascades > 0; - - renderGraph->AddPass("Shadow Depth MinMax", - [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) - { - using BufferUsage = Renderer::BufferPassUsage; - - data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); - - data.depthMinMaxBuffer = builder.Write(_depthMinMaxBuffer, BufferUsage::TRANSFER | BufferUsage::COMPUTE); - data.depthMinMaxReadBackBuffer = builder.Write(_depthMinMaxReadBackBuffer, BufferUsage::TRANSFER); - - data.passSet = builder.Use(_depthMinMaxDescriptorSet); - - return true; // Return true from setup to enable this pass, return false to disable it - }, - [this, frameIndex, dispatchEnabled](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) - { - GPU_SCOPED_PROFILER_ZONE(commandList, ShadowDepthMinMax); - - // Reset to the sentinel, if the dispatch is skipped or every pixel is sky it survives - // and the consumers fall back to the full range - commandList.FillBuffer(data.depthMinMaxBuffer, 0, sizeof(u32), 0xFFFFFFFF); - commandList.FillBuffer(data.depthMinMaxBuffer, sizeof(u32), sizeof(u32), 0); - commandList.BufferBarrier(data.depthMinMaxBuffer, Renderer::BufferPassUsage::TRANSFER); - - if (dispatchEnabled) - { - commandList.BeginPipeline(_depthMinMaxPipeline); - - data.passSet.Bind("_depth"_h, data.depth); - commandList.BindDescriptorSet(data.passSet, frameIndex); - - uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); - commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); - - commandList.EndPipeline(_depthMinMaxPipeline); - - commandList.BufferBarrier(data.depthMinMaxBuffer, Renderer::BufferPassUsage::COMPUTE); - } - - commandList.CopyBuffer(data.depthMinMaxReadBackBuffer, 0, data.depthMinMaxBuffer, 0, sizeof(u32) * 2); - }); -} - -void ShadowRenderer::AddCascadeFitPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) -{ - struct Data - { - Renderer::DepthImageResource depth; - - Renderer::BufferMutableResource cameras; - Renderer::BufferMutableResource sdsmDataBuffer; - Renderer::BufferMutableResource cascadeBoundsBuffer; - Renderer::BufferMutableResource sdsmDataReadBackBuffer; - Renderer::BufferMutableResource cascadeCamerasReadBackBuffer; - - Renderer::DescriptorSetResource rangeSet; - Renderer::DescriptorSetResource reduceSet; - Renderer::DescriptorSetResource camerasSet; - }; - - CVarSystem* cvarSystem = CVarSystem::Get(); - u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - - const bool freezeCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowFreezeCascades") != 0; - const bool useSVSM = CVAR_ShadowTechnique.Get() == 1; // SVSM Finalize owns the camera slots instead - const bool dispatchEnabled = CVAR_ShadowUseSDSM.Get() && CVAR_ShadowEnabled.Get() && numCascades > 0 && !freezeCascades && !useSVSM; - - if (!dispatchEnabled) - return; - - // Record-time inputs for the push constants, the shadow sun steps in discrete intervals - entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; - auto& dayNightCycle = registry->ctx().get(); - const f32 shadowTimeOfDay = ECS::Systems::GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32()); - const vec3 lightDirection = ECS::Systems::UpdateAreaLights::GetLightDirection(shadowTimeOfDay); - - renderGraph->AddPass("Shadow Cascade Fit", - [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) - { - using BufferUsage = Renderer::BufferPassUsage; - - data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); - - data.cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::TRANSFER); - builder.Read(_depthMinMaxBuffer, BufferUsage::COMPUTE); - data.sdsmDataBuffer = builder.Write(_sdsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); - data.cascadeBoundsBuffer = builder.Write(_cascadeBoundsBuffer, BufferUsage::TRANSFER | BufferUsage::COMPUTE); - data.sdsmDataReadBackBuffer = builder.Write(_sdsmDataReadBackBuffer, BufferUsage::TRANSFER); - data.cascadeCamerasReadBackBuffer = builder.Write(_cascadeCamerasReadBackBuffer, BufferUsage::TRANSFER); - - data.rangeSet = builder.Use(_cascadeFitRangeDescriptorSet); - data.reduceSet = builder.Use(_cascadeXYReduceDescriptorSet); - data.camerasSet = builder.Use(_cascadeFitCamerasDescriptorSet); - - return true; // Return true from setup to enable this pass, return false to disable it - }, - [this, frameIndex, numCascades, lightDirection, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) - { - GPU_SCOPED_PROFILER_ZONE(commandList, ShadowCascadeFit); - - struct CascadeFitConstants - { - vec4 lightDirection; - u32 numCascades; - f32 cascadeSplitLambda; - f32 cascadeTextureSize; - u32 stableShadows; - f32 shadowMaxDistance; - f32 quantizeStep; - f32 shrinkDelay; - f32 deltaTime; - u32 useDepthBounds; - f32 casterMargin; - u32 useXYBounds; - f32 xyMarginTexels; - }; - - CascadeFitConstants* constants = graphResources.FrameNew(); - constants->lightDirection = vec4(lightDirection, 0.0f); - constants->numCascades = numCascades; - constants->cascadeSplitLambda = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSplitLambda")); - constants->cascadeTextureSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeSize")); - constants->stableShadows = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStable") != 0; - constants->shadowMaxDistance = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowMaxDistance")); - constants->quantizeStep = CVAR_ShadowSDSMQuantizeStep.GetFloat(); - constants->shrinkDelay = CVAR_ShadowSDSMShrinkDelay.GetFloat(); - constants->deltaTime = _lastDeltaTime; - constants->useDepthBounds = CVAR_ShadowSDSMUseDepthBounds.Get(); - constants->casterMargin = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin")); - - // The XY path presumes the stable up rule and depth bounds, and parity compares - // against the legacy CPU math which it intentionally diverges from - u32 useXYBounds = static_cast(Math::Max(CVAR_ShadowSDSMUseXYBounds.Get(), 0)); - if (CVAR_ShadowSDSMValidateParity.Get() || !constants->useDepthBounds || !constants->stableShadows) - { - useXYBounds = Math::Min(useXYBounds, 1u); - } - constants->useXYBounds = useXYBounds; - constants->xyMarginTexels = CVAR_ShadowSDSMXYMarginTexels.GetFloat() + CVAR_ShadowNormalOffsetBias.GetFloat(); - - // Reset the light-space bounds to the encoded sentinels - commandList.FillBuffer(data.cascadeBoundsBuffer, 0, sizeof(u32) * 24, 0xFFFFFFFF); - commandList.FillBuffer(data.cascadeBoundsBuffer, sizeof(u32) * 24, sizeof(u32) * 24, 0); - commandList.BufferBarrier(data.cascadeBoundsBuffer, Renderer::BufferPassUsage::TRANSFER); - - // Range: reduced depth bounds -> working range + split distances - commandList.BeginPipeline(_cascadeFitRangePipeline); - commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - - commandList.BindDescriptorSet(data.rangeSet, frameIndex); - - commandList.Dispatch(1, 1, 1); - commandList.EndPipeline(_cascadeFitRangePipeline); - - commandList.BufferBarrier(data.sdsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - - // XY reduce: light-space AABB of the visible samples per cascade - if (useXYBounds >= 1) - { - commandList.BeginPipeline(_cascadeXYReducePipeline); - commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - - data.reduceSet.Bind("_depth"_h, data.depth); - commandList.BindDescriptorSet(data.reduceSet, frameIndex); - - uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); - commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); - - commandList.EndPipeline(_cascadeXYReducePipeline); - - commandList.BufferBarrier(data.cascadeBoundsBuffer, Renderer::BufferPassUsage::COMPUTE); - } - - // Cameras: build the cascade cameras from the splits - commandList.BeginPipeline(_cascadeFitCamerasPipeline); - commandList.PushConstant(constants, 0, sizeof(CascadeFitConstants)); - - commandList.BindDescriptorSet(data.camerasSet, frameIndex); - - commandList.Dispatch(1, 1, 1); - commandList.EndPipeline(_cascadeFitCamerasPipeline); - - commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.sdsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - - // Readbacks for the debug tooling, element 0 of the cameras buffer is the main camera - commandList.CopyBuffer(data.cascadeCamerasReadBackBuffer, 0, data.cameras, sizeof(Camera), sizeof(Camera) * numCascades); - commandList.CopyBuffer(data.sdsmDataReadBackBuffer, 0, data.sdsmDataBuffer, 0, sizeof(f32) * SDSM_DATA_FLOAT_COUNT); - }); } void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) @@ -722,7 +390,7 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende }; const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); - const bool enabled = CVAR_ShadowTechnique.Get() == 1 && CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get(); + const bool enabled = CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get(); if (!enabled || _svsmPagePool == Renderer::ImageID::Invalid()) return; @@ -1048,7 +716,7 @@ void ShadowRenderer::AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, const i32 poolMode = CVAR_SVSMDebugShowPool.Get(); // 1 = static pool, 2 = dynamic pool const bool showPool = poolMode != 0 && _svsmPagePool != Renderer::ImageID::Invalid(); const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); - if (CVAR_ShadowTechnique.Get() != 1 || !CVAR_ShadowEnabled.Get() || (debugClipmap < 0 && !showPool)) + if (!CVAR_ShadowEnabled.Get() || (debugClipmap < 0 && !showPool)) return; renderGraph->AddPass("SVSM Debug Overlay", @@ -1135,47 +803,6 @@ void ShadowRenderer::AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, }); } -bool ShadowRenderer::GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const -{ - f32 usedMin = _sdsmDataReadBack[2]; - f32 usedMax = _sdsmDataReadBack[3]; - if (usedMax <= usedMin) // Not fitted yet - return false; - - outMinDistance = usedMin; - outMaxDistance = usedMax; - return true; -} - -bool ShadowRenderer::GetCascadeFittedBounds(u32 cascadeIndex, vec3& outExtents, bool& outValid) const -{ - if (CVAR_ShadowSDSMUseXYBounds.Get() < 1 || cascadeIndex >= Renderer::Settings::MAX_SHADOW_CASCADES) - return false; - - // cascadeDiag lives after SDSMState (8) + splitDist (8) + splitDepth (8) + cascadeStable (32) - const f32* diag = &_sdsmDataReadBack[56 + cascadeIndex * 4]; - outExtents = vec3(diag[0], diag[1], diag[2]); - outValid = diag[3] > 0.5f; - return true; -} - -bool ShadowRenderer::GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const -{ - if (_depthMinMaxReadBack[1] == 0) // Sentinel, no valid depth samples reduced yet - return false; - - const vec4& nearFar = resources.cameras[0].nearFar; - f32 nearClip = nearFar.x; - f32 farClip = nearFar.y; - - // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane - auto Linearize = [nearClip, farClip](f32 depth) { return (nearClip * farClip) / (nearClip + depth * (farClip - nearClip)); }; - - outMinDistance = Linearize(glm::uintBitsToFloat(_depthMinMaxReadBack[1])); // Max depth bits = nearest sample - outMaxDistance = Linearize(glm::uintBitsToFloat(_depthMinMaxReadBack[0])); // Min depth bits = farthest sample - return true; -} - bool ShadowRenderer::GetSVSMClipmapStats(u32 clipmapIndex, SVSMClipmapStats& outStats) const { if (clipmapIndex >= SVSM_MAX_CLIPMAPS) @@ -1207,35 +834,21 @@ void ShadowRenderer::GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, outOverflow = _svsmDataReadBack[SVSMDataOffsets::StatsDynamicOverflow]; } -void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +void ShadowRenderer::AddSVSMBindPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { - struct ShadowPassData + struct Data { - Renderer::DepthImageMutableResource shadowDepthCascades[Renderer::Settings::MAX_SHADOW_CASCADES]; Renderer::ImageResource svsmPagePool; Renderer::ImageResource svsmDynamicPagePool; Renderer::DescriptorSetResource lightDescriptorSet; }; - CVarSystem* cvarSystem = CVarSystem::Get(); - u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum")); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - - if (numCascades == 0) - return; - - renderGraph->AddPass("Shadow Pass", - [=, &resources](ShadowPassData& data, Renderer::RenderGraphBuilder& builder) + // Runs unconditionally (even under svsmFreeze, when the update pass early-outs): every LIGHT + // set consumer needs the pool bindings valid, real pools or the placeholder before they exist + renderGraph->AddPass("SVSM Bind", + [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) { - // Under SVSM the cascade RTs are never rendered or sampled, skip the 4x4096 clears but - // keep them bound so the material pass layout stays satisfied - const Renderer::LoadMode cascadeLoadMode = CVAR_ShadowTechnique.Get() == 1 ? Renderer::LoadMode::LOAD : Renderer::LoadMode::CLEAR; - for (u32 i = 0; i < numCascades; i++) - { - data.shadowDepthCascades[i] = builder.Write(resources.shadowDepthCascades[i], Renderer::PipelineType::GRAPHICS, cascadeLoadMode); - } - data.svsmPagePool = builder.Read(GetSVSMPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); data.svsmDynamicPagePool = builder.Read(GetSVSMDynamicPagePoolOrPlaceholder(), Renderer::PipelineType::COMPUTE); @@ -1243,20 +856,8 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes return true; // Return true from setup to enable this pass, return false to disable it }, - [=](ShadowPassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + [](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) { - Renderer::DepthImageMutableResource cascadeDepthResource; - for (u32 i = 0; i < Renderer::Settings::MAX_SHADOW_CASCADES; i++) - { - if (i < numCascades) - { - cascadeDepthResource = data.shadowDepthCascades[i]; - } - - data.lightDescriptorSet.BindArray("_shadowCascadeRTs", cascadeDepthResource, i); - } - - // Every LIGHT set consumer needs the SVSM pool bindings valid, real pools or placeholder data.lightDescriptorSet.Bind("_svsmPagePool"_h, data.svsmPagePool); data.lightDescriptorSet.Bind("_svsmDynamicPagePool"_h, data.svsmDynamicPagePool); }); @@ -1264,133 +865,6 @@ void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderRes void ShadowRenderer::CreatePermanentResources(RenderResources& resources) { - Renderer::SamplerDesc samplerDesc; - samplerDesc.enabled = true; - samplerDesc.filter = Renderer::SamplerFilter::MIN_MAG_MIP_LINEAR; - samplerDesc.addressU = Renderer::TextureAddressMode::CLAMP; - samplerDesc.addressV = Renderer::TextureAddressMode::CLAMP; - samplerDesc.addressW = Renderer::TextureAddressMode::CLAMP; - samplerDesc.shaderVisibility = Renderer::ShaderVisibility::PIXEL; - samplerDesc.comparisonEnabled = true; - samplerDesc.comparisonFunc = Renderer::ComparisonFunc::GREATER; - - _shadowCmpSampler = _renderer->CreateSampler(samplerDesc); - resources.lightDescriptorSet.Bind("_shadowCmpSampler"_h, _shadowCmpSampler); - - samplerDesc.filter = Renderer::SamplerFilter::MIN_MAG_MIP_POINT; - samplerDesc.addressU = Renderer::TextureAddressMode::CLAMP; - samplerDesc.addressV = Renderer::TextureAddressMode::CLAMP; - samplerDesc.comparisonEnabled = false; - - _shadowPointClampSampler = _renderer->CreateSampler(samplerDesc); - resources.lightDescriptorSet.Bind("_shadowPointClampSampler"_h, _shadowPointClampSampler); - - // Depth min/max reduction for SDSM cascade fitting - { - Renderer::ComputePipelineDesc pipelineDesc; - pipelineDesc.debugName = "Shadow Depth MinMax"; - - Renderer::ComputeShaderDesc shaderDesc; - shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/DepthMinMax.cs"_h, "Shadows/DepthMinMax.cs"); - pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - - _depthMinMaxPipeline = _renderer->CreatePipeline(pipelineDesc); - - _depthMinMaxDescriptorSet.RegisterPipeline(_renderer, _depthMinMaxPipeline); - _depthMinMaxDescriptorSet.Init(_renderer); - - Renderer::BufferDesc bufferDesc; - bufferDesc.name = "ShadowDepthMinMax"; - bufferDesc.size = sizeof(u32) * 2; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; - - _depthMinMaxBuffer = _renderer->CreateAndFillBuffer(_depthMinMaxBuffer, bufferDesc, [](void* mappedMemory, size_t size) - { - u32* values = static_cast(mappedMemory); - values[0] = 0xFFFFFFFF; // Min depth bits sentinel - values[1] = 0; // Max depth bits sentinel - }); - _depthMinMaxDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); - - bufferDesc.name = "ShadowDepthMinMaxReadBack"; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; - bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; - _depthMinMaxReadBackBuffer = _renderer->CreateBuffer(_depthMinMaxReadBackBuffer, bufferDesc); - } - - // SDSM cascade fitting - { - Renderer::ComputePipelineDesc pipelineDesc; - pipelineDesc.debugName = "Shadow Cascade Fit Range"; - - Renderer::ComputeShaderDesc shaderDesc; - shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFitRange.cs"_h, "Shadows/CascadeFitRange.cs"); - pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - - _cascadeFitRangePipeline = _renderer->CreatePipeline(pipelineDesc); - - _cascadeFitRangeDescriptorSet.RegisterPipeline(_renderer, _cascadeFitRangePipeline); - _cascadeFitRangeDescriptorSet.Init(_renderer); - - pipelineDesc.debugName = "Shadow Cascade XY Reduce"; - shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeXYReduce.cs"_h, "Shadows/CascadeXYReduce.cs"); - pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - - _cascadeXYReducePipeline = _renderer->CreatePipeline(pipelineDesc); - - _cascadeXYReduceDescriptorSet.RegisterPipeline(_renderer, _cascadeXYReducePipeline); - _cascadeXYReduceDescriptorSet.Init(_renderer); - - pipelineDesc.debugName = "Shadow Cascade Fit Cameras"; - shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry("Shadows/CascadeFitCameras.cs"_h, "Shadows/CascadeFitCameras.cs"); - pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc); - - _cascadeFitCamerasPipeline = _renderer->CreatePipeline(pipelineDesc); - - _cascadeFitCamerasDescriptorSet.RegisterPipeline(_renderer, _cascadeFitCamerasPipeline); - _cascadeFitCamerasDescriptorSet.Init(_renderer); - - Renderer::BufferDesc bufferDesc; - bufferDesc.name = "ShadowSDSMData"; - bufferDesc.size = sizeof(f32) * SDSM_DATA_FLOAT_COUNT; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; - - _sdsmDataBuffer = _renderer->CreateAndFillBuffer(_sdsmDataBuffer, bufferDesc, [](void* mappedMemory, size_t size) - { - memset(mappedMemory, 0, size); // smoothedMax <= smoothedMin marks the state uninitialized - }); - - Renderer::BufferDesc boundsDesc; - boundsDesc.name = "ShadowCascadeBounds"; - boundsDesc.size = sizeof(u32) * 48; // Encoded light-space mins [0..23] and maxs [24..47], 3 axes per cascade - boundsDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; - _cascadeBoundsBuffer = _renderer->CreateAndFillBuffer(_cascadeBoundsBuffer, boundsDesc, [](void* mappedMemory, size_t size) - { - u32* values = static_cast(mappedMemory); - for (u32 i = 0; i < 24; i++) values[i] = 0xFFFFFFFF; - for (u32 i = 24; i < 48; i++) values[i] = 0; - }); - - _cascadeFitRangeDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); - _cascadeFitRangeDescriptorSet.Bind("_depthMinMax"_h, _depthMinMaxBuffer); - _cascadeXYReduceDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); - _cascadeXYReduceDescriptorSet.Bind("_cascadeBounds"_h, _cascadeBoundsBuffer); - _cascadeFitCamerasDescriptorSet.Bind("_sdsmData"_h, _sdsmDataBuffer); - _cascadeFitCamerasDescriptorSet.Bind("_cascadeBounds"_h, _cascadeBoundsBuffer); - // _srcCameras / _rwCameras / _depth are bound per frame in AddCascadeFitPass, those resources can be recreated - - bufferDesc.name = "ShadowSDSMDataReadBack"; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; - bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; - _sdsmDataReadBackBuffer = _renderer->CreateBuffer(_sdsmDataReadBackBuffer, bufferDesc); - - bufferDesc.name = "ShadowCascadeCamerasReadBack"; - bufferDesc.size = sizeof(Camera) * Renderer::Settings::MAX_SHADOW_CASCADES; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; - bufferDesc.cpuAccess = Renderer::BufferCPUAccess::ReadOnly; - _cascadeCamerasReadBackBuffer = _renderer->CreateBuffer(_cascadeCamerasReadBackBuffer, bufferDesc); - } - // SVSM page table lifecycle { Renderer::ComputePipelineDesc pipelineDesc; @@ -1643,9 +1117,6 @@ void ShadowRenderer::BindCameraBuffers(RenderResources& resources) { Renderer::BufferID camerasBuffer = resources.cameras.GetBuffer(); - _cascadeFitRangeDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); - _cascadeXYReduceDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); - _cascadeFitCamerasDescriptorSet.Bind("_rwCameras"_h, camerasBuffer); _svsmPrepareDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); _svsmPageMarkDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); _svsmFinalizeDescriptorSet.Bind("_rwCameras"_h, camerasBuffer); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index d3d91c7..0c872ed 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -34,23 +34,13 @@ class ShadowRenderer void Update(f32 deltaTime, RenderResources& resources); - void AddDepthMinMaxPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeFitPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - - // SVSM (shadowTechnique 1): page marking, page table lifecycle and allocation. Runs alongside - // the CSM path until the SVSM rendering and sampling stages land + // SVSM: page marking, page table lifecycle and allocation void AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - - // Reduced scene depth bounds as view distances, false while no valid depth has been reduced yet - bool GetDepthBoundsViewDistances(const RenderResources& resources, f32& outMinDistance, f32& outMaxDistance) const; - // Effective cascade range after hysteresis and quantization, false while SDSM has not fitted yet - bool GetEffectiveShadowRange(f32& outMinDistance, f32& outMaxDistance) const; - - // Fitted light-space extents per cascade, false while the XY reduction is not running - bool GetCascadeFittedBounds(u32 cascadeIndex, vec3& outExtents, bool& outValid) const; + // Binds the (lazily created) page pools into the LIGHT set every frame, runs even when the + // update pass is disabled (svsmFreeze) + void AddSVSMBindPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); struct SVSMClipmapStats { @@ -64,7 +54,7 @@ class ShadowRenderer f32 extent = 0.0f; }; - // One frame old readback values, only meaningful while shadowTechnique is 1 + // One frame old readback values bool GetSVSMClipmapStats(u32 clipmapIndex, SVSMClipmapStats& outStats) const; void GetSVSMGlobalStats(u32& outFreePages, u32& outTotalPages, u32& outOverflow, u32& outInvalidationCause) const; void GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, u32& outOverflow) const; @@ -95,7 +85,7 @@ class ShadowRenderer } // SVSM resources for the terrain/model page render passes, the pools are created lazily on - // the first shadowTechnique 1 frame + // the first shadow-enabled frame Renderer::BufferID GetSVSMDataBuffer() const { return _svsmDataBuffer; } Renderer::BufferID GetSVSMPageTableBuffer() const { return _svsmPageTableBuffer; } Renderer::ImageID GetSVSMPagePool() const { return _svsmPagePool; } @@ -117,30 +107,6 @@ class ShadowRenderer TerrainRenderer* _terrainRenderer = nullptr; ModelRenderer* _modelRenderer = nullptr; - Renderer::SamplerID _shadowCmpSampler; - Renderer::SamplerID _shadowPointClampSampler; - - Renderer::ComputePipelineID _depthMinMaxPipeline; - Renderer::DescriptorSet _depthMinMaxDescriptorSet; - Renderer::BufferID _depthMinMaxBuffer; - Renderer::BufferID _depthMinMaxReadBackBuffer; - u32 _depthMinMaxReadBack[2] = { 0xFFFFFFFF, 0 }; - - static constexpr u32 SDSM_DATA_FLOAT_COUNT = 8 + 8 + 8 + 4 * Renderer::Settings::MAX_SHADOW_CASCADES * 2; // SDSMState + splitDist + splitDepth + cascadeStable + cascadeDiag - - Renderer::ComputePipelineID _cascadeFitRangePipeline; - Renderer::ComputePipelineID _cascadeXYReducePipeline; - Renderer::ComputePipelineID _cascadeFitCamerasPipeline; - Renderer::DescriptorSet _cascadeFitRangeDescriptorSet; - Renderer::DescriptorSet _cascadeXYReduceDescriptorSet; - Renderer::DescriptorSet _cascadeFitCamerasDescriptorSet; - Renderer::BufferID _sdsmDataBuffer; - Renderer::BufferID _cascadeBoundsBuffer; - Renderer::BufferID _sdsmDataReadBackBuffer; - Renderer::BufferID _cascadeCamerasReadBackBuffer; - Camera _readBackCascadeCameras[Renderer::Settings::MAX_SHADOW_CASCADES]; - f32 _sdsmDataReadBack[SDSM_DATA_FLOAT_COUNT] = { 0.0f }; - // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp. // Tail: clipRect{MinX,MinY,MaxX,MaxY}[24] at 220..315 (3 clip rects x 8 clipmaps) static constexpr u32 SVSM_DATA_UINT_COUNT = 316; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index f60c2d6..478b5e5 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -127,14 +127,10 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render const bool disableTwoStepCulling = CVAR_TerrainDisableTwoStepCulling.Get(); - CVarSystem* cvarSystem = CVarSystem::Get(); - - const u32 numCascades = 0; // Occluders are main view only, cascades render in their own block late in the frame - struct Data { Renderer::ImageMutableResource visibilityBuffer; - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + Renderer::DepthImageMutableResource depth; Renderer::BufferMutableResource culledInstanceBuffer; Renderer::BufferMutableResource culledInstanceBitMaskBuffer; @@ -147,16 +143,12 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render }; renderGraph->AddPass("Terrain Occluders", - [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) // Setup + [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) // Setup { using BufferUsage = Renderer::BufferPassUsage; data.visibilityBuffer = builder.Write(resources.visibilityBuffer, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - data.depth[0] = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i-1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } + data.depth = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); builder.Read(_instanceDatas.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::GRAPHICS); builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); @@ -176,7 +168,7 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render return true; // Return true from setup to enable this pass, return false to disable it }, - [this, &resources, frameIndex, numCascades, disableTwoStepCulling, cullingEnabled, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + [this, &resources, frameIndex, disableTwoStepCulling, cullingEnabled](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute { GPU_SCOPED_PROFILER_ZONE(commandList, TerrainOccluders); @@ -189,80 +181,49 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render commandList.BufferBarrier(data.culledInstanceBitMaskBuffer, Renderer::BufferPassUsage::TRANSFER); } - for (u32 i = 0; i < numCascades + 1; i++) + // Reset the counters { - std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); - commandList.PushMarker(markerName, Color::White); - - // Reset the counters - { - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - } - - // Fill the occluders to draw - FillDrawCallsParams fillParams; - fillParams.passName = "Occluders"; - fillParams.cellCount = cellCount; - fillParams.viewIndex = i; - fillParams.diffAgainstPrev = false; - fillParams.currentBitmaskIndex = !frameIndex; // Occluders consume last frame's culling output - fillParams.fillSet = data.fillSet; - - FillDrawCalls(frameIndex, graphResources, commandList, fillParams); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); - - // Draw the occluders - if (CVAR_TerrainOccludersEnabled.Get()) - { - commandList.PushMarker("Occlusion Draw", Color::White); - - if (i == 1) - { - uvec2 shadowDepthDimensions = _renderer->GetImageDimensions(resources.shadowDepthCascades[0]); - - commandList.SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - commandList.SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - - f32 biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - f32 biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - f32 biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - commandList.SetDepthBias(biasConstantFactor, biasClamp, biasSlopeFactor); - } - - DrawParams drawParams; - drawParams.shadowPass = i != 0; - drawParams.viewIndex = i; - drawParams.cullingEnabled = cullingEnabled; - drawParams.visibilityBuffer = data.visibilityBuffer; - drawParams.depth = data.depth[i]; - drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); - drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + } - drawParams.globalDescriptorSet = data.globalSet; - drawParams.drawDescriptorSet = data.drawSet; + // Fill the occluders to draw + FillDrawCallsParams fillParams; + fillParams.passName = "Occluders"; + fillParams.cellCount = cellCount; + fillParams.viewIndex = 0; + fillParams.diffAgainstPrev = false; + fillParams.currentBitmaskIndex = !frameIndex; // Occluders consume last frame's culling output + fillParams.fillSet = data.fillSet; + + FillDrawCalls(frameIndex, graphResources, commandList, fillParams); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); + + // Draw the occluders + if (CVAR_TerrainOccludersEnabled.Get()) + { + commandList.PushMarker("Occlusion Draw", Color::White); - Draw(resources, frameIndex, graphResources, commandList, drawParams); + DrawParams drawParams; + drawParams.viewIndex = 0; + drawParams.cullingEnabled = cullingEnabled; + drawParams.visibilityBuffer = data.visibilityBuffer; + drawParams.depth = data.depth; + drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); + drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); - commandList.PopMarker(); - } + drawParams.globalDescriptorSet = data.globalSet; + drawParams.drawDescriptorSet = data.drawSet; - // Copy drawn count - { - u32 dstOffset = i * sizeof(u32); - commandList.CopyBuffer(data.occluderDrawCountReadBackBuffer, dstOffset, data.argumentBuffer, 4, 4); - } + Draw(resources, frameIndex, graphResources, commandList, drawParams); commandList.PopMarker(); } - // Finish by resetting the viewport, scissor and depth bias - vec2 renderSize = _renderer->GetRenderSize(); - commandList.SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); - commandList.SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); - commandList.SetDepthBias(0, 0, 0); + // Copy drawn count + commandList.CopyBuffer(data.occluderDrawCountReadBackBuffer, 0, data.argumentBuffer, 4, 4); }); } @@ -413,13 +374,10 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render const bool cullingEnabled = true;//CVAR_TerrainCullingEnabled.Get(); - CVarSystem* cvarSystem = CVarSystem::Get(); - const u32 numCascades = 0; // Main view only, cascades render in their own block late in the frame - struct Data { Renderer::ImageMutableResource visibilityBuffer; - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + Renderer::DepthImageMutableResource depth; Renderer::BufferMutableResource culledInstanceBuffer; @@ -433,16 +391,12 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render }; renderGraph->AddPass("Terrain Geometry", - [this, &resources, frameIndex, cullingEnabled, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) + [this, &resources, frameIndex, cullingEnabled](Data& data, Renderer::RenderGraphBuilder& builder) { using BufferUsage = Renderer::BufferPassUsage; data.visibilityBuffer = builder.Write(resources.visibilityBuffer, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - data.depth[0] = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } + data.depth = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); @@ -466,84 +420,56 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render return true; // Return true from setup to enable this pass, return false to disable it }, - [this, &resources, frameIndex, cullingEnabled, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + [this, &resources, frameIndex, cullingEnabled](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) { GPU_SCOPED_PROFILER_ZONE(commandList, TerrainGeometryPass); - for (u32 i = 0; i < numCascades + 1; i++) - { - std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); - commandList.PushMarker(markerName, Color::White); - - // Reset the counters - { - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - } - - if (CVAR_TerrainGeometryEnabled.Get()) - { - const u32 cellCount = static_cast(_cellDatas.Count()); - // Fill the occluders to draw - FillDrawCallsParams fillParams; - fillParams.passName = "Geometry"; - fillParams.cellCount = cellCount; - fillParams.viewIndex = i; - fillParams.diffAgainstPrev = true; - fillParams.currentBitmaskIndex = frameIndex; - fillParams.fillSet = data.fillSet; - - FillDrawCalls(frameIndex, graphResources, commandList, fillParams); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); - - if (i == 1) - { - uvec2 shadowDepthDimensions = _renderer->GetImageDimensions(resources.shadowDepthCascades[0]); - - commandList.SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - commandList.SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); + // Reset the counters + { + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + } - f32 biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - f32 biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - f32 biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - commandList.SetDepthBias(biasConstantFactor, biasClamp, biasSlopeFactor); - } + if (CVAR_TerrainGeometryEnabled.Get()) + { + const u32 cellCount = static_cast(_cellDatas.Count()); - commandList.PushMarker("Draw", Color::White); + // Fill the cells to draw + FillDrawCallsParams fillParams; + fillParams.passName = "Geometry"; + fillParams.cellCount = cellCount; + fillParams.viewIndex = 0; + fillParams.diffAgainstPrev = true; + fillParams.currentBitmaskIndex = frameIndex; + fillParams.fillSet = data.fillSet; - DrawParams drawParams; - drawParams.shadowPass = i != 0; - drawParams.viewIndex = i; - drawParams.cullingEnabled = cullingEnabled; - drawParams.visibilityBuffer = data.visibilityBuffer; - drawParams.depth = data.depth[i]; - drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); - drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); + FillDrawCalls(frameIndex, graphResources, commandList, fillParams); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); - drawParams.globalDescriptorSet = data.globalSet; - drawParams.drawDescriptorSet = data.geometryPassSet; + commandList.PushMarker("Draw", Color::White); - Draw(resources, frameIndex, graphResources, commandList, drawParams); + DrawParams drawParams; + drawParams.viewIndex = 0; + drawParams.cullingEnabled = cullingEnabled; + drawParams.visibilityBuffer = data.visibilityBuffer; + drawParams.depth = data.depth; + drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); + drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); - commandList.PopMarker(); - } + drawParams.globalDescriptorSet = data.globalSet; + drawParams.drawDescriptorSet = data.geometryPassSet; - if (cullingEnabled) - { - u32 dstOffset = i * sizeof(u32); - commandList.CopyBuffer(data.drawCountReadBackBuffer, dstOffset, data.argumentBuffer, 4, 4); - } + Draw(resources, frameIndex, graphResources, commandList, drawParams); commandList.PopMarker(); } - // Finish by resetting the viewport, scissor and depth bias - vec2 renderSize = _renderer->GetRenderSize(); - commandList.SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); - commandList.SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); - commandList.SetDepthBias(0, 0, 0); + if (cullingEnabled) + { + commandList.CopyBuffer(data.drawCountReadBackBuffer, 0, data.argumentBuffer, 4, 4); + } }); } @@ -563,21 +489,8 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, if (CVAR_TerrainCastShadow.Get() != 1) return; - // Under SVSM the same per-view culling runs against the clipmap cameras, which need no - // cascade depth images - const bool useSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1; - u32 numCascades; - if (useSVSM) - { - numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); - } - else - { - numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - } - if (numCascades == 0) - return; + // The same per-view culling the main view uses, run against the clipmap cameras + const u32 numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); struct Data { @@ -661,157 +574,6 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, }); } -void TerrainRenderer::AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) -{ - ZoneScoped; - - if (!CVAR_TerrainRendererEnabled.Get()) - return; - - if (_instanceDatas.Count() == 0) - return; - - CVarSystem* cvarSystem = CVarSystem::Get(); - - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 0) - return; - - if (CVAR_TerrainCastShadow.Get() != 1) - return; - - // SVSM renders pages through AddSVSMGeometryPass instead - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) == 1) - return; - - u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"_h)); - numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size())); - if (numCascades == 0) - return; - - struct Data - { - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; - - Renderer::BufferMutableResource culledInstanceBuffer; - - Renderer::BufferMutableResource argumentBuffer; - Renderer::BufferMutableResource drawCountReadBackBuffer; - - Renderer::DescriptorSetResource globalSet; - Renderer::DescriptorSetResource fillSet; - Renderer::DescriptorSetResource geometryPassSet; - }; - - renderGraph->AddPass("Terrain Cascade Geometry", - [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) // Setup - { - using BufferUsage = Renderer::BufferPassUsage; - - for (u32 i = 1; i < numCascades + 1; i++) - { - data.depth[i] = builder.Write(resources.shadowDepthCascades[i - 1], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - } - - builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); - builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); - data.culledInstanceBuffer = builder.Write(_culledInstanceBuffer, BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - - builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_cellDatas.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_chunkDatas.GetBuffer(), BufferUsage::GRAPHICS); - builder.Read(_instanceDatas.GetBuffer(), BufferUsage::COMPUTE | BufferUsage::GRAPHICS); - - data.argumentBuffer = builder.Write(_argumentBuffer, BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - data.drawCountReadBackBuffer = builder.Write(_drawCountReadBackBuffer, BufferUsage::TRANSFER); - - data.globalSet = builder.Use(resources.globalDescriptorSet); - data.fillSet = builder.Use(_geometryFillPassDescriptorSet); - data.geometryPassSet = builder.Use(resources.terrainDescriptorSet); - - return true; // Return true from setup to enable this pass, return false to disable it - }, - [this, &resources, frameIndex, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) - { - GPU_SCOPED_PROFILER_ZONE(commandList, TerrainCascadeGeometry); - - const u32 cellCount = static_cast(_cellDatas.Count()); - - for (u32 i = 1; i < numCascades + 1; i++) - { - std::string markerName = "Cascade " + std::to_string(i - 1); - commandList.PushMarker(markerName, Color::White); - - // Reset the counters - { - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); - } - - if (CVAR_TerrainGeometryEnabled.Get()) - { - // Cascades draw their full surviving set in one phase, occluders no longer pre-draw into them - FillDrawCallsParams fillParams; - fillParams.passName = "Cascade Geometry"; - fillParams.cellCount = cellCount; - fillParams.viewIndex = i; - fillParams.diffAgainstPrev = false; - fillParams.currentBitmaskIndex = frameIndex; - fillParams.fillSet = data.fillSet; - - FillDrawCalls(frameIndex, graphResources, commandList, fillParams); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); - - if (i == 1) - { - uvec2 shadowDepthDimensions = _renderer->GetImageDimensions(resources.shadowDepthCascades[0]); - - commandList.SetViewport(0, 0, static_cast(shadowDepthDimensions.x), static_cast(shadowDepthDimensions.y), 0.0f, 1.0f); - commandList.SetScissorRect(0, shadowDepthDimensions.x, 0, shadowDepthDimensions.y); - - f32 biasConstantFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasConstant")); - f32 biasClamp = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasClamp")); - f32 biasSlopeFactor = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDepthBiasSlope")); - commandList.SetDepthBias(biasConstantFactor, biasClamp, biasSlopeFactor); - } - - commandList.PushMarker("Draw", Color::White); - - DrawParams drawParams; - drawParams.shadowPass = true; - drawParams.viewIndex = i; - drawParams.cullingEnabled = true; - drawParams.depth = data.depth[i]; - drawParams.instanceBuffer = ToBufferResource(data.culledInstanceBuffer); - drawParams.argumentBuffer = ToBufferResource(data.argumentBuffer); - - drawParams.globalDescriptorSet = data.globalSet; - drawParams.drawDescriptorSet = data.geometryPassSet; - - Draw(resources, frameIndex, graphResources, commandList, drawParams); - - commandList.PopMarker(); - } - - // Copy drawn count - { - u32 dstOffset = i * sizeof(u32); - commandList.CopyBuffer(data.drawCountReadBackBuffer, dstOffset, data.argumentBuffer, 4, 4); - } - - commandList.PopMarker(); - } - - // Finish by resetting the viewport, scissor and depth bias - vec2 renderSize = _renderer->GetRenderSize(); - commandList.SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); - commandList.SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); - commandList.SetDepthBias(0, 0, 0); - }); -} - void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer) { ZoneScoped; @@ -830,9 +592,6 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re if (CVAR_TerrainCastShadow.Get() != 1) return; - if (*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTechnique"_h) != 1) - return; - if (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid()) return; @@ -950,7 +709,6 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re commandList.PushMarker("Draw", Color::White); DrawParams drawParams; - drawParams.shadowPass = true; drawParams.svsmPass = true; drawParams.viewIndex = i; drawParams.cullingEnabled = true; @@ -1487,7 +1245,6 @@ void TerrainRenderer::CreatePipelines() std::vector permutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "0" }, { "SVSM_PASS", "0" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); @@ -1516,41 +1273,6 @@ void TerrainRenderer::CreatePipelines() _drawPipeline = _renderer->CreatePipeline(pipelineDesc); } - // Draw shadow - { - Renderer::GraphicsPipelineDesc pipelineDesc; - pipelineDesc.debugName = "Terrain Draw Shadow"; - - // Shaders - Renderer::VertexShaderDesc vertexShaderDesc; - { - std::vector permutationFields = - { - { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1" }, - { "SVSM_PASS", "0" } - }; - u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); - vertexShaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Terrain/Draw.vs"); - pipelineDesc.states.vertexShader = _renderer->LoadShader(vertexShaderDesc); - } - - // Depth state - pipelineDesc.states.depthStencilState.depthEnable = true; - pipelineDesc.states.depthStencilState.depthWriteEnable = true; - pipelineDesc.states.depthStencilState.depthFunc = Renderer::ComparisonFunc::GREATER; - - // Rasterizer state - pipelineDesc.states.rasterizerState.cullMode = Renderer::CullMode::NONE; - pipelineDesc.states.rasterizerState.frontFaceMode = Renderer::Settings::FRONT_FACE_STATE; - pipelineDesc.states.rasterizerState.depthBiasEnabled = true; - pipelineDesc.states.rasterizerState.depthClampEnabled = true; - - // Render targets - pipelineDesc.states.depthStencilFormat = Renderer::DepthImageFormat::D32_FLOAT; - - _drawShadowPipeline = _renderer->CreatePipeline(pipelineDesc); - } // Draw SVSM pages: attachment-less, the pixel shader writes depth into the page pool with // image atomics. Depth clamp stays on so pancaked casters rasterize instead of clipping { @@ -1563,7 +1285,6 @@ void TerrainRenderer::CreatePipelines() std::vector permutationFields = { { "EDITOR_PASS", "0" }, - { "SHADOW_PASS", "1" }, { "SVSM_PASS", "1" } }; u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Terrain/Draw.vs", permutationFields); @@ -1696,16 +1417,13 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend } else { - if (!params.shadowPass) - { - renderPassDesc.renderTargets[0] = params.visibilityBuffer; - } + renderPassDesc.renderTargets[0] = params.visibilityBuffer; renderPassDesc.depthStencil = params.depth; } commandList.BeginRenderPass(renderPassDesc); // Set pipeline - Renderer::GraphicsPipelineID pipeline = params.svsmPass ? _drawSVSMPipeline : (params.shadowPass ? _drawShadowPipeline : _drawPipeline); + Renderer::GraphicsPipelineID pipeline = params.svsmPass ? _drawSVSMPipeline : _drawPipeline; commandList.BeginPipeline(pipeline); // Set index buffer diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index 273c69e..d78bc3b 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -49,8 +49,7 @@ class TerrainRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); + void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); // Called once by ShadowRenderer at init, buffer binds must happen before the first frame @@ -86,7 +85,6 @@ class TerrainRenderer struct DrawParams { public: - bool shadowPass = false; bool svsmPass = false; // Attachment-less page render, needs an explicit render area u32 svsmRectIndex = 0xFFFFFFFFu; // Clip rect this draw renders (0-2), SVSM_CLIP_RECT_DISABLED = no clipping u32 viewIndex = 0; @@ -167,7 +165,6 @@ class TerrainRenderer Renderer::ComputePipelineID _fillDrawCallsPipeline; Renderer::ComputePipelineID _cullingPipeline; Renderer::GraphicsPipelineID _drawPipeline; - Renderer::GraphicsPipelineID _drawShadowPipeline; Renderer::GraphicsPipelineID _drawSVSMPipeline; Renderer::GPUVector _cellIndices; diff --git a/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang b/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang index 0732edc..cfcfc26 100644 --- a/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang +++ b/Source/Shaders/Shaders/DescriptorSet/Light.inc.slang @@ -1,26 +1,19 @@ #ifndef LIGHT_SET_INCLUDED #define LIGHT_SET_INCLUDED -#ifndef MAX_SHADOW_CASCADES -#define MAX_SHADOW_CASCADES 8 // Has to be kept in sync with the one in RenderSettings.h -#endif - #include "Shadows/SVSM.inc.slang" // WARNING: For now, when you change this file, make sure you recompile ALL shaders or you might get descriptor set reflection issues // This will be fixed later when we have better dependency tracking [[vk::binding(0, LIGHT)]] StructuredBuffer _entityTiles; -[[vk::binding(1, LIGHT)]] SamplerComparisonState _shadowCmpSampler; -[[vk::binding(2, LIGHT)]] SamplerState _shadowPointClampSampler; -[[vk::binding(3, LIGHT)]] Texture2D _shadowCascadeRTs[MAX_SHADOW_CASCADES]; -[[vk::binding(4, LIGHT)]] StructuredBuffer _packedDecals; // All decals in the world +[[vk::binding(1, LIGHT)]] StructuredBuffer _packedDecals; // All decals in the world -// SVSM (shadowTechnique 1) sampling resources -[[vk::binding(5, LIGHT)]] StructuredBuffer _svsmData; -[[vk::binding(6, LIGHT)]] StructuredBuffer _svsmPageTable; -[[vk::binding(7, LIGHT)]] Texture2D _svsmPagePool; -[[vk::binding(8, LIGHT)]] StructuredBuffer _svsmDynamicPageTable; -[[vk::binding(9, LIGHT)]] Texture2D _svsmDynamicPagePool; +// SVSM sampling resources +[[vk::binding(2, LIGHT)]] StructuredBuffer _svsmData; +[[vk::binding(3, LIGHT)]] StructuredBuffer _svsmPageTable; +[[vk::binding(4, LIGHT)]] Texture2D _svsmPagePool; +[[vk::binding(5, LIGHT)]] StructuredBuffer _svsmDynamicPageTable; +[[vk::binding(6, LIGHT)]] Texture2D _svsmDynamicPagePool; -#endif // LIGHT_SET_INCLUDED \ No newline at end of file +#endif // LIGHT_SET_INCLUDED diff --git a/Source/Shaders/Shaders/Include/Culling.inc.slang b/Source/Shaders/Shaders/Include/Culling.inc.slang index 6116e86..8dbfebf 100644 --- a/Source/Shaders/Shaders/Include/Culling.inc.slang +++ b/Source/Shaders/Shaders/Include/Culling.inc.slang @@ -10,8 +10,6 @@ struct AABBMinMax float3 max; }; -#define NUM_CULL_VIEWS 1 + MAX_SHADOW_CASCADES // Main view plus max number of cascades - // Based on the work of WickedEngine, thank you! https://github.com/turanszkij/WickedEngine/ struct Plane { diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index eccbba6..46aaa4e 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -25,11 +25,9 @@ DirectionalLight LoadDirectionalLight(uint index) return light; } -float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVertexData, uint4 lightInfo, ShadowSettings shadowSettings) +float3 ApplyLighting(float3 materialColor, PixelVertexData pixelVertexData, uint4 lightInfo, ShadowSettings shadowSettings) { uint numDirectionalLights = lightInfo.x; - uint shadowTechnique = lightInfo.y; // 0 = cascaded shadow maps, 1 = sparse virtual shadow maps - uint numCascades = lightInfo.z; float3 ambientColor = float3(0.0f, 0.0f, 0.0f); float3 directionalColor = float3(0.0f, 0.0f, 0.0f); @@ -54,11 +52,10 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte float3 lightColor = lerp(light.color.rgb, float3(1.0f, 1.0f, 1.0f), nDotL); // Light color based on normal lightColor *= light.color.a; // Intensity in alpha channel - // Directional Light Shadows - if (shadowSettings.enableShadows && shadowTechnique == 1) + // Directional Light Shadows: clipmap selection, page walk and far fade all live in + // GetSVSMShadowFactor + if (shadowSettings.enableShadows) { - // Sparse virtual shadow maps: clipmap selection, page walk and far fade all live in - // GetSVSMShadowFactor float shadowFactor = GetSVSMShadowFactor(pixelVertexData.worldPos, pixelVertexData.worldNormal, shadowSettings); // Sun elevation strength, fades shadows out around dawn/dusk @@ -66,33 +63,6 @@ float3 ApplyLighting(float2 uv, float3 materialColor, PixelVertexData pixelVerte lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); } - else if (shadowSettings.enableShadows && numCascades > 0) - { - shadowSettings.cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); - Camera cascadeCamera = _cameras[shadowSettings.cascadeIndex + 1]; // +1 because the first camera is the main camera - - // Normal-offset bias: push the receiver along its surface normal by a fraction of the - // cascade texel size, robust against acne on hard angles where slope bias fails - float2 shadowTexDim; - _shadowCascadeRTs[shadowSettings.cascadeIndex].GetDimensions(shadowTexDim.x, shadowTexDim.y); - float texelWorldSize = 2.0f / (cascadeCamera.viewToClip[0][0] * shadowTexDim.x); - float3 offsetWorldPos = pixelVertexData.worldPos + pixelVertexData.worldNormal * (texelWorldSize * shadowSettings.normalOffset); - - float4 shadowPosition = mul(float4(offsetWorldPos, 1.0f), cascadeCamera.worldToClip); - - float shadowFactor = GetShadowFactor(uv, shadowPosition, shadowSettings); - - // Fade to unshadowed over the last 15% of the last cascade, beyond it there is no shadow data - float lastSplitDepth = _cameras[numCascades].eyePosition.w; - float fadeStartDepth = 0.85f * lastSplitDepth; - float fade = saturate((pixelVertexData.viewPos.z - fadeStartDepth) / (lastSplitDepth - fadeStartDepth)); - shadowFactor = lerp(shadowFactor, 1.0f, fade); - - // Sun elevation strength, fades shadows out around dawn/dusk - shadowFactor = lerp(1.0f, shadowFactor, shadowSettings.strength); - - lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); - } directionalColor += lightColor; } diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index 2707bec..aa6dfe1 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -7,216 +7,12 @@ struct ShadowSettings { bool enableShadows; - float filterSize; - float penumbraFilterSize; float strength; // Driven by sun elevation, fades shadows out around dawn/dusk - float normalOffset; // Receiver offset along the surface normal in cascade texels + float normalOffset; // Receiver offset along the surface normal in shadow texels - float svsmConstantBias; // SVSM only: compare bias toward the sun in world meters, no hardware bias in the software depth path - - uint cascadeIndex; // Filled in by ApplyLighting + float svsmConstantBias; // Compare bias toward the sun in world meters, no hardware bias in the software depth path }; -float TextureProj(float4 P, float2 offset, uint shadowCascadeIndex) -{ - float shadow = 1.0f; - - float4 shadowCoord = P / P.w; - shadowCoord.xy = shadowCoord.xy * 0.5f + 0.5f; - - if (shadowCoord.z > -1.0f && shadowCoord.z < 1.0f) - { - float3 sc = float3(float2(shadowCoord.x, 1.0f - shadowCoord.y) + offset, shadowCoord.z); - //shadow = _shadowRT.SampleLevel(_shadowSampler, sc.xy, sc.z); - shadow = _shadowCascadeRTs[shadowCascadeIndex].SampleCmpLevelZero(_shadowCmpSampler, sc.xy, sc.z); - } - return shadow; -} - -// 9 imad (+ 6 iops with final shuffle) -uint3 PCG3DHash(uint3 v) -{ - v = v * 1664525u + 1013904223u; - - v.x += v.y * v.z; - v.y += v.z * v.x; - v.z += v.x * v.y; - - v ^= v >> 16u; - - v.x += v.y * v.z; - v.y += v.z * v.x; - v.z += v.x * v.y; - - return v; -} - -// Percentage Closer Filtering -float FilterPCF(float2 screenUV, float4 shadowCoord, ShadowSettings shadowSettings) -{ - float2 texDim; - _shadowCascadeRTs[0].GetDimensions(texDim.x, texDim.y); - - const float scale = 0.5f; - float dx = scale * (1.0f / texDim.x); - float dy = scale * (1.0f / texDim.y); - - // Calculate a random per-pixel rotation for the filtering - uint2 pixelPos = uint2(screenUV * texDim); - uint3 hash = PCG3DHash(uint3(pixelPos, pixelPos.x ^ pixelPos.y)); - float2 rand = float2(hash.xy & 0xFFFFu) / 65535.0f; - - float2 dirA = normalize(rand * 2.0f - 1.0f); - float2 dirB = float2(-dirA.y, dirA.x); - - dirA *= dx; - dirB *= dy; - - // Add together all the filter taps - float shadowFactor = 0.0f; - const int range = 2; - int count = 0; - - [unroll] - for (int x = -range; x <= range; x++) - { - [unroll] - for (int y = -range; y <= range; y++) - { - shadowFactor += TextureProj(shadowCoord, dirA * x + dirB * y, shadowSettings.cascadeIndex); - count++; - } - } - - return shadowFactor / float(count); -} - - -// Percentage Closer Soft Shadows, credits: https://www.gamedev.net/tutorials/programming/graphics/contact-hardening-soft-shadows-made-fast-r4906/ -float InterleavedGradientNoise(float2 screenPos) -{ - float3 magic = float3(0.06711056f, 0.00583715f, 52.9829189f); - return frac(magic.z * frac(dot(screenPos, magic.xy))); -} - -float2 VogelDiskSample(int sampleIndex, int samplesCount, float phi) -{ - float goldenAngle = 2.4f; - - float r = sqrt(sampleIndex + 0.5f) / sqrt(samplesCount); - float theta = sampleIndex * goldenAngle + phi; - - float sine, cosine; - sincos(theta, sine, cosine); - - return float2(r * cosine, r * sine); -} - -// This should be tuned for the visuals we want -float AvgBlockersDepthToPenumbra(float shadowMapViewZ, float avgBlockersDepth) -{ - // Reversed Z, greater depth = closer to the light, so blocker - receiver is the occlusion gap - float penumbra = (avgBlockersDepth - shadowMapViewZ) / max(1.0f - avgBlockersDepth, 1e-4f); - penumbra *= penumbra; - return saturate(80.0f * penumbra); -} - -float Penumbra(float gradientNoise, float2 shadowMapUV, float shadowMapViewZ, float penumbraFilterSize, int samplesCount, uint shadowCascadeIndex) -{ - float avgBlockersDepth = 0.0f; - float blockersCount = 0.0f; - - for (int i = 0; i < samplesCount; i++) - { - float2 sampleUV = VogelDiskSample(i, samplesCount, gradientNoise); - sampleUV = shadowMapUV + penumbraFilterSize * sampleUV; - - float sampleDepth = _shadowCascadeRTs[shadowCascadeIndex].SampleLevel(_shadowPointClampSampler, sampleUV, 0).x; - - // Reversed Z, blockers are closer to the light than the receiver and thus have greater depth - if (sampleDepth > shadowMapViewZ) - { - avgBlockersDepth += sampleDepth; - blockersCount += 1.0f; - } - } - - if (blockersCount > 0.0f) - { - avgBlockersDepth /= blockersCount; - return AvgBlockersDepthToPenumbra(shadowMapViewZ, avgBlockersDepth); - } - else - { - return 0.0f; - } -} - -float FilterPCSS(float2 screenUV, float4 P, ShadowSettings shadowSettings) -{ - float2 texDim; - _shadowCascadeRTs[0].GetDimensions(texDim.x, texDim.y); - - float4 shadowCoord = P / P.w; - shadowCoord.xy = shadowCoord.xy * 0.5f + 0.5f; - shadowCoord.y = 1.0f - shadowCoord.y; // Same Y flip as TextureProj, keeps all UVs in [0, 1] - - const float tau = 6.28318; - float gradientNoise = tau * InterleavedGradientNoise(screenUV * texDim); - - float shadow = 0.0f; - { - float penumbra = 1.0f - Penumbra(gradientNoise, shadowCoord.xy, shadowCoord.z, shadowSettings.penumbraFilterSize, 16, shadowSettings.cascadeIndex); - - for (int i = 0; i < 16; i++) - { - float2 sampleUV = VogelDiskSample(i, 16, gradientNoise); - sampleUV = shadowCoord.xy + sampleUV * penumbra * shadowSettings.filterSize; - - shadow += _shadowCascadeRTs[shadowSettings.cascadeIndex].SampleCmpLevelZero(_shadowCmpSampler, sampleUV, shadowCoord.z); - } - } - shadow /= 16.0f; - - return shadow; -} - -#define SHADOW_FILTER_MODE_OFF 0 -#define SHADOW_FILTER_MODE_PCF 1 // Percentage Closer Filtering -#define SHADOW_FILTER_MODE_PCSS 2 // Percentage Closer Soft Shadows - -#ifndef SHADOW_FILTER_MODE -#define SHADOW_FILTER_MODE SHADOW_FILTER_MODE_OFF -#endif - -float GetShadowFactor(float2 screenUV, float4 shadowCoord, ShadowSettings shadowSettings) -{ -#if SHADOW_FILTER_MODE == SHADOW_FILTER_MODE_PCF - return lerp(0.0f, 1.0f, FilterPCF(screenUV, shadowCoord, shadowSettings)); -#elif SHADOW_FILTER_MODE == SHADOW_FILTER_MODE_PCSS - return saturate(FilterPCSS(screenUV, shadowCoord, shadowSettings)); -#else - return lerp(0.0f, 1.0f, TextureProj(shadowCoord, float2(0, 0), shadowSettings.cascadeIndex)); -#endif -} - -// Returns a 0-based cascade index, clamped to the last cascade for depths beyond its split -uint GetShadowCascadeIndexFromDepth(float depth, uint numCascades) -{ - uint cascadeIndex = 0; - for (uint i = 1; i < numCascades; i++) - { - // _cameras[i].eyePosition.w holds the split depth of cascade i - 1 - if (depth > _cameras[i].eyePosition.w) - { - cascadeIndex = i; - } - } - return cascadeIndex; -} - -// ---- SVSM (shadowTechnique 1) ---- - // One texel from the virtual shadow map through the page table, resident = false when the page // has no physical backing (the caller falls back to a coarser clipmap) float SVSMLoadDepth(uint clipmapIndex, int2 virtualTexel, uint pageTableSize, uint pageSize, uint poolPagesPerRow, out bool resident) diff --git a/Source/Shaders/Shaders/Include/VisibilityBuffers.inc.slang b/Source/Shaders/Shaders/Include/VisibilityBuffers.inc.slang index 95d7aee..49b9623 100644 --- a/Source/Shaders/Shaders/Include/VisibilityBuffers.inc.slang +++ b/Source/Shaders/Shaders/Include/VisibilityBuffers.inc.slang @@ -542,21 +542,4 @@ float3 IDToColor3(uint ID) return color; } -float3 CascadeIDToColor(uint cascadeID) -{ - const uint cascadeCount = 8; - static float3 cascadeColors[cascadeCount] = - { - float3(1.0f, 0.0f, 0.0f), // Red - float3(0.0f, 1.0f, 0.0f), // Green - float3(0.0f, 0.0f, 1.0f), // Blue - float3(1.0f, 1.0f, 0.0f), // Yellow - float3(1.0f, 0.0f, 1.0f), // Purple - float3(0.0f, 1.0f, 1.0f), // Cyan - float3(1.0f, 0.5f, 0.0f), // Orange - float3(0.0f, 0.5f, 1.0f) // Light Blue - }; - - return cascadeColors[cascadeID % cascadeCount]; -} #endif // VISIBILITYBUFFERS_INCLUDED \ No newline at end of file diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 86e1f5f..586b4e5 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -1,5 +1,4 @@ -permutation DEBUG_ID = [0, 1, 2, 3, 4, 5]; -permutation SHADOW_FILTER_MODE = [0, 1, 2]; // Off, PCF, PCSS +permutation DEBUG_ID = [0, 1, 2, 3, 4, 5]; // 4 (CascadeID) retired with CSM, kept so 5 = SVSM margin stays stable permutation EDITOR_MODE = [0, 1]; // Off, Terrain #include "DescriptorSet/Debug.inc.slang" @@ -17,7 +16,7 @@ permutation EDITOR_MODE = [0, 1]; // Off, Terrain struct Constants { float4 renderInfo; // x = Render Width, y = Render Height, z = 1/Width, w = 1/Height - uint4 lightInfo; // x = Num Directional Lights, y = Num Point Lights, z = Num Cascades, w = Shadows Enabled + uint4 lightInfo; // x = Num Directional Lights, y = Shadows Ready (enabled, strength > 0, pool created), zw = UNUSED uint4 tileInfo; // xy = Num Tiles float4 fogColor; float4 fogSettings; // x = Enabled, y = Begin Fog Blend Dist, z = End Fog Blend Dist, w = UNUSED @@ -28,8 +27,7 @@ struct Constants float4 patchEdgeColor; float4 vertexColor; float4 brushColor; - float4 shadowFilterSettings; // x = Filter Size, y = Penumbra Filter Size, z = Shadow Strength, w = Normal Offset Bias - float4 svsmSettings; // x = Constant Bias (world meters) + float4 shadowSettings; // x = Shadow Strength, y = Normal Offset Bias, z = SVSM Constant Bias (world meters), w = UNUSED }; [[vk::push_constant]] Constants _constants; @@ -139,19 +137,17 @@ float4 ShadeTerrain(const uint2 pixelPos, const float2 screenUV, const Visibilit color.rgb *= pixelVertexData.color; ShadowSettings shadowSettings; - shadowSettings.enableShadows = _constants.lightInfo.w == 1; - shadowSettings.filterSize = _constants.shadowFilterSettings.x; - shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; - shadowSettings.strength = _constants.shadowFilterSettings.z; - shadowSettings.normalOffset = _constants.shadowFilterSettings.w; - shadowSettings.svsmConstantBias = _constants.svsmSettings.x; + shadowSettings.enableShadows = _constants.lightInfo.y == 1; + shadowSettings.strength = _constants.shadowSettings.x; + shadowSettings.normalOffset = _constants.shadowSettings.y; + shadowSettings.svsmConstantBias = _constants.shadowSettings.z; // Apply lighting - color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); + color.rgb = ApplyLighting(color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); // Apply decals color.rgb += ApplyDecals(tileIndex, pixelVertexData.worldPos, pixelVertexData.worldNormal); - + /*#if EDITOR_MODE == 1 // Get settings from push constants const float brushHardness = _constants.brushSettings.x; @@ -287,15 +283,13 @@ float4 ShadeModel(const uint2 pixelPos, const float2 screenUV, const VisibilityB } ShadowSettings shadowSettings; - shadowSettings.enableShadows = _constants.lightInfo.w == 1; - shadowSettings.filterSize = _constants.shadowFilterSettings.x; - shadowSettings.penumbraFilterSize = _constants.shadowFilterSettings.y; - shadowSettings.strength = _constants.shadowFilterSettings.z; - shadowSettings.normalOffset = _constants.shadowFilterSettings.w; - shadowSettings.svsmConstantBias = _constants.svsmSettings.x; + shadowSettings.enableShadows = _constants.lightInfo.y == 1; + shadowSettings.strength = _constants.shadowSettings.x; + shadowSettings.normalOffset = _constants.shadowSettings.y; + shadowSettings.svsmConstantBias = _constants.shadowSettings.z; // Apply lighting - color.rgb = ApplyLighting(screenUV, color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); + color.rgb = ApplyLighting(color.rgb, pixelVertexData, _constants.lightInfo, shadowSettings); // Apply highlight color.rgb *= pixelVertexData.highlightIntensity; @@ -341,15 +335,12 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) float3 debugColor = IDToColor3(vBuffer.triangleID); _resolvedColor[pixelPos] = float4(debugColor, 1); return; -#elif DEBUG_ID == 4 // CascadeID - uint numCascades = _constants.lightInfo.z; - PixelVertexData pixelVertexData = GetPixelVertexData(pixelPos, vBuffer, 0, _constants.renderInfo.xy); - uint cascadeIndex = GetShadowCascadeIndexFromDepth(pixelVertexData.viewPos.z, numCascades); - _resolvedColor[pixelPos] = float4(CascadeIDToColor(cascadeIndex), 1); +#elif DEBUG_ID == 4 // Retired (was CascadeID) + _resolvedColor[pixelPos] = float4(0, 0, 0, 1); return; #elif DEBUG_ID == 5 // SVSM compare margin PixelVertexData pixelVertexData = GetPixelVertexData(pixelPos, vBuffer, 0, _constants.renderInfo.xy); - float3 debugColor = GetSVSMDebugColor(pixelVertexData.worldPos, pixelVertexData.worldNormal, _constants.shadowFilterSettings.w, _constants.svsmSettings.x); + float3 debugColor = GetSVSMDebugColor(pixelVertexData.worldPos, pixelVertexData.worldNormal, _constants.shadowSettings.y, _constants.shadowSettings.z); _resolvedColor[pixelPos] = float4(debugColor, 1); return; #endif diff --git a/Source/Shaders/Shaders/Model/Draw.ps.slang b/Source/Shaders/Shaders/Model/Draw.ps.slang index 0d0fcad..1671c42 100644 --- a/Source/Shaders/Shaders/Model/Draw.ps.slang +++ b/Source/Shaders/Shaders/Model/Draw.ps.slang @@ -1,4 +1,3 @@ -permutation SHADOW_PASS = [0, 1]; #define GEOMETRY_PASS 1 #include "DescriptorSet/Global.inc.slang" @@ -12,19 +11,13 @@ struct PSInput uint4 drawIDInstanceIDTextureDataIDInstanceRefID : TEXCOORD0; float4 uv01 : TEXCOORD1; -#if !SHADOW_PASS uint triangleID : SV_PrimitiveID; -#endif }; -#if !SHADOW_PASS struct PSOutput { uint2 visibilityBuffer : SV_Target0; }; -#else -#define PSOutput void -#endif [shader("fragment")] PSOutput main(PSInput input) @@ -44,7 +37,7 @@ PSOutput main(PSInput input) if (blendingMode != 1) // ALPHA KEY continue; - + uint texture0SamplerIndex = (textureUnit.data1 >> 1) & 0x3; uint texture1SamplerIndex = (textureUnit.data1 >> 3) & 0x3; uint materialType = (textureUnit.data1 >> 16) & 0xFFFF; @@ -74,10 +67,8 @@ PSOutput main(PSInput input) } } -#if !SHADOW_PASS PSOutput output; output.visibilityBuffer = PackVisibilityBuffer(ObjectType::ModelOpaque, instanceRefID, input.triangleID); return output; -#endif -} \ No newline at end of file +} diff --git a/Source/Shaders/Shaders/Model/Draw.vs.slang b/Source/Shaders/Shaders/Model/Draw.vs.slang index e454ff7..c29df9f 100644 --- a/Source/Shaders/Shaders/Model/Draw.vs.slang +++ b/Source/Shaders/Shaders/Model/Draw.vs.slang @@ -1,5 +1,4 @@ permutation EDITOR_PASS = [0, 1]; -permutation SHADOW_PASS = [0, 1]; permutation SVSM_PASS = [0, 1]; // 1 adds per-draw clip-rect distances for the SVSM page render #define GEOMETRY_PASS 1 diff --git a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang index 79b5c57..8b6f19c 100644 --- a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang +++ b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang @@ -9,10 +9,10 @@ permutation SVSM_DYNAMIC = [0, 1]; // 1 = dynamic caster split pass, renders int #include "Model/ModelShared.inc.slang" #include "Shadows/SVSM.inc.slang" -// SVSM page render for models: keeps the alpha-key discard from the shadow permutation of -// Model/Draw.ps, then writes reversed-Z depth into the physical page pool with image atomics. -// No attachments, viewport spans the full virtual texture so SV_Position.xy is the window-local -// virtual texel. Paired with the SHADOW_PASS=1 vertex permutation. +// SVSM page render for models: keeps the alpha-key discard from Model/Draw.ps, then writes +// reversed-Z depth into the physical page pool with image atomics. No attachments, viewport +// spans the full virtual texture so SV_Position.xy is the window-local virtual texel. Paired +// with the SVSM_PASS=1 vertex permutation. // // SVSM_DYNAMIC renders the dynamic-instance fill into the dynamic pool: same bindings, the pass // binds the dynamic table/pool instead, and dynamic pages render whenever RESIDENT (every frame) @@ -41,7 +41,7 @@ void main(PSInput input) { uint textureDataID = input.drawIDInstanceIDTextureDataIDInstanceRefID.z; - // Alpha-key discard, mirrors the SHADOW_PASS branch of Model/Draw.ps + // Alpha-key discard, mirrors Model/Draw.ps without the visibility-buffer write TextureData textureData = LoadModelTextureData(_packedModelTextureDatas, textureDataID); for (uint textureUnitIndex = textureData.textureUnitOffset; textureUnitIndex < textureData.textureUnitOffset + textureData.numTextureUnits; textureUnitIndex++) diff --git a/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang deleted file mode 100644 index 28de0dc..0000000 --- a/Source/Shaders/Shaders/Shadows/CascadeFitCameras.cs.slang +++ /dev/null @@ -1,283 +0,0 @@ -#include "Include/Camera.inc.slang" -#include "Shadows/SDSM.inc.slang" - -// Final fit dispatch: builds the cascade cameras and writes them into the shared cameras -// buffer that culling, drawing and the material resolve read. Splits come from -// CascadeFitRange via the SDSM data buffer. -// Single thread: the work is a handful of tiny ortho fits with a sequential -// lastSplitDist dependency, cross-lane sync would cost more than it saves. - -[[vk::push_constant]] SDSMConstants _constants; - -[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera -[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _sdsmData; -[[vk::binding(2, PER_PASS)]] StructuredBuffer _cascadeBounds; // Written by CascadeXYReduce - -// 1.25x multiplicative buckets with 5% down-hysteresis: within a bucket the texel scale is -// constant so bound changes are pure translation, which the texel snap absorbs -float BucketHalfExtent(float halfExtent, float previousBucket) -{ - const float bucketRatio = 1.25f; - float target = pow(bucketRatio, ceil(log(halfExtent) / log(bucketRatio))); - target = max(target, 2.0f); // Keep degenerate fits (single-texel receivers) sane - - if (previousBucket > 0.0f && target < previousBucket) - { - // Only shrink when the padded extent clearly fits the smaller bucket - if (halfExtent >= 0.76f * previousBucket) - { - return previousBucket; - } - } - return target; -} - -// Every world position maps to clip (0, 0, 10, 1): TextureProj's z window rejects it and the -// material pass renders lit without sampling. Culling rejects everything via the frustum planes -Camera BuildEmptyCascadeCamera(float3 mainCameraPos, float splitDepth) -{ - Camera camera = (Camera)0; - camera.worldToClip[3] = float4(0.0f, 0.0f, 10.0f, 1.0f); - camera.viewToClip = BuildOrtho(-0.5f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f); // Keeps the normal-offset texel math finite - camera.eyePosition = float4(mainCameraPos, splitDepth); // Split depth must stay valid for neighbor selection - - for (uint i = 0; i < 6; i++) - { - camera.frustum[i] = float4(0.0f, 1.0f, 0.0f, 3.0e38f); // dot(n, p) - w is always far below -radius - } - return camera; -} - -[shader("compute")] -[numthreads(1, 1, 1)] -void main() -{ - Camera mainCamera = _rwCameras[0]; - - float nearClip = mainCamera.nearFar.x; - float farClip = mainCamera.nearFar.y; - float clipRange = farClip - nearClip; - - // Unproject the NDC cube corners, z = 0 is the far plane with reversed Z - const float3 ndcCorners[8] = - { - float3(-1.0f, 1.0f, 0.0f), - float3( 1.0f, 1.0f, 0.0f), - float3( 1.0f, -1.0f, 0.0f), - float3(-1.0f, -1.0f, 0.0f), - float3(-1.0f, 1.0f, 1.0f), - float3( 1.0f, 1.0f, 1.0f), - float3( 1.0f, -1.0f, 1.0f), - float3(-1.0f, -1.0f, 1.0f) - }; - - float3 baseCorners[8]; - for (uint j = 0; j < 8; j++) - { - float4 corner = mul(float4(ndcCorners[j], 1.0f), mainCamera.clipToWorld); - baseCorners[j] = corner.xyz / corner.w; - } - - float3 lightDirection = _constants.lightDirection.xyz; - - float lastSplitDist = 1.0f; - for (uint cascadeIndex = 0; cascadeIndex < _constants.numCascades; cascadeIndex++) - { - float splitDist = _sdsmData[0].splitDist[cascadeIndex]; - - // Decode the reduced light-space bounds for this cascade, sentinel decodes as min > max - bool xyValid = false; - float3 boundsMin = float3(0.0f, 0.0f, 0.0f); - float3 boundsMax = float3(0.0f, 0.0f, 0.0f); - if (_constants.useXYBounds >= 1) - { - uint3 encodedMin = uint3(_cascadeBounds[cascadeIndex * 3 + 0], _cascadeBounds[cascadeIndex * 3 + 1], _cascadeBounds[cascadeIndex * 3 + 2]); - uint3 encodedMax = uint3(_cascadeBounds[24 + cascadeIndex * 3 + 0], _cascadeBounds[24 + cascadeIndex * 3 + 1], _cascadeBounds[24 + cascadeIndex * 3 + 2]); - - xyValid = all(encodedMin <= encodedMax); - if (xyValid) - { - boundsMin = float3(DecodeLightSpaceCoord(encodedMin.x), DecodeLightSpaceCoord(encodedMin.y), DecodeLightSpaceCoord(encodedMin.z)); - boundsMax = float3(DecodeLightSpaceCoord(encodedMax.x), DecodeLightSpaceCoord(encodedMax.y), DecodeLightSpaceCoord(encodedMax.z)); - } - - _sdsmData[0].cascadeDiag[cascadeIndex] = float4(xyValid ? boundsMax - boundsMin : float3(0.0f, 0.0f, 0.0f), xyValid ? 1.0f : 0.0f); - } - - if (_constants.useXYBounds == 2 && !xyValid) - { - // No visible receivers in this cascade's depth range: nothing can sample it - _rwCameras[1 + cascadeIndex] = BuildEmptyCascadeCamera(mainCamera.eyePosition.xyz, _sdsmData[0].splitDepth[cascadeIndex]); - _sdsmData[0].cascadeStable[cascadeIndex] = float4(0.0f, 0.0f, 0.0f, 0.0f); // Buckets reseed when the cascade refills - lastSplitDist = splitDist; - continue; - } - - float3 frustumCenter; - float3 upDir; - float3 minExtents; - float3 maxExtents; - - if (_constants.useXYBounds == 2) - { - upDir = StableUp(lightDirection); - - float3 centerLS = (boundsMin + boundsMax) * 0.5f; - float3 halfLS = (boundsMax - boundsMin) * 0.5f; - - // Filter + normal-offset margin, applied before bucketing so the bucket absorbs it - float marginScale = (_constants.cascadeTextureSize + 2.0f * _constants.xyMarginTexels) / _constants.cascadeTextureSize; - halfLS.xy *= marginScale; - - float previousBucketX = _sdsmData[0].cascadeStable[cascadeIndex].x; - float previousBucketY = _sdsmData[0].cascadeStable[cascadeIndex].y; - float bucketHalfX = BucketHalfExtent(halfLS.x, previousBucketX); - float bucketHalfY = BucketHalfExtent(halfLS.y, previousBucketY); - _sdsmData[0].cascadeStable[cascadeIndex] = float4(bucketHalfX, bucketHalfY, 0.0f, 0.0f); - - // Z: pad by the margin in world units and quantize outward, Z only affects the bias - // distribution so piecewise-constant is enough - float texelWorldSize = (2.0f * bucketHalfX) / _constants.cascadeTextureSize; - float zPadding = _constants.xyMarginTexels * texelWorldSize; - float zStep = max(_constants.quantizeStep, 1.0f); - float quantizedMinZ = floor((boundsMin.z - zPadding) / zStep) * zStep; - float quantizedMaxZ = ceil((boundsMax.z + zPadding) / zStep) * zStep; - quantizedMaxZ = max(quantizedMaxZ, quantizedMinZ + 1.0f); - - // The reduced bounds are camera-relative, rotate the center back and re-anchor on the camera - float3x3 lightRotation = BuildLightRotation(lightDirection); - frustumCenter = mainCamera.eyePosition.xyz + mul(centerLS, transpose(lightRotation)); - - minExtents = float3(-bucketHalfX, -bucketHalfY, quantizedMinZ - centerLS.z); - maxExtents = float3(bucketHalfX, bucketHalfY, quantizedMaxZ - centerLS.z); - } - else - { - - // Slice the frustum, corners 0-3 are on the far plane, the ray points from far to near - float3 frustumCorners[8]; - for (uint j = 0; j < 4; j++) - { - float3 cornerRay = baseCorners[j + 4] - baseCorners[j]; - frustumCorners[j + 4] = baseCorners[j] + cornerRay * splitDist; - frustumCorners[j] = baseCorners[j] + cornerRay * lastSplitDist; - } - - frustumCenter = float3(0.0f, 0.0f, 0.0f); - for (uint j = 0; j < 8; j++) - { - frustumCenter += frustumCorners[j]; - } - frustumCenter /= 8.0f; - - // Non-stable mode uses the camera right axis, world X basis of viewToWorld - upDir = -float3(mainCamera.viewToWorld[0].xyz); - - if (_constants.stableShadows) - { - upDir = StableUp(lightDirection); - - float sphereRadius = 0.0f; - for (uint j = 0; j < 8; j++) - { - float dist = length(frustumCorners[j] - frustumCenter); - sphereRadius = max(sphereRadius, dist); - } - - sphereRadius = ceil(sphereRadius * 16.0f) / 16.0f; - - if (_constants.useDepthBounds) - { - // Quantize the radius into multiplicative buckets: within a bucket the texel scale is - // constant so bound changes are pure translation, which the texel snap absorbs. Only a - // bucket crossing rescales the grid, and only for that cascade - const float bucketRatio = 1.25f; - sphereRadius = pow(bucketRatio, ceil(log(sphereRadius) / log(bucketRatio))); - } - - maxExtents = float3(sphereRadius, sphereRadius, sphereRadius); - minExtents = -maxExtents; - } - else - { - // AABB of the slice in light view space - float4x4 lightView = BuildLookAt(frustumCenter, frustumCenter + lightDirection, upDir); - - float3 mins = float3(3.402823466e+38f, 3.402823466e+38f, 3.402823466e+38f); - float3 maxes = -mins; - for (uint j = 0; j < 8; j++) - { - float4 corner = mul(float4(frustumCorners[j], 1.0f), lightView); - mins = min(mins, corner.xyz / corner.w); - maxes = max(maxes, corner.xyz / corner.w); - } - - minExtents = mins; - maxExtents = maxes; - - // Adjust the min/max to accommodate the filtering size - const float fixedFilterKernelSize = 3.0f; - float scale = (_constants.cascadeTextureSize + fixedFilterKernelSize) / _constants.cascadeTextureSize; - minExtents.xy *= scale; - maxExtents.xy *= scale; - } - - } - - // minExtents.z is negative, this places the shadow camera on the sun side - float3 shadowCameraPos = frustumCenter + lightDirection * minExtents.z; - - float farPlane = maxExtents.z; - float nearPlane = minExtents.z; - - float4x4 proj = BuildOrtho(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, farPlane - nearPlane, 0.0f); - float4x4 view = BuildLookAt(shadowCameraPos, frustumCenter, upDir); - - if (_constants.stableShadows) - { - // Texel snap: round the projected world origin to texel increments - float4x4 shadowMatrix = mul(view, proj); - float4 shadowOrigin = shadowMatrix[3]; // mul(float4(0,0,0,1), M) = row 3 = glm translation column - shadowOrigin *= _constants.cascadeTextureSize / 2.0f; - - float4 roundedOrigin = round(shadowOrigin); - float4 roundOffset = (roundedOrigin - shadowOrigin) * (2.0f / _constants.cascadeTextureSize); - - proj[3].xy += roundOffset.xy; - } - - Camera cascadeCamera = (Camera)0; - - cascadeCamera.worldToView = view; - cascadeCamera.viewToClip = proj; - cascadeCamera.viewToWorld = InverseLookAt(view, shadowCameraPos); - cascadeCamera.clipToView = InverseOrtho(proj); - cascadeCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView - cascadeCamera.clipToWorld = mul(cascadeCamera.clipToView, cascadeCamera.viewToWorld); // glm viewToWorld * clipToView - - cascadeCamera.eyePosition = float4(shadowCameraPos, _sdsmData[0].splitDepth[cascadeIndex]); - cascadeCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); - cascadeCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); - - // Frustum planes, slot order matches the FrustumPlane enum: Left, Right, Bottom, Top, Near, Far - float4 row0 = MatrixRow(cascadeCamera.worldToClip, 0); - float4 row1 = MatrixRow(cascadeCamera.worldToClip, 1); - float4 row2 = MatrixRow(cascadeCamera.worldToClip, 2); - float4 row3 = MatrixRow(cascadeCamera.worldToClip, 3); - - cascadeCamera.frustum[0] = ExtractPlane(row3 + row0); // Left - cascadeCamera.frustum[1] = ExtractPlane(row3 - row0); // Right - cascadeCamera.frustum[2] = ExtractPlane(row3 + row1); // Bottom - cascadeCamera.frustum[3] = ExtractPlane(row3 - row1); // Top - cascadeCamera.frustum[4] = ExtractPlane(row3 - row2); // Near, reversed Z so depth 1 is near - cascadeCamera.frustum[5] = ExtractPlane(row2); // Far - - // The near plane sits at the shadow camera on the sun side. Casters beyond it still render - // correctly (depth clamp pancakes them), so culling must not reject them - cascadeCamera.frustum[4].w -= _constants.casterMargin; - - _rwCameras[1 + cascadeIndex] = cascadeCamera; - - lastSplitDist = splitDist; - } -} diff --git a/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang deleted file mode 100644 index 445f3f0..0000000 --- a/Source/Shaders/Shaders/Shadows/CascadeFitRange.cs.slang +++ /dev/null @@ -1,113 +0,0 @@ -#include "Include/Camera.inc.slang" -#include "Shadows/SDSM.inc.slang" - -// First fit dispatch: turns the reduced depth min/max into the working cascade range -// (expand-instant, windowed-jump shrink, outward quantization) and the split distances. -// CascadeXYReduce assigns pixels to cascades from splitDepth, CascadeFitCameras builds -// the cameras from splitDist. Both forms are stored, not recomputed, to keep bit parity. - -[[vk::push_constant]] SDSMConstants _constants; - -[[vk::binding(0, PER_PASS)]] RWStructuredBuffer _sdsmData; -[[vk::binding(1, PER_PASS)]] StructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) -[[vk::binding(2, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera - -[shader("compute")] -[numthreads(1, 1, 1)] -void main() -{ - Camera mainCamera = _srcCameras[0]; - - float nearClip = mainCamera.nearFar.x; - float farClip = mainCamera.nearFar.y; - float clipRange = farClip - nearClip; - - // Working range for the cascade distribution - float rangeMin = max(nearClip, 1.0f); - float rangeMax = min(farClip, _constants.shadowMaxDistance); - - SDSMState state = _sdsmData[0].state; - - if (_constants.useDepthBounds && _depthMinMax[1] != 0) - { - // Reversed Z linearization, depth 1 = near plane, depth 0 = far plane - float minDepth = asfloat(_depthMinMax[1]); // Max depth bits = nearest sample - float maxDepth = asfloat(_depthMinMax[0]); // Min depth bits = farthest sample - - float rawMin = (nearClip * farClip) / (nearClip + minDepth * (farClip - nearClip)); - float rawMax = (nearClip * farClip) / (nearClip + maxDepth * (farClip - nearClip)); - - rawMin = clamp(rawMin, rangeMin, _constants.shadowMaxDistance - 1.0f); - rawMax = clamp(rawMax, rawMin + 1.0f, _constants.shadowMaxDistance); - - // Hysteresis: expand instantly, shrink in a single jump after the range has been smaller - // for a whole window. A continuous shrink rescales the cascades every step and the texel - // grid realignment reads as shimmer, one jump per window is one realignment - if (state.smoothedMax <= state.smoothedMin) // Uninitialized state - { - state.smoothedMin = rawMin; - state.smoothedMax = rawMax; - state.windowLowestMin = rawMin; - state.windowHighestMax = rawMax; - state.windowTimer = 0.0f; - } - else - { - state.smoothedMin = min(state.smoothedMin, rawMin); - state.smoothedMax = max(state.smoothedMax, rawMax); - - state.windowLowestMin = min(state.windowLowestMin, rawMin); - state.windowHighestMax = max(state.windowHighestMax, rawMax); - state.windowTimer += _constants.deltaTime; - - if (state.windowTimer >= _constants.shrinkDelay) - { - // Shrink to the widest bounds the window actually needed - state.smoothedMin = max(state.smoothedMin, state.windowLowestMin); - state.smoothedMax = min(state.smoothedMax, state.windowHighestMax); - - state.windowLowestMin = rawMin; - state.windowHighestMax = rawMax; - state.windowTimer = 0.0f; - } - } - - // Quantize outward AFTER smoothing so the working bounds are piecewise-constant - // and the stable texel snapping stays effective - float step = max(_constants.quantizeStep, 0.01f); - rangeMin = max(floor(state.smoothedMin / step) * step, max(nearClip, 1.0f)); - rangeMax = min(ceil(state.smoothedMax / step) * step, _constants.shadowMaxDistance); - rangeMax = max(rangeMax, rangeMin + 1.0f); - } - else - { - // Parity mode / no valid depth: keep the state seeded so enabling SDSM starts sane - state.smoothedMin = rangeMin; - state.smoothedMax = rangeMax; - state.windowLowestMin = rangeMin; - state.windowHighestMax = rangeMax; - state.windowTimer = 0.0f; - } - - state.usedMin = rangeMin; - state.usedMax = rangeMax; - _sdsmData[0].state = state; - - // Split distances, same log/uniform lambda mix as the legacy CPU path. - // The split fractions stay relative to the FULL camera clip range so the frustum - // corner slicing and the eyePosition.w = distance - near semantics hold - { - float ratio = rangeMax / rangeMin; - float range = rangeMax - rangeMin; - for (uint i = 0; i < _constants.numCascades; i++) - { - float p = (i + 1) / float(_constants.numCascades); - float logSplit = rangeMin * pow(ratio, p); - float uniformSplit = rangeMin + range * p; - float d = _constants.cascadeSplitLambda * (logSplit - uniformSplit) + uniformSplit; - - _sdsmData[0].splitDist[i] = 1.0f - ((d - nearClip) / clipRange); - _sdsmData[0].splitDepth[i] = d - nearClip; - } - } -} diff --git a/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang b/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang deleted file mode 100644 index 1fea56f..0000000 --- a/Source/Shaders/Shaders/Shadows/CascadeXYReduce.cs.slang +++ /dev/null @@ -1,120 +0,0 @@ -#include "Include/Camera.inc.slang" -#include "Shadows/SDSM.inc.slang" - -// Second fit dispatch: reduces the light-space AABB of the visible depth samples per cascade. -// Positions are reconstructed camera-relative (clipToView + rotate-only viewToWorld), going -// through clipToWorld at world-scale coordinates loses precision. Coordinates are encoded -// with the order-preserving signed-float transform so uint atomics work on them. - -[[vk::push_constant]] SDSMConstants _constants; - -[[vk::binding(0, PER_PASS)]] Texture2D _depth; -[[vk::binding(1, PER_PASS)]] StructuredBuffer _srcCameras; // Element 0 = main camera -[[vk::binding(2, PER_PASS)]] StructuredBuffer _sdsmData; -[[vk::binding(3, PER_PASS)]] RWStructuredBuffer _cascadeBounds; // [cascade * 3 + axis] encoded mins, [24 + cascade * 3 + axis] encoded maxs - -groupshared uint gMins[24]; -groupshared uint gMaxs[24]; - -struct CSInput -{ - uint3 dispatchThreadID : SV_DispatchThreadID; - uint groupIndex : SV_GroupIndex; -}; - -void Contribute(uint cascadeIndex, uint3 encoded) -{ - uint base = cascadeIndex * 3; - InterlockedMin(gMins[base + 0], encoded.x); - InterlockedMin(gMins[base + 1], encoded.y); - InterlockedMin(gMins[base + 2], encoded.z); - InterlockedMax(gMaxs[base + 0], encoded.x); - InterlockedMax(gMaxs[base + 1], encoded.y); - InterlockedMax(gMaxs[base + 2], encoded.z); -} - -[shader("compute")] -[numthreads(16, 16, 1)] -void main(CSInput input) -{ - if (input.groupIndex < 24) - { - gMins[input.groupIndex] = 0xFFFFFFFFu; - gMaxs[input.groupIndex] = 0u; - } - GroupMemoryBarrierWithGroupSync(); - - uint2 dim; - _depth.GetDimensions(dim.x, dim.y); - - uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); - float depth = _depth[pixel]; - - // Cleared depth is exactly 0.0 (reversed Z far plane), sky must not drive the bounds - if (depth != 0.0f) - { - Camera mainCamera = _srcCameras[0]; - - float2 uv = (float2(pixel) + 0.5f) / float2(dim); - float2 ndc = float2(uv.x, 1.0f - uv.y) * 2.0f - 1.0f; - - float4 viewPos = mul(float4(ndc, depth, 1.0f), mainCamera.clipToView); - viewPos.xyz /= viewPos.w; - - float viewZ = viewPos.z; - uint numCascades = _constants.numCascades; - float lastSplitDepth = _sdsmData[0].splitDepth[numCascades - 1]; - - // Beyond the last split the material pass is fully faded, those pixels never use cascade data - if (viewZ <= lastSplitDepth * 1.005f) - { - // Mirrors GetShadowCascadeIndexFromDepth, _cameras[i].eyePosition.w == splitDepth[i - 1] - uint cascadeIndex = 0; - for (uint i = 1; i < numCascades; i++) - { - if (viewZ > _sdsmData[0].splitDepth[i - 1]) - { - cascadeIndex = i; - } - } - - // Camera-relative world position, rotation only - float3 relativePos = mul(float4(viewPos.xyz, 0.0f), mainCamera.viewToWorld).xyz; - float3 lightSpacePos = mul(relativePos, BuildLightRotation(_constants.lightDirection.xyz)); - - uint3 encoded = uint3( - EncodeLightSpaceCoord(lightSpacePos.x), - EncodeLightSpaceCoord(lightSpacePos.y), - EncodeLightSpaceCoord(lightSpacePos.z)); - - Contribute(cascadeIndex, encoded); - - // Pixels near a split also contribute to the neighbor: the material pass reconstructs - // viewZ through a different float path and must never select a cascade this reduce - // considered empty - float epsilon = 0.005f * max(_sdsmData[0].splitDepth[cascadeIndex], 1.0f); - if (cascadeIndex > 0 && (viewZ - _sdsmData[0].splitDepth[cascadeIndex - 1]) < epsilon) - { - Contribute(cascadeIndex - 1, encoded); - } - if (cascadeIndex + 1 < numCascades && (_sdsmData[0].splitDepth[cascadeIndex] - viewZ) < epsilon) - { - Contribute(cascadeIndex + 1, encoded); - } - } - } - - GroupMemoryBarrierWithGroupSync(); - - if (input.groupIndex < 24) - { - if (gMins[input.groupIndex] != 0xFFFFFFFFu) - { - InterlockedMin(_cascadeBounds[input.groupIndex], gMins[input.groupIndex]); - } - if (gMaxs[input.groupIndex] != 0u) - { - InterlockedMax(_cascadeBounds[24 + input.groupIndex], gMaxs[input.groupIndex]); - } - } -} diff --git a/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang b/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang deleted file mode 100644 index 6bfe9ec..0000000 --- a/Source/Shaders/Shaders/Shadows/DepthMinMax.cs.slang +++ /dev/null @@ -1,60 +0,0 @@ -#include "DescriptorSet/Global.inc.slang" - -#include "Include/Common.inc.slang" - -// Reduces the scene depth buffer to global min/max depth for SDSM cascade fitting. -// Reversed Z: max depth bits = nearest sample, min depth bits = farthest sample. -// The buffer is reset to the sentinel { 0xFFFFFFFF, 0 } before dispatch, uint ordering -// matches float ordering for depth in [0, 1]. - -[[vk::binding(0, PER_PASS)]] Texture2D _depth; -[[vk::binding(1, PER_PASS)]] RWStructuredBuffer _depthMinMax; // [0] = min depth bits (farthest), [1] = max depth bits (nearest) - -groupshared uint gMinDepth; -groupshared uint gMaxDepth; - -struct CSInput -{ - uint3 dispatchThreadID : SV_DispatchThreadID; - uint groupIndex : SV_GroupIndex; -}; - -[shader("compute")] -[numthreads(16, 16, 1)] -void main(CSInput input) -{ - if (input.groupIndex == 0) - { - gMinDepth = 0xFFFFFFFFu; - gMaxDepth = 0u; - } - GroupMemoryBarrierWithGroupSync(); - - uint2 dim; - _depth.GetDimensions(dim.x, dim.y); - - uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); - float depth = _depth[pixel]; - - // Cleared depth is exactly 0.0 (reversed Z far plane), those pixels are sky and must not drive the bounds - bool isValid = depth != 0.0f; - float minCandidate = isValid ? depth : 1.0f; - float maxCandidate = isValid ? depth : 0.0f; - - float waveMin = WaveActiveMin(minCandidate); - float waveMax = WaveActiveMax(maxCandidate); - - if (WaveIsFirstLane()) - { - InterlockedMin(gMinDepth, asuint(waveMin)); - InterlockedMax(gMaxDepth, asuint(waveMax)); - } - - GroupMemoryBarrierWithGroupSync(); - - if (input.groupIndex == 0) - { - InterlockedMin(_depthMinMax[0], gMinDepth); - InterlockedMax(_depthMinMax[1], gMaxDepth); - } -} diff --git a/Source/Shaders/Shaders/Shadows/SDSM.inc.slang b/Source/Shaders/Shaders/Shadows/LightBasis.inc.slang similarity index 58% rename from Source/Shaders/Shaders/Shadows/SDSM.inc.slang rename to Source/Shaders/Shaders/Shadows/LightBasis.inc.slang index 8747429..3f78760 100644 --- a/Source/Shaders/Shaders/Shadows/SDSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/LightBasis.inc.slang @@ -1,52 +1,12 @@ -#ifndef SDSM_INCLUDED -#define SDSM_INCLUDED +#ifndef LIGHT_BASIS_INCLUDED +#define LIGHT_BASIS_INCLUDED -// Shared types and helpers for the SDSM cascade fitting chain: -// DepthMinMax.cs -> CascadeFitRange.cs -> CascadeXYReduce.cs -> CascadeFitCameras.cs +// Light-space basis and camera matrix helpers shared by the SVSM update chain and the sampler. // // Matrix layout rule (load-bearing): shaders consume glm column-major uploads with // mul(rowVector, M), so a Slang float4x4 row i must hold what glm stores in column i. // glm composition C = A * B therefore becomes C_s = mul(B_s, A_s) here. -// Push constants shared by all fit dispatches, identical bytes pushed to each pipeline -struct SDSMConstants -{ - float4 lightDirection; // xyz = direction the light travels - uint numCascades; - float cascadeSplitLambda; - float cascadeTextureSize; - uint stableShadows; - float shadowMaxDistance; - float quantizeStep; - float shrinkDelay; - float deltaTime; - uint useDepthBounds; - float casterMargin; - uint useXYBounds; // 0 = sphere fit, 1 = reduce runs for diagnostics only, 2 = XY bounds drive the cameras - float xyMarginTexels; -}; - -struct SDSMState -{ - float smoothedMin; - float smoothedMax; - float usedMin; - float usedMax; - float windowLowestMin; // Widest bounds seen during the current shrink window - float windowHighestMax; - float windowTimer; - float padding; -}; - -struct SDSMData -{ - SDSMState state; // Persistent Z-range hysteresis, float offsets [0..7] are read back by the CPU - float splitDist[8]; // Split as an NDC-corner lerp fraction, consumed by the slice corner math - float splitDepth[8]; // Split as view distance - nearClip, mirrors _cameras[i].eyePosition.w semantics - float4 cascadeStable[8]; // Persistent per cascade: x = bucketHalfX, y = bucketHalfY - float4 cascadeDiag[8]; // Per frame diagnostics: xyz = fitted extents, w = valid -}; - // Constant up for the stable path, falls back to Z when the light is near vertical float3 StableUp(float3 lightDirection) { @@ -69,8 +29,8 @@ float4x4 BuildLookAt(float3 eye, float3 center, float3 up) } // Rotation-only light basis, lightSpace = mul(cameraRelativeWorld, R). The rows mirror -// BuildLookAt(0, lightDirection, StableUp) term for term so CascadeXYReduce coordinates -// and the CascadeFitCameras view matrix agree bit for bit +// BuildLookAt(0, lightDirection, StableUp) term for term so window coordinates and the +// clipmap view matrices agree bit for bit float3x3 BuildLightRotation(float3 lightDirection) { float3 f = normalize(lightDirection); @@ -137,18 +97,4 @@ float4 ExtractPlane(float4 row) return float4(row.xyz / normalLength, -row.w / normalLength); } -// Order-preserving float <-> uint for atomic min/max on signed light-space coordinates. -// Branches on the stored sign bit so -0.0 orders below +0.0. Sentinels: 0xFFFFFFFF orders -// above all real keys, 0x00000000 below, so an untouched slot decodes as min > max (invalid) -uint EncodeLightSpaceCoord(float f) -{ - uint u = asuint(f); - return (u & 0x80000000u) != 0 ? ~u : (u | 0x80000000u); -} - -float DecodeLightSpaceCoord(uint u) -{ - return asfloat((u & 0x80000000u) != 0 ? (u ^ 0x80000000u) : ~u); -} - -#endif // SDSM_INCLUDED +#endif // LIGHT_BASIS_INCLUDED diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang index 13f84f6..d667d07 100644 --- a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -1,7 +1,7 @@ #ifndef SVSM_INCLUDED #define SVSM_INCLUDED -#include "Shadows/SDSM.inc.slang" +#include "Shadows/LightBasis.inc.slang" // Shared types and helpers for the Sparse Virtual Shadow Map chain: // SVSMPrepare.cs -> SVSMInvalidateAABBs.cs -> SVSMPageUpdateA.cs -> SVSMPageMark.cs -> SVSMPageUpdateB.cs @@ -59,7 +59,7 @@ struct SVSMConstants #define SVSM_CAUSE_AABB_OVERFLOW 8u // Persistent GPU state + per-frame stats. Scalar arrays only so the std430 layout matches the -// flat u32 readback on the CPU, same pattern as SDSMData +// flat u32 readback on the CPU struct SVSMData { float4 prevLightDirection; // w > 0.5 once valid diff --git a/Source/Shaders/Shaders/Terrain/Culling.cs.slang b/Source/Shaders/Shaders/Terrain/Culling.cs.slang index 6b389ff..d87b589 100644 --- a/Source/Shaders/Shaders/Terrain/Culling.cs.slang +++ b/Source/Shaders/Shaders/Terrain/Culling.cs.slang @@ -182,10 +182,10 @@ void main(CSInput input) CullForCamera(drawInput, _cameras[0], bitmaskInput, _constants.debugDrawView == 0); } - // Shadow cascades + // Shadow clipmap views for (uint i = 1; i < _constants.numCascades + 1; i++) { - drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow cascades... yet? + drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow views... yet? bitmaskInput.bitmaskOffset = _constants.bitMaskBufferSizePerView * i; diff --git a/Source/Shaders/Shaders/Terrain/Draw.vs.slang b/Source/Shaders/Shaders/Terrain/Draw.vs.slang index ffb3b5e..ff1978d 100644 --- a/Source/Shaders/Shaders/Terrain/Draw.vs.slang +++ b/Source/Shaders/Shaders/Terrain/Draw.vs.slang @@ -1,5 +1,4 @@ permutation EDITOR_PASS = [0, 1]; -permutation SHADOW_PASS = [0, 1]; permutation SVSM_PASS = [0, 1]; // 1 adds per-draw clip-rect distances for the SVSM page render #define GEOMETRY_PASS 1 @@ -33,7 +32,7 @@ struct VSInput struct VSOutput { float4 position : SV_Position; -#if !EDITOR_PASS && !SHADOW_PASS +#if !EDITOR_PASS && !SVSM_PASS uint instanceID : TEXCOORD0; #endif #if SVSM_PASS @@ -67,7 +66,7 @@ VSOutput main(VSInput input) output.position = mul(float4(vertex.position, 1.0f), _cameras[_constants.viewIndex].worldToClip); -#if !EDITOR_PASS && !SHADOW_PASS +#if !EDITOR_PASS && !SVSM_PASS output.instanceID = instanceData.globalCellID; #endif #if SVSM_PASS diff --git a/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang index 7b44770..4b19f22 100644 --- a/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang +++ b/Source/Shaders/Shaders/Terrain/DrawSVSM.ps.slang @@ -5,7 +5,7 @@ #include "Shadows/SVSM.inc.slang" // SVSM page render: no attachments, the fragment writes reversed-Z depth into the physical page -// pool with image atomics. Paired with the SHADOW_PASS=1 vertex permutation, viewport spans the +// pool with image atomics. Paired with the SVSM_PASS=1 vertex permutation, viewport spans the // full virtual texture so SV_Position.xy is the window-local virtual texel. struct Constants diff --git a/Source/Shaders/Shaders/Utils/Culling.cs.slang b/Source/Shaders/Shaders/Utils/Culling.cs.slang index c74009e..343fb13 100644 --- a/Source/Shaders/Shaders/Utils/Culling.cs.slang +++ b/Source/Shaders/Shaders/Utils/Culling.cs.slang @@ -45,7 +45,7 @@ struct CullingData [[vk::binding(2, PER_PASS)]] StructuredBuffer _cullingDatas; [[vk::binding(3, PER_PASS)]] StructuredBuffer _instanceMatrices; [[vk::binding(4, PER_PASS)]] SamplerState _depthSampler; -[[vk::binding(5, PER_PASS)]] Texture2D _depthPyramid; // TODO: Occlusion culling for shadow cascades? +[[vk::binding(5, PER_PASS)]] Texture2D _depthPyramid; // TODO: Occlusion culling for shadow views? #if USE_BITMASKS // Both bitmask generations stay bound; currentBitmaskIndex selects which one is written this frame @@ -322,10 +322,10 @@ void main(CSInput input) // Shadow is only supported if we USE_BITMASKS #if USE_BITMASKS - // Shadow cascades + // Shadow clipmap views for (uint i = 1; i < _constants.numCascades + 1; i++) { - drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow cascades... yet? + drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow views... yet? bitmaskInput.bitmaskOffset = _constants.bitMaskBufferUintsPerView * i; diff --git a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang index 249ca4a..4ba3b8c 100644 --- a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang +++ b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang @@ -57,7 +57,7 @@ struct CullingData [[vk::binding(3, PER_PASS)]] StructuredBuffer _cullingDatas; [[vk::binding(4, PER_PASS)]] StructuredBuffer _instanceMatrices; [[vk::binding(5, PER_PASS)]] SamplerState _depthSampler; -[[vk::binding(6, PER_PASS)]] Texture2D _depthPyramid; // TODO: Occlusion culling for shadow cascades? +[[vk::binding(6, PER_PASS)]] Texture2D _depthPyramid; // TODO: Occlusion culling for shadow views? #if USE_BITMASKS // TODO: When instanceCounts resizes we need to invalidate (zero out) the prev bitmask since a single index in current and prev would point to different actual drawcalls @@ -198,7 +198,7 @@ struct CullOutput }; #if USE_BITMASKS -// Shadow cascades only need the visibility bitmask, the per-view draw sets are filled from it later in the geometry pass +// Shadow clipmap views only need the visibility bitmask, the per-view draw sets are filled from it later in the geometry pass void CullForCascade(DrawInput drawInput, Camera camera, uint bitmaskOffset, bool debugDrawResults) { bool isVisible = drawInput.instanceCount > 0 && IsSphereInsideFrustum(camera, drawInput.sphere); @@ -418,7 +418,7 @@ void main(CSInput input) } #if USE_BITMASKS - // Shadow cascades + // Shadow clipmap views for (uint i = 1; i < _constants.numCascades + 1; i++) { bool debugDrawResults = (_constants.debugDrawView == i); From aa0b414823c4503b3277a057ff48645b306d0900 Mon Sep 17 00:00:00 2001 From: Pursche Date: Fri, 17 Jul 2026 17:00:18 +0200 Subject: [PATCH 16/24] Gate per-view SVSM fills with GPU-written indirect dispatch args --- .../Game-Lib/Rendering/CulledRenderer.cpp | 17 +++++++-- .../Game-Lib/Rendering/CulledRenderer.h | 6 +++- .../Rendering/Model/ModelRenderer.cpp | 10 ++---- .../Rendering/Shadow/ShadowRenderer.cpp | 31 ++++++++++++++-- .../Rendering/Shadow/ShadowRenderer.h | 23 ++++++------ .../Rendering/Terrain/TerrainRenderer.cpp | 15 +++++++- .../Rendering/Terrain/TerrainRenderer.h | 5 +++ Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 4 +-- .../Shaders/Shadows/SVSMFinalize.cs.slang | 36 ++++++++++++++++--- 9 files changed, 115 insertions(+), 32 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index ff97543..3af4fa0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -3,6 +3,7 @@ #include "Game-Lib/Rendering/Debug/DebugRenderer.h" #include "Game-Lib/Rendering/GameRenderer.h" #include "Game-Lib/Rendering/RenderUtils.h" +#include "Game-Lib/Rendering/Shadow/ShadowRenderer.h" #include @@ -679,7 +680,16 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + if (params.svsmPass && params.svsmFillArgsBuffer != Renderer::BufferMutableResource::Invalid()) + { + // Finalize zeroed the group count for rings with no page work this frame + u32 argsOffset = (viewIndex - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + (keepDynamic ? ShadowRenderer::SVSM_FILL_ARGS_DYNAMIC_OFFSET : 0); + params.commandList->DispatchIndirect(params.svsmFillArgsBuffer, argsOffset); + } + else + { + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + } params.commandList->EndPipeline(pipeline); params.commandList->PopMarker(); @@ -904,8 +914,9 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) // SVSM caster split: rebuild the shared buffers with only dynamic instances and draw them // into the dynamic pool. Runs after the static readback copies so the perf stats show the - // static (dominant) counts. Views without recent live dynamic pages skip the whole phase - if (params.svsmSplitFills && params.enableDrawing && i > 0 && params.svsmDynamicViewActive[i]) + // static (dominant) counts. Rings without resident dynamic pages cost a zero-group + // indirect fill and a zero-count draw (Finalize's fill args, same-frame GPU truth) + if (params.svsmSplitFills && params.enableDrawing && i > 0) { params.commandList->PushMarker("Dynamic", Color::PastelOrange); diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index 6e23787..a910cdb 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -322,7 +322,11 @@ class CulledRenderer bool svsmSplitFills = false; std::function drawCallbackDynamic; Renderer::BufferMutableResource svsmDynamicDrawCountReadBackBuffer; // Per-view dynamic surviving counts for the perf editor - bool svsmDynamicViewActive[Renderer::Settings::MAX_VIEWS] = { false }; // Views without recent live dynamic pages skip their dynamic fill+draw + + // Finalize-written per-view fill dispatch args: rings with no dirty static pages / no + // resident dynamic pages this frame get zero-group fills. Same-frame GPU truth — a + // CPU/readback gate here is a frame late and flickers freshly acquired dynamic pages + Renderer::BufferMutableResource svsmFillArgsBuffer; }; void GeometryPass(GeometryPassParams& params); void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic); // Shared-buffer rebuild from a view's bitmask slice diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 264ad49..4dd9faf 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -1014,6 +1014,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend Renderer::BufferMutableResource triangleCountReadBackBuffer; Renderer::BufferMutableResource svsmDynamicDrawCountReadBackBuffer; + Renderer::BufferMutableResource svsmFillArgsBuffer; Renderer::DescriptorSetResource globalSet; Renderer::DescriptorSetResource modelSet; @@ -1032,6 +1033,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); + data.svsmFillArgsBuffer = builder.Write(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed by DispatchIndirect in the per-view fills if (splitFills) { data.dynamicPagePool = builder.Write(shadowRenderer->GetSVSMDynamicPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); @@ -1124,15 +1126,9 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend params.svsmExtent = uvec2(virtualSize, virtualSize); params.svsmSplitFills = splitFills; + params.svsmFillArgsBuffer = data.svsmFillArgsBuffer; if (splitFills) { - // Rings with no live dynamic pages the last two frames skip their dynamic phase, - // a caster entering a fresh ring renders at most one frame late - for (u32 view = 1; view <= numClipmaps; view++) - { - params.svsmDynamicViewActive[view] = shadowRenderer->GetSVSMDynamicViewActive(view - 1); - } - params.svsmDynamicDrawCountReadBackBuffer = data.svsmDynamicDrawCountReadBackBuffer; params.drawCallbackDynamic = [&](DrawParams& drawParams) { diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 8468af4..ec633a1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -141,10 +141,11 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) _svsmPoolNeedsClear = true; // Fresh VRAM is garbage that must never be sampled, zero both once } + // Diagnostics only (perf editor stats), rendering decisions never read this — it is a + // frame old, the GPU gates its own per-view work through Finalize's fill dispatch args u32* svsmData = static_cast(_renderer->MapBuffer(_svsmDataReadBackBuffer)); if (svsmData != nullptr) { - memcpy(_svsmDynamicLivePrev, &_svsmDataReadBack[196], sizeof(_svsmDynamicLivePrev)); // SVSMDataOffsets::StatsDynamicLive memcpy(_svsmDataReadBack, svsmData, sizeof(u32) * SVSM_DATA_UINT_COUNT); } _renderer->UnmapBuffer(_svsmDataReadBackBuffer); @@ -436,6 +437,7 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende data.dynamicAABBBuffer = builder.Write(_svsmDynamicAABBBuffer, BufferUsage::COMPUTE); data.svsmDataReadBackBuffer = builder.Write(_svsmDataReadBackBuffer, BufferUsage::TRANSFER); data.dynamicValidateReadBackBuffer = builder.Write(_svsmDynamicValidateReadBackBuffer, BufferUsage::TRANSFER); + builder.Write(_svsmFillArgsBuffer, BufferUsage::COMPUTE); // Finalize writes the per-view fill dispatch args data.pagePool = builder.Write(_svsmPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); data.dynamicPagePool = builder.Write(_svsmDynamicPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); @@ -477,8 +479,8 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende u32 dynamicPoolPagesPerRow; u32 renderBudget; u32 dynamicPhase; - u32 padding3; - u32 padding4; + u32 fillInstanceCount; + u32 fillCellCount; }; CVarSystem* cvarSystem = CVarSystem::Get(); @@ -509,6 +511,10 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende constants->dynamicPoolPagesPerRow = dynamicSplit ? dynamicPoolPagesPerRow : 0; constants->renderBudget = static_cast(glm::max(CVAR_SVSMRenderBudget.Get(), 0)); constants->dynamicPhase = 0; + // The same record-time counts the geometry passes size their fills from, Finalize + // turns them into per-view indirect dispatch args gated on this frame's page stats + constants->fillInstanceCount = _modelRenderer->GetOpaqueCullingResources().GetNumInstances(); + constants->fillCellCount = _terrainRenderer->GetNumDrawCalls(); if (_svsmPoolNeedsClear) { @@ -1078,6 +1084,25 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) _svsmPageTableDebugDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); // _srcCameras / _depth / _target / _rwCameras / _pagePool are bound per frame, those resources can be recreated + // Per-view fill dispatch args written by Finalize, consumed by the geometry passes' + // DispatchIndirect: per clipmap 3 x uvec3 (model static, model dynamic, terrain static). + // Prefilled with zero groups so a pre-Finalize consumer launches nothing + Renderer::BufferDesc fillArgsDesc; + fillArgsDesc.name = "SVSMFillDispatchArgs"; + fillArgsDesc.size = SVSM_FILL_ARGS_VIEW_STRIDE * SVSM_MAX_CLIPMAPS; + fillArgsDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::INDIRECT_ARGUMENT_BUFFER; + _svsmFillArgsBuffer = _renderer->CreateAndFillBuffer(_svsmFillArgsBuffer, fillArgsDesc, [](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + for (u32 i = 0; i < SVSM_MAX_CLIPMAPS * 9; i += 3) + { + values[i + 0] = 0; + values[i + 1] = 1; + values[i + 2] = 1; + } + }); + _svsmFinalizeDescriptorSet.Bind("_fillDispatchArgs"_h, _svsmFillArgsBuffer); + bufferDesc.name = "SVSMDataReadBack"; bufferDesc.size = sizeof(u32) * SVSM_DATA_UINT_COUNT; bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 0c872ed..5f58faa 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -65,17 +65,11 @@ class ShadowRenderer // binds leave a hole) and again if the cameras buffer is ever recreated void BindCameraBuffers(RenderResources& resources); - // Current-frame CPU knowledge: with no dynamic casters at all, the dynamic fills/draws skip entirely + // Current-frame CPU knowledge: with no dynamic casters at all, the dynamic fills/draws skip + // entirely. Per-view skipping is GPU-driven (Finalize's fill dispatch args) — a readback-based + // per-view skip is a frame late and drops freshly acquired dynamic pages' draws for a frame bool HasSVSMDynamicCasters() const { return !_svsmDynamicAABBs.empty(); } - // True if the clipmap had live dynamic pages in either of the last two frames (readback is one - // frame old, the second frame is hysteresis). Inactive views skip their dynamic fill+draw, - // a caster entering a fresh ring renders at most one frame late - bool GetSVSMDynamicViewActive(u32 clipmapIndex) const - { - return _svsmDataReadBack[196 + clipmapIndex] != 0 || _svsmDynamicLivePrev[clipmapIndex] != 0; // SVSMDataOffsets::StatsDynamicLive - } - // CPU-side caster classification counts, rebuilt each Update void GetSVSMCasterStats(u32& outDynamicCasters, u32& outAnimatedCasters, u32& outDroppedAABBs) const { @@ -92,6 +86,13 @@ class ShadowRenderer Renderer::BufferID GetSVSMDynamicPageTableBuffer() const { return _svsmDynamicPageTableBuffer; } Renderer::ImageID GetSVSMDynamicPagePool() const { return _svsmDynamicPagePool; } + // Finalize-written per-view fill dispatch args: per clipmap 3 x uvec3 (model static fill, + // model dynamic fill, terrain static fill), byte stride SVSM_FILL_ARGS_VIEW_STRIDE + Renderer::BufferID GetSVSMFillArgsBuffer() const { return _svsmFillArgsBuffer; } + static constexpr u32 SVSM_FILL_ARGS_VIEW_STRIDE = 3 * 3 * sizeof(u32); + static constexpr u32 SVSM_FILL_ARGS_DYNAMIC_OFFSET = 3 * sizeof(u32); + static constexpr u32 SVSM_FILL_ARGS_TERRAIN_OFFSET = 6 * sizeof(u32); + // For the material pass LIGHT set bindings, which must stay valid before the pools exist. // Nothing samples the placeholder while the page tables have no resident entries Renderer::ImageID GetSVSMPagePoolOrPlaceholder() const { return _svsmPagePool != Renderer::ImageID::Invalid() ? _svsmPagePool : _svsmPagePoolPlaceholder; } @@ -149,13 +150,13 @@ class ShadowRenderer Renderer::BufferID _svsmDynamicFreeListBuffer; Renderer::BufferID _svsmDynamicClearListBuffer; Renderer::BufferID _svsmDynamicAABBBuffer; - Renderer::BufferID _svsmDataReadBackBuffer; + Renderer::BufferID _svsmDataReadBackBuffer; // Diagnostics only (perf editor stats), no rendering decision reads it Renderer::BufferID _svsmDynamicValidateReadBackBuffer; // svsmValidateDynamic one-shot: dynamic table + free list + Renderer::BufferID _svsmFillArgsBuffer; Renderer::ImageID _svsmPagePool; Renderer::ImageID _svsmDynamicPagePool; Renderer::ImageID _svsmPagePoolPlaceholder; u32 _svsmDataReadBack[SVSM_DATA_UINT_COUNT] = { 0 }; - u32 _svsmDynamicLivePrev[SVSM_MAX_CLIPMAPS] = { 0 }; // The readback generation before the current one, for skip hysteresis std::vector _svsmDirtyAABBs; // Static invalidation (min, max) pairs, uploaded by the update pass std::vector _svsmDynamicAABBs; // This frame's dynamic caster (min, max) pairs diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index 478b5e5..38a3a02 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -603,6 +603,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re Renderer::ImageMutableResource pagePool; Renderer::BufferResource svsmData; Renderer::BufferResource pageTable; + Renderer::BufferMutableResource svsmFillArgsBuffer; Renderer::BufferMutableResource culledInstanceBuffer; @@ -623,6 +624,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); + data.svsmFillArgsBuffer = builder.Write(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed by DispatchIndirect in the per-view fills builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); @@ -694,6 +696,10 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re fillParams.currentBitmaskIndex = frameIndex; fillParams.fillSet = data.fillSet; + // Finalize zeroed the group count for rings with no dirty pages this frame + fillParams.fillArgsBuffer = data.svsmFillArgsBuffer; + fillParams.fillArgsByteOffset = (i - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + ShadowRenderer::SVSM_FILL_ARGS_TERRAIN_OFFSET; + Profiled("Fill", i, [&] { FillDrawCalls(frameIndex, graphResources, commandList, fillParams); }); commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::COMPUTE); commandList.BufferBarrier(data.culledInstanceBuffer, Renderer::BufferPassUsage::GRAPHICS); @@ -1496,7 +1502,14 @@ void TerrainRenderer::FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResource // Bind descriptorset commandList.BindDescriptorSet(params.fillSet, frameIndex); - commandList.Dispatch((params.cellCount + 31) / 32, 1, 1); + if (params.fillArgsBuffer != Renderer::BufferMutableResource::Invalid()) + { + commandList.DispatchIndirect(params.fillArgsBuffer, params.fillArgsByteOffset); + } + else + { + commandList.Dispatch((params.cellCount + 31) / 32, 1, 1); + } commandList.EndPipeline(pipeline); commandList.PopMarker(); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index d78bc3b..185931b 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -116,6 +116,11 @@ class TerrainRenderer u32 currentBitmaskIndex = 0; Renderer::DescriptorSetResource fillSet; + + // When set the fill dispatches indirectly from these GPU-written args (SVSM per-view + // gating), zero groups for views with no page work this frame + Renderer::BufferMutableResource fillArgsBuffer; + u32 fillArgsByteOffset = 0; }; void FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, FillDrawCallsParams& params); diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang index d667d07..e71f0ca 100644 --- a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -39,8 +39,8 @@ struct SVSMConstants uint dynamicPoolPagesPerRow; // Caster-split dynamic pool pages per row, 0 while the split is off uint renderBudget; // Static pages rendered per frame, 0 = unlimited; the coarsest two rings are exempt uint dynamicPhase; // DynamicUpdate: 0 = release pages, 1 = acquire + bookkeeping. Split so free-list pushes and pops never share a dispatch - uint padding3; - uint padding4; + uint fillInstanceCount; // Model instances this frame, Finalize sizes the per-view fill dispatch args from it + uint fillCellCount; // Terrain cells this frame, same purpose }; // Page table entry bits. A zeroed entry is a free slot diff --git a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang index c06ef72..50af122 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang @@ -1,20 +1,27 @@ #include "Include/Camera.inc.slang" #include "Shadows/SVSM.inc.slang" -// Last SVSM dispatch, single thread, runs after UpdateB so the dirty rects are known: writes the -// clipmap render cameras into the shared cameras buffer that culling and the page render passes -// read. Mirrors the CascadeFitCameras tail, minus the texel snap: the window center is aligned to -// the absolute page grid by construction. +// Last SVSM dispatch, single thread, runs after UpdateB and DynamicUpdate so the dirty rects and +// per-ring page stats are final: writes the clipmap render cameras into the shared cameras buffer +// that culling and the page render passes read, and the per-view fill dispatch args that gate the +// geometry passes' instance fills. Mirrors the CascadeFitCameras tail, minus the texel snap: the +// window center is aligned to the absolute page grid by construction. // // The camera MATRICES always map the full clipmap window (the render viewport spans the whole // virtual texture). The cull PLANES are tightened to this frame's dirty page rect, XY-tight is // safe for an ortho light (a caster's light-space XY equals its shadow's XY); the near plane is // swept toward the sun by the caster margin, depth clamp pancakes those casters at render time. +// +// The fill args are the same-frame GPU truth the per-view skip must run on: a ring with no DIRTY +// static pages skips its static fill, a ring with no resident dynamic pages skips its dynamic +// fill. Gating this from a CPU readback (one frame old) drops freshly acquired dynamic pages' +// draws for a frame — resident-but-undrawn pages read lit and flicker dynamic shadows out [[vk::push_constant]] SVSMConstants _constants; [[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera [[vk::binding(1, PER_PASS)]] StructuredBuffer _svsmData; +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _fillDispatchArgs; // Per clipmap: 3 x uvec3 (model static fill, model dynamic fill, terrain static fill) [shader("compute")] [numthreads(1, 1, 1)] @@ -24,6 +31,27 @@ void main() float3x3 lightRotation = BuildLightRotation(lightDirection); float3 upDir = StableUp(lightDirection); + // Per-view fill dispatch args, zero groups for rings with no page work this frame + uint modelFillGroups = (_constants.fillInstanceCount + 31) / 32; + uint terrainFillGroups = (_constants.fillCellCount + 31) / 32; + for (uint k = 0; k < 8; k++) + { + bool inUse = k < _constants.numClipmaps; + bool hasDirty = inUse && _svsmData[0].statsDirty[k] != 0; + bool hasDynamic = inUse && _svsmData[0].statsDynamicLive[k] != 0; + + uint base = k * 9; + _fillDispatchArgs[base + 0] = hasDirty ? modelFillGroups : 0; + _fillDispatchArgs[base + 1] = 1; + _fillDispatchArgs[base + 2] = 1; + _fillDispatchArgs[base + 3] = hasDynamic ? modelFillGroups : 0; + _fillDispatchArgs[base + 4] = 1; + _fillDispatchArgs[base + 5] = 1; + _fillDispatchArgs[base + 6] = hasDirty ? terrainFillGroups : 0; + _fillDispatchArgs[base + 7] = 1; + _fillDispatchArgs[base + 8] = 1; + } + float zRangeMin = _svsmData[0].zRangeMin; float zRangeMax = _svsmData[0].zRangeMax; float zCenter = (zRangeMin + zRangeMax) * 0.5f; From 6666a3d63d521939da5f24a606d5dd4db409e3db Mon Sep 17 00:00:00 2001 From: Pursche Date: Fri, 17 Jul 2026 17:00:30 +0200 Subject: [PATCH 17/24] Treat receivers below the SVSM Z window as lit, fix retired debug bind --- .../Rendering/Material/MaterialRenderer.cpp | 7 ++----- .../Shaders/Shaders/Include/Shadows.inc.slang | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index 72a3ff1..0ed15e1 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -20,7 +20,7 @@ #include -AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - ShadowCascade, 5 - SVSM Compare Margin", 0); +AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - Retired, 5 - SVSM Compare Margin", 0); AutoCVar_ShowFlag CVAR_DrawTerrainWireframe(CVarCategory::Client | CVarCategory::Rendering, "drawTerrainWireframe", "Draw terrain wireframe", ShowFlag::DISABLED); AutoCVar_ShowFlag CVAR_EnableFog(CVarCategory::Client | CVarCategory::Rendering, "enableFog", "Toggle fog", ShowFlag::DISABLED); AutoCVar_VecFloat CVAR_FogColor(CVarCategory::Client | CVarCategory::Rendering, "fogColor", "Change fog color", vec4(0.33f, 0.2f, 0.38f, 1.0f), CVarFlags::None); @@ -209,10 +209,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende commandList.BindDescriptorSet(data.terrainSet, frameIndex); commandList.BindDescriptorSet(data.materialSet, frameIndex); break; - case 4: // CascadeID reconstructs vertex data and reads the cascade cameras - commandList.BindDescriptorSet(data.globalSet, frameIndex); - commandList.BindDescriptorSet(data.terrainSet, frameIndex); - commandList.BindDescriptorSet(data.modelSet, frameIndex); + case 4: // Retired (was CascadeID), the variant only reads the visibility buffer commandList.BindDescriptorSet(data.materialSet, frameIndex); break; case 5: // SVSM compare margin reconstructs vertex data and samples the virtual shadow map diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index aa6dfe1..ce7ef8e 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -100,6 +100,16 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh float camMinusZAnchor = _svsmData[0].configCamMinusZAnchor; float compareBias = shadowSettings.svsmConstantBias / zRangeLength; + // Receivers below the window's far plane have no representable occluder: the raster clamps + // their casters away (pages stay 0 = lit), but an unclamped receiver depth goes negative and + // then even EMPTY texels compare as shadowing — the camera-anchored window climbs with the + // camera, so terrain more than zHalfRange below it (along the light axis) blacks out square. + // No data means lit. The Z window is global, so no ring can do better — bail out entirely + if (zHalf - (mul(relativePos, lightRotation).z + camMinusZAnchor) < 0.0f) + { + return 1.0f; + } + float staticFactor = 1.0f; for (uint k = uint(selectedClipmap); k < numClipmaps; k++) { @@ -300,7 +310,8 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh } // Diagnostic view of the SVSM compare: single center-texel sample, classification colors. -// Dark purple = no SVSM state, blue = outside coverage, magenta = page never resident, +// Dark purple = no SVSM state, blue = outside coverage (XY window or below the Z window), +// magenta = page never resident, // red = shadowed (brightness = margin up to 8m, green channel rises toward 512m -> yellow), // green = lit (brightness = margin up to 8m, blue channel rises toward 512m -> cyan) float3 GetSVSMDebugColor(float3 worldPos, float3 worldNormal, float normalOffset, float constantBias) @@ -330,6 +341,12 @@ float3 GetSVSMDebugColor(float3 worldPos, float3 worldNormal, float normalOffset float zRangeMax = _svsmData[0].zRangeMax; float zRangeLength = max(zRangeMax - zRangeMin, 1.0f); + // Below the window's far plane, same bail as the sampler: no representable occluder + if (zRangeLength * 0.5f - (mul(relativePos, lightRotation).z + _svsmData[0].configCamMinusZAnchor) < 0.0f) + { + return float3(0.0f, 0.3f, 1.0f); + } + // Same independent static/dynamic resolves as GetSVSMShadowFactor, single center tap each. // The classification compares whichever depth is more occluding against its own receiver float deltaMeters = 0.0f; From e2592ebd0a035b37dc4dd5d5fcdf4df462d24eeb Mon Sep 17 00:00:00 2001 From: Pursche Date: Fri, 17 Jul 2026 17:04:02 +0200 Subject: [PATCH 18/24] Update Engine submodule for the SVSM backend support --- Submodules/Engine | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submodules/Engine b/Submodules/Engine index 55d000c..c9dcd36 160000 --- a/Submodules/Engine +++ b/Submodules/Engine @@ -1 +1 @@ -Subproject commit 55d000c5de19d504568eb2492907ac54ead8ae3b +Subproject commit c9dcd36315beaf9305efcdeb2ca8bc3667d26c4c From 6b7ec41416d43a8a3e95ab123dbe6ca9d747c9ee Mon Sep 17 00:00:00 2001 From: Pursche Date: Mon, 20 Jul 2026 01:40:14 +0200 Subject: [PATCH 19/24] Unify the SVSM dynamic caster classifier, review-pass fixes and rename --- .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 2 +- .../Editor/PerformanceDiagnostics.cpp | 56 +-- .../Game-Lib/Editor/PerformanceDiagnostics.h | 4 +- .../Game-Lib/Rendering/CulledRenderer.cpp | 162 +++---- .../Game-Lib/Rendering/CulledRenderer.h | 12 +- .../Game-Lib/Rendering/CullingResources.h | 4 +- .../Rendering/Debug/JoltDebugRenderer.cpp | 16 +- .../Game-Lib/Rendering/GameRenderer.cpp | 6 +- .../Rendering/Liquid/LiquidRenderer.cpp | 6 +- .../Rendering/Material/MaterialRenderer.cpp | 9 +- .../Rendering/Model/ModelRenderer.cpp | 239 +++++---- .../Game-Lib/Rendering/Model/ModelRenderer.h | 40 +- .../Rendering/Shadow/ShadowRenderer.cpp | 459 +++++++++--------- .../Rendering/Shadow/ShadowRenderer.h | 47 +- .../Rendering/Skybox/SkyboxRenderer.cpp | 4 +- .../Rendering/Terrain/TerrainRenderer.cpp | 30 +- .../Rendering/Terrain/TerrainRenderer.h | 4 +- .../Shaders/Shaders/Include/Shadows.inc.slang | 2 +- .../Shaders/Material/MaterialPass.cs.slang | 7 +- Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 5 +- .../Shaders/Shadows/SVSMDynamicMark.cs.slang | 4 +- .../Shaders/Shadows/SVSMFinalize.cs.slang | 7 +- .../Shaders/Shadows/SVSMPageMark.cs.slang | 6 +- .../Shaders/Shaders/Terrain/Culling.cs.slang | 6 +- Source/Shaders/Shaders/Utils/Culling.cs.slang | 4 +- .../Shaders/Utils/CullingInstanced.cs.slang | 10 +- 26 files changed, 626 insertions(+), 525 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index 5c55d06..b5e2cde 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -340,7 +340,7 @@ namespace ECS::Systems skyboxRenderer->SetSkybandColors(areaLightInfo.finalColorData.skybandTopColor, areaLightInfo.finalColorData.skybandMiddleColor, areaLightInfo.finalColorData.skybandBottomColor, areaLightInfo.finalColorData.skybandAboveHorizonColor, areaLightInfo.finalColorData.skybandHorizonColor); skyboxRenderer->SetSunDirection(-direction); // direction is the direction light travels, the sun sits opposite - // Fade shadows out as the sun approaches the horizon, below it the cascades would project the underside of the world + // Fade shadows out as the sun approaches the horizon, below it the shadow views would project the underside of the world f32 sunElevationSin = -direction.y; // Positive while the sun is above the horizon f32 shadowStrength = glm::clamp(sunElevationSin / 0.1f, 0.0f, 1.0f); *CVarSystem::Get()->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength"_h) = shadowStrength; diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index 1979b40..ee55d1b 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -80,7 +80,7 @@ namespace Editor const std::string rightHeaderText = "Survived / Total (%)"; static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit /*| ImGuiTableFlags_BordersOuter*/ | ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersH | ImGuiTableFlags_ContextMenuInBody; - const u32 numCascades = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); + const u32 numClipmaps = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); // SVSM stat block { @@ -113,10 +113,11 @@ namespace Editor shadowRenderer->GetSVSMDynamicStats(dynamicLive, dynamicTotal, dynamicOverflow); u32 dynamicCasters = 0; - u32 animatedCasters = 0; + u32 transitionsIn = 0; + u32 transitionsOut = 0; u32 droppedAABBs = 0; - shadowRenderer->GetSVSMCasterStats(dynamicCasters, animatedCasters, droppedAABBs); - snprintf(line, sizeof(line), "SVSMDynamic;live;%u;total;%u;overflow;%u;dynamicCasters;%u;animatedCasters;%u;droppedAABBs;%u\n", dynamicLive, dynamicTotal, dynamicOverflow, dynamicCasters, animatedCasters, droppedAABBs); + shadowRenderer->GetSVSMCasterStats(dynamicCasters, transitionsIn, transitionsOut, droppedAABBs); + snprintf(line, sizeof(line), "SVSMDynamic;live;%u;total;%u;overflow;%u;dynamicCasters;%u;transitionsIn;%u;transitionsOut;%u;droppedAABBs;%u\n", dynamicLive, dynamicTotal, dynamicOverflow, dynamicCasters, transitionsIn, transitionsOut, droppedAABBs); csv += line; snprintf(line, sizeof(line), "SVSMBudgetUsed;%u\n", shadowRenderer->GetSVSMBudgetUsed()); @@ -125,7 +126,7 @@ namespace Editor if (ModelRenderer* statsModelRenderer = gameRenderer->GetModelRenderer()) { std::string dynInstances = "SVSMDynInstances"; - for (u32 view = 1; view <= numCascades && view < Renderer::Settings::MAX_VIEWS; view++) + for (u32 view = 1; view <= numClipmaps && view < Renderer::Settings::MAX_VIEWS; view++) { dynInstances += ";" + std::to_string(statsModelRenderer->GetNumSVSMDynamicInstances(view)); } @@ -133,7 +134,7 @@ namespace Editor } csv += "Clipmap;extent;marked;resident;dirty;invalidated;evicted;dynamic;deferred\n"; - for (u32 i = 0; i < numCascades; i++) + for (u32 i = 0; i < numClipmaps; i++) { ShadowRenderer::SVSMClipmapStats stats; if (!shadowRenderer->GetSVSMClipmapStats(i, stats)) @@ -170,24 +171,24 @@ namespace Editor shadowRenderer->GetSVSMDynamicStats(dynamicLive, dynamicTotal, dynamicOverflow); u32 dynamicCasters = 0; - u32 animatedCasters = 0; + u32 transitionsIn = 0; + u32 transitionsOut = 0; u32 droppedAABBs = 0; - shadowRenderer->GetSVSMCasterStats(dynamicCasters, animatedCasters, droppedAABBs); - if (dynamicLive > 0 || dynamicOverflow > 0 || dynamicCasters > 0 || animatedCasters > 0) + shadowRenderer->GetSVSMCasterStats(dynamicCasters, transitionsIn, transitionsOut, droppedAABBs); + if (dynamicLive > 0 || dynamicOverflow > 0 || dynamicCasters > 0 || transitionsIn > 0 || transitionsOut > 0) { - ImGui::Text("SVSM Dynamic: %u / %u pages live%s | casters: %u dynamic + %u animated", dynamicLive, dynamicTotal, dynamicOverflow > 0 ? " (overflowing!)" : "", dynamicCasters, animatedCasters); + ImGui::Text("SVSM Dynamic: %u / %u pages live%s | %u casters (+%u / -%u)", dynamicLive, dynamicTotal, dynamicOverflow > 0 ? " (overflowing!)" : "", dynamicCasters, transitionsIn, transitionsOut); if (droppedAABBs > 0) { - ImGui::Text("SVSM Dynamic: %u caster AABBs dropped!", droppedAABBs); + ImGui::Text("SVSM Dynamic: %u casters spilled to static!", droppedAABBs); } // Per-clipmap surviving dynamic instances of the SVSM dynamic fills, one // frame old. A bouncing count here = the fill/cull chain loses casters if (ModelRenderer* svsmModelRenderer = gameRenderer->GetModelRenderer()) { - u32 numClipmapViews = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); std::string dynDraws = "SVSM Dyn instances:"; - for (u32 view = 1; view <= numClipmapViews && view < Renderer::Settings::MAX_VIEWS; view++) + for (u32 view = 1; view <= numClipmaps && view < Renderer::Settings::MAX_VIEWS; view++) { dynDraws += " " + std::to_string(svsmModelRenderer->GetNumSVSMDynamicInstances(view)); } @@ -211,7 +212,6 @@ namespace Editor ImGui::Text("SVSM Invalidation: %s", causes.c_str()); } - u32 numClipmaps = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h)); for (u32 i = 0; i < numClipmaps; i++) { ShadowRenderer::SVSMClipmapStats stats; @@ -273,10 +273,10 @@ namespace Editor switch (i) { case 0: - DrawSurvivingDrawCalls(heightConstraint, rightHeaderText, numCascades); + DrawSurvivingDrawCalls(heightConstraint, rightHeaderText, numClipmaps); break; case 1: - DrawSurvivingTriangles(heightConstraint, rightHeaderText, numCascades); + DrawSurvivingTriangles(heightConstraint, rightHeaderText, numClipmaps); break; case 2: DrawFrameTimes(heightConstraint, average, flags); @@ -322,10 +322,10 @@ namespace Editor switch (i) { case 0: - DrawSurvivingDrawCalls(heightConstraint, rightHeaderText, numCascades, widthConstraint); + DrawSurvivingDrawCalls(heightConstraint, rightHeaderText, numClipmaps, widthConstraint); break; case 1: - DrawSurvivingTriangles(heightConstraint, rightHeaderText, numCascades, widthConstraint); + DrawSurvivingTriangles(heightConstraint, rightHeaderText, numClipmaps, widthConstraint); break; case 2: DrawFrameTimes(heightConstraint, average, flags, widthConstraint); @@ -383,9 +383,9 @@ namespace Editor { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - DrawSurvivingDrawCalls(newHeightProportions[0], rightHeaderText, numCascades); + DrawSurvivingDrawCalls(newHeightProportions[0], rightHeaderText, numClipmaps); ImGui::TableSetColumnIndex(1); - DrawSurvivingTriangles(newHeightProportions[1], rightHeaderText, numCascades); + DrawSurvivingTriangles(newHeightProportions[1], rightHeaderText, numClipmaps); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -410,8 +410,8 @@ namespace Editor height *= ImGui::GetContentRegionAvail().y; } - DrawSurvivingDrawCalls(newHeightProportions[0], rightHeaderText, numCascades); - DrawSurvivingTriangles(newHeightProportions[1], rightHeaderText, numCascades); + DrawSurvivingDrawCalls(newHeightProportions[0], rightHeaderText, numClipmaps); + DrawSurvivingTriangles(newHeightProportions[1], rightHeaderText, numClipmaps); DrawFrameTimes(newHeightProportions[2], average, flags); DrawRenderPass(newHeightProportions[3], renderer, stats, flags); DrawFrameTimesGraph(newHeightProportions[4], stats); @@ -463,7 +463,7 @@ namespace Editor } } - void PerformanceDiagnostics::DrawSurvivingDrawCalls(f32 constraint, const std::string& text, u32 numCascades, f32 widthConstraint) + void PerformanceDiagnostics::DrawSurvivingDrawCalls(f32 constraint, const std::string& text, u32 numClipmaps, f32 widthConstraint) { if (!_showSurvivingDrawCalls) return; @@ -498,7 +498,7 @@ namespace Editor if (!_drawCallStatsOnlyForMainView) { - for (u32 i = 1; i < numCascades + 1; i++) + for (u32 i = 1; i < numClipmaps + 1; i++) { DrawCullingDrawCallStatsView(i, textPos, totalDrawCalls, totalDrawCallsSurvived); } @@ -511,7 +511,7 @@ namespace Editor } } - void PerformanceDiagnostics::DrawSurvivingTriangles(f32 constraint, const std::string& text, u32 numCascades, f32 widthConstraint) + void PerformanceDiagnostics::DrawSurvivingTriangles(f32 constraint, const std::string& text, u32 numClipmaps, f32 widthConstraint) { if (!_showSurvivingTriangle) return; @@ -546,7 +546,7 @@ namespace Editor if (!_drawCallStatsOnlyForMainView) { - for (u32 i = 1; i < numCascades + 1; i++) + for (u32 i = 1; i < numClipmaps + 1; i++) { DrawCullingTriangleStatsView(i, textPos, totalTriangles, totalTrianglesSurvived); } @@ -763,7 +763,7 @@ namespace Editor u32 viewDrawCalls = 0; u32 viewDrawCallsSurvived = 0; - bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Cascades draw their full surviving set in one phase, no occluders + bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Clipmap views draw their full surviving set in one phase, no occluders bool viewSupportsModelsOcclusionCulling = viewID == 0; bool viewRendersTerrainCulling = true; @@ -872,7 +872,7 @@ namespace Editor u32 viewTriangles = 0; u32 viewTrianglesSurvived = 0; - bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Cascades draw their full surviving set in one phase, no occluders + bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Clipmap views draw their full surviving set in one phase, no occluders bool viewSupportsModelsOcclusionCulling = viewID == 0; bool viewRendersTerrainCulling = true; // Only main view supports terrain culling so far diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h index 69fd067..06091d3 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h @@ -39,8 +39,8 @@ namespace Editor void DrawCullingStatsEntry(std::string_view name, u32 drawCalls, u32 survivedDrawCalls); - void DrawSurvivingDrawCalls(f32 constraint, const std::string& text, u32 numCascades, f32 widthConstraint = -1.f); - void DrawSurvivingTriangles(f32 constraint, const std::string& text, u32 numCascades, f32 widthConstraint = -1.f); + void DrawSurvivingDrawCalls(f32 constraint, const std::string& text, u32 numClipmaps, f32 widthConstraint = -1.f); + void DrawSurvivingTriangles(f32 constraint, const std::string& text, u32 numClipmaps, f32 widthConstraint = -1.f); void DrawFrameTimes(f32 constraint, const ECS::Singletons::FrameTimes& average, const ImGuiTableFlags& flags, f32 widthConstraint = -1.f); void DrawRenderPass(f32 constraint, Renderer::Renderer* renderer, ECS::Singletons::EngineStats& stats, const ImGuiTableFlags& flags, f32 widthConstraint = -1.f); void DrawFrameTimesGraph(f32 constraint, const ECS::Singletons::EngineStats& stats, f32 widthConstraint = -1.f); diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index 3af4fa0..d1e5212 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -209,7 +209,7 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) drawParams.viewIndex = 0; drawParams.rt0 = params.rt0; drawParams.rt1 = params.rt1; - drawParams.depth = params.depth[0]; + drawParams.depth = params.depth; drawParams.argumentBuffer = params.culledDrawCallsBuffer; drawParams.drawCountBuffer = params.culledDrawCallCountBuffer; drawParams.drawCountIndex = 0; @@ -240,97 +240,83 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) params.commandList->BufferBarrier(params.culledDrawCallsBitMaskBuffer, Renderer::BufferPassUsage::TRANSFER); } - for (u32 i = 0; i < params.numCascades + 1; i++) + // Reset the counters { - std::string markerName = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1); - params.commandList->PushMarker(markerName, Color::PastelYellow); - - // Reset the counters - { - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); - // Reset the counters - params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); - params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); + params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); + params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); - } + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); + } - // Fill the occluders to draw - { - std::string debugName = params.passName + " Occlusion Fill"; - params.commandList->PushMarker(debugName, Color::White); + // Fill the occluders to draw + { + std::string debugName = params.passName + " Occlusion Fill"; + params.commandList->PushMarker(debugName, Color::White); - Renderer::ComputePipelineID pipeline = _fillDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); + Renderer::ComputePipelineID pipeline = _fillDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); - struct FillDrawCallConstants - { - u32 numTotalDraws; - u32 bitmaskOffset; - u32 diffAgainstPrev; - u32 currentBitmaskIndex; - }; + struct FillDrawCallConstants + { + u32 numTotalDraws; + u32 bitmaskOffset; + u32 diffAgainstPrev; + u32 currentBitmaskIndex; + }; - FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); - fillConstants->numTotalDraws = numDrawCalls; - fillConstants->bitmaskOffset = i * params.cullingResources->GetBitMaskBufferUintsPerView(); - fillConstants->diffAgainstPrev = 0; // Occluders should not diff against prev - fillConstants->currentBitmaskIndex = !params.frameIndex; // Occluders consume last frame's culling output - params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); + FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); + fillConstants->numTotalDraws = numDrawCalls; + fillConstants->bitmaskOffset = 0; // Occluders only draw the main view + fillConstants->diffAgainstPrev = 0; // Occluders should not diff against prev + fillConstants->currentBitmaskIndex = !params.frameIndex; // Occluders consume last frame's culling output + params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); - // Bind descriptorset - params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.occluderFillDescriptorSet, params.frameIndex); + // Bind descriptorset + params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); + params.commandList->BindDescriptorSet(params.occluderFillDescriptorSet, params.frameIndex); - params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); + params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); - params.commandList->EndPipeline(pipeline); + params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } + params.commandList->PopMarker(); + } - params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); - params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); - params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE); + params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE); - if (params.enableDrawing) - { - // Draw Occluders - params.commandList->PushMarker(params.passName + " Occlusion Draw " + std::to_string(numDrawCalls), Color::White); - - DrawParams drawParams; - drawParams.cullingEnabled = true; // The occuder pass only makes sense if culling is enabled - drawParams.viewIndex = i; - drawParams.rt0 = params.rt0; - drawParams.rt1 = params.rt1; - drawParams.depth = params.depth[i]; - drawParams.argumentBuffer = params.culledDrawCallsBuffer; - drawParams.drawCountBuffer = params.drawCountBuffer; - drawParams.drawCountIndex = 0; - drawParams.numMaxDrawCalls = numDrawCalls; + if (params.enableDrawing) + { + // Draw Occluders + params.commandList->PushMarker(params.passName + " Occlusion Draw " + std::to_string(numDrawCalls), Color::White); - params.drawCallback(drawParams); - } + DrawParams drawParams; + drawParams.cullingEnabled = true; // The occuder pass only makes sense if culling is enabled + drawParams.viewIndex = 0; + drawParams.rt0 = params.rt0; + drawParams.rt1 = params.rt1; + drawParams.depth = params.depth; + drawParams.argumentBuffer = params.culledDrawCallsBuffer; + drawParams.drawCountBuffer = params.drawCountBuffer; + drawParams.drawCountIndex = 0; + drawParams.numMaxDrawCalls = numDrawCalls; - // Copy from our draw count buffer to the readback buffer - params.commandList->CopyBuffer(params.drawCountReadBackBuffer, sizeof(u32) * i, params.drawCountBuffer, 0, sizeof(u32)); - params.commandList->CopyBuffer(params.triangleCountReadBackBuffer, sizeof(u32) * i, params.triangleCountBuffer, 0, sizeof(u32)); + params.drawCallback(drawParams); + } - if (params.enableDrawing) - { - params.commandList->PopMarker(); - } + // Copy from our draw count buffer to the readback buffer + params.commandList->CopyBuffer(params.drawCountReadBackBuffer, 0, params.drawCountBuffer, 0, sizeof(u32)); + params.commandList->CopyBuffer(params.triangleCountReadBackBuffer, 0, params.triangleCountBuffer, 0, sizeof(u32)); + if (params.enableDrawing) + { params.commandList->PopMarker(); } - - // Finish by resetting the viewport and scissor - vec2 renderSize = _renderer->GetRenderSize(); - params.commandList->SetViewport(0, 0, renderSize.x, renderSize.y, 0.0f, 1.0f); - params.commandList->SetScissorRect(0, static_cast(renderSize.x), 0, static_cast(renderSize.y)); } } @@ -384,7 +370,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) u32 cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... u32 debugDrawColliders; u32 currentBitmaskIndex; - u32 numCascades; + u32 numShadowViews; u32 bitMaskBufferUintsPerView; u32 debugDrawView; u32 cullMainView; @@ -406,7 +392,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; cullConstants->debugDrawColliders = params.debugDrawColliders; cullConstants->currentBitmaskIndex = params.frameIndex; - cullConstants->numCascades = params.numCascades; + cullConstants->numShadowViews = params.numShadowViews; cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); cullConstants->cullMainView = params.cullMainView; @@ -497,7 +483,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) u32 viewportSizeX; u32 viewportSizeY; u32 maxDrawCount; - u32 numCascades; + u32 numShadowViews; u32 occlusionCull; u32 instanceIDOffset; u32 modelIDOffset; @@ -512,7 +498,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) cullConstants->viewportSizeX = u32(viewportSize.x); cullConstants->viewportSizeY = u32(viewportSize.y); cullConstants->maxDrawCount = numDrawCalls; - cullConstants->numCascades = params.numCascades; + cullConstants->numShadowViews = params.numShadowViews; cullConstants->occlusionCull = params.occlusionCull; cullConstants->instanceIDOffset = params.instanceIDOffset; cullConstants->modelIDOffset = params.modelIDOffset; @@ -557,20 +543,20 @@ void CulledRenderer::CullingPass(CullingPassParams& params) } } -void CulledRenderer::CascadeCullingPass(CullingPassParams& params) +void CulledRenderer::ClipmapCullingPass(CullingPassParams& params) { - NC_ASSERT(params.drawCallDataSize > 0, "CulledRenderer : CascadeCullingPass params provided an invalid drawCallDataSize"); - NC_ASSERT(params.cullingResources->IsInstanced(), "CulledRenderer : CascadeCullingPass only supports instanced culling resources"); + NC_ASSERT(params.drawCallDataSize > 0, "CulledRenderer : ClipmapCullingPass params provided an invalid drawCallDataSize"); + NC_ASSERT(params.cullingResources->IsInstanced(), "CulledRenderer : ClipmapCullingPass only supports instanced culling resources"); const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); u32 numInstances = params.cullingResources->GetNumInstances(); - if (numDrawCalls == 0 || numInstances == 0 || params.numCascades == 0) + if (numDrawCalls == 0 || numInstances == 0 || params.numShadowViews == 0) return; // Frustum-only cull of the cascade views into their bitmask slices. No counter resets (the // per-cascade fill in the geometry pass resets and rebuilds the shared draw sets), no occlusion - std::string debugName = params.passName + " Cascade Culling"; + std::string debugName = params.passName + " Clipmap Culling"; params.commandList->PushMarker(debugName, Color::Yellow); Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[1]; // Cascades require the bitmask permutation @@ -592,7 +578,7 @@ void CulledRenderer::CascadeCullingPass(CullingPassParams& params) u32 cullingDataIsWorldspace; u32 debugDrawColliders; u32 currentBitmaskIndex; - u32 numCascades; + u32 numShadowViews; u32 bitMaskBufferUintsPerView; u32 debugDrawView; u32 cullMainView; @@ -614,7 +600,7 @@ void CulledRenderer::CascadeCullingPass(CullingPassParams& params) cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; cullConstants->debugDrawColliders = false; cullConstants->currentBitmaskIndex = params.frameIndex; - cullConstants->numCascades = params.numCascades; + cullConstants->numShadowViews = params.numShadowViews; cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); cullConstants->cullMainView = false; @@ -680,7 +666,7 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); - if (params.svsmPass && params.svsmFillArgsBuffer != Renderer::BufferMutableResource::Invalid()) + if (params.svsmPass && params.svsmFillArgsBuffer != Renderer::BufferResource::Invalid()) { // Finalize zeroed the group count for rings with no page work this frame u32 argsOffset = (viewIndex - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + (keepDynamic ? ShadowRenderer::SVSM_FILL_ARGS_DYNAMIC_OFFSET : 0); @@ -760,7 +746,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) params.commandList->EndTimeQuery(timeQuery); }; - for (u32 i = params.firstViewIndex; i < params.numCascades + 1; i++) + for (u32 i = params.firstViewIndex; i < params.numShadowViews + 1; i++) { std::string markerName = (i == 0) ? "Main" : "Clipmap " + std::to_string(i - 1); params.commandList->PushMarker(markerName, Color::PastelYellow); @@ -988,12 +974,12 @@ void CulledRenderer::CreatePipelines() // Fill Drawcalls From Bitmask pipelines Renderer::ComputePipelineDesc pipelineDesc; { - pipelineDesc.debugName = "FillInstancedDrawcallsFromBitmask"; - for (u32 i = 0; i < 2; i++) { for (u32 filtered = 0; filtered < 2; filtered++) { + pipelineDesc.debugName = (filtered == 0) ? "FillInstancedDrawcallsFromBitmask" : "FillInstancedDrawcallsFromBitmaskFiltered"; + std::vector permutationFields = { { "IS_INDEXED", std::to_string(i) }, diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index a910cdb..289d054 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -128,7 +128,7 @@ class CulledRenderer public: Renderer::ImageMutableResource rt0; Renderer::ImageMutableResource rt1; - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + Renderer::DepthImageMutableResource depth; Renderer::BufferMutableResource culledDrawCallsBuffer; Renderer::BufferMutableResource culledDrawCallCountBuffer; @@ -152,8 +152,6 @@ class CulledRenderer u32 baseInstanceLookupOffset = 0; u32 drawCallDataSize = 0; - u32 numCascades = 0; - bool enableDrawing = false; // Allows us to do everything but the actual drawcall, for debugging bool disableTwoStepCulling = false; }; @@ -223,7 +221,7 @@ class CulledRenderer Renderer::DescriptorSetResource cullingDescriptorSet; Renderer::DescriptorSetResource createIndirectAfterCullSet; - u32 numCascades = 0; + u32 numShadowViews = 0; bool cullMainView = true; bool occlusionCull = true; bool disableTwoStepCulling = false; @@ -309,7 +307,7 @@ class CulledRenderer u32 drawCallDataSize = 0; // Instanced only u32 firstViewIndex = 0; // 0 = main view first, 1 = clipmap views only - u32 numCascades = 0; + u32 numShadowViews = 0; bool enableDrawing = false; // Allows us to do everything but the actual drawcall, for debugging bool cullingEnabled = false; @@ -326,11 +324,11 @@ class CulledRenderer // Finalize-written per-view fill dispatch args: rings with no dirty static pages / no // resident dynamic pages this frame get zero-group fills. Same-frame GPU truth — a // CPU/readback gate here is a frame late and flickers freshly acquired dynamic pages - Renderer::BufferMutableResource svsmFillArgsBuffer; + Renderer::BufferResource svsmFillArgsBuffer; // Finalize-written dispatch args, consumed read-only via DispatchIndirect }; void GeometryPass(GeometryPassParams& params); void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic); // Shared-buffer rebuild from a view's bitmask slice - void CascadeCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets + void ClipmapCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets void SyncToGPU(); void BindCullingResource(CullingResourcesBase& resources); diff --git a/Source/Game-Lib/Game-Lib/Rendering/CullingResources.h b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.h index 3199dba..e58a875 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CullingResources.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.h @@ -117,11 +117,11 @@ class CullingResourcesBase u32 _numInstances = 0; u32 _numSurvivingOccluderInstances = 0; - u32 _numSurvivingInstances[Renderer::Settings::MAX_VIEWS] = { 0 }; // One for the main view, then one per shadow cascade + u32 _numSurvivingInstances[Renderer::Settings::MAX_VIEWS] = { 0 }; // One for the main view, then one per shadow clipmap view u32 _numTriangles = 0; u32 _numSurvivingOccluderTriangles = 0; - u32 _numSurvivingTriangles[Renderer::Settings::MAX_VIEWS] = { 0 }; // One for the main view, then one per shadow cascade + u32 _numSurvivingTriangles[Renderer::Settings::MAX_VIEWS] = { 0 }; // One for the main view, then one per shadow clipmap view }; class CullingResourcesIndexedBase : public CullingResourcesBase diff --git a/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp index 9dbb26a..45ed1fc 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp @@ -110,7 +110,7 @@ void JoltDebugRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Rend //if (!CVAR_JoltDebugCullingEnabled.Get()) // return; - u32 numCascades = 0; // Debug meshes should never cast shadows + u32 numShadowViews = 0; // Debug meshes should never cast shadows struct Data { @@ -172,7 +172,7 @@ void JoltDebugRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Rend params.frameIndex = frameIndex; params.rt0 = data.visibilityBuffer; - params.depth[0] = data.depth; + params.depth = data.depth; params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; @@ -245,7 +245,7 @@ void JoltDebugRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Rend params.frameIndex = frameIndex; params.rt0 = data.visibilityBuffer; - params.depth[0] = data.depth; + params.depth = data.depth; params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; @@ -290,7 +290,7 @@ void JoltDebugRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, Rende //if (!CVAR_JoltDebugCullingEnabled.Get()) // return; - u32 numCascades = 0; // Debug meshes should never cast shadows + u32 numShadowViews = 0; // Debug meshes should never cast shadows struct Data { @@ -362,7 +362,7 @@ void JoltDebugRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, Rende params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.createIndirectAfterCullSet; - params.numCascades = 0; + params.numShadowViews = 0; params.occlusionCull = CVAR_JoltDebugOcclusionCullingEnabled.Get(); params.cullingDataIsWorldspace = false; @@ -423,7 +423,7 @@ void JoltDebugRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, Rende params.globalDescriptorSet = data.globalSet; params.cullingDescriptorSet = data.cullingSet; - params.numCascades = 0; + params.numShadowViews = 0; params.occlusionCull = CVAR_JoltDebugOcclusionCullingEnabled.Get(); params.cullingDataIsWorldspace = false; @@ -522,7 +522,7 @@ void JoltDebugRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Rend params.enableDrawing = CVAR_JoltDebugDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; - params.numCascades = 0; + params.numShadowViews = 0; GeometryPass(params); @@ -602,7 +602,7 @@ void JoltDebugRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Rend params.enableDrawing = CVAR_JoltDebugDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; - params.numCascades = 0; + params.numShadowViews = 0; GeometryPass(params); diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index 0ec05b2..cbef8a4 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -181,7 +181,7 @@ GameRenderer::GameRenderer(InputManager* inputManager) _canvasRenderer = new CanvasRenderer(_renderer, this, _debugRenderer); _uiRenderer = new UIRenderer(_renderer); _effectRenderer = new EffectRenderer(_renderer, this); - _shadowRenderer = new ShadowRenderer(_renderer, this, _debugRenderer, _terrainRenderer, _modelRenderer, _resources); + _shadowRenderer = new ShadowRenderer(_renderer, this, _terrainRenderer, _modelRenderer, _resources); _pixelQuery = new PixelQuery(_renderer, this); _nameHashToCursor.reserve(128); @@ -414,8 +414,8 @@ f32 GameRenderer::Render() // samples, then allocates, culls per clipmap view and renders the dirty pages the same frame _shadowRenderer->AddSVSMUpdatePass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddSVSMBindPass(&renderGraph, _resources, _frameIndex); - _terrainRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); - _modelRenderer->AddCascadeCullingPass(&renderGraph, _resources, _frameIndex); + _terrainRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); + _modelRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); _terrainRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); _modelRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp index d61d45e..7b1c7c6 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp @@ -268,7 +268,7 @@ void LiquidRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderRe if (_cullingResources.GetDrawCalls().Count() == 0) return; - u32 numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + u32 numShadowViews = 0; struct Data { @@ -340,7 +340,7 @@ void LiquidRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderRe params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.cullingSet; - params.numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + params.numShadowViews = 0; params.occlusionCull = CVAR_LiquidOcclusionCullingEnabled.Get(); params.disableTwoStepCulling = true; // Transparent objects don't write depth, so we don't need to two step cull them @@ -496,7 +496,7 @@ void LiquidRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderR params.enableDrawing = true;//CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; - params.numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + params.numShadowViews = 0; GeometryPass(params); }); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index 0ed15e1..33beb82 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -20,7 +20,7 @@ #include -AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - Retired, 5 - SVSM Compare Margin", 0); +AutoCVar_Int CVAR_VisibilityBufferDebugID(CVarCategory::Client | CVarCategory::Rendering, "visibilityBufferDebugID", "Debug visualizers: 0 - Off, 1 - TypeID, 2 - ObjectID, 3 - TriangleID, 4 - SVSM Compare Margin", 0); AutoCVar_ShowFlag CVAR_DrawTerrainWireframe(CVarCategory::Client | CVarCategory::Rendering, "drawTerrainWireframe", "Draw terrain wireframe", ShowFlag::DISABLED); AutoCVar_ShowFlag CVAR_EnableFog(CVarCategory::Client | CVarCategory::Rendering, "enableFog", "Toggle fog", ShowFlag::DISABLED); AutoCVar_VecFloat CVAR_FogColor(CVarCategory::Client | CVarCategory::Rendering, "fogColor", "Change fog color", vec4(0.33f, 0.2f, 0.38f, 1.0f), CVarFlags::None); @@ -140,7 +140,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende Renderer::DescriptorSetResource materialSet; }; - const i32 visibilityBufferDebugID = Math::Clamp(CVAR_VisibilityBufferDebugID.Get(), 0, 5); + const i32 visibilityBufferDebugID = Math::Clamp(CVAR_VisibilityBufferDebugID.Get(), 0, 4); renderGraph->AddPass("Material Pass", [this, &resources](MaterialPassData& data, Renderer::RenderGraphBuilder& builder) // Setup @@ -209,10 +209,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende commandList.BindDescriptorSet(data.terrainSet, frameIndex); commandList.BindDescriptorSet(data.materialSet, frameIndex); break; - case 4: // Retired (was CascadeID), the variant only reads the visibility buffer - commandList.BindDescriptorSet(data.materialSet, frameIndex); - break; - case 5: // SVSM compare margin reconstructs vertex data and samples the virtual shadow map + case 4: // SVSM compare margin reconstructs vertex data and samples the virtual shadow map commandList.BindDescriptorSet(data.globalSet, frameIndex); commandList.BindDescriptorSet(data.lightSet, frameIndex); commandList.BindDescriptorSet(data.terrainSet, frameIndex); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 4dd9faf..b67bf99 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -113,6 +113,14 @@ void ModelRenderer::Update(f32 deltaTime) mat4x4& matrix = _instanceMatrices[instanceID]; + // Transitioning into the dynamic class: the shadow baked at the PREVIOUS position must + // come out of the static pages too, a single-frame move can be arbitrarily large. + // Already-classified instances need nothing, the dynamic pool is transient per frame + if (!_dynamicCasterLastSignal.contains(instanceID)) + { + QueueShadowInvalidation(instanceID, matrix); // Pre-move matrix + } + matrix = transform.GetMatrix(); _instanceMatrices.SetDirtyElement(instanceID); @@ -121,25 +129,62 @@ void ModelRenderer::Update(f32 deltaTime) }); } - // Animated doodads (static position, moving geometry) within svsmAnimatedCasterRange of the - // camera are classified as dynamic shadow casters so their shadows move every frame; beyond - // the range they stay static with their pose baked, where coarse rings can't resolve the - // motion anyway. Range transitions re-bake the static pages under the instance + // Dynamic shadow casters: one classifier for everything that moved or pushed bone matrices, + // at instance granularity. Producers enqueue signals from any thread (the transform view + // above, the instanced bone-push path, and the in-range placements of uninstanced animated + // models below); draining them here stamps a per-instance last-signal time, and an instance + // stays classified for a grace period after its last signal. Entering pulls the baked pose + // out of the static pages, expiring bakes the final pose back — so a death pose persists and + // an idle pose never ghosts under a mover. Signals raised after this block runs are seen next + // Update, one frame of latency the grace swallows { - ZoneScopedN("Animated Shadow Casters"); + ZoneScopedN("Dynamic Shadow Casters"); - _animatedCasterFrame++; - _animatedCasterAABBs.clear(); + _dynamicCasterTime += deltaTime; + _dynamicCasterLiveIDs.clear(); + _dynamicCasterAABBs.clear(); + _dynamicCastersDropped = 0; + _dynamicCasterTransitionsIn = 0; + _dynamicCasterTransitionsOut = 0; CVarSystem* cvarSystem = CVarSystem::Get(); const bool shadowsEnabled = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 1; const f32 range = shadowsEnabled ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; const bool split = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1; + // Brief animation pauses don't churn the static cache + constexpr f32 dynamicGraceSeconds = 0.5f; + + // A first-time stamp is a transition IN: the instance's baked pose must come out of the + // static pages (movers additionally invalidated their pre-move footprint above) + auto Stamp = [&](u32 instanceID) + { + auto [it, inserted] = _dynamicCasterLastSignal.try_emplace(instanceID, _dynamicCasterTime); + if (inserted) + { + _dynamicCasterTransitionsIn++; + QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + } + else + { + it->second = _dynamicCasterTime; + } + }; + + u32 instanceID; + while (_dynamicInstanceQueue.try_dequeue(instanceID)) + { + Stamp(instanceID); + } + + // Uninstanced animated models (windmills, flags: static position, moving geometry) push + // bones per MODEL; the range scan promotes their in-camera-range placements to + // per-instance signals. Beyond the range the pose stays baked, where coarse rings can't + // resolve the motion anyway u32 modelID; while (_uninstancedAnimatedModelQueue.try_dequeue(modelID)) { - _animatedModelLastPushFrame[modelID] = _animatedCasterFrame; + _animatedModelLastPushTime[modelID] = _dynamicCasterTime; } vec3 cameraPos = vec3(0.0f); @@ -153,81 +198,88 @@ void ModelRenderer::Update(f32 deltaTime) } } - robin_hood::unordered_set newSet; if (range > 0.0f && hasCamera) { - // A model stays "animated" for a grace period after its last bone push so a brief - // animation pause doesn't mass-invalidate and re-invalidate everything in range - constexpr u64 animatedGraceFrames = 30; const f32 enterDistSq = range * range; const f32 leaveDist = range * 1.15f; // Hysteresis against camera jitter at the boundary const f32 leaveDistSq = leaveDist * leaveDist; - for (auto it = _animatedModelLastPushFrame.begin(); it != _animatedModelLastPushFrame.end();) + for (auto it = _animatedModelLastPushTime.begin(); it != _animatedModelLastPushTime.end();) { - if (_animatedCasterFrame - it->second > animatedGraceFrames) + if (_dynamicCasterTime - it->second > dynamicGraceSeconds) { - it = _animatedModelLastPushFrame.erase(it); + it = _animatedModelLastPushTime.erase(it); continue; } u32 animatedModelID = it->first; std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]); - for (u32 instanceID : _modelManifests[animatedModelID].instances) + for (u32 placementID : _modelManifests[animatedModelID].instances) { - vec3 toCamera = vec3(_instanceMatrices[instanceID][3]) - cameraPos; + vec3 toCamera = vec3(_instanceMatrices[placementID][3]) - cameraPos; f32 distSq = glm::dot(toCamera, toCamera); - f32 limitSq = _animatedCasterInstances.contains(instanceID) ? leaveDistSq : enterDistSq; + f32 limitSq = _dynamicCasterLastSignal.contains(placementID) ? leaveDistSq : enterDistSq; if (distSq < limitSq) { - newSet.insert(instanceID); + Stamp(placementID); } } ++it; } } - // Entering instances: their baked pose must come out of the static pages - for (u32 instanceID : newSet) + // Oversized casters would exceed the dynamic marker's 1024-page cutoff in EVERY clipmap + // ring (span quarters per coarser ring) and must never rely on the dynamic pool: extent + // beyond 32 pages of the coarsest ring routes to the static path instead + const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), 1, 8)); + const f32 clipmap0Extent = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipmap0Extent"_h)); + const f32 oversizeLimit = clipmap0Extent * static_cast(1u << (numClipmaps - 1)) * 0.5f; + + // Tick the classifier: expired entries transition OUT (their final pose bakes back into + // the static pages — a despawn-recycled ID costs one spurious page refresh), live entries + // emit this frame's mask feed and dynamic AABB list. Overflow and oversize SPILL to the + // static path instead of dropping: a spilled caster re-bakes every frame (page churn) but + // is never excluded from both pools + for (auto it = _dynamicCasterLastSignal.begin(); it != _dynamicCasterLastSignal.end();) { - if (!_animatedCasterInstances.contains(instanceID)) - { - QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); - } - } + u32 liveInstanceID = it->first; - // Leaving instances (or animation stopped, or range/technique turned off): their final - // pose must bake back in. Instances despawned while in the set already invalidated via - // RemoveInstance; a recycled ID here costs one spurious page refresh at worst - for (u32 instanceID : _animatedCasterInstances) - { - if (!newSet.contains(instanceID)) + if (_dynamicCasterTime - it->second > dynamicGraceSeconds) { - QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + _dynamicCasterTransitionsOut++; + QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); + it = _dynamicCasterLastSignal.erase(it); + continue; } - } - for (u32 instanceID : newSet) - { if (split) { - // Dynamic-class: excluded from static pages via the instance mask, rendered into - // the transient dynamic pool over the AABBs collected here - _dynamicInstanceQueue.enqueue(instanceID); - vec3 aabbMin, aabbMax; - ComputeInstanceShadowAABB(instanceID, _instanceMatrices[instanceID], aabbMin, aabbMax); - _animatedCasterAABBs.push_back(vec4(aabbMin, 0.0f)); - _animatedCasterAABBs.push_back(vec4(aabbMax, 0.0f)); + ComputeInstanceShadowAABB(liveInstanceID, _instanceMatrices[liveInstanceID], aabbMin, aabbMax); + + vec3 extent = aabbMax - aabbMin; + const bool oversized = glm::max(extent.x, glm::max(extent.y, extent.z)) > oversizeLimit; + const bool overCap = _dynamicCasterAABBs.size() >= ShadowRenderer::SVSM_MAX_DYNAMIC_AABBS * 2; + if (oversized || overCap) + { + _dynamicCastersDropped++; + QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); + } + else + { + _dynamicCasterLiveIDs.push_back(liveInstanceID); + _dynamicCasterAABBs.push_back(vec4(aabbMin, 0.0f)); + _dynamicCasterAABBs.push_back(vec4(aabbMax, 0.0f)); + } } else { - // Split off: v1-style per-frame static invalidation, range-bounded - QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); + // Split off: v1-style per-frame static invalidation + QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); } - } - _animatedCasterInstances = std::move(newSet); + ++it; + } } { @@ -514,9 +566,28 @@ void ModelRenderer::Clear() // Queued modelIDs/instanceIDs are invalid after a clear u32 drainedID; while (_uninstancedAnimatedModelQueue.try_dequeue(drainedID)) {} - _animatedModelLastPushFrame.clear(); - _animatedCasterInstances.clear(); - _animatedCasterAABBs.clear(); + while (_dynamicInstanceQueue.try_dequeue(drainedID)) {} + ShadowInvalidation drainedInvalidation; + while (_shadowInvalidationQueue.try_dequeue(drainedInvalidation)) {} + _animatedModelLastPushTime.clear(); + _dynamicCasterLastSignal.clear(); + _dynamicCasterLiveIDs.clear(); + _dynamicCasterAABBs.clear(); + + // Zero the mask bits set last frame BEFORE the instance capacity shrinks under the word + // count — recycled IDs on the next map must not start masked out of the static pages. Sized + // against the mask's own count, the trailing SyncToGPU shrinks nothing + u32 maskWordCount = static_cast(_dynamicInstanceMask.Count()); + for (u32 instanceID : _dynamicInstanceIDs) + { + u32 word = instanceID / 32; + if (word >= maskWordCount) + continue; + + _dynamicInstanceMask[word] &= ~(1u << (instanceID & 31)); + _dynamicInstanceMask.SetDirtyElement(word); + } + _dynamicInstanceIDs.clear(); _renderer->UnloadTexturesInArray(_textures, 1); @@ -538,12 +609,10 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe CVarSystem* cvarSystem = CVarSystem::Get(); - const u32 numCascades = 0; // Occluders are main view only, cascades render in their own block late in the frame - struct Data { Renderer::ImageMutableResource visibilityBuffer; - Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS]; + Renderer::DepthImageMutableResource depth; Renderer::BufferMutableResource culledDrawCallsBuffer; Renderer::BufferMutableResource culledDrawCallCountBuffer; @@ -566,12 +635,12 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe }; renderGraph->AddPass("Model (O) Occluders", - [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) + [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) { using BufferUsage = Renderer::BufferPassUsage; data.visibilityBuffer = builder.Write(resources.visibilityBuffer, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); - data.depth[0] = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); + data.depth = builder.Write(resources.depth, Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); builder.Read(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS); @@ -596,7 +665,7 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe return true; // Return true from setup to enable this pass, return false to disable it }, - [this, &resources, frameIndex, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + [this, &resources, frameIndex, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute { GPU_SCOPED_PROFILER_ZONE(commandList, ModelOccluders); @@ -608,10 +677,7 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe params.frameIndex = frameIndex; params.rt0 = data.visibilityBuffer; - for (u32 i = 0; i < numCascades + 1; i++) - { - params.depth[i] = data.depth[i]; - } + params.depth = data.depth; params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; @@ -639,8 +705,6 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); params.drawCallDataSize = sizeof(DrawCallData); - - params.numCascades = numCascades; params.enableDrawing = CVAR_ModelDrawOccluders.Get(); params.disableTwoStepCulling = CVAR_ModelDisableTwoStepCulling.Get(); @@ -728,7 +792,7 @@ void ModelRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderRes params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.createIndirectAfterCullSet; - params.numCascades = 0; // Main view only, cascades are culled in their own block late in the frame + params.numShadowViews = 0; // Main view only, cascades are culled in their own block late in the frame params.cullMainView = true; params.occlusionCull = CVAR_ModelOcclusionCullingEnabled.Get(); params.disableTwoStepCulling = CVAR_ModelDisableTwoStepCulling.Get(); @@ -757,7 +821,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe CVarSystem* cvarSystem = CVarSystem::Get(); const bool cullingEnabled = CVAR_ModelCullingEnabled.Get(); - const u32 numCascades = 0; // Main view only, cascades render in their own block late in the frame + const u32 numShadowViews = 0; // Main view only, cascades render in their own block late in the frame struct Data { @@ -783,7 +847,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe }; renderGraph->AddPass("Model (O) Geometry", - [this, &resources, frameIndex, numCascades](Data& data, Renderer::RenderGraphBuilder& builder) + [this, &resources, frameIndex, numShadowViews](Data& data, Renderer::RenderGraphBuilder& builder) { using BufferUsage = Renderer::BufferPassUsage; @@ -817,7 +881,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe return true; // Return true from setup to enable this pass, return false to disable it }, - [this, &resources, frameIndex, cullingEnabled, numCascades, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + [this, &resources, frameIndex, cullingEnabled, numShadowViews, cvarSystem](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) { GPU_SCOPED_PROFILER_ZONE(commandList, ModelGeometry); @@ -829,7 +893,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.frameIndex = frameIndex; params.rt0 = data.visibilityBuffer; - for (u32 i = 0; i < numCascades + 1; i++) + for (u32 i = 0; i < numShadowViews + 1; i++) { params.depth[i] = data.depth[i]; } @@ -861,7 +925,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset); params.drawCallDataSize = sizeof(DrawCallData); - params.numCascades = numCascades; + params.numShadowViews = numShadowViews; params.enableDrawing = CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; @@ -870,7 +934,7 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe }); } -void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +void ModelRenderer::AddClipmapCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { ZoneScoped; @@ -890,7 +954,7 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re return; // The same per-view culling the main view uses, run against the clipmap cameras - const u32 numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 numShadowViews = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(ShadowRenderer::SVSM_MAX_CLIPMAPS))); struct Data { @@ -901,7 +965,7 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re Renderer::DescriptorSetResource cullingSet; }; - renderGraph->AddPass("Model (O) Cascade Culling", + renderGraph->AddPass("Model (O) Clipmap Culling", [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) // Setup { using BufferUsage = Renderer::BufferPassUsage; @@ -930,9 +994,9 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re return true; // Return true from setup to enable this pass, return false to disable it }, - [this, frameIndex, numCascades](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + [this, frameIndex, numShadowViews](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute { - GPU_SCOPED_PROFILER_ZONE(commandList, ModelCascadeCulling); + GPU_SCOPED_PROFILER_ZONE(commandList, ModelClipmapCulling); CulledRenderer::CullingPassParams params; params.passName = "Opaque"; @@ -947,7 +1011,7 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re params.globalDescriptorSet = data.globalSet; params.cullingDescriptorSet = data.cullingSet; - params.numCascades = numCascades; + params.numShadowViews = numShadowViews; params.cullMainView = false; params.occlusionCull = false; @@ -957,7 +1021,7 @@ void ModelRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Re params.modelIDOffset = offsetof(DrawCallData, modelID); params.drawCallDataSize = sizeof(DrawCallData); - CascadeCullingPass(params); + ClipmapCullingPass(params); }); } @@ -985,7 +1049,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend if (_opaqueCullingResources.GetDrawCalls().Count() == 0) return; - const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(ShadowRenderer::SVSM_MAX_CLIPMAPS))); const u32 virtualSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize"_h)); // No dynamic casters this frame -> skip the dynamic fills/draws outright. The instance mask @@ -1014,7 +1078,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend Renderer::BufferMutableResource triangleCountReadBackBuffer; Renderer::BufferMutableResource svsmDynamicDrawCountReadBackBuffer; - Renderer::BufferMutableResource svsmFillArgsBuffer; + Renderer::BufferResource svsmFillArgsBuffer; Renderer::DescriptorSetResource globalSet; Renderer::DescriptorSetResource modelSet; @@ -1033,7 +1097,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); - data.svsmFillArgsBuffer = builder.Write(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed by DispatchIndirect in the per-view fills + data.svsmFillArgsBuffer = builder.Read(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed read-only by DispatchIndirect in the per-view fills if (splitFills) { data.dynamicPagePool = builder.Write(shadowRenderer->GetSVSMDynamicPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); @@ -1120,7 +1184,7 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend params.drawCallDataSize = sizeof(DrawCallData); params.firstViewIndex = 1; - params.numCascades = numClipmaps; + params.numShadowViews = numClipmaps; params.svsmPass = true; params.svsmExtent = uvec2(virtualSize, virtualSize); @@ -1158,7 +1222,7 @@ void ModelRenderer::AddTransparencyCullingPass(Renderer::RenderGraph* renderGrap if (!CVAR_ModelCullingEnabled.Get()) return; - u32 numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + u32 numShadowViews = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); struct Data { @@ -1228,7 +1292,7 @@ void ModelRenderer::AddTransparencyCullingPass(Renderer::RenderGraph* renderGrap params.cullingDescriptorSet = data.cullingSet; params.createIndirectAfterCullSet = data.createIndirectAfterCullSet; - params.numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + params.numShadowViews = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); params.occlusionCull = CVAR_ModelOcclusionCullingEnabled.Get(); params.disableTwoStepCulling = true; // Transparent objects don't write depth, so we don't need to two step cull them @@ -1344,7 +1408,7 @@ void ModelRenderer::AddTransparencyGeometryPass(Renderer::RenderGraph* renderGra DrawTransparent(resources, frameIndex, graphResources, commandList, drawParams); }; - params.numCascades = 0; + params.numShadowViews = 0; params.enableDrawing = CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = cullingEnabled; @@ -1451,7 +1515,7 @@ void ModelRenderer::AddSkyboxPass(Renderer::RenderGraph* renderGraph, RenderReso params.enableDrawing = CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = false; - params.numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + params.numShadowViews = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); GeometryPass(params); }); @@ -1551,7 +1615,7 @@ void ModelRenderer::AddSkyboxPass(Renderer::RenderGraph* renderGraph, RenderReso params.enableDrawing = CVAR_ModelDrawGeometry.Get(); params.cullingEnabled = false; - params.numCascades = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); + params.numShadowViews = 0;// *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "numShadowCascades"_h); GeometryPass(params); }); @@ -3864,8 +3928,8 @@ void ModelRenderer::SyncToGPU() BindCullingResource(_opaqueSkyboxCullingResources); BindCullingResource(_transparentSkyboxCullingResources); - // Rebuild the SVSM dynamic instance mask from this frame's producers (moved + bone-pushed). - // Runs after every ECS system, so ordering between the producers doesn't matter + // Rebuild the SVSM dynamic instance mask from the classifier's live set (Update drained the + // raw signals into it earlier this frame) { ZoneScopedN("Sync Dynamic Instance Mask"); @@ -3889,8 +3953,7 @@ void ModelRenderer::SyncToGPU() } _dynamicInstanceIDs.clear(); - u32 instanceID; - while (_dynamicInstanceQueue.try_dequeue(instanceID)) + for (u32 instanceID : _dynamicCasterLiveIDs) { u32 word = instanceID / 32; if (word >= wordsNeeded) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index f0f63b4..969f73a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -348,7 +348,7 @@ class ModelRenderer : CulledRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices + void AddClipmapCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); // Called once by ShadowRenderer at init, buffer binds must happen before the first frame @@ -366,8 +366,16 @@ class ModelRenderer : CulledRenderer // SVSM: appends world (min, max) pairs of spawned/despawned instances, returns pairs appended u32 DrainShadowInvalidations(std::vector& outMinMaxPairs, u32 maxPairs); - // SVSM: world (min, max) pairs of in-range animated shadow casters, rebuilt each Update - const std::vector& GetAnimatedCasterAABBs() const { return _animatedCasterAABBs; } + // SVSM dynamic-caster classifier outputs, rebuilt each Update: world (min, max) pairs of the + // live set, its size, spill count, and this frame's enter/leave transitions + const std::vector& GetDynamicCasterAABBs() const { return _dynamicCasterAABBs; } + u32 GetNumDynamicCasters() const { return static_cast(_dynamicCasterLiveIDs.size()); } + u32 GetNumDynamicCastersDropped() const { return _dynamicCastersDropped; } + void GetDynamicCasterTransitions(u32& outTransitionsIn, u32& outTransitionsOut) const + { + outTransitionsIn = _dynamicCasterTransitionsIn; + outTransitionsOut = _dynamicCasterTransitionsOut; + } CullingResourcesIndexed& GetOpaqueCullingResources() { return _opaqueCullingResources; } CullingResourcesIndexed& GetTransparentCullingResources() { return _transparentCullingResources; } @@ -465,11 +473,26 @@ class ModelRenderer : CulledRenderer Renderer::DescriptorSet _svsmDynamicDrawDescriptorSet; // SVSM static/dynamic caster split: instances that moved or pushed bone matrices this frame. - // Producers enqueue from any thread, SyncToGPU rebuilds the bit mask once per frame + // Producers enqueue from any thread; Update drains them into the classifier below, SyncToGPU + // rebuilds the bit mask once per frame from the classifier's live set moodycamel::ConcurrentQueue _dynamicInstanceQueue; - std::vector _dynamicInstanceIDs; // Last frame's live set, backs the incremental bit updates + std::vector _dynamicInstanceIDs; // Last frame's masked set, backs the incremental bit updates Renderer::GPUVector _dynamicInstanceMask; + // The single dynamic-caster classifier: instanceID -> accumulated-seconds stamp of its last + // dynamic signal (move or bone push). Instances stay classified for a grace period after the + // last signal so brief pauses don't churn the static cache; entering pulls the baked pose out + // of the static pages, expiring bakes it back. A despawned instance's stale entry expires + // within the grace and costs one spurious page refresh at worst (RemoveInstance already + // invalidated its real footprint) + robin_hood::unordered_map _dynamicCasterLastSignal; + std::vector _dynamicCasterLiveIDs; // This frame's live set, feeds the mask sync + std::vector _dynamicCasterAABBs; // World (min, max) pairs of the live set, rebuilt each frame + f32 _dynamicCasterTime = 0.0f; + u32 _dynamicCastersDropped = 0; // Spilled to static this frame (cap or oversize), never dropped from both pools + u32 _dynamicCasterTransitionsIn = 0; + u32 _dynamicCasterTransitionsOut = 0; + // Spawned/despawned/re-modeled instances must invalidate the cached static shadow pages under // them, ShadowRenderer drains this each frame struct ShadowInvalidation @@ -486,12 +509,9 @@ class ModelRenderer : CulledRenderer // Animated doodads (windmills, flags, swaying vegetation) share one uninstanced bone stream // per model; the queue signals which models advanced their animation this frame so Update can - // classify their placements within svsmAnimatedCasterRange as dynamic shadow casters + // promote their placements within svsmAnimatedCasterRange to per-instance dynamic signals moodycamel::ConcurrentQueue _uninstancedAnimatedModelQueue; - robin_hood::unordered_map _animatedModelLastPushFrame; // modelID -> last frame it pushed bones - robin_hood::unordered_set _animatedCasterInstances; // In-range animated instances, persists as last frame's set - std::vector _animatedCasterAABBs; // World (min, max) pairs of the set, rebuilt each frame - u64 _animatedCasterFrame = 0; + robin_hood::unordered_map _animatedModelLastPushTime; // modelID -> last accumulated-seconds it pushed bones // GPU-only workbuffers Renderer::BufferID _occluderArgumentBuffer; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index ec633a1..0a19ff0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -1,14 +1,12 @@ #include "ShadowRenderer.h" #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -25,23 +23,25 @@ #include +#include + AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 1, CVarFlags::EditCheckbox); -AutoCVar_Float CVAR_ShadowStrength(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength", "directional shadow strength, overwritten each frame from the sun elevation", 1.0f); +AutoCVar_Float CVAR_ShadowStrength(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength", "directional shadow strength, overwritten each frame from the sun elevation", 1.0f, CVarFlags::EditReadOnly); AutoCVar_Float CVAR_ShadowNormalOffsetBias(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias", "receiver offset along the surface normal in shadow texels, fights acne on hard angles", 1.0f); AutoCVar_Float CVAR_ShadowCasterMargin(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin", "extends clipmap culling toward the sun so far-away casters with long shadows are not culled, depth clamp pancakes them onto the near plane", 2500.0f); AutoCVar_Int CVAR_SVSMNumClipmaps(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps", "number of SVSM clipmap rings, each ring doubles the covered area", 6); AutoCVar_Float CVAR_SVSMClipmap0Extent(CVarCategory::Client | CVarCategory::Rendering, "svsmClipmap0Extent", "world extent of the finest SVSM clipmap window in meters, finer rings multiply the near-field page demand", 64.0f); AutoCVar_Int CVAR_SVSMVirtualSize(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize", "virtual texture resolution per clipmap", 8192); AutoCVar_Int CVAR_SVSMPageSize(CVarCategory::Client | CVarCategory::Rendering, "svsmPageSize", "texels per page", 128); -AutoCVar_Int CVAR_SVSMPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmPoolSize", "physical page pool texture resolution, page count is fixed at startup", 8192); +AutoCVar_Int CVAR_SVSMPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmPoolSize", "physical page pool texture resolution, restart-only once shadows have been enabled", 8192); AutoCVar_Int CVAR_SVSMPageEvictAge(CVarCategory::Client | CVarCategory::Rendering, "svsmPageEvictAge", "frames without a visible sample before a cached page returns to the pool, the age counter caps at 254", 240); AutoCVar_Float CVAR_SVSMMarkBorderTexels(CVarCategory::Client | CVarCategory::Rendering, "svsmMarkBorderTexels", "filter footprint margin in texels, samples near a page border also mark the neighbor page", 4.0f); AutoCVar_Float CVAR_SVSMResolutionScale(CVarCategory::Client | CVarCategory::Rendering, "svsmResolutionScale", "eye-distance clipmap floor: skip rings finer than the sample's screen footprint times this, 0 disables, lower = sharper distant shadows for more pages. Below ~0.25 the pool pressure-evicts and churns", 1.0f); AutoCVar_Int CVAR_SVSMDynamicSplit(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit", "static/dynamic caster split: animated and moving casters render into a transient dynamic pool instead of churning the static cache, 0 reverts to v1 behavior", 1, CVarFlags::EditCheckbox); -AutoCVar_Int CVAR_SVSMDynamicPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicPoolSize", "dynamic page pool texture resolution, page count is fixed at startup", 2048); +AutoCVar_Int CVAR_SVSMDynamicPoolSize(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicPoolSize", "dynamic page pool texture resolution, restart-only once shadows have been enabled", 2048); AutoCVar_Int CVAR_SVSMRenderBudget(CVarCategory::Client | CVarCategory::Rendering, "svsmRenderBudget", "static pages rendered per frame, 0 = unlimited; overflow refines over following frames coarse-to-fine, the coarsest two rings are exempt", 0); AutoCVar_Float CVAR_SVSMAnimatedCasterRange(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange", "camera range in meters within which animated doodads (windmills, flags) cast dynamic shadows, beyond it their pose bakes static, 0 disables", 128.0f); -AutoCVar_Int CVAR_SVSMFreeze(CVarCategory::Client | CVarCategory::Rendering, "svsmFreeze", "freeze SVSM page marking and lifecycle to inspect the cached state", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMFreeze(CVarCategory::Client | CVarCategory::Rendering, "svsmFreeze", "freeze SVSM page marking and lifecycle to inspect the cached state. Stale dirty state stays live and pages never clear, so dynamic content ghost-accumulates while frozen; unfreezing re-bakes the cache if anything spawned/despawned meanwhile", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_SVSMInvalidateAll(CVarCategory::Client | CVarCategory::Rendering, "svsmInvalidateAll", "one shot, invalidate every cached SVSM page", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_SVSMValidateDynamic(CVarCategory::Client | CVarCategory::Rendering, "svsmValidateDynamic", "one shot, read back the dynamic page table and free list and validate pool invariants (aliasing, leaks)", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_SVSMDebugClipmap(CVarCategory::Client | CVarCategory::Rendering, "svsmDebugClipmap", "draw this clipmap's page table as an overlay, -1 disables", -1); @@ -77,10 +77,38 @@ namespace SVSMCause constexpr u32 AABBOverflow = 8; } -ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) +// pageTableSize must be a power of two in [16, maxPageTableSize] (WrapPageSlot masks with +// pageTableSize - 1), and every consumer — the geometry passes' viewport extents included — +// derives it from these cvars, so invalid values are corrected at the source. Returns true when +// anything was corrected: the pre-snap values were live for a few frames and mis-addressed the +// cached pages, so the caller must invalidate the cache +static bool SanitizeSVSMConfigCVars(u32 maxPageTableSize) +{ + const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + const u32 virtualSize = static_cast(glm::max(CVAR_SVSMVirtualSize.Get(), 1)); + + const u32 pageTableSize = glm::clamp(std::bit_floor(virtualSize / pageSize), 16u, maxPageTableSize); + const u32 correctedVirtualSize = pageTableSize * pageSize; + + bool corrected = false; + if (correctedVirtualSize != static_cast(CVAR_SVSMVirtualSize.Get())) + { + NC_LOG_WARNING("SVSM: svsmVirtualSize {0} is not a power-of-two multiple of the page size in [16, {1}] pages, corrected to {2}", CVAR_SVSMVirtualSize.Get(), maxPageTableSize, correctedVirtualSize); + CVAR_SVSMVirtualSize.Set(static_cast(correctedVirtualSize)); + corrected = true; + } + if (pageSize != static_cast(CVAR_SVSMPageSize.Get())) + { + NC_LOG_WARNING("SVSM: svsmPageSize {0} is below the 16 texel minimum, corrected to {1}", CVAR_SVSMPageSize.Get(), pageSize); + CVAR_SVSMPageSize.Set(static_cast(pageSize)); + corrected = true; + } + return corrected; +} + +ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) , _gameRenderer(gameRenderer) - , _debugRenderer(debugRenderer) , _terrainRenderer(terrainRenderer) , _modelRenderer(modelRenderer) , _svsmPrepareDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS) @@ -107,19 +135,66 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) { ZoneScoped; - _lastDeltaTime = deltaTime; + if (SanitizeSVSMConfigCVars(SVSM_MAX_PAGE_TABLE_SIZE)) + { + _svsmForceInvalidateAll = true; // Pages cached under the pre-snap addressing are garbage + } + + // Live config edits: a pageSize change reshapes the pool page counts and free lists in place + // (the pool textures keep their dimensions, only the page grid over them changes). The pool + // size cvars are restart-only once the pool images exist — the Engine cannot destroy images, + // so recreating them at new dimensions would leak the old texture — and revert with a warning + { + if (_svsmPagePool != Renderer::ImageID::Invalid() && + (static_cast(CVAR_SVSMPoolSize.Get()) != _svsmAppliedPoolSize || static_cast(CVAR_SVSMDynamicPoolSize.Get()) != _svsmAppliedDynamicPoolSize)) + { + NC_LOG_WARNING("SVSM: svsmPoolSize/svsmDynamicPoolSize changes need a restart once the pools exist, reverting to {0}/{1}", _svsmAppliedPoolSize, _svsmAppliedDynamicPoolSize); + CVAR_SVSMPoolSize.Set(static_cast(_svsmAppliedPoolSize)); + CVAR_SVSMDynamicPoolSize.Set(static_cast(_svsmAppliedDynamicPoolSize)); + } + + const bool configChanged = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)) != _svsmAppliedPageSize + || static_cast(CVAR_SVSMPoolSize.Get()) != _svsmAppliedPoolSize + || static_cast(CVAR_SVSMDynamicPoolSize.Get()) != _svsmAppliedDynamicPoolSize; + if (configChanged) + { + ResetSVSMPoolState(resources); + _svsmForceInvalidateAll = true; + if (_svsmPagePool != Renderer::ImageID::Invalid()) + { + _svsmPoolNeedsClear = true; // The old page grid's depth is garbage under the new one + } + } + } - // SVSM: last frame's page table stats plus this frame's caster bounds. With the caster split, - // the current dynamic set (moved + animated) feeds dynamic page marking, and only - // classification transitions and spawns/despawns invalidate cached static content. With the - // split off, everything feeds static invalidation like v1 + // Caster-toggle transitions re-bake the whole static cache: resident pages hold the toggled + // class's baked depth, and marked pages never age out while visible, so without this the old + // shadows persist indefinitely after a toggle + { + CVarSystem* cvarSystem = CVarSystem::Get(); + const bool modelsCastShadow = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowModelsCastShadow"_h) == 1; + const bool terrainCastShadow = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowTerrainCastShadow"_h) == 1; + if (modelsCastShadow != _svsmModelsCastShadow || terrainCastShadow != _svsmTerrainCastShadow) + { + _svsmModelsCastShadow = modelsCastShadow; + _svsmTerrainCastShadow = terrainCastShadow; + _svsmForceInvalidateAll = true; + } + } + + // SVSM: last frame's page table stats plus this frame's caster bounds. The ModelRenderer's + // per-instance classifier owns the dynamic set (moved or bone-pushed within the grace window) + // and its transition invalidations; this just copies the results and uploads. With the split + // off, the classifier feeds everything through static invalidation like v1 _svsmDirtyAABBs.clear(); _svsmDynamicAABBs.clear(); _svsmDirtyAABBOverflow = false; _svsmNumDynamicCasters = 0; - _svsmNumAnimatedCasters = 0; + _svsmCasterTransitionsIn = 0; + _svsmCasterTransitionsOut = 0; _svsmDynamicAABBsDropped = 0; - if (CVAR_ShadowEnabled.Get()) + const bool frozen = CVAR_SVSMFreeze.Get() != 0; + if (CVAR_ShadowEnabled.Get() && !frozen) { // The pools are the SVSM VRAM cost, only allocated once shadows are actually used if (_svsmPagePool == Renderer::ImageID::Invalid()) @@ -142,7 +217,9 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } // Diagnostics only (perf editor stats), rendering decisions never read this — it is a - // frame old, the GPU gates its own per-view work through Finalize's fill dispatch args + // frame old, the GPU gates its own per-view work through Finalize's fill dispatch args. + // Single-buffered across frames in flight: this map can race the in-flight GPU copy, so + // values may tear (house readback pattern, accepted — stats can read inconsistent) u32* svsmData = static_cast(_renderer->MapBuffer(_svsmDataReadBackBuffer)); if (svsmData != nullptr) { @@ -224,112 +301,24 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) CVAR_SVSMValidateDynamic.Set(0); } - entt::registry* gameRegistry = ServiceLocator::GetEnttRegistries()->gameRegistry; - const bool split = CVAR_SVSMDynamicSplit.Get() == 1; - - auto AppendStaticAABB = [this](const vec4& min, const vec4& max) - { - if (_svsmDirtyAABBs.size() >= SVSM_MAX_DIRTY_AABBS * 2) - { - _svsmDirtyAABBOverflow = true; // Falls back to a full static invalidation - return; - } - - _svsmDirtyAABBs.push_back(min); - _svsmDirtyAABBs.push_back(max); - }; - - // Dynamic list overflow must NOT full-invalidate the static cache: the dropped casters' - // shadows just freeze for the frame, visible in the perf editor - auto AppendDynamicAABB = [this](const vec4& min, const vec4& max) - { - if (_svsmDynamicAABBs.size() >= SVSM_MAX_DYNAMIC_AABBS * 2) - { - _svsmDynamicAABBsDropped++; - return; - } - - _svsmDynamicAABBs.push_back(min); - _svsmDynamicAABBs.push_back(max); - }; - - // Spawned/despawned/re-modeled instances always invalidate cached static pages - if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, SVSM_MAX_DIRTY_AABBS) * 2 > SVSM_MAX_DIRTY_AABBS * 2) + // Spawned/despawned/re-modeled instances and the classifier's transition/spill + // invalidations always re-bake cached static pages + if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, SVSM_MAX_DIRTY_AABBS) > SVSM_MAX_DIRTY_AABBS) { _svsmDirtyAABBOverflow = true; } - // The current dynamic caster set: moved this frame or actively bone-simulated - robin_hood::unordered_set currentDynamicEntities; - - auto CollectDynamic = [&](entt::entity entity, const ECS::Components::Model& model, const ECS::Components::WorldAABB& worldAABB) - { - if (model.instanceID == std::numeric_limits::max()) - return; - - u32 entityHandle = static_cast(entity); - if (!currentDynamicEntities.insert(entityHandle).second) - return; // Already collected through the other view - - vec4 aabbMin = vec4(worldAABB.min, 0.0f); - vec4 aabbMax = vec4(worldAABB.max, 0.0f); - if (split) - { - AppendDynamicAABB(aabbMin, aabbMax); - - // Newly dynamic: its shadow is baked into static pages and must come out - if (!_svsmPrevDynamicEntities.contains(entityHandle)) - { - AppendStaticAABB(aabbMin, aabbMax); - } - } - else - { - AppendStaticAABB(aabbMin, aabbMax); // v1 behavior - } - }; - - gameRegistry->view().each([&](entt::entity entity, ECS::Components::Model& model, ECS::Components::WorldAABB& worldAABB, ECS::Components::DirtyTransform&) - { - CollectDynamic(entity, model, worldAABB); - }); - - gameRegistry->view().each([&](entt::entity entity, ECS::Components::Model& model, ECS::Components::WorldAABB& worldAABB, ECS::Components::AnimationData&) - { - CollectDynamic(entity, model, worldAABB); - }); - - // Turned static (or split just toggled off): its last pose must bake back into static pages. - // Destroyed-while-dynamic entities are covered by the despawn queue above - for (u32 entityHandle : _svsmPrevDynamicEntities) - { - if (currentDynamicEntities.contains(entityHandle)) - continue; - - entt::entity entity = static_cast(entityHandle); - if (!gameRegistry->valid(entity) || !gameRegistry->all_of(entity)) - continue; - - const ECS::Components::WorldAABB& worldAABB = gameRegistry->get(entity); - AppendStaticAABB(vec4(worldAABB.min, 0.0f), vec4(worldAABB.max, 0.0f)); - } - - _svsmNumDynamicCasters = static_cast(currentDynamicEntities.size()); - _svsmPrevDynamicEntities = std::move(currentDynamicEntities); - if (!split) - { - _svsmPrevDynamicEntities.clear(); // Re-enabling the split re-transitions everything - } + // The dynamic caster set — moved or bone-pushed within the grace window — is classified + // per instance by the ModelRenderer (one classifier feeds both this list and the GPU + // instance mask, so they can never disagree). Capped and oversize-spilled at the source, + // the copy just has to fit + const std::vector& dynamicAABBs = _modelRenderer->GetDynamicCasterAABBs(); + NC_ASSERT(dynamicAABBs.size() <= SVSM_MAX_DYNAMIC_AABBS * 2, "SVSM: ModelRenderer emitted more dynamic caster AABBs than the buffer holds, the source cap is broken"); + _svsmDynamicAABBs.assign(dynamicAABBs.begin(), dynamicAABBs.end()); - // Animated doodads in range, classified and range-diffed by the ModelRenderer. With the - // split off the ModelRenderer feeds them through the static invalidation queue instead - // and this list is empty - const std::vector& animatedAABBs = _modelRenderer->GetAnimatedCasterAABBs(); - _svsmNumAnimatedCasters = static_cast(animatedAABBs.size() / 2); - for (size_t i = 0; i + 1 < animatedAABBs.size(); i += 2) - { - AppendDynamicAABB(animatedAABBs[i], animatedAABBs[i + 1]); - } + _svsmNumDynamicCasters = _modelRenderer->GetNumDynamicCasters(); + _svsmDynamicAABBsDropped = _modelRenderer->GetNumDynamicCastersDropped(); + _modelRenderer->GetDynamicCasterTransitions(_svsmCasterTransitionsIn, _svsmCasterTransitionsOut); // Upload this frame's AABB lists through the frame-synced staging ring, the render graph // issues a global upload barrier before any pass executes @@ -348,8 +337,10 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } else { - // Shadows off: spawn/despawn invalidations would accumulate in the queue unboundedly. - // Discard them (maxPairs 0 appends nothing) and re-bake the whole cache on re-enable instead + // Shadows off or frozen (the update pass doesn't run, so nothing would consume the + // queue): spawn/despawn and classifier-transition invalidations would accumulate + // unboundedly. Discard them (maxPairs 0 appends nothing) and re-bake the whole cache on + // resume instead — the classifier keeps ticking meanwhile, so its state stays current if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, 0) > 0) { _svsmForceInvalidateAll = true; @@ -367,12 +358,12 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende Renderer::BufferMutableResource svsmDataBuffer; Renderer::BufferMutableResource pageTableBuffer; Renderer::BufferMutableResource freeListBuffer; - Renderer::BufferMutableResource dirtyAABBBuffer; + Renderer::BufferResource dirtyAABBBuffer; Renderer::BufferMutableResource clearListBuffer; Renderer::BufferMutableResource dynamicPageTableBuffer; Renderer::BufferMutableResource dynamicFreeListBuffer; Renderer::BufferMutableResource dynamicClearListBuffer; - Renderer::BufferMutableResource dynamicAABBBuffer; + Renderer::BufferResource dynamicAABBBuffer; Renderer::BufferMutableResource svsmDataReadBackBuffer; Renderer::BufferMutableResource dynamicValidateReadBackBuffer; Renderer::ImageMutableResource pagePool; @@ -429,12 +420,12 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende data.svsmDataBuffer = builder.Write(_svsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); data.pageTableBuffer = builder.Write(_svsmPageTableBuffer, BufferUsage::COMPUTE); data.freeListBuffer = builder.Write(_svsmFreeListBuffer, BufferUsage::COMPUTE); - data.dirtyAABBBuffer = builder.Write(_svsmDirtyAABBBuffer, BufferUsage::COMPUTE); + data.dirtyAABBBuffer = builder.Read(_svsmDirtyAABBBuffer, BufferUsage::COMPUTE); // CPU-uploaded input, the shaders only read it data.clearListBuffer = builder.Write(_svsmClearListBuffer, BufferUsage::COMPUTE); - data.dynamicPageTableBuffer = builder.Write(_svsmDynamicPageTableBuffer, BufferUsage::COMPUTE); - data.dynamicFreeListBuffer = builder.Write(_svsmDynamicFreeListBuffer, BufferUsage::COMPUTE); + data.dynamicPageTableBuffer = builder.Write(_svsmDynamicPageTableBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); // TRANSFER: the svsmValidateDynamic snapshot copies from it + data.dynamicFreeListBuffer = builder.Write(_svsmDynamicFreeListBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); data.dynamicClearListBuffer = builder.Write(_svsmDynamicClearListBuffer, BufferUsage::COMPUTE); - data.dynamicAABBBuffer = builder.Write(_svsmDynamicAABBBuffer, BufferUsage::COMPUTE); + data.dynamicAABBBuffer = builder.Read(_svsmDynamicAABBBuffer, BufferUsage::COMPUTE); // CPU-uploaded input, the shaders only read it data.svsmDataReadBackBuffer = builder.Write(_svsmDataReadBackBuffer, BufferUsage::TRANSFER); data.dynamicValidateReadBackBuffer = builder.Write(_svsmDynamicValidateReadBackBuffer, BufferUsage::TRANSFER); builder.Write(_svsmFillArgsBuffer, BufferUsage::COMPUTE); // Finalize writes the per-view fill dispatch args @@ -483,7 +474,6 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende u32 fillCellCount; }; - CVarSystem* cvarSystem = CVarSystem::Get(); const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); const u32 pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / pageSize, 16u, SVSM_MAX_PAGE_TABLE_SIZE); u32 poolPagesPerRow = static_cast(CVAR_SVSMPoolSize.Get()) / pageSize; @@ -503,7 +493,7 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende constants->invalidateAll = invalidateCause; constants->numDirtyAABBs = numDirtyAABBs; constants->allocClipmap = 0; - constants->casterMargin = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCasterMargin")); + constants->casterMargin = CVAR_ShadowCasterMargin.GetFloat(); constants->zHalfRange = CVAR_SVSMZHalfRange.GetFloat(); constants->padding0 = 0.0f; constants->padding1 = 0.0f; @@ -540,6 +530,10 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende commandList.EndPipeline(_svsmPreparePipeline); commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + // The clear-list header resets must be visible to UpdateB / DynamicUpdate's atomic + // appends; barriers are per-buffer, the svsmData one above does not cover them + commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); // Moved and animated instances invalidate the pages under them if (numDirtyAABBs > 0) @@ -871,6 +865,8 @@ void ShadowRenderer::AddSVSMBindPass(Renderer::RenderGraph* renderGraph, RenderR void ShadowRenderer::CreatePermanentResources(RenderResources& resources) { + SanitizeSVSMConfigCVars(SVSM_MAX_PAGE_TABLE_SIZE); + // SVSM page table lifecycle { Renderer::ComputePipelineDesc pipelineDesc; @@ -957,48 +953,7 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) _svsmPoolDebugDescriptorSet.RegisterPipeline(_renderer, _svsmPoolDebugPipeline); _svsmPoolDebugDescriptorSet.Init(_renderer); - // The physical page index is 12 bits in the table entry, the pool page counts are fixed at - // startup (svsmPoolSize/svsmDynamicPoolSize changes need a restart) - const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); - const u32 poolPagesPerRow = static_cast(CVAR_SVSMPoolSize.Get()) / pageSize; - _svsmPoolPages = glm::min(poolPagesPerRow * poolPagesPerRow, SVSM_MAX_POOL_PAGES); - const u32 dynamicPoolPagesPerRow = static_cast(CVAR_SVSMDynamicPoolSize.Get()) / pageSize; - _svsmDynamicPoolPages = glm::min(dynamicPoolPagesPerRow * dynamicPoolPagesPerRow, SVSM_MAX_POOL_PAGES); - Renderer::BufferDesc bufferDesc; - bufferDesc.name = "SVSMData"; - bufferDesc.size = sizeof(u32) * SVSM_DATA_UINT_COUNT; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; - _svsmDataBuffer = _renderer->CreateAndFillBuffer(_svsmDataBuffer, bufferDesc, [](void* mappedMemory, size_t size) - { - memset(mappedMemory, 0, size); // prevLightDirection.w = 0 marks the state uninitialized - }); - - bufferDesc.name = "SVSMPageTable"; - bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; - _svsmPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) - { - memset(mappedMemory, 0, size); // A zeroed entry is a free slot - }); - - bufferDesc.name = "SVSMFreeList"; - bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; - const u32 poolPages = _svsmPoolPages; - _svsmFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmFreeListBuffer, bufferDesc, [poolPages](void* mappedMemory, size_t size) - { - u32* values = static_cast(mappedMemory); - values[0] = poolPages; // [0] = count, [1..3] padding, entries from [4] - values[1] = 0; - values[2] = 0; - values[3] = 0; - for (u32 i = 0; i < SVSM_MAX_POOL_PAGES; i++) - { - values[4 + i] = i < poolPages ? i : 0; - } - }); - bufferDesc.name = "SVSMDirtyAABBs"; bufferDesc.size = sizeof(vec4) * SVSM_MAX_DIRTY_AABBS * 2; bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION; @@ -1008,30 +963,6 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) bufferDesc.size = sizeof(vec4) * SVSM_MAX_DYNAMIC_AABBS * 2; _svsmDynamicAABBBuffer = _renderer->CreateBuffer(_svsmDynamicAABBBuffer, bufferDesc); - // Static/dynamic caster split resources - bufferDesc.name = "SVSMDynamicPageTable"; - bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; - _svsmDynamicPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) - { - memset(mappedMemory, 0, size); - }); - - bufferDesc.name = "SVSMDynamicFreeList"; - bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); - bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; - const u32 dynamicPoolPages = _svsmDynamicPoolPages; - _svsmDynamicFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicFreeListBuffer, bufferDesc, [dynamicPoolPages](void* mappedMemory, size_t size) - { - u32* values = static_cast(mappedMemory); - memset(values, 0, size); - values[0] = dynamicPoolPages; - for (u32 i = 0; i < dynamicPoolPages; i++) - { - values[4 + i] = i; - } - }); - bufferDesc.name = "SVSMDynamicClearList"; bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::INDIRECT_ARGUMENT_BUFFER; @@ -1054,34 +985,16 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) values[2] = 1; }); - _svsmPrepareDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmPrepareDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + // The config-independent buffer binds. The config-shaped buffers (SVSMData, page tables, + // free lists) are created and bound by ResetSVSMPoolState below _svsmPrepareDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); _svsmPrepareDescriptorSet.Bind("_dynamicClearList"_h, _svsmDynamicClearListBuffer); - _svsmInvalidateAABBsDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmInvalidateAABBsDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); _svsmInvalidateAABBsDescriptorSet.Bind("_dirtyAABBs"_h, _svsmDirtyAABBBuffer); - _svsmPageUpdateADescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmPageUpdateADescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); - _svsmPageUpdateADescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); - _svsmPageMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmPageMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); - _svsmPageUpdateBDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmPageUpdateBDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); - _svsmPageUpdateBDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); _svsmPageUpdateBDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); - _svsmDynamicMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmDynamicMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); - _svsmDynamicMarkDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); _svsmDynamicMarkDescriptorSet.Bind("_dynamicAABBs"_h, _svsmDynamicAABBBuffer); - _svsmDynamicUpdateDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - _svsmDynamicUpdateDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); - _svsmDynamicUpdateDescriptorSet.Bind("_dynamicFreeList"_h, _svsmDynamicFreeListBuffer); _svsmDynamicUpdateDescriptorSet.Bind("_dynamicClearList"_h, _svsmDynamicClearListBuffer); - _svsmFinalizeDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); _svsmPageClearDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer); _svsmDynamicPageClearDescriptorSet.Bind("_clearList"_h, _svsmDynamicClearListBuffer); - _svsmPageTableDebugDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); // _srcCameras / _depth / _target / _rwCameras / _pagePool are bound per frame, those resources can be recreated // Per-view fill dispatch args written by Finalize, consumed by the geometry passes' @@ -1113,12 +1026,6 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) bufferDesc.size = sizeof(u32) * (SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE + 4 + SVSM_MAX_POOL_PAGES); _svsmDynamicValidateReadBackBuffer = _renderer->CreateBuffer(_svsmDynamicValidateReadBackBuffer, bufferDesc); - // The material pass samples SVSM through the LIGHT set. The buffers bind here, the pools - // bind per frame in the material pass (real pools once created, a placeholder before) - resources.lightDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); - resources.lightDescriptorSet.Bind("_svsmPageTable"_h, _svsmPageTableBuffer); - resources.lightDescriptorSet.Bind("_svsmDynamicPageTable"_h, _svsmDynamicPageTableBuffer); - Renderer::ImageDesc placeholderDesc; placeholderDesc.debugName = "SVSMPagePoolPlaceholder"; placeholderDesc.dimensions = vec2(1, 1); @@ -1129,15 +1036,123 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) _svsmPagePoolPlaceholder = _renderer->CreateImage(placeholderDesc); - // The terrain/model SVSM page-render sets read these permanent buffers, bind them once - // here (the pools stay per-frame, they are created lazily). Same for the cameras buffer - _terrainRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer); - _modelRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer, _svsmDynamicPageTableBuffer); + // Creates the config-shaped buffers and binds them everywhere, here and again on live + // config edits + ResetSVSMPoolState(resources); } BindCameraBuffers(resources); } +void ShadowRenderer::ResetSVSMPoolState(RenderResources& resources) +{ + // (Re)shapes everything svsmPageSize touches: pool page counts, free lists, page tables and + // the SVSMData scratch (zeroed SVSMData reads as uninitialized, Prepare re-derives the + // anchors). The refills happen CPU-side so GPU eviction never runs against a half-rebuilt + // free list, and CreateAndFillBuffer swaps in a fresh buffer (deferred-destroying the old one + // past the frames in flight), so every consumer set rebinds below. The pool textures are NOT + // recreated — their dimensions only depend on the pool size cvars, which are restart-only + // once the pools exist (the Engine cannot destroy images) + _svsmAppliedPageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + _svsmAppliedPoolSize = static_cast(CVAR_SVSMPoolSize.Get()); + _svsmAppliedDynamicPoolSize = static_cast(CVAR_SVSMDynamicPoolSize.Get()); + + // The physical page index is 12 bits in the table entry + const u32 poolPagesPerRow = _svsmAppliedPoolSize / _svsmAppliedPageSize; + _svsmPoolPages = glm::min(poolPagesPerRow * poolPagesPerRow, SVSM_MAX_POOL_PAGES); + const u32 dynamicPoolPagesPerRow = _svsmAppliedDynamicPoolSize / _svsmAppliedPageSize; + _svsmDynamicPoolPages = glm::min(dynamicPoolPagesPerRow * dynamicPoolPagesPerRow, SVSM_MAX_POOL_PAGES); + + Renderer::BufferDesc bufferDesc; + bufferDesc.name = "SVSMData"; + bufferDesc.size = sizeof(u32) * SVSM_DATA_UINT_COUNT; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_DESTINATION | Renderer::BufferUsage::TRANSFER_SOURCE; + _svsmDataBuffer = _renderer->CreateAndFillBuffer(_svsmDataBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); // prevLightDirection.w = 0 marks the state uninitialized + }); + + bufferDesc.name = "SVSMPageTable"; + bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + _svsmPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); // A zeroed entry is a free slot + }); + + bufferDesc.name = "SVSMFreeList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER; + const u32 poolPages = _svsmPoolPages; + _svsmFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmFreeListBuffer, bufferDesc, [poolPages](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + values[0] = poolPages; // [0] = count, [1..3] padding, entries from [4] + values[1] = 0; + values[2] = 0; + values[3] = 0; + for (u32 i = 0; i < SVSM_MAX_POOL_PAGES; i++) + { + values[4 + i] = i < poolPages ? i : 0; + } + }); + + bufferDesc.name = "SVSMDynamicPageTable"; + bufferDesc.size = sizeof(u32) * SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_SOURCE; // Source of the svsmValidateDynamic snapshot copy + _svsmDynamicPageTableBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicPageTableBuffer, bufferDesc, [](void* mappedMemory, size_t size) + { + memset(mappedMemory, 0, size); + }); + + bufferDesc.name = "SVSMDynamicFreeList"; + bufferDesc.size = sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES); + bufferDesc.usage = Renderer::BufferUsage::STORAGE_BUFFER | Renderer::BufferUsage::TRANSFER_SOURCE; + const u32 dynamicPoolPages = _svsmDynamicPoolPages; + _svsmDynamicFreeListBuffer = _renderer->CreateAndFillBuffer(_svsmDynamicFreeListBuffer, bufferDesc, [dynamicPoolPages](void* mappedMemory, size_t size) + { + u32* values = static_cast(mappedMemory); + memset(values, 0, size); + values[0] = dynamicPoolPages; + for (u32 i = 0; i < dynamicPoolPages; i++) + { + values[4 + i] = i; + } + }); + + _svsmPrepareDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPrepareDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmInvalidateAABBsDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmInvalidateAABBsDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateADescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageUpdateADescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateADescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmPageMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmPageUpdateBDescriptorSet.Bind("_freeList"_h, _svsmFreeListBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + _svsmDynamicMarkDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_dynamicPageTable"_h, _svsmDynamicPageTableBuffer); + _svsmDynamicUpdateDescriptorSet.Bind("_dynamicFreeList"_h, _svsmDynamicFreeListBuffer); + _svsmFinalizeDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + _svsmPageTableDebugDescriptorSet.Bind("_pageTable"_h, _svsmPageTableBuffer); + + // The material pass samples SVSM through the LIGHT set. The buffers bind here, the pools + // bind per frame in the material pass (real pools once created, a placeholder before) + resources.lightDescriptorSet.Bind("_svsmData"_h, _svsmDataBuffer); + resources.lightDescriptorSet.Bind("_svsmPageTable"_h, _svsmPageTableBuffer); + resources.lightDescriptorSet.Bind("_svsmDynamicPageTable"_h, _svsmDynamicPageTableBuffer); + + // The terrain/model SVSM page-render sets read these permanent buffers, bind them once + // here (the pools stay per-frame, they are created lazily). Same for the cameras buffer + _terrainRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer); + _modelRenderer->BindSVSMBuffers(_svsmDataBuffer, _svsmPageTableBuffer, _svsmDynamicPageTableBuffer); +} + void ShadowRenderer::BindCameraBuffers(RenderResources& resources) { Renderer::BufferID camerasBuffer = resources.cameras.GetBuffer(); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 5f58faa..2899cd7 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -21,7 +21,6 @@ namespace Renderer } struct RenderResources; -class DebugRenderer; class GameRenderer; class ModelRenderer; class TerrainRenderer; @@ -29,7 +28,7 @@ class TerrainRenderer; class ShadowRenderer { public: - ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources); + ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources); ~ShadowRenderer(); void Update(f32 deltaTime, RenderResources& resources); @@ -60,7 +59,7 @@ class ShadowRenderer void GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, u32& outOverflow) const; u32 GetSVSMBudgetUsed() const { return _svsmDataReadBack[216]; } // SVSMDataOffsets::StatsBudgetUsed - // Binds the cameras buffer into every SDSM/SVSM set that reads or writes it. Called at init + // Binds the cameras buffer into every SVSM set that reads or writes it. Called at init // (buffer binds only reach the canonical descriptor copies at a later FlipFrame, so mid-frame // binds leave a hole) and again if the cameras buffer is ever recreated void BindCameraBuffers(RenderResources& resources); @@ -70,11 +69,13 @@ class ShadowRenderer // per-view skip is a frame late and drops freshly acquired dynamic pages' draws for a frame bool HasSVSMDynamicCasters() const { return !_svsmDynamicAABBs.empty(); } - // CPU-side caster classification counts, rebuilt each Update - void GetSVSMCasterStats(u32& outDynamicCasters, u32& outAnimatedCasters, u32& outDroppedAABBs) const + // CPU-side caster classification counts, rebuilt each Update from the ModelRenderer's + // classifier: live set size, this frame's enter/leave transitions, and static-spilled casters + void GetSVSMCasterStats(u32& outDynamicCasters, u32& outTransitionsIn, u32& outTransitionsOut, u32& outDroppedAABBs) const { outDynamicCasters = _svsmNumDynamicCasters; - outAnimatedCasters = _svsmNumAnimatedCasters; + outTransitionsIn = _svsmCasterTransitionsIn; + outTransitionsOut = _svsmCasterTransitionsOut; outDroppedAABBs = _svsmDynamicAABBsDropped; } @@ -93,6 +94,15 @@ class ShadowRenderer static constexpr u32 SVSM_FILL_ARGS_DYNAMIC_OFFSET = 3 * sizeof(u32); static constexpr u32 SVSM_FILL_ARGS_TERRAIN_OFFSET = 6 * sizeof(u32); + // The single clipmap-count cap every clamp site uses. The camera buffer and bitmask slices + // are laid out against the Engine's view cap, the two must stay equal + static constexpr u32 SVSM_MAX_CLIPMAPS = 8; + static_assert(SVSM_MAX_CLIPMAPS == Renderer::Settings::MAX_SHADOW_CASCADES, "SVSM clipmap cap must match the Engine shadow view cap, both size the camera buffer and bitmask slices"); + + // Dynamic AABB buffer capacity; the ModelRenderer's classifier caps its emission against this + // and spills the excess to the static path (never excluded from both pools) + static constexpr u32 SVSM_MAX_DYNAMIC_AABBS = 4096; + // For the material pass LIGHT set bindings, which must stay valid before the pools exist. // Nothing samples the placeholder while the page tables have no resident entries Renderer::ImageID GetSVSMPagePoolOrPlaceholder() const { return _svsmPagePool != Renderer::ImageID::Invalid() ? _svsmPagePool : _svsmPagePoolPlaceholder; } @@ -100,22 +110,20 @@ class ShadowRenderer private: void CreatePermanentResources(RenderResources& resources); + void ResetSVSMPoolState(RenderResources& resources); private: Renderer::Renderer* _renderer = nullptr; GameRenderer* _gameRenderer = nullptr; - DebugRenderer* _debugRenderer = nullptr; TerrainRenderer* _terrainRenderer = nullptr; ModelRenderer* _modelRenderer = nullptr; // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp. // Tail: clipRect{MinX,MinY,MaxX,MaxY}[24] at 220..315 (3 clip rects x 8 clipmaps) static constexpr u32 SVSM_DATA_UINT_COUNT = 316; - static constexpr u32 SVSM_MAX_CLIPMAPS = 8; static constexpr u32 SVSM_MAX_PAGE_TABLE_SIZE = 64; // Pages per row, buffers are sized for this cap static constexpr u32 SVSM_MAX_POOL_PAGES = 4096; // Physical page index is 12 bits in the table entry static constexpr u32 SVSM_MAX_DIRTY_AABBS = 1024; - static constexpr u32 SVSM_MAX_DYNAMIC_AABBS = 4096; // Animated forests are many small casters, overflow drops (never full-invalidates) Renderer::ComputePipelineID _svsmPreparePipeline; Renderer::ComputePipelineID _svsmInvalidateAABBsPipeline; @@ -160,16 +168,27 @@ class ShadowRenderer std::vector _svsmDirtyAABBs; // Static invalidation (min, max) pairs, uploaded by the update pass std::vector _svsmDynamicAABBs; // This frame's dynamic caster (min, max) pairs - robin_hood::unordered_set _svsmPrevDynamicEntities; // Last frame's dynamic entity handles for transition detection bool _svsmDirtyAABBOverflow = false; - u32 _svsmNumDynamicCasters = 0; // ECS entities: moved or actively bone-simulated - u32 _svsmNumAnimatedCasters = 0; // In-range animated doodad instances from the ModelRenderer - u32 _svsmDynamicAABBsDropped = 0; // Dynamic list overflow: dropped casters freeze for the frame + u32 _svsmNumDynamicCasters = 0; // Classifier live set: moved or bone-pushed within the grace window + u32 _svsmCasterTransitionsIn = 0; // Classifier enter/leave this frame, each re-bakes static pages + u32 _svsmCasterTransitionsOut = 0; + u32 _svsmDynamicAABBsDropped = 0; // Spilled to the static path this frame (cap or oversize) bool _svsmForceInvalidateAll = false; // Set when invalidations were discarded while SVSM was inactive bool _svsmValidatePending = false; // A validation copy was recorded, Update maps and checks it next frame bool _svsmPoolNeedsClear = false; u32 _svsmPoolPages = 0; u32 _svsmDynamicPoolPages = 0; - f32 _lastDeltaTime = 0.0f; + // The config ResetSVSMPoolState last shaped the free lists and page counts for. pageSize is + // live-editable (a change refills them in place); the pool sizes are restart-only once the + // pool images exist, the Engine cannot destroy images to recreate them at new dimensions + u32 _svsmAppliedPageSize = 0; + u32 _svsmAppliedPoolSize = 0; + u32 _svsmAppliedDynamicPoolSize = 0; + + // Caster-toggle cvar states as of last Update: a flip must re-bake the whole static cache, + // resident pages keep the toggled class's baked depth forever otherwise (marked pages never + // age out while visible). Initialized to the cvar defaults so startup does not invalidate + bool _svsmModelsCastShadow = true; + bool _svsmTerrainCastShadow = true; }; \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp index 8cca132..5e95528 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp @@ -99,8 +99,8 @@ void SkyboxRenderer::SetSkybandColors(const vec3& skyTopColor, const vec3& skyMi void SkyboxRenderer::SetSunDirection(const vec3& directionToSun) { - f32 enabled = _skybandColors.sunDirection.w; - _skybandColors.sunDirection = vec4(glm::normalize(directionToSun), enabled); + // w is stomped from skyboxDrawSun in AddSkyboxPass every frame + _skybandColors.sunDirection = vec4(glm::normalize(directionToSun), 0.0f); } void SkyboxRenderer::CreatePermanentResources() diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index 38a3a02..f763ba9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -241,7 +241,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR if (_instanceDatas.Count() == 0) return; - const u32 numCascades = 0; // Main view only, cascades are culled in their own block late in the frame + const u32 numShadowViews = 0; // Main view only, cascades are culled in their own block late in the frame struct Data { @@ -281,7 +281,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR return true; // Return true from setup to enable this pass, return false to disable it }, - [this, frameIndex, numCascades](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + [this, frameIndex, numShadowViews](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute { GPU_SCOPED_PROFILER_ZONE(commandList, TerrainCulling); @@ -325,7 +325,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR { u32 viewportSizeX; u32 viewportSizeY; - u32 numCascades; + u32 numShadowViews; u32 occlusionEnabled; u32 bitMaskBufferSizePerView; u32 currentBitmaskIndex; @@ -338,7 +338,7 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR CullConstants* cullConstants = graphResources.FrameNew(); cullConstants->viewportSizeX = u32(viewportSize.x); cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numCascades = numCascades; + cullConstants->numShadowViews = numShadowViews; cullConstants->occlusionEnabled = CVAR_OcclusionCullingEnabled.Get(); const u32 cellCount = static_cast(_cellDatas.Count()); cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); @@ -473,7 +473,7 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render }); } -void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +void TerrainRenderer::AddClipmapCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { ZoneScoped; @@ -490,7 +490,7 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, return; // The same per-view culling the main view uses, run against the clipmap cameras - const u32 numCascades = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 numShadowViews = static_cast(glm::clamp(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(ShadowRenderer::SVSM_MAX_CLIPMAPS))); struct Data { @@ -503,7 +503,7 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, Renderer::DescriptorSetResource cullingSet; }; - renderGraph->AddPass("Terrain Cascade Culling", + renderGraph->AddPass("Terrain Clipmap Culling", [this, &resources, frameIndex](Data& data, Renderer::RenderGraphBuilder& builder) // Setup { using BufferUsage = Renderer::BufferPassUsage; @@ -526,9 +526,9 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, return true; // Return true from setup to enable this pass, return false to disable it }, - [this, frameIndex, numCascades](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute + [this, frameIndex, numShadowViews](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute { - GPU_SCOPED_PROFILER_ZONE(commandList, TerrainCascadeCulling); + GPU_SCOPED_PROFILER_ZONE(commandList, TerrainClipmapCulling); // Frustum-only cull of the cascade views into their bitmask slices, no occlusion culling for cascades Renderer::ComputePipelineID pipeline = _cullingPipeline; @@ -538,7 +538,7 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, { u32 viewportSizeX; u32 viewportSizeY; - u32 numCascades; + u32 numShadowViews; u32 occlusionEnabled; u32 bitMaskBufferSizePerView; u32 currentBitmaskIndex; @@ -553,7 +553,7 @@ void TerrainRenderer::AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, CullConstants* cullConstants = graphResources.FrameNew(); cullConstants->viewportSizeX = u32(viewportSize.x); cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numCascades = numCascades; + cullConstants->numShadowViews = numShadowViews; cullConstants->occlusionEnabled = false; cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); cullConstants->currentBitmaskIndex = frameIndex; @@ -595,7 +595,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re if (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid()) return; - const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(Renderer::Settings::MAX_SHADOW_CASCADES))); + const u32 numClipmaps = static_cast(glm::clamp(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h), i32(1), i32(ShadowRenderer::SVSM_MAX_CLIPMAPS))); const u32 virtualSize = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmVirtualSize"_h)); struct Data @@ -603,7 +603,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re Renderer::ImageMutableResource pagePool; Renderer::BufferResource svsmData; Renderer::BufferResource pageTable; - Renderer::BufferMutableResource svsmFillArgsBuffer; + Renderer::BufferResource svsmFillArgsBuffer; Renderer::BufferMutableResource culledInstanceBuffer; @@ -624,7 +624,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re data.pagePool = builder.Write(shadowRenderer->GetSVSMPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); - data.svsmFillArgsBuffer = builder.Write(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed by DispatchIndirect in the per-view fills + data.svsmFillArgsBuffer = builder.Read(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed read-only by DispatchIndirect in the per-view fills builder.Write(_culledInstanceBitMaskBuffer.Get(frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); builder.Write(_culledInstanceBitMaskBuffer.Get(!frameIndex), BufferUsage::COMPUTE | BufferUsage::TRANSFER); @@ -1502,7 +1502,7 @@ void TerrainRenderer::FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResource // Bind descriptorset commandList.BindDescriptorSet(params.fillSet, frameIndex); - if (params.fillArgsBuffer != Renderer::BufferMutableResource::Invalid()) + if (params.fillArgsBuffer != Renderer::BufferResource::Invalid()) { commandList.DispatchIndirect(params.fillArgsBuffer, params.fillArgsByteOffset); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index 185931b..3145602 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -49,7 +49,7 @@ class TerrainRenderer void AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); void AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); - void AddCascadeCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices + void AddClipmapCullingPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex); // Per-clipmap-view frustum culling into the bitmask slices void AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer); // Called once by ShadowRenderer at init, buffer binds must happen before the first frame @@ -119,7 +119,7 @@ class TerrainRenderer // When set the fill dispatches indirectly from these GPU-written args (SVSM per-view // gating), zero groups for views with no page work this frame - Renderer::BufferMutableResource fillArgsBuffer; + Renderer::BufferResource fillArgsBuffer; // Finalize-written dispatch args, consumed read-only via DispatchIndirect u32 fillArgsByteOffset = 0; }; void FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, FillDrawCallsParams& params); diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index ce7ef8e..159bb7c 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -120,7 +120,7 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh float3 lightSpacePos = mul(relativePos + worldNormal * (texelWorld * shadowSettings.normalOffset), lightRotation); float2 windowPos = lightSpacePos.xy + float2(_svsmData[0].camMinusWindowMinX[k], _svsmData[0].camMinusWindowMinY[k]); - // Reversed Z like the cascades: 1 = sun side. lightSpacePos is camera-relative, the raster + // Reversed Z: 1 = sun side. lightSpacePos is camera-relative, the raster // maps absolute light-space Z, camMinusZAnchor reconciles the two with small quantities. // Positive bias moves the receiver toward the sun float compareDepth = (zHalf - (lightSpacePos.z + camMinusZAnchor)) / zRangeLength + compareBias; diff --git a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang index 586b4e5..bdd3d27 100644 --- a/Source/Shaders/Shaders/Material/MaterialPass.cs.slang +++ b/Source/Shaders/Shaders/Material/MaterialPass.cs.slang @@ -1,4 +1,4 @@ -permutation DEBUG_ID = [0, 1, 2, 3, 4, 5]; // 4 (CascadeID) retired with CSM, kept so 5 = SVSM margin stays stable +permutation DEBUG_ID = [0, 1, 2, 3, 4]; // Off, TypeID, ObjectID, TriangleID, SVSM compare margin permutation EDITOR_MODE = [0, 1]; // Off, Terrain #include "DescriptorSet/Debug.inc.slang" @@ -335,10 +335,7 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) float3 debugColor = IDToColor3(vBuffer.triangleID); _resolvedColor[pixelPos] = float4(debugColor, 1); return; -#elif DEBUG_ID == 4 // Retired (was CascadeID) - _resolvedColor[pixelPos] = float4(0, 0, 0, 1); - return; -#elif DEBUG_ID == 5 // SVSM compare margin +#elif DEBUG_ID == 4 // SVSM compare margin PixelVertexData pixelVertexData = GetPixelVertexData(pixelPos, vBuffer, 0, _constants.renderInfo.xy); float3 debugColor = GetSVSMDebugColor(pixelVertexData.worldPos, pixelVertexData.worldNormal, _constants.shadowSettings.y, _constants.shadowSettings.z); _resolvedColor[pixelPos] = float4(debugColor, 1); diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang index e71f0ca..f053888 100644 --- a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -17,7 +17,8 @@ // offset inside the window (camMinusWindowMin), and every pass works with // windowPos = lightSpace(P - camera) + camMinusWindowMin, which is at most one window extent. -// Push constants shared by all SVSM dispatches, identical bytes pushed to each pipeline +// Push constants shared by all SVSM dispatches. One layout everywhere, but allocClipmap, +// dynamicPhase and numDirtyAABBs vary per dispatch struct SVSMConstants { float4 lightDirection; // xyz = direction the light travels, quantized shadow sun @@ -29,7 +30,7 @@ struct SVSMConstants float markBorderTexels; // filter footprint margin, marks neighbor pages near page borders uint evictAge; // frames without a mark before a resident page returns to the pool uint invalidateAll; // CPU-requested invalidation cause bits (SVSM_CAUSE_*) - uint numDirtyAABBs; + uint numDirtyAABBs; // Static dirty AABB count; for DynamicMark this carries the DYNAMIC AABB count instead uint allocClipmap; // UpdateB runs once per clipmap, finest first, so pool starvation always lands on the coarsest ring float casterMargin; // Sun-side sweep on the near cull plane, depth clamp pancakes those casters float zHalfRange; // Half depth range of every clipmap window around the quantized Z anchor diff --git a/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang index cc16cee..da45f20 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang @@ -54,7 +54,9 @@ void main(CSInput input) return; // Outside this clipmap's window } - // A huge dynamic mover marks at most this many pages, beyond it the finer clipmaps cover it + // A huge dynamic mover marks at most this many pages per ring, coarser clipmaps cover it + // (span quarters per ring). The CPU classifier routes casters that would exceed this in EVERY + // ring to the static path instead, so the cutoff never orphans a caster from both pools int2 span = pageMax - pageMin + int2(1, 1); if (span.x * span.y > 1024) { diff --git a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang index 50af122..a398012 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang @@ -4,8 +4,9 @@ // Last SVSM dispatch, single thread, runs after UpdateB and DynamicUpdate so the dirty rects and // per-ring page stats are final: writes the clipmap render cameras into the shared cameras buffer // that culling and the page render passes read, and the per-view fill dispatch args that gate the -// geometry passes' instance fills. Mirrors the CascadeFitCameras tail, minus the texel snap: the -// window center is aligned to the absolute page grid by construction. +// geometry passes' instance fills. Same GPU-writes-the-camera-slots shape the retired SDSM fit +// pass used, minus the texel snap: the window center is aligned to the absolute page grid by +// construction. // // The camera MATRICES always map the full clipmap window (the render viewport spans the whole // virtual texture). The cull PLANES are tightened to this frame's dirty page rect, XY-tight is @@ -84,7 +85,7 @@ void main() clipmapCamera.worldToClip = mul(view, proj); // glm viewToClip * worldToView clipmapCamera.clipToWorld = mul(clipmapCamera.clipToView, clipmapCamera.viewToWorld); // glm viewToWorld * clipToView - clipmapCamera.eyePosition = float4(shadowCameraPos, halfExtent); // w = window coverage radius, the far fade reads it + clipmapCamera.eyePosition = float4(shadowCameraPos, 0.0f); clipmapCamera.eyeRotation = float4(0.0f, 0.0f, 0.0f, 0.0f); clipmapCamera.nearFar = float4(0.0f, 0.0f, 0.0f, 0.0f); diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang index 7c38112..224cff0 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMPageMark.cs.slang @@ -1,8 +1,8 @@ #include "Include/Camera.inc.slang" #include "Shadows/SVSM.inc.slang" -// Marks the pages touched by visible depth samples, structurally CascadeXYReduce with a page -// table as the write target. Positions are reconstructed camera-relative (clipToView + rotate-only +// Marks the pages touched by visible depth samples: one thread per depth pixel reconstructs the +// world position and marks the page it samples. Positions are reconstructed camera-relative (clipToView + rotate-only // viewToWorld) and addressed through the anchor-relative window math from SVSM.inc. // // Samples within markBorderTexels of a page border also mark the neighbor page so the filter @@ -58,6 +58,8 @@ void main(CSInput input) uint2 dim; _depth.GetDimensions(dim.x, dim.y); + // Clamp instead of early return: the LDS mark-cache flush below needs every thread at the + // group barrier, and the duplicate edge marks dedup through the cache anyway uint2 pixel = min(input.dispatchThreadID.xy, dim - 1); float depth = _depth[pixel]; diff --git a/Source/Shaders/Shaders/Terrain/Culling.cs.slang b/Source/Shaders/Shaders/Terrain/Culling.cs.slang index d87b589..b06c2d1 100644 --- a/Source/Shaders/Shaders/Terrain/Culling.cs.slang +++ b/Source/Shaders/Shaders/Terrain/Culling.cs.slang @@ -11,12 +11,12 @@ struct Constants { uint viewportSizeX; uint viewportSizeY; - uint numCascades; + uint numShadowViews; uint occlusionCull; uint bitMaskBufferSizePerView; uint currentBitmaskIndex; uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off - uint cullMainView; // The late cascade dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice + uint cullMainView; // The late clipmap dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice }; struct HeightRange @@ -183,7 +183,7 @@ void main(CSInput input) } // Shadow clipmap views - for (uint i = 1; i < _constants.numCascades + 1; i++) + for (uint i = 1; i < _constants.numShadowViews + 1; i++) { drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow views... yet? diff --git a/Source/Shaders/Shaders/Utils/Culling.cs.slang b/Source/Shaders/Shaders/Utils/Culling.cs.slang index 343fb13..c94e69e 100644 --- a/Source/Shaders/Shaders/Utils/Culling.cs.slang +++ b/Source/Shaders/Shaders/Utils/Culling.cs.slang @@ -12,7 +12,7 @@ struct Constants uint viewportSizeX; uint viewportSizeY; uint maxDrawCount; - uint numCascades; + uint numShadowViews; uint occlusionCull; uint instanceIDOffset; uint modelIDOffset; @@ -323,7 +323,7 @@ void main(CSInput input) // Shadow is only supported if we USE_BITMASKS #if USE_BITMASKS // Shadow clipmap views - for (uint i = 1; i < _constants.numCascades + 1; i++) + for (uint i = 1; i < _constants.numShadowViews + 1; i++) { drawInput.shouldOcclusionCull = false; // No occlusion culling for shadow views... yet? diff --git a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang index 4ba3b8c..cf02eee 100644 --- a/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang +++ b/Source/Shaders/Shaders/Utils/CullingInstanced.cs.slang @@ -21,10 +21,10 @@ struct Constants uint cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... uint debugDrawColliders; uint currentBitmaskIndex; - uint numCascades; + uint numShadowViews; uint bitMaskBufferUintsPerView; uint debugDrawView; // viewIndex of the view to debug draw culling results for, 0xFFFFFFFF = off - uint cullMainView; // The late cascade dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice and the instance lookup + uint cullMainView; // The late clipmap dispatch must not re-cull view 0, it would overwrite the occlusion-culled bitmask slice and the instance lookup }; struct InstanceRef @@ -199,7 +199,7 @@ struct CullOutput #if USE_BITMASKS // Shadow clipmap views only need the visibility bitmask, the per-view draw sets are filled from it later in the geometry pass -void CullForCascade(DrawInput drawInput, Camera camera, uint bitmaskOffset, bool debugDrawResults) +void CullForClipmap(DrawInput drawInput, Camera camera, uint bitmaskOffset, bool debugDrawResults) { bool isVisible = drawInput.instanceCount > 0 && IsSphereInsideFrustum(camera, drawInput.sphere); @@ -419,10 +419,10 @@ void main(CSInput input) #if USE_BITMASKS // Shadow clipmap views - for (uint i = 1; i < _constants.numCascades + 1; i++) + for (uint i = 1; i < _constants.numShadowViews + 1; i++) { bool debugDrawResults = (_constants.debugDrawView == i); - CullForCascade(drawInput, _cameras[i], _constants.bitMaskBufferUintsPerView * i, debugDrawResults); + CullForClipmap(drawInput, _cameras[i], _constants.bitMaskBufferUintsPerView * i, debugDrawResults); } #endif } \ No newline at end of file From f699f388e4bfa6e4ba6944941e877f6615a7b663 Mon Sep 17 00:00:00 2001 From: Pursche Date: Mon, 20 Jul 2026 16:47:10 +0200 Subject: [PATCH 20/24] SVSM micro-perf sweep, frame-stable perf editor layout --- .../Editor/PerformanceDiagnostics.cpp | 49 ++++++-------- .../Game-Lib/Editor/PerformanceDiagnostics.h | 2 + .../Rendering/Model/ModelRenderer.cpp | 66 ++++++++++++++++--- .../Game-Lib/Rendering/Model/ModelRenderer.h | 19 +++++- .../Rendering/Shadow/ShadowRenderer.h | 2 +- .../Shaders/Include/Lighting.inc.slang | 21 +++--- .../Shaders/Shaders/Include/Shadows.inc.slang | 16 ++++- Source/Shaders/Shaders/Model/Draw.vs.slang | 18 ++--- .../Shaders/Shaders/Model/DrawSkybox.vs.slang | 18 ++--- .../Shaders/Model/DrawTransparent.vs.slang | 18 ++--- .../Shaders/Shadows/SVSMPageUpdateB.cs.slang | 4 ++ 11 files changed, 156 insertions(+), 77 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp index ee55d1b..d4e453f 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.cpp @@ -158,12 +158,10 @@ namespace Editor u32 invalidationCause = 0; shadowRenderer->GetSVSMGlobalStats(freePages, totalPages, overflow, invalidationCause); + // Every line renders unconditionally: one-frame events must never add or + // remove lines, a layout that jumps for a frame is unreadable ImGui::Spacing(); - ImGui::Text("SVSM Pool: %u / %u pages used", totalPages - glm::min(freePages, totalPages), totalPages); - if (overflow > 0) - { - ImGui::Text("SVSM Overflow: %u page requests dropped", overflow); - } + ImGui::Text("SVSM Pool: %u / %u pages used%s", totalPages - glm::min(freePages, totalPages), totalPages, overflow > 0 ? " (request overflow!)" : ""); u32 dynamicLive = 0; u32 dynamicTotal = 0; @@ -175,42 +173,37 @@ namespace Editor u32 transitionsOut = 0; u32 droppedAABBs = 0; shadowRenderer->GetSVSMCasterStats(dynamicCasters, transitionsIn, transitionsOut, droppedAABBs); - if (dynamicLive > 0 || dynamicOverflow > 0 || dynamicCasters > 0 || transitionsIn > 0 || transitionsOut > 0) - { - ImGui::Text("SVSM Dynamic: %u / %u pages live%s | %u casters (+%u / -%u)", dynamicLive, dynamicTotal, dynamicOverflow > 0 ? " (overflowing!)" : "", dynamicCasters, transitionsIn, transitionsOut); - if (droppedAABBs > 0) - { - ImGui::Text("SVSM Dynamic: %u casters spilled to static!", droppedAABBs); - } + ImGui::Text("SVSM Dynamic: %u / %u pages live%s | %u casters (+%u / -%u total) | %u spilled", dynamicLive, dynamicTotal, dynamicOverflow > 0 ? " (overflowing!)" : "", dynamicCasters, transitionsIn, transitionsOut, droppedAABBs); - // Per-clipmap surviving dynamic instances of the SVSM dynamic fills, one - // frame old. A bouncing count here = the fill/cull chain loses casters - if (ModelRenderer* svsmModelRenderer = gameRenderer->GetModelRenderer()) + // Per-clipmap surviving dynamic instances of the SVSM dynamic fills, one + // frame old. A bouncing count here = the fill/cull chain loses casters + if (ModelRenderer* svsmModelRenderer = gameRenderer->GetModelRenderer()) + { + std::string dynDraws = "SVSM Dyn instances:"; + for (u32 view = 1; view <= numClipmaps && view < Renderer::Settings::MAX_VIEWS; view++) { - std::string dynDraws = "SVSM Dyn instances:"; - for (u32 view = 1; view <= numClipmaps && view < Renderer::Settings::MAX_VIEWS; view++) - { - dynDraws += " " + std::to_string(svsmModelRenderer->GetNumSVSMDynamicInstances(view)); - } - ImGui::Text("%s", dynDraws.c_str()); + dynDraws += " " + std::to_string(svsmModelRenderer->GetNumSVSMDynamicInstances(view)); } + ImGui::Text("%s", dynDraws.c_str()); } i32 renderBudget = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmRenderBudget"_h); if (renderBudget > 0) { - ImGui::Text("SVSM Budget: %u requested / %d allowed", shadowRenderer->GetSVSMBudgetUsed(), renderBudget); + ImGui::Text("SVSM Budget: %u rendered / %d allowed", shadowRenderer->GetSVSMBudgetUsed(), renderBudget); } + // Invalidations happen on a single frame, latch the last cause so the line is + // stable and readable if (invalidationCause != 0) { - std::string causes; - if (invalidationCause & 1) causes += "sun step "; - if (invalidationCause & 2) causes += "z range "; - if (invalidationCause & 4) causes += "manual "; - if (invalidationCause & 8) causes += "aabb overflow "; - ImGui::Text("SVSM Invalidation: %s", causes.c_str()); + _svsmLastInvalidationCause.clear(); + if (invalidationCause & 1) _svsmLastInvalidationCause += "sun step "; + if (invalidationCause & 2) _svsmLastInvalidationCause += "z range "; + if (invalidationCause & 4) _svsmLastInvalidationCause += "manual "; + if (invalidationCause & 8) _svsmLastInvalidationCause += "aabb overflow "; } + ImGui::Text("SVSM Last Invalidation: %s", _svsmLastInvalidationCause.empty() ? "-" : _svsmLastInvalidationCause.c_str()); for (u32 i = 0; i < numClipmaps; i++) { diff --git a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h index 06091d3..d321842 100644 --- a/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h +++ b/Source/Game-Lib/Game-Lib/Editor/PerformanceDiagnostics.h @@ -52,6 +52,8 @@ namespace Editor private: bool _drawCallStatsOnlyForMainView = true; + std::string _svsmLastInvalidationCause; // Latched, invalidations only exist for one frame + bool _showSurvivingDrawCalls = true; bool _showSurvivingTriangle = true; bool _showFrameTime = true; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index b67bf99..673dd47 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -129,6 +129,10 @@ void ModelRenderer::Update(f32 deltaTime) }); } + CVarSystem* cvarSystem = CVarSystem::Get(); + const bool shadowsEnabled = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 1; + const bool split = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1; + // Dynamic shadow casters: one classifier for everything that moved or pushed bone matrices, // at instance granularity. Producers enqueue signals from any thread (the transform view // above, the instanced bone-push path, and the in-range placements of uninstanced animated @@ -140,17 +144,14 @@ void ModelRenderer::Update(f32 deltaTime) { ZoneScopedN("Dynamic Shadow Casters"); + const f32 range = shadowsEnabled ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; + _dynamicCasterTime += deltaTime; _dynamicCasterLiveIDs.clear(); _dynamicCasterAABBs.clear(); _dynamicCastersDropped = 0; - _dynamicCasterTransitionsIn = 0; - _dynamicCasterTransitionsOut = 0; - - CVarSystem* cvarSystem = CVarSystem::Get(); - const bool shadowsEnabled = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"_h) == 1; - const f32 range = shadowsEnabled ? static_cast(glm::max(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmAnimatedCasterRange"_h), 0.0)) : 0.0f; - const bool split = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmDynamicSplit"_h) == 1; + // Transition counters accumulate (reset on Clear) — per-frame events are unreadable in + // the perf editor and made its layout jump // Brief animation pauses don't churn the static cache constexpr f32 dynamicGraceSeconds = 0.5f; @@ -184,7 +185,7 @@ void ModelRenderer::Update(f32 deltaTime) u32 modelID; while (_uninstancedAnimatedModelQueue.try_dequeue(modelID)) { - _animatedModelLastPushTime[modelID] = _dynamicCasterTime; + _animatedModelLastPushTime[modelID].lastPushTime = _dynamicCasterTime; } vec3 cameraPos = vec3(0.0f); @@ -204,9 +205,15 @@ void ModelRenderer::Update(f32 deltaTime) const f32 leaveDist = range * 1.15f; // Hysteresis against camera jitter at the boundary const f32 leaveDistSq = leaveDist * leaveDist; + // Rescan slack: a placement beyond leaveDist at scan time cannot reach the enter + // radius until the camera has moved at least the hysteresis margin + const f32 rescanDist = leaveDist - range; + const f32 rescanDistSq = rescanDist * rescanDist; + for (auto it = _animatedModelLastPushTime.begin(); it != _animatedModelLastPushTime.end();) { - if (_dynamicCasterTime - it->second > dynamicGraceSeconds) + AnimatedModelState& state = it->second; + if (_dynamicCasterTime - state.lastPushTime > dynamicGraceSeconds) { it = _animatedModelLastPushTime.erase(it); continue; @@ -214,7 +221,29 @@ void ModelRenderer::Update(f32 deltaTime) u32 animatedModelID = it->first; std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]); - for (u32 placementID : _modelManifests[animatedModelID].instances) + ModelManifest& manifest = _modelManifests[animatedModelID]; + + // The full placement scan only runs when the cached near-camera subset can be + // stale: first scan, camera moved past the hysteresis slack, or placements changed + vec3 toScanCamera = state.scanCameraPos - cameraPos; + if (!state.scanned || glm::dot(toScanCamera, toScanCamera) > rescanDistSq || manifest.instanceSetDirty) + { + state.scanned = true; + state.scanCameraPos = cameraPos; + state.nearInstances.clear(); + manifest.instanceSetDirty = false; + + for (u32 placementID : manifest.instances) + { + vec3 toCamera = vec3(_instanceMatrices[placementID][3]) - cameraPos; + if (glm::dot(toCamera, toCamera) < leaveDistSq) + { + state.nearInstances.push_back(placementID); + } + } + } + + for (u32 placementID : state.nearInstances) { vec3 toCamera = vec3(_instanceMatrices[placementID][3]) - cameraPos; f32 distSq = glm::dot(toCamera, toCamera); @@ -282,6 +311,8 @@ void ModelRenderer::Update(f32 deltaTime) } } + // Diagnostics only, nothing to read back while the dynamic draws don't run + if (shadowsEnabled && split) { ZoneScopedN("SVSM Dynamic Draw Count ReadBack"); @@ -291,6 +322,13 @@ void ModelRenderer::Update(f32 deltaTime) memcpy(_numSvsmDynamicInstances, counts, sizeof(u32) * Renderer::Settings::MAX_VIEWS); } _renderer->UnmapBuffer(_svsmDynamicDrawCountReadBackBuffer); + + _numSvsmDynamicInstancesZeroed = false; + } + else if (!_numSvsmDynamicInstancesZeroed) + { + memset(_numSvsmDynamicInstances, 0, sizeof(_numSvsmDynamicInstances)); + _numSvsmDynamicInstancesZeroed = true; } { @@ -573,6 +611,8 @@ void ModelRenderer::Clear() _dynamicCasterLastSignal.clear(); _dynamicCasterLiveIDs.clear(); _dynamicCasterAABBs.clear(); + _dynamicCasterTransitionsIn = 0; + _dynamicCasterTransitionsOut = 0; // Zero the mask bits set last frame BEFORE the instance capacity shrinks under the word // count — recycled IDs on the next map must not start masked out of the static pages. Sized @@ -2102,6 +2142,7 @@ u32 ModelRenderer::AddInstance(entt::entity entityID, u32 modelID, Model::Comple { std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]); manifest.instances.insert(instanceOffsets.instanceIndex); + manifest.instanceSetDirty = true; } // Set up InstanceManifest @@ -2150,6 +2191,7 @@ void ModelRenderer::RemoveInstance(u32 instanceID) std::scoped_lock lock(*_modelManifestsInstancesMutexes[instanceData.modelID]); manifest.instances.erase(instanceID); manifest.skyboxInstances.erase(instanceID); + manifest.instanceSetDirty = true; } std::scoped_lock lock(_instanceOffsetsMutex); @@ -2191,6 +2233,7 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo std::scoped_lock lock(*_modelManifestsInstancesMutexes[oldModelID]); oldManifest.instances.erase(instanceID); oldManifest.skyboxInstances.erase(instanceID); + oldManifest.instanceSetDirty = true; } // Deallocate old animation data @@ -2205,6 +2248,7 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo { std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]); newManifest.instances.insert(instanceID); + newManifest.instanceSetDirty = true; } // Modify InstanceData @@ -3647,6 +3691,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc // Remove the non skybox instance modelManifest.instances.erase(instanceID); + modelManifest.instanceSetDirty = true; } else { @@ -3655,6 +3700,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc // Add the non skybox instance modelManifest.instances.insert(instanceID); + modelManifest.instanceSetDirty = true; } } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index 969f73a..247bf44 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -109,6 +109,8 @@ class ModelRenderer : CulledRenderer robin_hood::unordered_set instances; robin_hood::unordered_set skyboxInstances; robin_hood::unordered_set originallyTransparentDrawIDs; + + bool instanceSetDirty = false; // Set under the instances mutex on add/remove, consumed by the animated-caster scan cache }; struct InstanceManifest @@ -490,7 +492,7 @@ class ModelRenderer : CulledRenderer std::vector _dynamicCasterAABBs; // World (min, max) pairs of the live set, rebuilt each frame f32 _dynamicCasterTime = 0.0f; u32 _dynamicCastersDropped = 0; // Spilled to static this frame (cap or oversize), never dropped from both pools - u32 _dynamicCasterTransitionsIn = 0; + u32 _dynamicCasterTransitionsIn = 0; // Running totals since map load, per-frame ticks are unreadable in the perf editor u32 _dynamicCasterTransitionsOut = 0; // Spawned/despawned/re-modeled instances must invalidate the cached static shadow pages under @@ -506,12 +508,23 @@ class ModelRenderer : CulledRenderer // for diagnosing missing dynamic shadow draws Renderer::BufferID _svsmDynamicDrawCountReadBackBuffer; u32 _numSvsmDynamicInstances[Renderer::Settings::MAX_VIEWS] = { 0 }; + bool _numSvsmDynamicInstancesZeroed = false; // One-shot zero of the stats when the readback is gated off // Animated doodads (windmills, flags, swaying vegetation) share one uninstanced bone stream // per model; the queue signals which models advanced their animation this frame so Update can - // promote their placements within svsmAnimatedCasterRange to per-instance dynamic signals + // promote their placements within svsmAnimatedCasterRange to per-instance dynamic signals. + // The full placement scan is expensive (large vegetation models have tens of thousands of + // placements), so each in-grace model caches its near-camera subset and rescans only when the + // camera moves past the hysteresis slack or the placement set changes + struct AnimatedModelState + { + f32 lastPushTime = 0.0f; // Accumulated seconds of the last bone push + vec3 scanCameraPos = vec3(0.0f); // Camera position the cached subset was scanned from + bool scanned = false; + std::vector nearInstances; // Placements within leaveDist at scan time + }; moodycamel::ConcurrentQueue _uninstancedAnimatedModelQueue; - robin_hood::unordered_map _animatedModelLastPushTime; // modelID -> last accumulated-seconds it pushed bones + robin_hood::unordered_map _animatedModelLastPushTime; // Keyed by modelID // GPU-only workbuffers Renderer::BufferID _occluderArgumentBuffer; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 2899cd7..d2ec681 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -170,7 +170,7 @@ class ShadowRenderer std::vector _svsmDynamicAABBs; // This frame's dynamic caster (min, max) pairs bool _svsmDirtyAABBOverflow = false; u32 _svsmNumDynamicCasters = 0; // Classifier live set: moved or bone-pushed within the grace window - u32 _svsmCasterTransitionsIn = 0; // Classifier enter/leave this frame, each re-bakes static pages + u32 _svsmCasterTransitionsIn = 0; // Classifier enter/leave running totals, each re-bakes static pages u32 _svsmCasterTransitionsOut = 0; u32 _svsmDynamicAABBsDropped = 0; // Spilled to the static path this frame (cap or oversize) bool _svsmForceInvalidateAll = false; // Set when invalidations were discarded while SVSM was inactive diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index 46aaa4e..35114e3 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -34,6 +34,18 @@ float3 ApplyLighting(float3 materialColor, PixelVertexData pixelVertexData, uint float ambientOcclusion = _ambientOcclusion.Load(int3(pixelVertexData.pixelPos, 0)).r; + // Directional Light Shadows: clipmap selection, page walk and far fade all live in + // GetSVSMShadowFactor. Light-independent (the sun direction comes from the SVSM state, not + // the light), so it samples once outside the loop — it is the most expensive part of the pass + float shadowFactor = 1.0f; + if (shadowSettings.enableShadows && numDirectionalLights > 0) + { + shadowFactor = GetSVSMShadowFactor(pixelVertexData.worldPos, pixelVertexData.worldNormal, shadowSettings); + + // Sun elevation strength, fades shadows out around dawn/dusk + shadowFactor = lerp(1.0f, shadowFactor, shadowSettings.strength); + } + for (uint i = 0; i < numDirectionalLights; i++) { DirectionalLight light = LoadDirectionalLight(i); @@ -51,16 +63,9 @@ float3 ApplyLighting(float3 materialColor, PixelVertexData pixelVertexData, uint float nDotL = saturate(dot(pixelVertexData.worldNormal, -light.direction.xyz)); // Dot product between normal and light direction float3 lightColor = lerp(light.color.rgb, float3(1.0f, 1.0f, 1.0f), nDotL); // Light color based on normal lightColor *= light.color.a; // Intensity in alpha channel - - // Directional Light Shadows: clipmap selection, page walk and far fade all live in - // GetSVSMShadowFactor + if (shadowSettings.enableShadows) { - float shadowFactor = GetSVSMShadowFactor(pixelVertexData.worldPos, pixelVertexData.worldNormal, shadowSettings); - - // Sun elevation strength, fades shadows out around dawn/dusk - shadowFactor = lerp(1.0f, shadowFactor, shadowSettings.strength); - lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); } diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index 159bb7c..f34093d 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -61,6 +61,18 @@ float SVSMLoadDynamicDepth(uint clipmapIndex, int2 virtualTexel, uint pageTableS return asfloat(_svsmDynamicPagePool[physicalBase + pageTexel]); } +// Residency alone: one page-table read, no pool load — for probes that only need to know whether +// a dynamic page exists at the texel +bool SVSMIsDynamicPageResident(uint clipmapIndex, int2 virtualTexel, uint pageTableSize, uint pageSize) +{ + int2 localPage = virtualTexel / int(pageSize); + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 slot = WrapPageSlot(anchorPageMin + localPage, pageTableSize); + + uint entry = _svsmDynamicPageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + return (entry & SVSM_PAGE_RESIDENT) != 0; +} + // Software percentage-closer filtering over the sparse clipmaps: select the finest covering // clipmap, walk to coarser ones when a touched page is not resident, 3x3 bilinear-weighted taps // over a shared 4x4 texel block. Returns 1 (lit) when nothing is resident, the marking pass @@ -235,9 +247,7 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh // Center-texel residency probe: DynamicMark pads the caster footprint by a full page, // far wider than the filter, so a non-resident center means no dynamic caster here - bool centerResident; - SVSMLoadDynamicDepth(kd, int2(floor(texelPos)), pageTableSize, pageSize, centerResident); - if (!centerResident) + if (!SVSMIsDynamicPageResident(kd, int2(floor(texelPos)), pageTableSize, pageSize)) { continue; } diff --git a/Source/Shaders/Shaders/Model/Draw.vs.slang b/Source/Shaders/Shaders/Model/Draw.vs.slang index c29df9f..0246f65 100644 --- a/Source/Shaders/Shaders/Model/Draw.vs.slang +++ b/Source/Shaders/Shaders/Model/Draw.vs.slang @@ -58,14 +58,16 @@ VSOutput main(VSInput input) float4x4 boneTransformMatrix = CalcBoneTransformMatrix(_instanceBoneMatrices, instanceData, vertex); float4 position = mul(float4(vertex.position, 1.0f), boneTransformMatrix); - // Save the skinned vertex position (in model-space) if this vertex was animated - if (instanceData.boneMatrixOffset != 4294967295) - { - uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model - uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance - - StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); - } + // TODO: Separate skinning compute shader — the cache below is currently dead (its consumer + // left with the visibility-buffer barycentrics; the material pass re-skins analytically) + //// Save the skinned vertex position (in model-space) if this vertex was animated + //if (instanceData.boneMatrixOffset != 4294967295) + //{ + // uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model + // uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance + // + // StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); + //} float4x4 instanceMatrix = _modelInstanceMatrices[instanceID]; position = mul(position, instanceMatrix); diff --git a/Source/Shaders/Shaders/Model/DrawSkybox.vs.slang b/Source/Shaders/Shaders/Model/DrawSkybox.vs.slang index 830c8f6..c1bd405 100644 --- a/Source/Shaders/Shaders/Model/DrawSkybox.vs.slang +++ b/Source/Shaders/Shaders/Model/DrawSkybox.vs.slang @@ -37,14 +37,16 @@ VSOutput main(VSInput input) float4x4 boneTransformMatrix = CalcBoneTransformMatrix(_instanceBoneMatrices, instanceData, vertex); float4 position = mul(float4(vertex.position, 1.0f), boneTransformMatrix); - // Save the skinned vertex position (in model-space) if this vertex was animated - if (instanceData.boneMatrixOffset != 4294967295) - { - uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model - uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance - - StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); - } + // TODO: Separate skinning compute shader — the cache below is currently dead (its consumer + // left with the visibility-buffer barycentrics; the material pass re-skins analytically) + //// Save the skinned vertex position (in model-space) if this vertex was animated + //if (instanceData.boneMatrixOffset != 4294967295) + //{ + // uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model + // uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance + // + // StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); + //} float4x4 instanceMatrix = _modelInstanceMatrices[instanceID]; position = mul(position, instanceMatrix); diff --git a/Source/Shaders/Shaders/Model/DrawTransparent.vs.slang b/Source/Shaders/Shaders/Model/DrawTransparent.vs.slang index 75fb90d..b0c08c0 100644 --- a/Source/Shaders/Shaders/Model/DrawTransparent.vs.slang +++ b/Source/Shaders/Shaders/Model/DrawTransparent.vs.slang @@ -39,14 +39,16 @@ VSOutput main(VSInput input) float4x4 boneTransformMatrix = CalcBoneTransformMatrix(_instanceBoneMatrices, instanceData, vertex); float4 position = mul(float4(vertex.position, 1.0f), boneTransformMatrix); - // Save the skinned vertex position (in model-space) if this vertex was animated - if (instanceData.boneMatrixOffset != 4294967295) - { - uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model - uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance - - StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); - } + // TODO: Separate skinning compute shader — the cache below is currently dead (its consumer + // left with the visibility-buffer barycentrics; the material pass re-skins analytically) + //// Save the skinned vertex position (in model-space) if this vertex was animated + //if (instanceData.boneMatrixOffset != 4294967295) + //{ + // uint localVertexID = input.vertexID - instanceData.modelVertexOffset; // This gets the local vertex ID relative to the model + // uint animatedVertexID = localVertexID + instanceData.animatedVertexOffset; // This makes it relative to the animated instance + // + // StoreAnimatedVertexPosition(_animatedModelVertexPositions, animatedVertexID, position.xyz); + //} float4x4 instanceMatrix = _modelInstanceMatrices[instanceID]; position = mul(position, instanceMatrix); diff --git a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang index c482249..3228b1d 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMPageUpdateB.cs.slang @@ -88,6 +88,10 @@ void main(CSInput input) { InterlockedAdd(gDeferredCount, 1); deferred = true; + + // Give the claim back so the stat reads pages actually rendered, not attempts. + // Deferral decisions are unaffected, each uses its own pre-add value + InterlockedAdd(_svsmData[0].statsBudgetUsed, uint(-1)); } } From 256510eca55e8eda1afdf565b7fa635d34e77f1f Mon Sep 17 00:00:00 2001 From: Pursche Date: Mon, 20 Jul 2026 20:31:42 +0200 Subject: [PATCH 21/24] Land the SVSM perf-round defaults, data-layout tripwire and shared fill/cull/PCF helpers --- .../Game-Lib/Rendering/CulledRenderer.cpp | 557 +++++++----------- .../Game-Lib/Rendering/CulledRenderer.h | 23 + .../Game-Lib/Rendering/GameRenderer.cpp | 16 +- .../Rendering/Model/ModelRenderer.cpp | 16 +- .../Game-Lib/Rendering/RenderUtils.cpp | 16 + .../Game-Lib/Game-Lib/Rendering/RenderUtils.h | 47 ++ .../Rendering/Shadow/ShadowRenderer.cpp | 222 +++++-- .../Rendering/Shadow/ShadowRenderer.h | 29 +- .../Rendering/Terrain/TerrainRenderer.cpp | 148 ++--- .../Rendering/Terrain/TerrainRenderer.h | 5 + .../Shaders/Shaders/Include/Shadows.inc.slang | 222 +++---- Source/Shaders/Shaders/Model/Draw.ps.slang | 43 +- .../Shaders/Shaders/Model/DrawSVSM.ps.slang | 48 +- .../Shaders/Model/ModelAlphaKey.inc.slang | 43 ++ Source/Shaders/Shaders/Shadows/SVSM.inc.slang | 60 +- .../Shaders/Shadows/SVSMDynamicMark.cs.slang | 17 +- .../Shaders/Shadows/SVSMFinalize.cs.slang | 16 +- .../Shadows/SVSMInvalidateAABBs.cs.slang | 20 +- .../Utils/CreateIndirectAfterCulling.cs.slang | 7 +- 19 files changed, 790 insertions(+), 765 deletions(-) create mode 100644 Source/Shaders/Shaders/Model/ModelAlphaKey.inc.slang diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index d1e5212..b7d9444 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -3,11 +3,10 @@ #include "Game-Lib/Rendering/Debug/DebugRenderer.h" #include "Game-Lib/Rendering/GameRenderer.h" #include "Game-Lib/Rendering/RenderUtils.h" -#include "Game-Lib/Rendering/Shadow/ShadowRenderer.h" #include -AutoCVar_Int CVAR_ShadowDebugCullingView(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView", "debug draw culling results for a view as AABBs (green = kept, red = culled), 0 = main view, 1..N = cascades, -1 = off", -1); +AutoCVar_Int CVAR_ShadowDebugCullingView(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView", "debug draw culling results for a view as AABBs (green = kept, red = culled), 0 = main view, 1..N = clipmaps, -1 = off", -1); bool CulledRenderer::_pipelinesCreated = false; Renderer::ComputePipelineID CulledRenderer::_fillInstancedDrawCallsFromBitmaskPipeline[2]; // [0] = non-indexed, [1] = indexed @@ -73,6 +72,104 @@ void CulledRenderer::Clear() } +// One thread per instance against a view's bitmask slice, appending survivors to the shared +// per-drawcall counts/lookup buffers. An indirect args buffer routes the dispatch through +// Finalize-written per-view group counts instead of covering every instance +void CulledRenderer::DispatchInstancedFill(PassParams& params, const std::string& markerName, Renderer::DescriptorSetResource& fillSet, bool filtered, u32 currentBitmaskIndex, u32 bitmaskOffset, bool keepDynamic, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset) +{ + const u32 numInstances = params.cullingResources->GetNumInstances(); + + params.commandList->PushMarker(params.passName + " " + markerName, Color::White); + + Renderer::ComputePipelineID pipeline = filtered ? _fillInstancedDrawCallsFilteredPipeline[params.cullingResources->IsIndexed()] + : _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct FillDrawCallConstants + { + u32 numTotalInstances; + u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored + u32 drawCallDataSize; + u32 currentBitmaskIndex; + u32 bitmaskOffset; + u32 keepDynamic; + }; + + FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); + fillConstants->numTotalInstances = numInstances; + fillConstants->baseInstanceLookupOffset = baseInstanceLookupOffset; + fillConstants->drawCallDataSize = drawCallDataSize; + fillConstants->currentBitmaskIndex = currentBitmaskIndex; + fillConstants->bitmaskOffset = bitmaskOffset; + fillConstants->keepDynamic = keepDynamic; + params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); + + params.commandList->BindDescriptorSet(fillSet, params.frameIndex); + + if (indirectArgsBuffer != Renderer::BufferResource::Invalid()) + { + // Finalize zeroed the group count for rings with no page work this frame + params.commandList->DispatchIndirect(indirectArgsBuffer, indirectArgsByteOffset); + } + else + { + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + } + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); +} + +// Per-drawcall instance counts become indirect draw args, each count cleared after consumption +// (the counts buffer is always zero between uses). An indirect args buffer applies the same +// Finalize gate as the fill that produced the counts +void CulledRenderer::DispatchCreateIndirect(PassParams& params, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset) +{ + const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); + const bool debugOrdered = false; // Single-group ordered variant for determinism debugging + + params.commandList->PushMarker(params.passName + " Create Indirect", Color::Yellow); + + Renderer::ComputePipelineID pipeline = debugOrdered ? _createIndirectAfterCullingOrderedPipeline[params.cullingResources->IsIndexed()] + : _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; + params.commandList->BeginPipeline(pipeline); + + struct CreateIndirectConstants + { + u32 numTotalDrawCalls; + u32 baseInstanceLookupOffset; + u32 drawCallDataSize; + }; + CreateIndirectConstants* createIndirectConstants = params.graphResources->FrameNew(); + createIndirectConstants->numTotalDrawCalls = numDrawCalls; + createIndirectConstants->baseInstanceLookupOffset = baseInstanceLookupOffset; + createIndirectConstants->drawCallDataSize = drawCallDataSize; + params.commandList->PushConstant(createIndirectConstants, 0, sizeof(CreateIndirectConstants)); + + if (debugSet != nullptr) + { + params.commandList->BindDescriptorSet(*debugSet, params.frameIndex); + } + params.commandList->BindDescriptorSet(createIndirectSet, params.frameIndex); + + if (indirectArgsBuffer != Renderer::BufferResource::Invalid()) + { + // Zero groups for views with no page work this frame, same gate as the fill + params.commandList->DispatchIndirect(indirectArgsBuffer, indirectArgsByteOffset); + } + else if (debugOrdered) + { + params.commandList->Dispatch(1, 1, 1); + } + else + { + params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); + } + + params.commandList->EndPipeline(pipeline); + params.commandList->PopMarker(); +} + void CulledRenderer::OccluderPass(OccluderPassParams& params) { NC_ASSERT(params.drawCallback != nullptr, "CulledRenderer : OccluderPass got params with invalid drawCallback"); @@ -94,8 +191,6 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) if (params.cullingResources->IsInstanced()) { - const bool debugOrdered = false; - params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); // CreateIndirect accumulates the surviving counts, reset or the readback stats carry over from last frame params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); @@ -110,92 +205,14 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) params.commandList->BufferBarrier(params.culledDrawCallsBitMaskBuffer, Renderer::BufferPassUsage::TRANSFER); } - // Fill the occluders to draw - { - std::string debugName = params.passName + " Instanced Occlusion Fill"; - params.commandList->PushMarker(debugName, Color::White); - - Renderer::ComputePipelineID pipeline = _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); - - struct FillDrawCallConstants - { - u32 numTotalInstances; - u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored - u32 drawCallDataSize; - u32 currentBitmaskIndex; - u32 bitmaskOffset; - }; - - FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); - fillConstants->numTotalInstances = numInstances; - fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - fillConstants->drawCallDataSize = params.drawCallDataSize; - fillConstants->currentBitmaskIndex = !params.frameIndex; // Occluders consume last frame's culling output - fillConstants->bitmaskOffset = 0; // Occluders only draw the main view - - params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); - - // Bind descriptorset - params.commandList->BindDescriptorSet(params.occluderFillDescriptorSet, params.frameIndex); - - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); - - params.commandList->EndPipeline(pipeline); - - params.commandList->PopMarker(); - } + // Fill the occluders to draw: last frame's main-view culling output + DispatchInstancedFill(params, "Instanced Occlusion Fill", params.occluderFillDescriptorSet, false, !params.frameIndex, 0, false, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Create indirect argument buffer - { - std::string debugName = params.passName + " Create Indirect"; - params.commandList->PushMarker(debugName, Color::Yellow); - - Renderer::ComputePipelineDesc cullingPipelineDesc; - cullingPipelineDesc.debugName = debugName; - params.graphResources->InitializePipelineDesc(cullingPipelineDesc); - - Renderer::ComputePipelineID pipeline; - if (debugOrdered) - { - pipeline = _createIndirectAfterCullingOrderedPipeline[params.cullingResources->IsIndexed()]; - } - else - { - pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; - } - params.commandList->BeginPipeline(pipeline); - - struct CullConstants - { - u32 numTotalDrawCalls; - u32 baseInstanceLookupOffset; - u32 drawCallDataSize; - }; - CullConstants* cullConstants = params.graphResources->FrameNew(); - - cullConstants->numTotalDrawCalls = numDrawCalls; - cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - cullConstants->drawCallDataSize = params.drawCallDataSize; - params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); - - params.commandList->BindDescriptorSet(params.createIndirectDescriptorSet, params.frameIndex); - - if (debugOrdered) - { - params.commandList->Dispatch(1, 1, 1); - } - else - { - params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); - } - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } + DispatchCreateIndirect(params, params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); @@ -320,6 +337,75 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) } } +// The instanced cull dispatch shared by CullingPass and ClipmapCullingPass: one thread per +// instance against the per-view frustums, writing bitmask slices and instance counts. +// occlusionCull/cullMainView/debugDrawColliders come from params; the clipmap pass overrides +// them before calling +void CulledRenderer::RunInstancedCullingDispatch(CullingPassParams& params, bool useBitmasks, bool bindDepthPyramid) +{ + const u32 numInstances = params.cullingResources->GetNumInstances(); + + Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[useBitmasks]; + params.commandList->BeginPipeline(pipeline); + + vec2 viewportSize = _renderer->GetRenderSize(); + + struct CullConstants + { + u32 viewportSizeX; + u32 viewportSizeY; + u32 numTotalInstances; + u32 occlusionCull; + u32 instanceCountOffset; // Byte offset into drawCalls where the instanceCount is stored + u32 drawCallSize; + u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored + u32 modelIDOffset; // Byte offset into drawCallDatas where the modelID is stored + u32 drawCallDataSize; + u32 cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... + u32 debugDrawColliders; + u32 currentBitmaskIndex; + u32 numShadowViews; + u32 bitMaskBufferUintsPerView; + u32 debugDrawView; + u32 cullMainView; + }; + CullConstants* cullConstants = params.graphResources->FrameNew(); + cullConstants->viewportSizeX = u32(viewportSize.x); + cullConstants->viewportSizeY = u32(viewportSize.y); + cullConstants->numTotalInstances = numInstances; + cullConstants->occlusionCull = params.occlusionCull; + + u32 instanceCountOffset = params.cullingResources->IsIndexed() ? offsetof(Renderer::IndexedIndirectDraw, Renderer::IndexedIndirectDraw::instanceCount) : offsetof(Renderer::IndirectDraw, Renderer::IndirectDraw::instanceCount); + cullConstants->instanceCountOffset = instanceCountOffset; + cullConstants->drawCallSize = params.cullingResources->IsIndexed() ? sizeof(Renderer::IndexedIndirectDraw) : sizeof(Renderer::IndirectDraw); + + cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; + cullConstants->modelIDOffset = params.modelIDOffset; + cullConstants->drawCallDataSize = params.drawCallDataSize; + + cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; + cullConstants->debugDrawColliders = params.debugDrawColliders; + cullConstants->currentBitmaskIndex = params.frameIndex; + cullConstants->numShadowViews = params.numShadowViews; + cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); + cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); + cullConstants->cullMainView = params.cullMainView; + params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); + + if (bindDepthPyramid) + { + params.cullingDescriptorSet.Bind("_depthPyramid"_h, params.depthPyramid); + } + + params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex); + params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); + params.commandList->BindDescriptorSet(params.cullingDescriptorSet, params.frameIndex); + + params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + + params.commandList->EndPipeline(pipeline); +} + void CulledRenderer::CullingPass(CullingPassParams& params) { NC_ASSERT(params.drawCallDataSize > 0, "CulledRenderer : CullingPass params provided an invalid drawCallDataSize"); @@ -331,8 +417,6 @@ void CulledRenderer::CullingPass(CullingPassParams& params) { if (params.cullingResources->IsInstanced()) { - const bool debugOrdered = false; - params.commandList->PushMarker(params.passName + " Culling", Color::Yellow); // Reset the counters @@ -351,112 +435,15 @@ void CulledRenderer::CullingPass(CullingPassParams& params) std::string debugName = params.passName + " Instanced Culling"; params.commandList->PushMarker(debugName, Color::Yellow); - Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[!params.disableTwoStepCulling]; - params.commandList->BeginPipeline(pipeline); - - vec2 viewportSize = _renderer->GetRenderSize(); - - struct CullConstants - { - u32 viewportSizeX; - u32 viewportSizeY; - u32 numTotalInstances; - u32 occlusionCull; - u32 instanceCountOffset; // Byte offset into drawCalls where the instanceCount is stored - u32 drawCallSize; - u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored - u32 modelIDOffset; // Byte offset into drawCallDatas where the modelID is stored - u32 drawCallDataSize; - u32 cullingDataIsWorldspace; // TODO: This controls two things, are both needed? I feel like one counters the other but I'm not sure... - u32 debugDrawColliders; - u32 currentBitmaskIndex; - u32 numShadowViews; - u32 bitMaskBufferUintsPerView; - u32 debugDrawView; - u32 cullMainView; - }; - CullConstants* cullConstants = params.graphResources->FrameNew(); - cullConstants->viewportSizeX = u32(viewportSize.x); - cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numTotalInstances = numInstances; - cullConstants->occlusionCull = params.occlusionCull; - - u32 instanceCountOffset = params.cullingResources->IsIndexed() ? offsetof(Renderer::IndexedIndirectDraw, Renderer::IndexedIndirectDraw::instanceCount) : offsetof(Renderer::IndirectDraw, Renderer::IndirectDraw::instanceCount); - cullConstants->instanceCountOffset = instanceCountOffset; - cullConstants->drawCallSize = params.cullingResources->IsIndexed() ? sizeof(Renderer::IndexedIndirectDraw) : sizeof(Renderer::IndirectDraw); - - cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - cullConstants->modelIDOffset = params.modelIDOffset; - cullConstants->drawCallDataSize = params.drawCallDataSize; - - cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; - cullConstants->debugDrawColliders = params.debugDrawColliders; - cullConstants->currentBitmaskIndex = params.frameIndex; - cullConstants->numShadowViews = params.numShadowViews; - cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); - cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); - cullConstants->cullMainView = params.cullMainView; - params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); - - params.cullingDescriptorSet.Bind("_depthPyramid"_h, params.depthPyramid); - - params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.cullingDescriptorSet, params.frameIndex); - - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + RunInstancedCullingDispatch(params, !params.disableTwoStepCulling, true); - params.commandList->EndPipeline(pipeline); params.commandList->PopMarker(); } params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); // Create indirect argument buffer - { - std::string debugName = params.passName + " Create Indirect"; - params.commandList->PushMarker(debugName, Color::Yellow); - - - Renderer::ComputePipelineID pipeline; - if (debugOrdered) - { - pipeline = _createIndirectAfterCullingOrderedPipeline[params.cullingResources->IsIndexed()]; - } - else - { - pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; - } - params.commandList->BeginPipeline(pipeline); - - struct CullConstants - { - u32 numTotalDrawCalls; - u32 baseInstanceLookupOffset; - u32 drawCallDataSize; - }; - CullConstants* cullConstants = params.graphResources->FrameNew(); - - cullConstants->numTotalDrawCalls = numDrawCalls; - cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - cullConstants->drawCallDataSize = params.drawCallDataSize; - params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); - - params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.createIndirectAfterCullSet, params.frameIndex); - - if (debugOrdered) - { - params.commandList->Dispatch(1, 1, 1); - } - else - { - params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); - } - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } + DispatchCreateIndirect(params, params.createIndirectAfterCullSet, ¶ms.debugDescriptorSet, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->PopMarker(); } @@ -554,67 +541,19 @@ void CulledRenderer::ClipmapCullingPass(CullingPassParams& params) if (numDrawCalls == 0 || numInstances == 0 || params.numShadowViews == 0) return; - // Frustum-only cull of the cascade views into their bitmask slices. No counter resets (the - // per-cascade fill in the geometry pass resets and rebuilds the shared draw sets), no occlusion + // Frustum-only cull of the clipmap views into their bitmask slices. No counter resets (the + // per-clipmap fill in the geometry pass resets and rebuilds the shared draw sets), no occlusion std::string debugName = params.passName + " Clipmap Culling"; params.commandList->PushMarker(debugName, Color::Yellow); - Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[1]; // Cascades require the bitmask permutation - params.commandList->BeginPipeline(pipeline); - - vec2 viewportSize = _renderer->GetRenderSize(); - - struct CullConstants - { - u32 viewportSizeX; - u32 viewportSizeY; - u32 numTotalInstances; - u32 occlusionCull; - u32 instanceCountOffset; - u32 drawCallSize; - u32 baseInstanceLookupOffset; - u32 modelIDOffset; - u32 drawCallDataSize; - u32 cullingDataIsWorldspace; - u32 debugDrawColliders; - u32 currentBitmaskIndex; - u32 numShadowViews; - u32 bitMaskBufferUintsPerView; - u32 debugDrawView; - u32 cullMainView; - }; - CullConstants* cullConstants = params.graphResources->FrameNew(); - cullConstants->viewportSizeX = u32(viewportSize.x); - cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numTotalInstances = numInstances; - cullConstants->occlusionCull = false; - - u32 instanceCountOffset = params.cullingResources->IsIndexed() ? offsetof(Renderer::IndexedIndirectDraw, Renderer::IndexedIndirectDraw::instanceCount) : offsetof(Renderer::IndirectDraw, Renderer::IndirectDraw::instanceCount); - cullConstants->instanceCountOffset = instanceCountOffset; - cullConstants->drawCallSize = params.cullingResources->IsIndexed() ? sizeof(Renderer::IndexedIndirectDraw) : sizeof(Renderer::IndirectDraw); - - cullConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - cullConstants->modelIDOffset = params.modelIDOffset; - cullConstants->drawCallDataSize = params.drawCallDataSize; - - cullConstants->cullingDataIsWorldspace = params.cullingDataIsWorldspace; - cullConstants->debugDrawColliders = false; - cullConstants->currentBitmaskIndex = params.frameIndex; - cullConstants->numShadowViews = params.numShadowViews; - cullConstants->bitMaskBufferUintsPerView = params.cullingResources->GetBitMaskBufferUintsPerView(); - cullConstants->debugDrawView = static_cast(CVAR_ShadowDebugCullingView.Get()); - cullConstants->cullMainView = false; - params.commandList->PushConstant(cullConstants, 0, sizeof(CullConstants)); - - // _depthPyramid stays bound from the main culling pass, rebinding here would rewrite an already-bound set - - params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex); - params.commandList->BindDescriptorSet(params.cullingDescriptorSet, params.frameIndex); + params.occlusionCull = false; + params.cullMainView = false; + params.debugDrawColliders = false; - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); + // Clipmaps require the bitmask permutation; _depthPyramid stays bound from the main culling + // pass, rebinding here would rewrite an already-bound set + RunInstancedCullingDispatch(params, true, false); - params.commandList->EndPipeline(pipeline); params.commandList->PopMarker(); } @@ -623,97 +562,43 @@ void CulledRenderer::ClipmapCullingPass(CullingPassParams& params) void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic) { const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); - const u32 numInstances = params.cullingResources->GetNumInstances(); - // Reset the counters and per-drawcall instance counts - params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + // SVSM fills gate their drawcall-granularity setup (instance-count clear + CreateIndirect) + // on the Finalize-written per-view args; the counts buffer stays always-zero between uses + // because CreateIndirect clears after consumption. The args byte layout comes through params + // (the ShadowRenderer's stride/offset constants) + const bool gatedSetup = params.svsmPass && params.svsmFillArgsBuffer != Renderer::BufferResource::Invalid(); + Renderer::BufferResource fillArgsBuffer = gatedSetup ? params.svsmFillArgsBuffer : Renderer::BufferResource::Invalid(); + const u32 fillArgsOffset = gatedSetup ? (viewIndex - 1) * params.svsmFillArgsViewStride + (keepDynamic ? params.svsmFillArgsDynamicOffset : 0) : 0; + const u32 overheadArgsOffset = gatedSetup ? (viewIndex - 1) * params.svsmFillArgsViewStride + (keepDynamic ? params.svsmFillArgsDynamicOverheadOffset : params.svsmFillArgsStaticOverheadOffset) : 0; + + // Reset the counters. The per-drawcall instance counts skip their FillBuffer in the gated + // path: CreateIndirect clears each count after consuming it, so the buffer is always zero + // between uses (and a gated-off CreateIndirect means nothing read the counts this view) params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); - params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); + if (!gatedSetup) + { + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + params.commandList->FillBuffer(params.culledInstanceCountsBuffer, 0, sizeof(u32) * numDrawCalls, 0); + params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); + } params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0); params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0); - params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::TRANSFER); params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Fill the instances visible in this view - { - std::string debugName = params.passName + " Instanced Geometry Fill"; - params.commandList->PushMarker(debugName, Color::White); - - Renderer::ComputePipelineID pipeline = filtered ? _fillInstancedDrawCallsFilteredPipeline[params.cullingResources->IsIndexed()] - : _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); - - struct FillDrawCallConstants - { - u32 numTotalInstances; - u32 baseInstanceLookupOffset; // Byte offset into drawCallDatas where the baseInstanceLookup is stored - u32 drawCallDataSize; - u32 currentBitmaskIndex; - u32 bitmaskOffset; - u32 keepDynamic; - }; - - FillDrawCallConstants* fillConstants = params.graphResources->FrameNew(); - fillConstants->numTotalInstances = numInstances; - fillConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - fillConstants->drawCallDataSize = params.drawCallDataSize; - fillConstants->currentBitmaskIndex = params.frameIndex; - fillConstants->bitmaskOffset = viewIndex * params.cullingResources->GetBitMaskBufferUintsPerView(); - fillConstants->keepDynamic = keepDynamic; - params.commandList->PushConstant(fillConstants, 0, sizeof(FillDrawCallConstants)); - - params.commandList->BindDescriptorSet(params.fillDescriptorSet, params.frameIndex); - - if (params.svsmPass && params.svsmFillArgsBuffer != Renderer::BufferResource::Invalid()) - { - // Finalize zeroed the group count for rings with no page work this frame - u32 argsOffset = (viewIndex - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + (keepDynamic ? ShadowRenderer::SVSM_FILL_ARGS_DYNAMIC_OFFSET : 0); - params.commandList->DispatchIndirect(params.svsmFillArgsBuffer, argsOffset); - } - else - { - params.commandList->Dispatch((numInstances + 31) / 32, 1, 1); - } - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } + DispatchInstancedFill(params, "Instanced Geometry Fill", params.fillDescriptorSet, filtered, params.frameIndex, viewIndex * params.cullingResources->GetBitMaskBufferUintsPerView(), keepDynamic, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, fillArgsOffset); params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); + // The draws consume this count, a zero-work view must still draw zero params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Create indirect argument buffer - { - std::string debugName = params.passName + " Create Indirect"; - params.commandList->PushMarker(debugName, Color::Yellow); - - Renderer::ComputePipelineID pipeline = _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; - params.commandList->BeginPipeline(pipeline); - - struct CreateIndirectConstants - { - u32 numTotalDrawCalls; - u32 baseInstanceLookupOffset; - u32 drawCallDataSize; - }; - CreateIndirectConstants* createIndirectConstants = params.graphResources->FrameNew(); - - createIndirectConstants->numTotalDrawCalls = numDrawCalls; - createIndirectConstants->baseInstanceLookupOffset = params.baseInstanceLookupOffset; - createIndirectConstants->drawCallDataSize = params.drawCallDataSize; - params.commandList->PushConstant(createIndirectConstants, 0, sizeof(CreateIndirectConstants)); - - params.commandList->BindDescriptorSet(params.createIndirectDescriptorSet, params.frameIndex); - - params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1); - - params.commandList->EndPipeline(pipeline); - params.commandList->PopMarker(); - } + DispatchCreateIndirect(params, params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, overheadArgsOffset); params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); @@ -728,23 +613,9 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); // svsmProfileGeometry: per-view fill/draw GPU timings, they surface in the perf editor's - // render pass list. Off by default, queries around tiny dispatches serialize slightly + // render pass list const bool profileSVSM = params.svsmPass && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry"_h) != 0; - auto Profiled = [&](const char* stageName, u32 viewIndex, auto&& work) - { - if (!profileSVSM) - { - work(); - return; - } - - Renderer::TimeQueryDesc timeQueryDesc; - timeQueryDesc.name = "SVSM " + std::string(stageName) + " v" + std::to_string(viewIndex); - Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); - params.commandList->BeginTimeQuery(timeQuery); - work(); - params.commandList->EndTimeQuery(timeQuery); - }; + RenderUtils::SVSMGeometryProfiler Profiled(_renderer, *params.commandList, "SVSM", profileSVSM); for (u32 i = params.firstViewIndex; i < params.numShadowViews + 1; i++) { @@ -868,25 +739,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) const bool svsmClipRects = params.svsmPass && i > 0 && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; - if (svsmClipRects) - { - // One draw per clip rect (new X columns, new Y rows, other): the vertex shader - // clips fragments to the rect, so an L-shaped toroidal update rasterizes two thin - // stripes instead of its whole-window bounding box - Profiled("Draw", i, [&] - { - for (u32 rectIndex = 0; rectIndex < 3; rectIndex++) - { - drawParams.svsmRectIndex = rectIndex; - params.drawCallback(drawParams); - } - }); - } - else - { - // svsmClipRects 0: single draw, rect index stays at the disabled sentinel - Profiled("Draw", i, [&] { params.drawCallback(drawParams); }); - } + Profiled("Draw", i, [&] { RenderUtils::DrawSVSMClipRects(svsmClipRects, drawParams, [&](DrawParams& rectDrawParams) { params.drawCallback(rectDrawParams); }); }); } // Copy from our draw count buffer to the readback buffer diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index 289d054..efd843a 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -325,11 +325,34 @@ class CulledRenderer // resident dynamic pages this frame get zero-group fills. Same-frame GPU truth — a // CPU/readback gate here is a frame late and flickers freshly acquired dynamic pages Renderer::BufferResource svsmFillArgsBuffer; // Finalize-written dispatch args, consumed read-only via DispatchIndirect + + // Byte layout of svsmFillArgsBuffer (the ShadowRenderer's stride/offset constants), + // passed in by the pass owner so this shared base never depends on ShadowRenderer + u32 svsmFillArgsViewStride = 0; + u32 svsmFillArgsDynamicOffset = 0; + u32 svsmFillArgsStaticOverheadOffset = 0; + u32 svsmFillArgsDynamicOverheadOffset = 0; }; void GeometryPass(GeometryPassParams& params); void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic); // Shared-buffer rebuild from a view's bitmask slice void ClipmapCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets + // The instanced cull dispatch shared by CullingPass and ClipmapCullingPass: one thread per + // instance against the per-view frustums, writing bitmask slices and instance counts. + // occlusionCull/cullMainView/debugDrawColliders come from params; the clipmap pass overrides + // them before calling + void RunInstancedCullingDispatch(CullingPassParams& params, bool useBitmasks, bool bindDepthPyramid); + + // The bitmask-driven instanced fill dispatch shared by OccluderPass (last frame's main-view + // bits) and RunInstancedGeometryFill (this frame's per-view slices, optionally gated through + // Finalize-written indirect args) + void DispatchInstancedFill(PassParams& params, const std::string& markerName, Renderer::DescriptorSetResource& fillSet, bool filtered, u32 currentBitmaskIndex, u32 bitmaskOffset, bool keepDynamic, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset); + + // The CreateIndirectAfterCulling dispatch shared by the occluder, culling and geometry-fill + // paths: per-drawcall instance counts become indirect draw args (each count is cleared after + // consumption, keeping the counts buffer always-zero between uses). debugSet is optional + void DispatchCreateIndirect(PassParams& params, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset); + void SyncToGPU(); void BindCullingResource(CullingResourcesBase& resources); diff --git a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp index cbef8a4..895f9ad 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp @@ -411,13 +411,19 @@ f32 GameRenderer::Render() _liquidRenderer->AddGeometryPass(&renderGraph, _resources, _frameIndex); // SVSM block, runs after the main depth is complete so page marking can analyze the visible - // samples, then allocates, culls per clipmap view and renders the dirty pages the same frame + // samples, then allocates, culls per clipmap view and renders the dirty pages the same frame. + // The night gate skips the whole producer side while the sun is below the horizon (the + // material pass already samples nothing at strength 0); the bind pass stays, bindings must + // remain valid _shadowRenderer->AddSVSMUpdatePass(&renderGraph, _resources, _frameIndex); _shadowRenderer->AddSVSMBindPass(&renderGraph, _resources, _frameIndex); - _terrainRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); - _modelRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); - _terrainRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); - _modelRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); + if (_shadowRenderer->IsSVSMActive()) + { + _terrainRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); + _modelRenderer->AddClipmapCullingPass(&renderGraph, _resources, _frameIndex); + _terrainRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); + _modelRenderer->AddSVSMGeometryPass(&renderGraph, _resources, _frameIndex, _shadowRenderer); + } _lightRenderer->AddClassificationPass(&renderGraph, _resources, _frameIndex); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index 673dd47..a5e506b 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -872,8 +872,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe Renderer::BufferMutableResource culledDrawCallsBuffer; Renderer::BufferMutableResource culledDrawCallCountBuffer; - Renderer::BufferMutableResource culledInstanceCountsBuffer; - Renderer::BufferMutableResource drawCountBuffer; Renderer::BufferMutableResource triangleCountBuffer; Renderer::BufferMutableResource drawCountReadBackBuffer; @@ -882,7 +880,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe Renderer::DescriptorSetResource globalSet; Renderer::DescriptorSetResource modelSet; Renderer::DescriptorSetResource fillSet; - Renderer::DescriptorSetResource createIndirectSet; Renderer::DescriptorSetResource drawSet; }; @@ -910,14 +907,16 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); builder.Write(_opaqueCullingResources.GetCulledDrawCallsBitMaskBuffer(!frameIndex), BufferUsage::TRANSFER | BufferUsage::GRAPHICS | BufferUsage::COMPUTE); - data.culledInstanceCountsBuffer = builder.Write(_opaqueCullingResources.GetCulledInstanceCountsBuffer(), BufferUsage::TRANSFER | BufferUsage::COMPUTE); + // No culled-instance-counts / createIndirect registrations: the main-view-only pass + // consumes the culling pass's output directly and never runs the per-view fill — + // registering them created a false render-graph dependency against the SVSM passes + // sharing those buffers builder.Read(_opaqueCullingResources.GetDrawCallDatas().GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE); data.globalSet = builder.Use(resources.globalDescriptorSet); data.modelSet = builder.Use(resources.modelDescriptorSet); data.fillSet = builder.Use(_opaqueCullingResources.GetGeometryFillDescriptorSet()); - data.createIndirectSet = builder.Use(_opaqueCullingResources.GetCreateIndirectAfterCullDescriptorSet()); return true; // Return true from setup to enable this pass, return false to disable it }, @@ -941,7 +940,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.drawCallsBuffer = data.drawCallsBuffer; params.culledDrawCallsBuffer = data.culledDrawCallsBuffer; params.culledDrawCallCountBuffer = data.culledDrawCallCountBuffer; - params.culledInstanceCountsBuffer = data.culledInstanceCountsBuffer; params.drawCountBuffer = data.drawCountBuffer; params.triangleCountBuffer = data.triangleCountBuffer; @@ -950,7 +948,6 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe params.globalDescriptorSet = data.globalSet; params.fillDescriptorSet = data.fillSet; - params.createIndirectDescriptorSet = data.createIndirectSet; params.drawDescriptorSet = data.drawSet; params.drawCallback = [&](DrawParams& drawParams) @@ -1231,6 +1228,10 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend params.svsmSplitFills = splitFills; params.svsmFillArgsBuffer = data.svsmFillArgsBuffer; + params.svsmFillArgsViewStride = ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE; + params.svsmFillArgsDynamicOffset = ShadowRenderer::SVSM_FILL_ARGS_DYNAMIC_OFFSET; + params.svsmFillArgsStaticOverheadOffset = ShadowRenderer::SVSM_FILL_ARGS_STATIC_OVERHEAD_OFFSET; + params.svsmFillArgsDynamicOverheadOffset = ShadowRenderer::SVSM_FILL_ARGS_DYNAMIC_OVERHEAD_OFFSET; if (splitFills) { params.svsmDynamicDrawCountReadBackBuffer = data.svsmDynamicDrawCountReadBackBuffer; @@ -3453,6 +3454,7 @@ void ModelRenderer::InitDescriptorSets() _svsmDynamicDrawDescriptorSet.RegisterPipeline(_renderer, _drawSVSMDynamicPipeline); _svsmDynamicDrawDescriptorSet.Init(_renderer); } + } void ModelRenderer::AllocateModel(const Model::ComplexModel& model, ModelOffsets& offsets) diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp index ee63455..af80599 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp @@ -33,6 +33,22 @@ void RenderUtils::Init(Renderer::Renderer* renderer, GameRenderer* gameRenderer) } } +void RenderUtils::SVSMGeometryProfiler::operator()(const char* stageName, u32 viewIndex, const std::function& work) const +{ + if (!_enabled) + { + work(); + return; + } + + Renderer::TimeQueryDesc timeQueryDesc; + timeQueryDesc.name = _namePrefix + " " + stageName + " v" + std::to_string(viewIndex); + Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); + _commandList->BeginTimeQuery(timeQuery); + work(); + _commandList->EndTimeQuery(timeQuery); +} + void RenderUtils::Blit(Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, u32 frameIndex, const BlitParams& params) { commandList.PushMarker("Blit", Color::White); diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h index 5d75100..641fc92 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h @@ -7,6 +7,9 @@ #include #include +#include +#include + namespace Renderer { class Renderer; @@ -143,6 +146,50 @@ class RenderUtils static void CopyDepthToColor(Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, u32 frameIndex, const CopyDepthToColorParams& params); static Renderer::ComputePipelineID GetCopyDepthToColorPipeline() { return _copyDepthToColorPipeline; } + + // svsmProfileGeometry: wraps a per-view SVSM fill/draw stage in a named GPU time query so it + // surfaces in the perf editor's render pass list. Off by default, queries around tiny + // dispatches serialize slightly + class SVSMGeometryProfiler + { + public: + SVSMGeometryProfiler(Renderer::Renderer* renderer, Renderer::CommandList& commandList, std::string namePrefix, bool enabled) + : _renderer(renderer) + , _commandList(&commandList) + , _namePrefix(std::move(namePrefix)) + , _enabled(enabled) + { + } + + void operator()(const char* stageName, u32 viewIndex, const std::function& work) const; + + private: + Renderer::Renderer* _renderer = nullptr; + Renderer::CommandList* _commandList = nullptr; + std::string _namePrefix; + bool _enabled = false; + }; + + // One draw per SVSM clip rect (new X columns, new Y rows, other): the vertex shader clips + // fragments to the rect, so an L-shaped toroidal update rasterizes two thin stripes instead + // of its whole-window bounding box. svsmClipRects 0: single draw, the rect index stays at the + // disabled sentinel + template + static void DrawSVSMClipRects(bool clipRectsEnabled, DrawParamsT& drawParams, DrawFn&& draw) + { + if (clipRectsEnabled) + { + for (u32 rectIndex = 0; rectIndex < 3; rectIndex++) + { + drawParams.svsmRectIndex = rectIndex; + draw(drawParams); + } + } + else + { + draw(drawParams); + } + } private: static Renderer::Renderer* _renderer; static GameRenderer* _gameRenderer; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 0a19ff0..25b8db3 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -24,6 +24,7 @@ #include #include +#include AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 1, CVarFlags::EditCheckbox); AutoCVar_Float CVAR_ShadowStrength(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength", "directional shadow strength, overwritten each frame from the sun elevation", 1.0f, CVarFlags::EditReadOnly); @@ -50,24 +51,104 @@ AutoCVar_Float CVAR_SVSMZHalfRange(CVarCategory::Client | CVarCategory::Renderin AutoCVar_Float CVAR_SVSMConstantBias(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias", "SVSM compare bias toward the sun in world meters, the software depth path has no hardware bias", 0.15f); AutoCVar_Int CVAR_SVSMProfileGeometry(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry", "debug: per-view fill/draw GPU time queries in the SVSM geometry passes, shown in the render pass list", 0, CVarFlags::EditCheckbox); AutoCVar_Int CVAR_SVSMClipRects(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects", "clip the static page draws to the classified dirty rects (3 draws per view), 0 reverts to one unclipped draw for A/B", 1, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SVSMNightGate(CVarCategory::Client | CVarCategory::Rendering, "svsmNightGate", "skip all SVSM update/render work while the sun is below the horizon (shadow strength 0), dawn resumes with a full re-bake", 1, CVarFlags::EditCheckbox); -// u32 indices into the flat SVSMData readback, mirrors Shadows/SVSM.inc.slang +// CPU mirror of SVSMData in Shadows/SVSM.inc.slang (scalar arrays only, so std430 matches this +// exactly). Never read as a struct: it exists so the readback offsets below are compiler-derived +// and the size tripwire fires when the two sides drift instead of the readback silently +// misreporting. Insert/rename fields here in lockstep with the Slang struct +struct SVSMDataMirror +{ + f32 prevLightDirection[4]; + + f32 zRangeMin; + f32 zRangeMax; + u32 frameInvalidationFlags; + u32 padding0; + + i32 anchorPageMinX[8]; + i32 anchorPageMinY[8]; + i32 prevAnchorPageMinX[8]; + i32 prevAnchorPageMinY[8]; + f32 camMinusWindowMinX[8]; + f32 camMinusWindowMinY[8]; + f32 extent[8]; + f32 pageWorld[8]; + u32 clipmapInvalidate[8]; + + i32 dirtyRectMinX[8]; + i32 dirtyRectMinY[8]; + i32 dirtyRectMaxX[8]; + i32 dirtyRectMaxY[8]; + + u32 statsMarked[8]; + u32 statsResident[8]; + u32 statsDirty[8]; + u32 statsEvicted[8]; + u32 statsInvalidated[8]; + u32 statsOverflow; + u32 statsInvalidationCause; + u32 statsFreeListCount; + + u32 configPageTableSize; + u32 configPageSize; + u32 configPoolPagesPerRow; + u32 configNumClipmaps; + f32 configMarkBorderTexels; + f32 configPadding0; + f32 configCamMinusZAnchor; + f32 configResolutionScale; + u32 padding3; + + i32 dynamicRectMinX[8]; + i32 dynamicRectMinY[8]; + i32 dynamicRectMaxX[8]; + i32 dynamicRectMaxY[8]; + + u32 statsDynamicLive[8]; + u32 statsDynamicOverflow; + u32 statsDynamicTotal; + u32 configDynamicPoolPagesPerRow; + u32 padding4; + + u32 statsDeferred[8]; + u32 statsBudgetUsed; + u32 padding5; + u32 padding6; + u32 padding7; + + i32 clipRectMinX[24]; + i32 clipRectMinY[24]; + i32 clipRectMaxX[24]; + i32 clipRectMaxY[24]; +}; +static_assert(sizeof(SVSMDataMirror) == ShadowRenderer::SVSM_DATA_UINT_COUNT * sizeof(u32), "SVSMDataMirror drifted from SVSMData in Shadows/SVSM.inc.slang, update both sides and the uint count together"); + +// u32 indices into the flat SVSMData readback, derived from the mirror so they cannot drift +// from the layout independently namespace SVSMDataOffsets { - constexpr u32 Extent = 56; - constexpr u32 StatsMarked = 112; - constexpr u32 StatsResident = 120; - constexpr u32 StatsDirty = 128; - constexpr u32 StatsEvicted = 136; - constexpr u32 StatsInvalidated = 144; - constexpr u32 StatsOverflow = 152; - constexpr u32 StatsInvalidationCause = 153; - constexpr u32 StatsFreeListCount = 154; - constexpr u32 StatsDynamicLive = 196; - constexpr u32 StatsDynamicOverflow = 204; - constexpr u32 StatsDynamicTotal = 205; - constexpr u32 StatsDeferred = 208; - constexpr u32 StatsBudgetUsed = 216; + constexpr u32 Extent = offsetof(SVSMDataMirror, extent) / sizeof(u32); + constexpr u32 StatsMarked = offsetof(SVSMDataMirror, statsMarked) / sizeof(u32); + constexpr u32 StatsResident = offsetof(SVSMDataMirror, statsResident) / sizeof(u32); + constexpr u32 StatsDirty = offsetof(SVSMDataMirror, statsDirty) / sizeof(u32); + constexpr u32 StatsEvicted = offsetof(SVSMDataMirror, statsEvicted) / sizeof(u32); + constexpr u32 StatsInvalidated = offsetof(SVSMDataMirror, statsInvalidated) / sizeof(u32); + constexpr u32 StatsOverflow = offsetof(SVSMDataMirror, statsOverflow) / sizeof(u32); + constexpr u32 StatsInvalidationCause = offsetof(SVSMDataMirror, statsInvalidationCause) / sizeof(u32); + constexpr u32 StatsFreeListCount = offsetof(SVSMDataMirror, statsFreeListCount) / sizeof(u32); + constexpr u32 StatsDynamicLive = offsetof(SVSMDataMirror, statsDynamicLive) / sizeof(u32); + constexpr u32 StatsDynamicOverflow = offsetof(SVSMDataMirror, statsDynamicOverflow) / sizeof(u32); + constexpr u32 StatsDynamicTotal = offsetof(SVSMDataMirror, statsDynamicTotal) / sizeof(u32); + constexpr u32 StatsDeferred = offsetof(SVSMDataMirror, statsDeferred) / sizeof(u32); + constexpr u32 StatsBudgetUsed = offsetof(SVSMDataMirror, statsBudgetUsed) / sizeof(u32); +} + +// Page table entry bits, mirrors Shadows/SVSM.inc.slang +namespace SVSMPageEntry +{ + constexpr u32 PhysMask = 0xFFFu; // SVSM_PAGE_PHYS_MASK + constexpr u32 Resident = 1u << 25; // SVSM_PAGE_RESIDENT } // SVSM invalidation cause bits, shared with Shadows/SVSM.inc.slang @@ -106,6 +187,26 @@ static bool SanitizeSVSMConfigCVars(u32 maxPageTableSize) return corrected; } +// The derived page/pool geometry every consumer must agree on: the update pass constants, the +// debug overlay and the pool state reset all read this instead of re-deriving it from the cvars +struct SVSMDerivedConfig +{ + u32 pageSize = 0; + u32 pageTableSize = 0; + u32 poolPagesPerRow = 0; + u32 dynamicPoolPagesPerRow = 0; +}; + +static SVSMDerivedConfig DeriveSVSMConfig(u32 maxPageTableSize) +{ + SVSMDerivedConfig config; + config.pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + config.pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / config.pageSize, 16u, maxPageTableSize); + config.poolPagesPerRow = glm::min(static_cast(CVAR_SVSMPoolSize.Get()) / config.pageSize, maxPageTableSize); + config.dynamicPoolPagesPerRow = glm::min(static_cast(CVAR_SVSMDynamicPoolSize.Get()) / config.pageSize, maxPageTableSize); + return config; +} + ShadowRenderer::ShadowRenderer(Renderer::Renderer* renderer, GameRenderer* gameRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources) : _renderer(renderer) , _gameRenderer(gameRenderer) @@ -182,6 +283,31 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } } + // Night gate: below the horizon the material pass multiplies shadows to nothing + // (shadowStrength 0, and lightInfo.y already stops the sampler), so the whole producer side + // can sleep. Enter after a sustained second so the dusk threshold can't flicker the cache; + // leave immediately with a full re-bake — the sun moved all night, the cache is garbage at + // dawn regardless + { + if (CVAR_SVSMNightGate.Get() != 0 && CVAR_ShadowStrength.GetFloat() <= 0.0f) + { + _svsmNightTimer += deltaTime; + if (_svsmNightTimer > 1.0f) + { + _svsmNightActive = true; + } + } + else + { + if (_svsmNightActive) + { + _svsmForceInvalidateAll = true; + } + _svsmNightActive = false; + _svsmNightTimer = 0.0f; + } + } + // SVSM: last frame's page table stats plus this frame's caster bounds. The ModelRenderer's // per-instance classifier owns the dynamic set (moved or bone-pushed within the grace window) // and its transition invalidations; this just copies the results and uploads. With the split @@ -194,7 +320,7 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) _svsmCasterTransitionsOut = 0; _svsmDynamicAABBsDropped = 0; const bool frozen = CVAR_SVSMFreeze.Get() != 0; - if (CVAR_ShadowEnabled.Get() && !frozen) + if (CVAR_ShadowEnabled.Get() && !frozen && !_svsmNightActive) { // The pools are the SVSM VRAM cost, only allocated once shadows are actually used if (_svsmPagePool == Renderer::ImageID::Invalid()) @@ -248,11 +374,11 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) for (u32 i = 0; i < tableUints; i++) { u32 entry = validateData[i]; - if (entry == 0 || (entry & (1u << 25)) == 0) // SVSM_PAGE_RESIDENT + if (entry == 0 || (entry & SVSMPageEntry::Resident) == 0) continue; numResident++; - u32 physicalPage = entry & 0xFFFu; // SVSM_PAGE_PHYS_MASK + u32 physicalPage = entry & SVSMPageEntry::PhysMask; if (physicalPage >= poolPages) { numBadPhys++; @@ -337,10 +463,11 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } else { - // Shadows off or frozen (the update pass doesn't run, so nothing would consume the - // queue): spawn/despawn and classifier-transition invalidations would accumulate - // unboundedly. Discard them (maxPairs 0 appends nothing) and re-bake the whole cache on - // resume instead — the classifier keeps ticking meanwhile, so its state stays current + // Shadows off, frozen or night-gated (the update pass doesn't run, so nothing would + // consume the queue): spawn/despawn and classifier-transition invalidations would + // accumulate unboundedly. Discard them (maxPairs 0 appends nothing) and re-bake the whole + // cache on resume instead — the classifier keeps ticking meanwhile, so its state stays + // current if (_modelRenderer->DrainShadowInvalidations(_svsmDirtyAABBs, 0) > 0) { _svsmForceInvalidateAll = true; @@ -348,6 +475,11 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources) } } +bool ShadowRenderer::IsSVSMActive() const +{ + return CVAR_ShadowEnabled.Get() != 0 && !_svsmNightActive; +} + void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { struct Data @@ -382,7 +514,7 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende }; const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); - const bool enabled = CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get(); + const bool enabled = CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get() && !_svsmNightActive; if (!enabled || _svsmPagePool == Renderer::ImageID::Invalid()) return; @@ -464,7 +596,7 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende u32 allocClipmap; f32 casterMargin; f32 zHalfRange; - f32 padding0; + u32 fillDrawCallCount; f32 padding1; f32 resolutionScale; u32 dynamicPoolPagesPerRow; @@ -474,19 +606,15 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende u32 fillCellCount; }; - const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); - const u32 pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / pageSize, 16u, SVSM_MAX_PAGE_TABLE_SIZE); - u32 poolPagesPerRow = static_cast(CVAR_SVSMPoolSize.Get()) / pageSize; - poolPagesPerRow = glm::min(poolPagesPerRow, SVSM_MAX_PAGE_TABLE_SIZE); - u32 dynamicPoolPagesPerRow = static_cast(CVAR_SVSMDynamicPoolSize.Get()) / pageSize; - dynamicPoolPagesPerRow = glm::min(dynamicPoolPagesPerRow, SVSM_MAX_PAGE_TABLE_SIZE); + const SVSMDerivedConfig config = DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE); + const u32 pageTableSize = config.pageTableSize; SVSMConstants* constants = graphResources.FrameNew(); constants->lightDirection = vec4(lightDirection, 0.0f); constants->numClipmaps = numClipmaps; - constants->pageTableSize = pageTableSize; - constants->pageSize = pageSize; - constants->poolPagesPerRow = poolPagesPerRow; + constants->pageTableSize = config.pageTableSize; + constants->pageSize = config.pageSize; + constants->poolPagesPerRow = config.poolPagesPerRow; constants->clipmap0Extent = CVAR_SVSMClipmap0Extent.GetFloat(); constants->markBorderTexels = CVAR_SVSMMarkBorderTexels.GetFloat(); constants->evictAge = static_cast(glm::clamp(CVAR_SVSMPageEvictAge.Get(), 1, 254)); @@ -495,16 +623,16 @@ void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, Rende constants->allocClipmap = 0; constants->casterMargin = CVAR_ShadowCasterMargin.GetFloat(); constants->zHalfRange = CVAR_SVSMZHalfRange.GetFloat(); - constants->padding0 = 0.0f; constants->padding1 = 0.0f; constants->resolutionScale = CVAR_SVSMResolutionScale.GetFloat(); - constants->dynamicPoolPagesPerRow = dynamicSplit ? dynamicPoolPagesPerRow : 0; + constants->dynamicPoolPagesPerRow = dynamicSplit ? config.dynamicPoolPagesPerRow : 0; constants->renderBudget = static_cast(glm::max(CVAR_SVSMRenderBudget.Get(), 0)); constants->dynamicPhase = 0; // The same record-time counts the geometry passes size their fills from, Finalize // turns them into per-view indirect dispatch args gated on this frame's page stats constants->fillInstanceCount = _modelRenderer->GetOpaqueCullingResources().GetNumInstances(); constants->fillCellCount = _terrainRenderer->GetNumDrawCalls(); + constants->fillDrawCallCount = _modelRenderer->GetOpaqueCullingResources().GetDrawCallCount(); if (_svsmPoolNeedsClear) { @@ -751,8 +879,7 @@ void ShadowRenderer::AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, ivec2 screenOffset; }; - const u32 pageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); - const u32 pageTableSize = glm::clamp(static_cast(CVAR_SVSMVirtualSize.Get()) / pageSize, 16u, SVSM_MAX_PAGE_TABLE_SIZE); + const u32 pageTableSize = DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE).pageTableSize; const u32 cellSize = 8; const u32 regionSize = pageTableSize * cellSize; @@ -834,6 +961,11 @@ void ShadowRenderer::GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, outOverflow = _svsmDataReadBack[SVSMDataOffsets::StatsDynamicOverflow]; } +u32 ShadowRenderer::GetSVSMBudgetUsed() const +{ + return _svsmDataReadBack[SVSMDataOffsets::StatsBudgetUsed]; +} + void ShadowRenderer::AddSVSMBindPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) { struct Data @@ -998,8 +1130,9 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) // _srcCameras / _depth / _target / _rwCameras / _pagePool are bound per frame, those resources can be recreated // Per-view fill dispatch args written by Finalize, consumed by the geometry passes' - // DispatchIndirect: per clipmap 3 x uvec3 (model static, model dynamic, terrain static). - // Prefilled with zero groups so a pre-Finalize consumer launches nothing + // DispatchIndirect: per clipmap 5 x uvec3 (model static, model dynamic, terrain static, + // model static overhead, model dynamic overhead). Prefilled with zero groups so a + // pre-Finalize consumer launches nothing Renderer::BufferDesc fillArgsDesc; fillArgsDesc.name = "SVSMFillDispatchArgs"; fillArgsDesc.size = SVSM_FILL_ARGS_VIEW_STRIDE * SVSM_MAX_CLIPMAPS; @@ -1007,7 +1140,7 @@ void ShadowRenderer::CreatePermanentResources(RenderResources& resources) _svsmFillArgsBuffer = _renderer->CreateAndFillBuffer(_svsmFillArgsBuffer, fillArgsDesc, [](void* mappedMemory, size_t size) { u32* values = static_cast(mappedMemory); - for (u32 i = 0; i < SVSM_MAX_CLIPMAPS * 9; i += 3) + for (u32 i = 0; i < SVSM_MAX_CLIPMAPS * 15; i += 3) { values[i + 0] = 0; values[i + 1] = 1; @@ -1053,15 +1186,14 @@ void ShadowRenderer::ResetSVSMPoolState(RenderResources& resources) // past the frames in flight), so every consumer set rebinds below. The pool textures are NOT // recreated — their dimensions only depend on the pool size cvars, which are restart-only // once the pools exist (the Engine cannot destroy images) - _svsmAppliedPageSize = static_cast(glm::max(CVAR_SVSMPageSize.Get(), 16)); + const SVSMDerivedConfig config = DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE); + _svsmAppliedPageSize = config.pageSize; _svsmAppliedPoolSize = static_cast(CVAR_SVSMPoolSize.Get()); _svsmAppliedDynamicPoolSize = static_cast(CVAR_SVSMDynamicPoolSize.Get()); // The physical page index is 12 bits in the table entry - const u32 poolPagesPerRow = _svsmAppliedPoolSize / _svsmAppliedPageSize; - _svsmPoolPages = glm::min(poolPagesPerRow * poolPagesPerRow, SVSM_MAX_POOL_PAGES); - const u32 dynamicPoolPagesPerRow = _svsmAppliedDynamicPoolSize / _svsmAppliedPageSize; - _svsmDynamicPoolPages = glm::min(dynamicPoolPagesPerRow * dynamicPoolPagesPerRow, SVSM_MAX_POOL_PAGES); + _svsmPoolPages = glm::min(config.poolPagesPerRow * config.poolPagesPerRow, SVSM_MAX_POOL_PAGES); + _svsmDynamicPoolPages = glm::min(config.dynamicPoolPagesPerRow * config.dynamicPoolPagesPerRow, SVSM_MAX_POOL_PAGES); Renderer::BufferDesc bufferDesc; bufferDesc.name = "SVSMData"; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index d2ec681..5ad5b65 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -57,7 +57,7 @@ class ShadowRenderer bool GetSVSMClipmapStats(u32 clipmapIndex, SVSMClipmapStats& outStats) const; void GetSVSMGlobalStats(u32& outFreePages, u32& outTotalPages, u32& outOverflow, u32& outInvalidationCause) const; void GetSVSMDynamicStats(u32& outLivePages, u32& outTotalPages, u32& outOverflow) const; - u32 GetSVSMBudgetUsed() const { return _svsmDataReadBack[216]; } // SVSMDataOffsets::StatsBudgetUsed + u32 GetSVSMBudgetUsed() const; // Defined in the .cpp next to the SVSMData mirror offsets // Binds the cameras buffer into every SVSM set that reads or writes it. Called at init // (buffer binds only reach the canonical descriptor copies at a later FlipFrame, so mid-frame @@ -69,6 +69,10 @@ class ShadowRenderer // per-view skip is a frame late and drops freshly acquired dynamic pages' draws for a frame bool HasSVSMDynamicCasters() const { return !_svsmDynamicAABBs.empty(); } + // Shadows enabled and not night-gated: the single condition the clipmap culling and SVSM + // geometry passes gate on (freeze deliberately excluded, frozen frames keep recording draws) + bool IsSVSMActive() const; + // CPU-side caster classification counts, rebuilt each Update from the ModelRenderer's // classifier: live set size, this frame's enter/leave transitions, and static-spilled casters void GetSVSMCasterStats(u32& outDynamicCasters, u32& outTransitionsIn, u32& outTransitionsOut, u32& outDroppedAABBs) const @@ -87,12 +91,16 @@ class ShadowRenderer Renderer::BufferID GetSVSMDynamicPageTableBuffer() const { return _svsmDynamicPageTableBuffer; } Renderer::ImageID GetSVSMDynamicPagePool() const { return _svsmDynamicPagePool; } - // Finalize-written per-view fill dispatch args: per clipmap 3 x uvec3 (model static fill, - // model dynamic fill, terrain static fill), byte stride SVSM_FILL_ARGS_VIEW_STRIDE + // Finalize-written per-view fill dispatch args: per clipmap 5 x uvec3 (model static fill, + // model dynamic fill, terrain static fill, model static overhead, model dynamic overhead — + // the overhead args gate the drawcall-granularity clear + CreateIndirect companions), + // byte stride SVSM_FILL_ARGS_VIEW_STRIDE Renderer::BufferID GetSVSMFillArgsBuffer() const { return _svsmFillArgsBuffer; } - static constexpr u32 SVSM_FILL_ARGS_VIEW_STRIDE = 3 * 3 * sizeof(u32); + static constexpr u32 SVSM_FILL_ARGS_VIEW_STRIDE = 5 * 3 * sizeof(u32); static constexpr u32 SVSM_FILL_ARGS_DYNAMIC_OFFSET = 3 * sizeof(u32); static constexpr u32 SVSM_FILL_ARGS_TERRAIN_OFFSET = 6 * sizeof(u32); + static constexpr u32 SVSM_FILL_ARGS_STATIC_OVERHEAD_OFFSET = 9 * sizeof(u32); + static constexpr u32 SVSM_FILL_ARGS_DYNAMIC_OVERHEAD_OFFSET = 12 * sizeof(u32); // The single clipmap-count cap every clamp site uses. The camera buffer and bitmask slices // are laid out against the Engine's view cap, the two must stay equal @@ -103,6 +111,11 @@ class ShadowRenderer // and spills the excess to the static path (never excluded from both pools) static constexpr u32 SVSM_MAX_DYNAMIC_AABBS = 4096; + // Scalar layout size of SVSMData in Shadows/SVSM.inc.slang. The .cpp mirrors the struct + // (SVSMDataMirror) and static_asserts against this so layout drift breaks the build instead + // of silently misreporting the readback. Tail: clipRect{MinX,MinY,MaxX,MaxY}[24] at 220..315 + static constexpr u32 SVSM_DATA_UINT_COUNT = 316; + // For the material pass LIGHT set bindings, which must stay valid before the pools exist. // Nothing samples the placeholder while the page tables have no resident entries Renderer::ImageID GetSVSMPagePoolOrPlaceholder() const { return _svsmPagePool != Renderer::ImageID::Invalid() ? _svsmPagePool : _svsmPagePoolPlaceholder; } @@ -118,9 +131,6 @@ class ShadowRenderer TerrainRenderer* _terrainRenderer = nullptr; ModelRenderer* _modelRenderer = nullptr; - // SVSM: scalar layout of SVSMData in Shadows/SVSM.inc.slang, offsets in ShadowRenderer.cpp. - // Tail: clipRect{MinX,MinY,MaxX,MaxY}[24] at 220..315 (3 clip rects x 8 clipmaps) - static constexpr u32 SVSM_DATA_UINT_COUNT = 316; static constexpr u32 SVSM_MAX_PAGE_TABLE_SIZE = 64; // Pages per row, buffers are sized for this cap static constexpr u32 SVSM_MAX_POOL_PAGES = 4096; // Physical page index is 12 bits in the table entry static constexpr u32 SVSM_MAX_DIRTY_AABBS = 1024; @@ -191,4 +201,9 @@ class ShadowRenderer // age out while visible). Initialized to the cvar defaults so startup does not invalidate bool _svsmModelsCastShadow = true; bool _svsmTerrainCastShadow = true; + + // Night gate: entered after shadowStrength sits at 0 for a sustained second (the dusk + // threshold must not flicker the cache), left immediately with a full re-bake + bool _svsmNightActive = false; + f32 _svsmNightTimer = 0.0f; }; \ No newline at end of file diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index f763ba9..a76747e 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -318,47 +318,8 @@ void TerrainRenderer::AddCullingPass(Renderer::RenderGraph* renderGraph, RenderR commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::COMPUTE); // Cull instances on GPU - Renderer::ComputePipelineID pipeline = _cullingPipeline; - commandList.BeginPipeline(pipeline); - - struct CullConstants - { - u32 viewportSizeX; - u32 viewportSizeY; - u32 numShadowViews; - u32 occlusionEnabled; - u32 bitMaskBufferSizePerView; - u32 currentBitmaskIndex; - u32 debugDrawView; - u32 cullMainView; - }; - - vec2 viewportSize = _renderer->GetRenderSize(); - - CullConstants* cullConstants = graphResources.FrameNew(); - cullConstants->viewportSizeX = u32(viewportSize.x); - cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numShadowViews = numShadowViews; - cullConstants->occlusionEnabled = CVAR_OcclusionCullingEnabled.Get(); - const u32 cellCount = static_cast(_cellDatas.Count()); - cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); - cullConstants->currentBitmaskIndex = frameIndex; - cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); - cullConstants->cullMainView = true; - - commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); - data.cullingSet.Bind("_depthPyramid"_h, data.depthPyramid); - - // Bind descriptorset - commandList.BindDescriptorSet(data.debugSet, frameIndex); - commandList.BindDescriptorSet(data.globalSet, frameIndex); - //commandList.BindDescriptorSet(Renderer::DescriptorSetSlot::SHADOWS, &resources.shadowDescriptorSet, frameIndex); - commandList.BindDescriptorSet(data.cullingSet, frameIndex); - - commandList.Dispatch((cellCount + 31) / 32, 1, 1); - - commandList.EndPipeline(pipeline); + RunCullingDispatch(graphResources, commandList, frameIndex, numShadowViews, CVAR_OcclusionCullingEnabled.Get() != 0, true, data.debugSet, data.globalSet, data.cullingSet); }); } @@ -530,48 +491,53 @@ void TerrainRenderer::AddClipmapCullingPass(Renderer::RenderGraph* renderGraph, { GPU_SCOPED_PROFILER_ZONE(commandList, TerrainClipmapCulling); - // Frustum-only cull of the cascade views into their bitmask slices, no occlusion culling for cascades - Renderer::ComputePipelineID pipeline = _cullingPipeline; - commandList.BeginPipeline(pipeline); + // Frustum-only cull of the clipmap views into their bitmask slices, no occlusion. + // _depthPyramid stays bound from the main culling pass, rebinding here would rewrite + // an already-bound set + RunCullingDispatch(graphResources, commandList, frameIndex, numShadowViews, false, false, data.debugSet, data.globalSet, data.cullingSet); + }); +} - struct CullConstants - { - u32 viewportSizeX; - u32 viewportSizeY; - u32 numShadowViews; - u32 occlusionEnabled; - u32 bitMaskBufferSizePerView; - u32 currentBitmaskIndex; - u32 debugDrawView; - u32 cullMainView; - }; +void TerrainRenderer::RunCullingDispatch(Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, u8 frameIndex, u32 numShadowViews, bool occlusionEnabled, bool cullMainView, Renderer::DescriptorSetResource& debugSet, Renderer::DescriptorSetResource& globalSet, Renderer::DescriptorSetResource& cullingSet) +{ + Renderer::ComputePipelineID pipeline = _cullingPipeline; + commandList.BeginPipeline(pipeline); - vec2 viewportSize = _renderer->GetRenderSize(); + struct CullConstants + { + u32 viewportSizeX; + u32 viewportSizeY; + u32 numShadowViews; + u32 occlusionEnabled; + u32 bitMaskBufferSizePerView; + u32 currentBitmaskIndex; + u32 debugDrawView; + u32 cullMainView; + }; - const u32 cellCount = static_cast(_cellDatas.Count()); + vec2 viewportSize = _renderer->GetRenderSize(); - CullConstants* cullConstants = graphResources.FrameNew(); - cullConstants->viewportSizeX = u32(viewportSize.x); - cullConstants->viewportSizeY = u32(viewportSize.y); - cullConstants->numShadowViews = numShadowViews; - cullConstants->occlusionEnabled = false; - cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); - cullConstants->currentBitmaskIndex = frameIndex; - cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); - cullConstants->cullMainView = false; + const u32 cellCount = static_cast(_cellDatas.Count()); - commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); + CullConstants* cullConstants = graphResources.FrameNew(); + cullConstants->viewportSizeX = u32(viewportSize.x); + cullConstants->viewportSizeY = u32(viewportSize.y); + cullConstants->numShadowViews = numShadowViews; + cullConstants->occlusionEnabled = occlusionEnabled; + cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount); + cullConstants->currentBitmaskIndex = frameIndex; + cullConstants->debugDrawView = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowDebugCullingView"_h)); + cullConstants->cullMainView = cullMainView; - // _depthPyramid stays bound from the main culling pass, rebinding here would rewrite an already-bound set + commandList.PushConstant(cullConstants, 0, sizeof(CullConstants)); - commandList.BindDescriptorSet(data.debugSet, frameIndex); - commandList.BindDescriptorSet(data.globalSet, frameIndex); - commandList.BindDescriptorSet(data.cullingSet, frameIndex); + commandList.BindDescriptorSet(debugSet, frameIndex); + commandList.BindDescriptorSet(globalSet, frameIndex); + commandList.BindDescriptorSet(cullingSet, frameIndex); - commandList.Dispatch((cellCount + 31) / 32, 1, 1); + commandList.Dispatch((cellCount + 31) / 32, 1, 1); - commandList.EndPipeline(pipeline); - }); + commandList.EndPipeline(pipeline); } void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex, ShadowRenderer* shadowRenderer) @@ -654,21 +620,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re // svsmProfileGeometry: per-view fill/draw GPU timings for the perf editor's render pass list const bool profileSVSM = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry"_h) != 0; - auto Profiled = [&](const char* stageName, u32 viewIndex, auto&& work) - { - if (!profileSVSM) - { - work(); - return; - } - - Renderer::TimeQueryDesc timeQueryDesc; - timeQueryDesc.name = "Terrain SVSM " + std::string(stageName) + " v" + std::to_string(viewIndex); - Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); - commandList.BeginTimeQuery(timeQuery); - work(); - commandList.EndTimeQuery(timeQuery); - }; + RenderUtils::SVSMGeometryProfiler Profiled(_renderer, commandList, "Terrain SVSM", profileSVSM); // _svsmData/_pageTable are bound once at init through BindSVSMBuffers, only the lazily // created pool binds per frame (image binds write the descriptor immediately) @@ -727,25 +679,7 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re drawParams.svsmDescriptorSet = data.svsmSet; const bool svsmClipRects = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; - if (svsmClipRects) - { - // One draw per clip rect (new X columns, new Y rows, other): the vertex - // shader clips fragments to the rect, so an L-shaped toroidal update - // rasterizes two thin stripes instead of its whole-window bounding box - Profiled("Draw", i, [&] - { - for (u32 rectIndex = 0; rectIndex < 3; rectIndex++) - { - drawParams.svsmRectIndex = rectIndex; - Draw(resources, frameIndex, graphResources, commandList, drawParams); - } - }); - } - else - { - // svsmClipRects 0: single draw, rect index stays at the disabled sentinel - Profiled("Draw", i, [&] { Draw(resources, frameIndex, graphResources, commandList, drawParams); }); - } + Profiled("Draw", i, [&] { RenderUtils::DrawSVSMClipRects(svsmClipRects, drawParams, [&](DrawParams& rectDrawParams) { Draw(resources, frameIndex, graphResources, commandList, rectDrawParams); }); }); commandList.PopMarker(); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index 3145602..fba4cd0 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -124,6 +124,11 @@ class TerrainRenderer }; void FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, FillDrawCallsParams& params); + // The terrain cull dispatch shared by AddCullingPass and AddClipmapCullingPass: one thread + // per cell against the per-view frustums, writing the bitmask slices. The depth pyramid bind + // stays at the main culling callsite (the clipmap pass reuses the already-bound set) + void RunCullingDispatch(Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, u8 frameIndex, u32 numShadowViews, bool occlusionEnabled, bool cullMainView, Renderer::DescriptorSetResource& debugSet, Renderer::DescriptorSetResource& globalSet, Renderer::DescriptorSetResource& cullingSet); + private: struct TerrainVertex { diff --git a/Source/Shaders/Shaders/Include/Shadows.inc.slang b/Source/Shaders/Shaders/Include/Shadows.inc.slang index f34093d..cdd43a7 100644 --- a/Source/Shaders/Shaders/Include/Shadows.inc.slang +++ b/Source/Shaders/Shaders/Include/Shadows.inc.slang @@ -73,11 +73,98 @@ bool SVSMIsDynamicPageResident(uint clipmapIndex, int2 virtualTexel, uint pageTa return (entry & SVSM_PAGE_RESIDENT) != 0; } +// One ring of the SVSM PCF filter: the 4x4 texel block shared by all 9 bilinear taps spans at +// most 2x2 pages, so their table entries resolve once and the taps only pay pool loads. One +// implementation for both the static and dynamic halves of GetSVSMShadowFactor, parameterized on +// the (table, pool, pool row count) pair and the missing-page policy: +// - missingPagesReadLit = false (static cache): INVALID content may live in a dead depth mapping +// and counts as missing; any missing touched page aborts the ring (returns false, the caller +// falls back to a coarser clipmap, worst case ends lit). +// - missingPagesReadLit = true (dynamic pool): non-resident pages read depth 0 = lit — correct, +// no dynamic caster covers that texel — and the ring always resolves. +// The callers' containment margin keeps the whole block inside the window +bool SVSMFilterRing(uint clipmapIndex, float2 texelPos, float compareDepth, uint pageTableSize, uint pageSize, uint poolPagesPerRow, bool missingPagesReadLit, StructuredBuffer pageTable, Texture2D pagePool, out float shadowFactor) +{ + shadowFactor = 1.0f; + + float2 blockBaseF = floor(texelPos - 0.5f); + int2 blockBase = int2(blockBaseF) - 1; + float2 fracPart = texelPos - 0.5f - blockBaseF; + + int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); + int2 basePage = blockBase / int(pageSize); + int2 baseRel = blockBase - basePage * int(pageSize); + + bool pageResident[2][2]; + uint2 pagePhysBase[2][2]; + + [unroll] + for (int py = 0; py < 2; py++) + { + [unroll] + for (int px = 0; px < 2; px++) + { + int2 slot = WrapPageSlot(anchorPageMin + basePage + int2(px, py), pageTableSize); + uint entry = pageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + + pageResident[py][px] = missingPagesReadLit ? (entry & SVSM_PAGE_RESIDENT) != 0 + : (entry & (SVSM_PAGE_RESIDENT | SVSM_PAGE_INVALID)) == SVSM_PAGE_RESIDENT; + uint physicalPage = GetPagePhysical(entry); + pagePhysBase[py][px] = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; + } + } + + if (!missingPagesReadLit) + { + bool2 crossesPage = bool2(baseRel.x + 3 >= int(pageSize), baseRel.y + 3 >= int(pageSize)); + bool allResident = pageResident[0][0] + && (!crossesPage.x || pageResident[0][1]) + && (!crossesPage.y || pageResident[1][0]) + && (!(crossesPage.x && crossesPage.y) || pageResident[1][1]); + if (!allResident) + { + return false; + } + } + + float lit[4][4]; + + [unroll] + for (int y = 0; y < 4; y++) + { + [unroll] + for (int x = 0; x < 4; x++) + { + int2 rel = baseRel + int2(x, y); + int2 pageSel = int2(rel.x >= int(pageSize) ? 1 : 0, rel.y >= int(pageSize) ? 1 : 0); + uint2 pageTexel = uint2(rel - pageSel * int(pageSize)); + float storedDepth = pageResident[pageSel.y][pageSel.x] ? asfloat(pagePool[pagePhysBase[pageSel.y][pageSel.x] + pageTexel]) : 0.0f; + lit[y][x] = compareDepth > storedDepth ? 1.0f : 0.0f; + } + } + + float factor = 0.0f; + + [unroll] + for (int ty = 0; ty < 3; ty++) + { + [unroll] + for (int tx = 0; tx < 3; tx++) + { + float litX0 = lerp(lit[ty + 0][tx + 0], lit[ty + 0][tx + 1], fracPart.x); + float litX1 = lerp(lit[ty + 1][tx + 0], lit[ty + 1][tx + 1], fracPart.x); + factor += lerp(litX0, litX1, fracPart.y); + } + } + shadowFactor = factor / 9.0f; + return true; +} + // Software percentage-closer filtering over the sparse clipmaps: select the finest covering // clipmap, walk to coarser ones when a touched page is not resident, 3x3 bilinear-weighted taps -// over a shared 4x4 texel block. Returns 1 (lit) when nothing is resident, the marking pass -// guarantees residency for everything the main view actually sees. Static and dynamic resolve -// their rings independently (see the dynamic block below) and combine via min +// over a shared 4x4 texel block (SVSMFilterRing). Returns 1 (lit) when nothing is resident, the +// marking pass guarantees residency for everything the main view actually sees. Static and +// dynamic resolve their rings independently (see the dynamic block below) and combine via min float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings shadowSettings) { uint numClipmaps = _svsmData[0].configNumClipmaps; @@ -137,78 +224,14 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh // Positive bias moves the receiver toward the sun float compareDepth = (zHalf - (lightSpacePos.z + camMinusZAnchor)) / zRangeLength + compareBias; - // 4x4 texel block shared by all 9 bilinear taps float2 texelPos = windowPos / texelWorld; - float2 blockBaseF = floor(texelPos - 0.5f); - int2 blockBase = int2(blockBaseF) - 1; - float2 fracPart = texelPos - 0.5f - blockBaseF; - - // The block spans at most 2x2 pages: resolve their table entries once, the taps then only - // pay pool loads. The containment margin keeps the whole block inside the window - int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[k], _svsmData[0].anchorPageMinY[k]); - int2 basePage = blockBase / int(pageSize); - int2 baseRel = blockBase - basePage * int(pageSize); - bool2 crossesPage = bool2(baseRel.x + 3 >= int(pageSize), baseRel.y + 3 >= int(pageSize)); - bool pageResident[2][2]; - uint2 pagePhysBase[2][2]; - - [unroll] - for (int py = 0; py < 2; py++) - { - [unroll] - for (int px = 0; px < 2; px++) - { - int2 slot = WrapPageSlot(anchorPageMin + basePage + int2(px, py), pageTableSize); - uint entry = _svsmPageTable[PageEntryIndex(k, slot, pageTableSize)]; - - // INVALID content may live in a dead depth mapping, never sample it - pageResident[py][px] = (entry & (SVSM_PAGE_RESIDENT | SVSM_PAGE_INVALID)) == SVSM_PAGE_RESIDENT; - uint physicalPage = GetPagePhysical(entry); - pagePhysBase[py][px] = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; - } - } - - bool allResident = pageResident[0][0] - && (!crossesPage.x || pageResident[0][1]) - && (!crossesPage.y || pageResident[1][0]) - && (!(crossesPage.x && crossesPage.y) || pageResident[1][1]); - if (!allResident) + float shadowFactor; + if (!SVSMFilterRing(k, texelPos, compareDepth, pageTableSize, pageSize, poolPagesPerRow, false, _svsmPageTable, _svsmPagePool, shadowFactor)) { continue; // Fall back to a coarser clipmap, worst case ends lit } - float lit[4][4]; - - [unroll] - for (int y = 0; y < 4; y++) - { - [unroll] - for (int x = 0; x < 4; x++) - { - int2 rel = baseRel + int2(x, y); - int2 pageSel = int2(rel.x >= int(pageSize) ? 1 : 0, rel.y >= int(pageSize) ? 1 : 0); - uint2 pageTexel = uint2(rel - pageSel * int(pageSize)); - float storedDepth = asfloat(_svsmPagePool[pagePhysBase[pageSel.y][pageSel.x] + pageTexel]); - lit[y][x] = compareDepth > storedDepth ? 1.0f : 0.0f; - } - } - - float shadowFactor = 0.0f; - - [unroll] - for (int ty = 0; ty < 3; ty++) - { - [unroll] - for (int tx = 0; tx < 3; tx++) - { - float litX0 = lerp(lit[ty + 0][tx + 0], lit[ty + 0][tx + 1], fracPart.x); - float litX1 = lerp(lit[ty + 1][tx + 0], lit[ty + 1][tx + 1], fracPart.x); - shadowFactor += lerp(litX0, litX1, fracPart.y); - } - } - shadowFactor /= 9.0f; - // Fade to unshadowed over the outer 15% of the coarsest window, beyond it there is no shadow data if (k == numClipmaps - 1) { @@ -254,64 +277,9 @@ float GetSVSMShadowFactor(float3 worldPos, float3 worldNormal, ShadowSettings sh float compareDepth = (zHalf - (lightSpacePos.z + camMinusZAnchor)) / zRangeLength + compareBias; - float2 blockBaseF = floor(texelPos - 0.5f); - int2 blockBase = int2(blockBaseF) - 1; - float2 fracPart = texelPos - 0.5f - blockBaseF; - - // Same 2x2 page resolve as the static filter. Non-resident pages read depth 0 = lit, - // correct: no dynamic caster covers that texel - uint dynamicPoolPagesPerRow = _svsmData[0].configDynamicPoolPagesPerRow; - int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[kd], _svsmData[0].anchorPageMinY[kd]); - int2 basePage = blockBase / int(pageSize); - int2 baseRel = blockBase - basePage * int(pageSize); - - bool pageResident[2][2]; - uint2 pagePhysBase[2][2]; - - [unroll] - for (int py = 0; py < 2; py++) - { - [unroll] - for (int px = 0; px < 2; px++) - { - int2 slot = WrapPageSlot(anchorPageMin + basePage + int2(px, py), pageTableSize); - uint entry = _svsmDynamicPageTable[PageEntryIndex(kd, slot, pageTableSize)]; - pageResident[py][px] = (entry & SVSM_PAGE_RESIDENT) != 0; - uint physicalPage = GetPagePhysical(entry); - pagePhysBase[py][px] = uint2(physicalPage % dynamicPoolPagesPerRow, physicalPage / dynamicPoolPagesPerRow) * pageSize; - } - } - - float lit[4][4]; - - [unroll] - for (int y = 0; y < 4; y++) - { - [unroll] - for (int x = 0; x < 4; x++) - { - int2 rel = baseRel + int2(x, y); - int2 pageSel = int2(rel.x >= int(pageSize) ? 1 : 0, rel.y >= int(pageSize) ? 1 : 0); - uint2 pageTexel = uint2(rel - pageSel * int(pageSize)); - float storedDepth = pageResident[pageSel.y][pageSel.x] ? asfloat(_svsmDynamicPagePool[pagePhysBase[pageSel.y][pageSel.x] + pageTexel]) : 0.0f; - lit[y][x] = compareDepth > storedDepth ? 1.0f : 0.0f; - } - } - - float shadowFactor = 0.0f; - - [unroll] - for (int ty = 0; ty < 3; ty++) - { - [unroll] - for (int tx = 0; tx < 3; tx++) - { - float litX0 = lerp(lit[ty + 0][tx + 0], lit[ty + 0][tx + 1], fracPart.x); - float litX1 = lerp(lit[ty + 1][tx + 0], lit[ty + 1][tx + 1], fracPart.x); - shadowFactor += lerp(litX0, litX1, fracPart.y); - } - } - dynamicFactor = shadowFactor / 9.0f; + // Same filter as the static ring against the dynamic table/pool; non-resident pages + // read lit, so this always resolves + SVSMFilterRing(kd, texelPos, compareDepth, pageTableSize, pageSize, _svsmData[0].configDynamicPoolPagesPerRow, true, _svsmDynamicPageTable, _svsmDynamicPagePool, dynamicFactor); break; } } diff --git a/Source/Shaders/Shaders/Model/Draw.ps.slang b/Source/Shaders/Shaders/Model/Draw.ps.slang index 1671c42..9e2e982 100644 --- a/Source/Shaders/Shaders/Model/Draw.ps.slang +++ b/Source/Shaders/Shaders/Model/Draw.ps.slang @@ -4,6 +4,7 @@ #include "Include/Common.inc.slang" #include "Include/VisibilityBuffers.inc.slang" +#include "Model/ModelAlphaKey.inc.slang" #include "Model/ModelShared.inc.slang" struct PSInput @@ -22,50 +23,10 @@ struct PSOutput [shader("fragment")] PSOutput main(PSInput input) { - uint drawCallID = input.drawIDInstanceIDTextureDataIDInstanceRefID.x; - uint instanceID = input.drawIDInstanceIDTextureDataIDInstanceRefID.y; uint textureDataID = input.drawIDInstanceIDTextureDataIDInstanceRefID.z; uint instanceRefID = input.drawIDInstanceIDTextureDataIDInstanceRefID.w; - TextureData textureData = LoadModelTextureData(_packedModelTextureDatas, textureDataID); - - for (uint textureUnitIndex = textureData.textureUnitOffset; textureUnitIndex < textureData.textureUnitOffset + textureData.numTextureUnits; textureUnitIndex++) - { - ModelTextureUnit textureUnit = _modelTextureUnits[textureUnitIndex]; - - uint blendingMode = (textureUnit.data1 >> 11) & 0x7; - - if (blendingMode != 1) // ALPHA KEY - continue; - - uint texture0SamplerIndex = (textureUnit.data1 >> 1) & 0x3; - uint texture1SamplerIndex = (textureUnit.data1 >> 3) & 0x3; - uint materialType = (textureUnit.data1 >> 16) & 0xFFFF; - - if (materialType == 0x8000) - continue; - - float4 texture1 = _modelTextures[NonUniformResourceIndex(textureUnit.textureIDs[0])].Sample(_samplers[texture0SamplerIndex], input.uv01.xy); - float4 texture2 = float4(1, 1, 1, 1); - - uint vertexShaderId = materialType & 0xFF; - if (vertexShaderId > 2) - { - // ENV uses generated UVCoords based on camera pos + geometry normal in frame space - texture2 = _modelTextures[NonUniformResourceIndex(textureUnit.textureIDs[1])].Sample(_samplers[texture1SamplerIndex], input.uv01.zw); - } - - // Experimental alphakey discard without shading, if this has issues check github history for cModel.ps.hlsl - float4 diffuseColor = float4(1, 1, 1, 1); - // TODO: per-instance diffuseColor - - // TODO : minAlpha is set based on the Pixel Shader Id - float minAlpha = texture1.a; // min(texture1.a, min(texture2.a, diffuseColor.a)); - if (minAlpha < (128.0 / 255.0)) - { - discard; - } - } + AlphaKeyDiscard(textureDataID, input.uv01.xy); PSOutput output; output.visibilityBuffer = PackVisibilityBuffer(ObjectType::ModelOpaque, instanceRefID, input.triangleID); diff --git a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang index 8b6f19c..9cc00c4 100644 --- a/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang +++ b/Source/Shaders/Shaders/Model/DrawSVSM.ps.slang @@ -6,6 +6,7 @@ permutation SVSM_DYNAMIC = [0, 1]; // 1 = dynamic caster split pass, renders int #include "DescriptorSet/Model.inc.slang" #include "Include/Common.inc.slang" +#include "Model/ModelAlphaKey.inc.slang" #include "Model/ModelShared.inc.slang" #include "Shadows/SVSM.inc.slang" @@ -39,38 +40,27 @@ struct PSInput [shader("fragment")] void main(PSInput input) { - uint textureDataID = input.drawIDInstanceIDTextureDataIDInstanceRefID.z; - - // Alpha-key discard, mirrors Model/Draw.ps without the visibility-buffer write - TextureData textureData = LoadModelTextureData(_packedModelTextureDatas, textureDataID); +#if SVSM_DYNAMIC + const uint requiredFlags = SVSM_PAGE_RESIDENT; + const uint poolPagesPerRow = _svsmData[0].configDynamicPoolPagesPerRow; +#else + const uint requiredFlags = SVSM_PAGE_RESIDENT | SVSM_PAGE_DIRTY; + const uint poolPagesPerRow = _svsmData[0].configPoolPagesPerRow; +#endif - for (uint textureUnitIndex = textureData.textureUnitOffset; textureUnitIndex < textureData.textureUnitOffset + textureData.numTextureUnits; textureUnitIndex++) + // Fragments over pages that don't render this frame skip the alpha-key sampling entirely. + // The skip diverges only along page borders, where a wrong implicit mip for one quad row in a + // depth-only alpha test is invisible — Sample stays Sample + uint pageEntry = SVSMLoadPageEntry(input.position.xy, _constants.viewIndex - 1, _svsmData, _pageTable); + if ((pageEntry & requiredFlags) != requiredFlags) { - ModelTextureUnit textureUnit = _modelTextureUnits[textureUnitIndex]; - - uint blendingMode = (textureUnit.data1 >> 11) & 0x7; - - if (blendingMode != 1) // ALPHA KEY - continue; - - uint texture0SamplerIndex = (textureUnit.data1 >> 1) & 0x3; - uint materialType = (textureUnit.data1 >> 16) & 0xFFFF; - - if (materialType == 0x8000) - continue; + return; + } - float4 texture1 = _modelTextures[NonUniformResourceIndex(textureUnit.textureIDs[0])].Sample(_samplers[texture0SamplerIndex], input.uv01.xy); + uint textureDataID = input.drawIDInstanceIDTextureDataIDInstanceRefID.z; - float minAlpha = texture1.a; - if (minAlpha < (128.0 / 255.0)) - { - discard; - } - } + // Same alpha-key discard as Model/Draw.ps, without the visibility-buffer write + AlphaKeyDiscard(textureDataID, input.uv01.xy); -#if SVSM_DYNAMIC - SVSMWritePageDepth(input.position.xyz, _constants.viewIndex - 1, SVSM_PAGE_RESIDENT, _svsmData[0].configDynamicPoolPagesPerRow, _svsmData, _pageTable, _pagePool); -#else - SVSMWritePageDepth(input.position.xyz, _constants.viewIndex - 1, SVSM_PAGE_RESIDENT | SVSM_PAGE_DIRTY, _svsmData[0].configPoolPagesPerRow, _svsmData, _pageTable, _pagePool); -#endif + SVSMWritePageDepthEntry(input.position.xyz, pageEntry, requiredFlags, poolPagesPerRow, _svsmData, _pagePool); } diff --git a/Source/Shaders/Shaders/Model/ModelAlphaKey.inc.slang b/Source/Shaders/Shaders/Model/ModelAlphaKey.inc.slang new file mode 100644 index 0000000..fe5726f --- /dev/null +++ b/Source/Shaders/Shaders/Model/ModelAlphaKey.inc.slang @@ -0,0 +1,43 @@ +#ifndef MODEL_ALPHA_KEY_INCLUDED +#define MODEL_ALPHA_KEY_INCLUDED + +#include "DescriptorSet/Model.inc.slang" +#include "Model/ModelShared.inc.slang" + +// Alpha-key discard shared by the model fragment passes that need cutouts without shading +// (Model/Draw.ps, Model/DrawSVSM.ps). Fragment-only (Sample + discard), keep it out of the +// includes compute shaders pull in. +// Experimental alphakey discard without shading, if this has issues check github history for +// cModel.ps.hlsl +void AlphaKeyDiscard(uint textureDataID, float2 uv0) +{ + TextureData textureData = LoadModelTextureData(_packedModelTextureDatas, textureDataID); + + for (uint textureUnitIndex = textureData.textureUnitOffset; textureUnitIndex < textureData.textureUnitOffset + textureData.numTextureUnits; textureUnitIndex++) + { + ModelTextureUnit textureUnit = _modelTextureUnits[textureUnitIndex]; + + uint blendingMode = (textureUnit.data1 >> 11) & 0x7; + + if (blendingMode != 1) // ALPHA KEY + continue; + + uint texture0SamplerIndex = (textureUnit.data1 >> 1) & 0x3; + uint materialType = (textureUnit.data1 >> 16) & 0xFFFF; + + if (materialType == 0x8000) + continue; + + float4 texture1 = _modelTextures[NonUniformResourceIndex(textureUnit.textureIDs[0])].Sample(_samplers[texture0SamplerIndex], uv0); + + // TODO : minAlpha is set based on the Pixel Shader Id, and per-instance diffuseColor + // should participate + float minAlpha = texture1.a; + if (minAlpha < (128.0 / 255.0)) + { + discard; + } + } +} + +#endif // MODEL_ALPHA_KEY_INCLUDED diff --git a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang index f053888..d2ec409 100644 --- a/Source/Shaders/Shaders/Shadows/SVSM.inc.slang +++ b/Source/Shaders/Shaders/Shadows/SVSM.inc.slang @@ -34,7 +34,7 @@ struct SVSMConstants uint allocClipmap; // UpdateB runs once per clipmap, finest first, so pool starvation always lands on the coarsest ring float casterMargin; // Sun-side sweep on the near cull plane, depth clamp pancakes those casters float zHalfRange; // Half depth range of every clipmap window around the quantized Z anchor - float padding0; + uint fillDrawCallCount; // Model draw calls this frame, Finalize sizes the per-view fill overhead args from it float padding1; float resolutionScale; // Eye-distance clipmap floor scale, <= 0 disables uint dynamicPoolPagesPerRow; // Caster-split dynamic pool pages per row, 0 while the split is off @@ -197,6 +197,31 @@ uint PageEntryIndex(uint clipmapIndex, int2 slot, uint pageTableSize) return clipmapIndex * pageTableSize * pageTableSize + uint(slot.y) * pageTableSize + uint(slot.x); } +// Window-local page rect of a world AABB in one clipmap: light-space XY rect via the |R| +// extents trick, one page of pad so filter taps just outside the caster footprint are covered +// too, clamped to the window. Returns false when the AABB misses this clipmap's window. +// Page granularity is coarse (quarter meters at the finest clipmap) so plain world-magnitude +// light-space math is precise enough here. windowMin uses the same reconstruction Prepare used +// for camMinusWindowMin, deterministic vs the stored anchors +bool SVSMComputeAABBPageRect(float3 aabbMin, float3 aabbMax, float3 lightDirection, float pageWorld, int2 anchorPageMin, uint pageTableSize, out int2 pageMin, out int2 pageMax) +{ + float3 center = (aabbMin + aabbMax) * 0.5f; + float3 halfExtents = (aabbMax - aabbMin) * 0.5f; + + float3x3 lightRotation = BuildLightRotation(lightDirection); + float2 centerLS = mul(center, lightRotation).xy; + float2 halfLS = mul(halfExtents, float3x3(abs(lightRotation[0]), abs(lightRotation[1]), abs(lightRotation[2]))).xy; + + float2 windowMin = float2(anchorPageMin) * pageWorld; + + pageMin = int2(floor((centerLS - halfLS - windowMin) / pageWorld)) - 1; + pageMax = int2(floor((centerLS + halfLS - windowMin) / pageWorld)) + 1; + pageMin = max(pageMin, int2(0, 0)); + pageMax = min(pageMax, int2(pageTableSize - 1, pageTableSize - 1)); + + return all(pageMax >= pageMin); +} + // Finest clipmap whose window contains the position with margin to spare, -1 if none. // One shared function for marking and sampling: the margin guarantees the filter footprint and // the border page marks never leave the selected window. lightSpaceRelCam = mul(P - cam, R).xy @@ -241,27 +266,37 @@ int SelectClipmap(float2 lightSpaceRelCam, float eyeDistance, StructuredBuffer svsmData, StructuredBuffer pageTable, RWTexture2D pagePool) +// The page-table entry under a page-render fragment. svPosition.xy comes straight from +// SV_Position: window-local virtual texel coordinates (the viewport spans the full virtual +// texture) +uint SVSMLoadPageEntry(float2 svPositionXY, uint clipmapIndex, StructuredBuffer svsmData, StructuredBuffer pageTable) { uint pageTableSize = svsmData[0].configPageTableSize; uint pageSize = svsmData[0].configPageSize; - int2 texel = int2(svPosition.xy); + int2 texel = int2(svPositionXY); int2 localPage = texel / int(pageSize); int2 anchorPageMin = int2(svsmData[0].anchorPageMinX[clipmapIndex], svsmData[0].anchorPageMinY[clipmapIndex]); int2 slot = WrapPageSlot(anchorPageMin + localPage, pageTableSize); - uint entry = pageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; + return pageTable[PageEntryIndex(clipmapIndex, slot, pageTableSize)]; +} + +// Software depth write for the SVSM page render passes, taking a pre-loaded page entry (the pixel +// shader may have fetched it for its early alpha-key skip). z is the reversed ortho depth; depth +// clamp disables clipping so pancaked casters saturate here instead. +// requiredFlags: static pages render only while DIRTY, dynamic pages render whenever RESIDENT +void SVSMWritePageDepthEntry(float3 svPosition, uint entry, uint requiredFlags, uint poolPagesPerRow, StructuredBuffer svsmData, RWTexture2D pagePool) +{ if ((entry & requiredFlags) != requiredFlags) { return; } + uint pageSize = svsmData[0].configPageSize; + int2 texel = int2(svPosition.xy); + int2 localPage = texel / int(pageSize); + uint physicalPage = GetPagePhysical(entry); uint2 physicalBase = uint2(physicalPage % poolPagesPerRow, physicalPage / poolPagesPerRow) * pageSize; uint2 pageTexel = uint2(texel - localPage * int(pageSize)); @@ -269,4 +304,11 @@ void SVSMWritePageDepth(float3 svPosition, uint clipmapIndex, uint requiredFlags InterlockedMax(pagePool[physicalBase + pageTexel], asuint(saturate(svPosition.z))); } +// Fragments outside pages that render this frame exit after one table load +void SVSMWritePageDepth(float3 svPosition, uint clipmapIndex, uint requiredFlags, uint poolPagesPerRow, StructuredBuffer svsmData, StructuredBuffer pageTable, RWTexture2D pagePool) +{ + uint entry = SVSMLoadPageEntry(svPosition.xy, clipmapIndex, svsmData, pageTable); + SVSMWritePageDepthEntry(svPosition, entry, requiredFlags, poolPagesPerRow, svsmData, pagePool); +} + #endif // SVSM_INCLUDED diff --git a/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang index da45f20..3f49752 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMDynamicMark.cs.slang @@ -31,25 +31,14 @@ void main(CSInput input) float3 aabbMin = _dynamicAABBs[aabbIndex * 2 + 0].xyz; float3 aabbMax = _dynamicAABBs[aabbIndex * 2 + 1].xyz; - float3 center = (aabbMin + aabbMax) * 0.5f; - float3 halfExtents = (aabbMax - aabbMin) * 0.5f; - - float3x3 lightRotation = BuildLightRotation(_constants.lightDirection.xyz); - float2 centerLS = mul(center, lightRotation).xy; - float2 halfLS = mul(halfExtents, float3x3(abs(lightRotation[0]), abs(lightRotation[1]), abs(lightRotation[2]))).xy; uint pageTableSize = _constants.pageTableSize; float pageWorld = _svsmData[0].pageWorld[clipmapIndex]; int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); - float2 windowMin = float2(anchorPageMin) * pageWorld; - - // One page of pad so filter taps just beyond the caster footprint see dynamic depth too - int2 pageMin = int2(floor((centerLS - halfLS - windowMin) / pageWorld)) - 1; - int2 pageMax = int2(floor((centerLS + halfLS - windowMin) / pageWorld)) + 1; - pageMin = max(pageMin, int2(0, 0)); - pageMax = min(pageMax, int2(pageTableSize - 1, pageTableSize - 1)); - if (any(pageMax < pageMin)) + int2 pageMin; + int2 pageMax; + if (!SVSMComputeAABBPageRect(aabbMin, aabbMax, _constants.lightDirection.xyz, pageWorld, anchorPageMin, pageTableSize, pageMin, pageMax)) { return; // Outside this clipmap's window } diff --git a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang index a398012..01afdbb 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMFinalize.cs.slang @@ -22,7 +22,7 @@ [[vk::binding(0, PER_PASS)]] RWStructuredBuffer _rwCameras; // Same buffer as the global _cameras, element 0 = main camera [[vk::binding(1, PER_PASS)]] StructuredBuffer _svsmData; -[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _fillDispatchArgs; // Per clipmap: 3 x uvec3 (model static fill, model dynamic fill, terrain static fill) +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _fillDispatchArgs; // Per clipmap: 5 x uvec3 (model static fill, model dynamic fill, terrain static fill, model static overhead, model dynamic overhead) [shader("compute")] [numthreads(1, 1, 1)] @@ -32,16 +32,20 @@ void main() float3x3 lightRotation = BuildLightRotation(lightDirection); float3 upDir = StableUp(lightDirection); - // Per-view fill dispatch args, zero groups for rings with no page work this frame + // Per-view fill dispatch args, zero groups for rings with no page work this frame. The + // overhead args gate the fill's drawcall-granularity companions (instance-count clear + + // CreateIndirect) on the same bits — safe because a skipped CreateIndirect means nothing + // reads the stale instance counts, and the tiny per-fill counter reset stays unconditional uint modelFillGroups = (_constants.fillInstanceCount + 31) / 32; uint terrainFillGroups = (_constants.fillCellCount + 31) / 32; + uint modelOverheadGroups = (_constants.fillDrawCallCount + 31) / 32; for (uint k = 0; k < 8; k++) { bool inUse = k < _constants.numClipmaps; bool hasDirty = inUse && _svsmData[0].statsDirty[k] != 0; bool hasDynamic = inUse && _svsmData[0].statsDynamicLive[k] != 0; - uint base = k * 9; + uint base = k * 15; _fillDispatchArgs[base + 0] = hasDirty ? modelFillGroups : 0; _fillDispatchArgs[base + 1] = 1; _fillDispatchArgs[base + 2] = 1; @@ -51,6 +55,12 @@ void main() _fillDispatchArgs[base + 6] = hasDirty ? terrainFillGroups : 0; _fillDispatchArgs[base + 7] = 1; _fillDispatchArgs[base + 8] = 1; + _fillDispatchArgs[base + 9] = hasDirty ? modelOverheadGroups : 0; + _fillDispatchArgs[base + 10] = 1; + _fillDispatchArgs[base + 11] = 1; + _fillDispatchArgs[base + 12] = hasDynamic ? modelOverheadGroups : 0; + _fillDispatchArgs[base + 13] = 1; + _fillDispatchArgs[base + 14] = 1; } float zRangeMin = _svsmData[0].zRangeMin; diff --git a/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang b/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang index 0001b8c..8f6a3b3 100644 --- a/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang +++ b/Source/Shaders/Shaders/Shadows/SVSMInvalidateAABBs.cs.slang @@ -28,28 +28,14 @@ void main(CSInput input) float3 aabbMin = _dirtyAABBs[aabbIndex * 2 + 0].xyz; float3 aabbMax = _dirtyAABBs[aabbIndex * 2 + 1].xyz; - float3 center = (aabbMin + aabbMax) * 0.5f; - float3 halfExtents = (aabbMax - aabbMin) * 0.5f; - - // Light-space XY rect of the world AABB, |R| trick for the extents - float3x3 lightRotation = BuildLightRotation(_constants.lightDirection.xyz); - float2 centerLS = mul(center, lightRotation).xy; - float2 halfLS = mul(halfExtents, float3x3(abs(lightRotation[0]), abs(lightRotation[1]), abs(lightRotation[2]))).xy; uint pageTableSize = _constants.pageTableSize; float pageWorld = _svsmData[0].pageWorld[clipmapIndex]; int2 anchorPageMin = int2(_svsmData[0].anchorPageMinX[clipmapIndex], _svsmData[0].anchorPageMinY[clipmapIndex]); - // Same reconstruction Prepare used for camMinusWindowMin, deterministic vs the stored anchors - float2 windowMin = float2(anchorPageMin) * pageWorld; - - // One page of pad so filter taps just outside the caster footprint refresh too - int2 pageMin = int2(floor((centerLS - halfLS - windowMin) / pageWorld)) - 1; - int2 pageMax = int2(floor((centerLS + halfLS - windowMin) / pageWorld)) + 1; - pageMin = max(pageMin, int2(0, 0)); - pageMax = min(pageMax, int2(pageTableSize - 1, pageTableSize - 1)); - - if (any(pageMax < pageMin)) + int2 pageMin; + int2 pageMax; + if (!SVSMComputeAABBPageRect(aabbMin, aabbMax, _constants.lightDirection.xyz, pageWorld, anchorPageMin, pageTableSize, pageMin, pageMax)) { return; // Outside this clipmap's window } diff --git a/Source/Shaders/Shaders/Utils/CreateIndirectAfterCulling.cs.slang b/Source/Shaders/Shaders/Utils/CreateIndirectAfterCulling.cs.slang index a16d1a1..cad5966 100644 --- a/Source/Shaders/Shaders/Utils/CreateIndirectAfterCulling.cs.slang +++ b/Source/Shaders/Shaders/Utils/CreateIndirectAfterCulling.cs.slang @@ -33,7 +33,7 @@ typedef Draw DrawType; [[vk::push_constant]] Constants _constants; [[vk::binding(0, PER_PASS)]] StructuredBuffer _drawCalls; [[vk::binding(1, PER_PASS)]] ByteAddressBuffer _drawCallDatas; -[[vk::binding(2, PER_PASS)]] StructuredBuffer _culledInstanceCounts; // One uint per draw call +[[vk::binding(2, PER_PASS)]] RWStructuredBuffer _culledInstanceCounts; // One uint per draw call, cleared after consumption [[vk::binding(3, PER_PASS)]] RWStructuredBuffer _culledDrawCalls; [[vk::binding(4, PER_PASS)]] RWByteAddressBuffer _culledDrawCallCount; @@ -75,7 +75,9 @@ void main(CSInput input) DrawType drawCall = _drawCalls[drawCallID]; - // Load instanceCount and baseInstanceLookup + // Load instanceCount and baseInstanceLookup. Clearing after consumption keeps the buffer + // always-zero between uses, so the per-fill FillBuffer reset can be skipped + // (svsmGatedFillSetup) — a fill only ever adds onto zeros uint instanceCount = _culledInstanceCounts[drawCallID]; if (instanceCount == 0) { @@ -85,6 +87,7 @@ void main(CSInput input) return; #endif } + _culledInstanceCounts[drawCallID] = 0; uint baseInstanceLookup = GetBaseInstanceLookup(drawCallID); From 45a59738d4b801fc78784d179f542a7aae763580 Mon Sep 17 00:00:00 2001 From: Pursche Date: Mon, 20 Jul 2026 21:37:07 +0200 Subject: [PATCH 22/24] Cache classifier shadow AABBs, lock-free scan epochs, hoist SVSM record-path invariants --- .../Game-Lib/Rendering/CulledRenderer.cpp | 33 ++++--- .../Game-Lib/Rendering/CulledRenderer.h | 7 +- .../Rendering/Model/ModelRenderer.cpp | 98 ++++++++++++------- .../Game-Lib/Rendering/Model/ModelRenderer.h | 20 +++- .../Game-Lib/Rendering/RenderUtils.cpp | 14 ++- .../Game-Lib/Game-Lib/Rendering/RenderUtils.h | 19 +++- .../Rendering/Terrain/TerrainRenderer.cpp | 28 +++--- .../Rendering/Terrain/TerrainRenderer.h | 2 +- 8 files changed, 141 insertions(+), 80 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index b7d9444..b5d2844 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -79,7 +79,7 @@ void CulledRenderer::DispatchInstancedFill(PassParams& params, const std::string { const u32 numInstances = params.cullingResources->GetNumInstances(); - params.commandList->PushMarker(params.passName + " " + markerName, Color::White); + params.commandList->PushMarker(markerName, Color::White); Renderer::ComputePipelineID pipeline = filtered ? _fillInstancedDrawCallsFilteredPipeline[params.cullingResources->IsIndexed()] : _fillInstancedDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()]; @@ -123,12 +123,12 @@ void CulledRenderer::DispatchInstancedFill(PassParams& params, const std::string // Per-drawcall instance counts become indirect draw args, each count cleared after consumption // (the counts buffer is always zero between uses). An indirect args buffer applies the same // Finalize gate as the fill that produced the counts -void CulledRenderer::DispatchCreateIndirect(PassParams& params, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset) +void CulledRenderer::DispatchCreateIndirect(PassParams& params, const std::string& markerName, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset) { const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); const bool debugOrdered = false; // Single-group ordered variant for determinism debugging - params.commandList->PushMarker(params.passName + " Create Indirect", Color::Yellow); + params.commandList->PushMarker(markerName, Color::Yellow); Renderer::ComputePipelineID pipeline = debugOrdered ? _createIndirectAfterCullingOrderedPipeline[params.cullingResources->IsIndexed()] : _createIndirectAfterCullingPipeline[params.cullingResources->IsIndexed()]; @@ -206,13 +206,13 @@ void CulledRenderer::OccluderPass(OccluderPassParams& params) } // Fill the occluders to draw: last frame's main-view culling output - DispatchInstancedFill(params, "Instanced Occlusion Fill", params.occluderFillDescriptorSet, false, !params.frameIndex, 0, false, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); + DispatchInstancedFill(params, params.passName + " Instanced Occlusion Fill", params.occluderFillDescriptorSet, false, !params.frameIndex, 0, false, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Create indirect argument buffer - DispatchCreateIndirect(params, params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); + DispatchCreateIndirect(params, params.passName + " Create Indirect", params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); @@ -443,7 +443,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params) params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); // Create indirect argument buffer - DispatchCreateIndirect(params, params.createIndirectAfterCullSet, ¶ms.debugDescriptorSet, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); + DispatchCreateIndirect(params, params.passName + " Create Indirect", params.createIndirectAfterCullSet, ¶ms.debugDescriptorSet, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0); params.commandList->PopMarker(); } @@ -559,7 +559,7 @@ void CulledRenderer::ClipmapCullingPass(CullingPassParams& params) // Rebuilds the shared instance counts/lookup/argument buffers from a view's bitmask slice. // filtered selects the SVSM caster-split fill variant, keepDynamic its instance class -void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic) +void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic, const std::string& fillMarkerName, const std::string& createIndirectMarkerName) { const u32 numDrawCalls = params.cullingResources->GetDrawCallCount(); @@ -589,7 +589,7 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Fill the instances visible in this view - DispatchInstancedFill(params, "Instanced Geometry Fill", params.fillDescriptorSet, filtered, params.frameIndex, viewIndex * params.cullingResources->GetBitMaskBufferUintsPerView(), keepDynamic, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, fillArgsOffset); + DispatchInstancedFill(params, fillMarkerName, params.fillDescriptorSet, filtered, params.frameIndex, viewIndex * params.cullingResources->GetBitMaskBufferUintsPerView(), keepDynamic, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, fillArgsOffset); params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); @@ -598,7 +598,7 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); // Create indirect argument buffer - DispatchCreateIndirect(params, params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, overheadArgsOffset); + DispatchCreateIndirect(params, createIndirectMarkerName, params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, fillArgsBuffer, overheadArgsOffset); params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::COMPUTE); @@ -617,6 +617,12 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) const bool profileSVSM = params.svsmPass && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmProfileGeometry"_h) != 0; RenderUtils::SVSMGeometryProfiler Profiled(_renderer, *params.commandList, "SVSM", profileSVSM); + // View-invariant marker strings and cvar reads, the loop body runs up to 8 views x 2 fills + const std::string fillMarkerName = params.passName + " Instanced Geometry Fill"; + const std::string createIndirectMarkerName = params.passName + " Create Indirect"; + const std::string drawMarkerName = params.passName + " " + std::to_string(numDrawCalls); + const bool svsmClipRectsEnabled = params.svsmPass && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; + for (u32 i = params.firstViewIndex; i < params.numShadowViews + 1; i++) { std::string markerName = (i == 0) ? "Main" : "Clipmap " + std::to_string(i - 1); @@ -679,7 +685,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) { // The main view (i == 0) consumes the culling pass's output directly, cascades rebuild the // shared instance counts/lookup/argument buffers from their bitmask slice before drawing - Profiled("Fill", i, [&] { RunInstancedGeometryFill(params, i, params.svsmSplitFills, false); }); + Profiled("Fill", i, [&] { RunInstancedGeometryFill(params, i, params.svsmSplitFills, false, fillMarkerName, createIndirectMarkerName); }); } if (!params.cullingEnabled) @@ -692,7 +698,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) if (params.enableDrawing) { - params.commandList->PushMarker(params.passName + " " + std::to_string(numDrawCalls), Color::White); + params.commandList->PushMarker(drawMarkerName, Color::White); const u32 debugDrawCallBufferIndex = 0;//CVAR_ComplexModelDebugShadowDraws.Get(); @@ -737,8 +743,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) } - const bool svsmClipRects = params.svsmPass && i > 0 - && *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; + const bool svsmClipRects = svsmClipRectsEnabled && i > 0; Profiled("Draw", i, [&] { RenderUtils::DrawSVSMClipRects(svsmClipRects, drawParams, [&](DrawParams& rectDrawParams) { params.drawCallback(rectDrawParams); }); }); } @@ -759,7 +764,7 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) { params.commandList->PushMarker("Dynamic", Color::PastelOrange); - Profiled("DynFill", i, [&] { RunInstancedGeometryFill(params, i, true, true); }); + Profiled("DynFill", i, [&] { RunInstancedGeometryFill(params, i, true, true, fillMarkerName, createIndirectMarkerName); }); DrawParams drawParams; drawParams.cullingEnabled = params.cullingEnabled; diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h index efd843a..8601ea9 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h @@ -334,7 +334,7 @@ class CulledRenderer u32 svsmFillArgsDynamicOverheadOffset = 0; }; void GeometryPass(GeometryPassParams& params); - void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic); // Shared-buffer rebuild from a view's bitmask slice + void RunInstancedGeometryFill(GeometryPassParams& params, u32 viewIndex, bool filtered, bool keepDynamic, const std::string& fillMarkerName, const std::string& createIndirectMarkerName); // Shared-buffer rebuild from a view's bitmask slice; markers prebuilt by the caller, this runs per view void ClipmapCullingPass(CullingPassParams& params); // Instanced-only frustum cull of cascade views, no occlusion, no counter resets // The instanced cull dispatch shared by CullingPass and ClipmapCullingPass: one thread per @@ -350,8 +350,9 @@ class CulledRenderer // The CreateIndirectAfterCulling dispatch shared by the occluder, culling and geometry-fill // paths: per-drawcall instance counts become indirect draw args (each count is cleared after - // consumption, keeping the counts buffer always-zero between uses). debugSet is optional - void DispatchCreateIndirect(PassParams& params, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset); + // consumption, keeping the counts buffer always-zero between uses). debugSet is optional. + // Both helpers take the full marker string prebuilt so per-view callers pay it once + void DispatchCreateIndirect(PassParams& params, const std::string& markerName, Renderer::DescriptorSetResource& createIndirectSet, Renderer::DescriptorSetResource* debugSet, u32 baseInstanceLookupOffset, u32 drawCallDataSize, Renderer::BufferResource indirectArgsBuffer, u32 indirectArgsByteOffset); void SyncToGPU(); void BindCullingResource(CullingResourcesBase& resources); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index a5e506b..dea72c3 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -115,8 +115,10 @@ void ModelRenderer::Update(f32 deltaTime) // Transitioning into the dynamic class: the shadow baked at the PREVIOUS position must // come out of the static pages too, a single-frame move can be arbitrarily large. - // Already-classified instances need nothing, the dynamic pool is transient per frame - if (!_dynamicCasterLastSignal.contains(instanceID)) + // Already-classified instances only refresh their cached shadow AABB, the dynamic + // pool is transient per frame + auto casterIt = _dynamicCasterStates.find(instanceID); + if (casterIt == _dynamicCasterStates.end()) { QueueShadowInvalidation(instanceID, matrix); // Pre-move matrix } @@ -124,6 +126,11 @@ void ModelRenderer::Update(f32 deltaTime) matrix = transform.GetMatrix(); _instanceMatrices.SetDirtyElement(instanceID); + if (casterIt != _dynamicCasterStates.end()) + { + ComputeInstanceShadowAABB(instanceID, matrix, casterIt->second.aabbMin, casterIt->second.aabbMax); + } + // Moved this frame -> dynamic shadow caster this frame _dynamicInstanceQueue.enqueue(instanceID); }); @@ -157,18 +164,19 @@ void ModelRenderer::Update(f32 deltaTime) constexpr f32 dynamicGraceSeconds = 0.5f; // A first-time stamp is a transition IN: the instance's baked pose must come out of the - // static pages (movers additionally invalidated their pre-move footprint above) + // static pages (movers additionally invalidated their pre-move footprint above). The + // shadow AABB caches in the entry — it only changes with the transform, which the + // dirty-transform view refreshes, so re-stamps just bump the time auto Stamp = [&](u32 instanceID) { - auto [it, inserted] = _dynamicCasterLastSignal.try_emplace(instanceID, _dynamicCasterTime); + auto [it, inserted] = _dynamicCasterStates.try_emplace(instanceID); + DynamicCasterState& state = it->second; + state.lastSignal = _dynamicCasterTime; if (inserted) { _dynamicCasterTransitionsIn++; - QueueShadowInvalidation(instanceID, _instanceMatrices[instanceID]); - } - else - { - it->second = _dynamicCasterTime; + ComputeInstanceShadowAABB(instanceID, _instanceMatrices[instanceID], state.aabbMin, state.aabbMax); + QueueShadowInvalidation(state.aabbMin, state.aabbMax); } }; @@ -220,19 +228,23 @@ void ModelRenderer::Update(f32 deltaTime) } u32 animatedModelID = it->first; - std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]); - ModelManifest& manifest = _modelManifests[animatedModelID]; // The full placement scan only runs when the cached near-camera subset can be - // stale: first scan, camera moved past the hysteresis slack, or placements changed + // stale: first scan, camera moved past the hysteresis slack, or the instance-set + // epoch advanced (any model's add/remove — spare rescans are cheap and rare). + // The manifest mutex is only taken for the scan itself; the steady-state tick + // walks the cached subset lock-free + const u32 instanceSetEpoch = _instanceSetEpoch.load(std::memory_order_acquire); vec3 toScanCamera = state.scanCameraPos - cameraPos; - if (!state.scanned || glm::dot(toScanCamera, toScanCamera) > rescanDistSq || manifest.instanceSetDirty) + if (!state.scanned || glm::dot(toScanCamera, toScanCamera) > rescanDistSq || state.lastSeenInstanceEpoch != instanceSetEpoch) { state.scanned = true; state.scanCameraPos = cameraPos; + state.lastSeenInstanceEpoch = instanceSetEpoch; state.nearInstances.clear(); - manifest.instanceSetDirty = false; + std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]); + const ModelManifest& manifest = _modelManifests[animatedModelID]; for (u32 placementID : manifest.instances) { vec3 toCamera = vec3(_instanceMatrices[placementID][3]) - cameraPos; @@ -247,10 +259,20 @@ void ModelRenderer::Update(f32 deltaTime) { vec3 toCamera = vec3(_instanceMatrices[placementID][3]) - cameraPos; f32 distSq = glm::dot(toCamera, toCamera); - f32 limitSq = _dynamicCasterLastSignal.contains(placementID) ? leaveDistSq : enterDistSq; + + // One lookup serves both the hysteresis limit and the re-stamp + auto casterIt = _dynamicCasterStates.find(placementID); + f32 limitSq = casterIt != _dynamicCasterStates.end() ? leaveDistSq : enterDistSq; if (distSq < limitSq) { - Stamp(placementID); + if (casterIt != _dynamicCasterStates.end()) + { + casterIt->second.lastSignal = _dynamicCasterTime; + } + else + { + Stamp(placementID); + } } } ++it; @@ -269,42 +291,40 @@ void ModelRenderer::Update(f32 deltaTime) // emit this frame's mask feed and dynamic AABB list. Overflow and oversize SPILL to the // static path instead of dropping: a spilled caster re-bakes every frame (page churn) but // is never excluded from both pools - for (auto it = _dynamicCasterLastSignal.begin(); it != _dynamicCasterLastSignal.end();) + for (auto it = _dynamicCasterStates.begin(); it != _dynamicCasterStates.end();) { u32 liveInstanceID = it->first; + const DynamicCasterState& state = it->second; - if (_dynamicCasterTime - it->second > dynamicGraceSeconds) + if (_dynamicCasterTime - state.lastSignal > dynamicGraceSeconds) { _dynamicCasterTransitionsOut++; - QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); - it = _dynamicCasterLastSignal.erase(it); + QueueShadowInvalidation(state.aabbMin, state.aabbMax); + it = _dynamicCasterStates.erase(it); continue; } if (split) { - vec3 aabbMin, aabbMax; - ComputeInstanceShadowAABB(liveInstanceID, _instanceMatrices[liveInstanceID], aabbMin, aabbMax); - - vec3 extent = aabbMax - aabbMin; + vec3 extent = state.aabbMax - state.aabbMin; const bool oversized = glm::max(extent.x, glm::max(extent.y, extent.z)) > oversizeLimit; const bool overCap = _dynamicCasterAABBs.size() >= ShadowRenderer::SVSM_MAX_DYNAMIC_AABBS * 2; if (oversized || overCap) { _dynamicCastersDropped++; - QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); + QueueShadowInvalidation(state.aabbMin, state.aabbMax); } else { _dynamicCasterLiveIDs.push_back(liveInstanceID); - _dynamicCasterAABBs.push_back(vec4(aabbMin, 0.0f)); - _dynamicCasterAABBs.push_back(vec4(aabbMax, 0.0f)); + _dynamicCasterAABBs.push_back(vec4(state.aabbMin, 0.0f)); + _dynamicCasterAABBs.push_back(vec4(state.aabbMax, 0.0f)); } } else { // Split off: v1-style per-frame static invalidation - QueueShadowInvalidation(liveInstanceID, _instanceMatrices[liveInstanceID]); + QueueShadowInvalidation(state.aabbMin, state.aabbMax); } ++it; @@ -608,7 +628,7 @@ void ModelRenderer::Clear() ShadowInvalidation drainedInvalidation; while (_shadowInvalidationQueue.try_dequeue(drainedInvalidation)) {} _animatedModelLastPushTime.clear(); - _dynamicCasterLastSignal.clear(); + _dynamicCasterStates.clear(); _dynamicCasterLiveIDs.clear(); _dynamicCasterAABBs.clear(); _dynamicCasterTransitionsIn = 0; @@ -2080,7 +2100,11 @@ void ModelRenderer::QueueShadowInvalidation(u32 instanceID, const mat4x4& transf { vec3 aabbMin, aabbMax; ComputeInstanceShadowAABB(instanceID, transformMatrix, aabbMin, aabbMax); + QueueShadowInvalidation(aabbMin, aabbMax); +} +void ModelRenderer::QueueShadowInvalidation(const vec3& aabbMin, const vec3& aabbMax) +{ ShadowInvalidation invalidation; invalidation.min = vec4(aabbMin, 0.0f); invalidation.max = vec4(aabbMax, 0.0f); @@ -2143,7 +2167,7 @@ u32 ModelRenderer::AddInstance(entt::entity entityID, u32 modelID, Model::Comple { std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]); manifest.instances.insert(instanceOffsets.instanceIndex); - manifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } // Set up InstanceManifest @@ -2192,7 +2216,7 @@ void ModelRenderer::RemoveInstance(u32 instanceID) std::scoped_lock lock(*_modelManifestsInstancesMutexes[instanceData.modelID]); manifest.instances.erase(instanceID); manifest.skyboxInstances.erase(instanceID); - manifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } std::scoped_lock lock(_instanceOffsetsMutex); @@ -2234,7 +2258,7 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo std::scoped_lock lock(*_modelManifestsInstancesMutexes[oldModelID]); oldManifest.instances.erase(instanceID); oldManifest.skyboxInstances.erase(instanceID); - oldManifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } // Deallocate old animation data @@ -2249,7 +2273,7 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo { std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]); newManifest.instances.insert(instanceID); - newManifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } // Modify InstanceData @@ -3693,7 +3717,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc // Remove the non skybox instance modelManifest.instances.erase(instanceID); - modelManifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } else { @@ -3702,7 +3726,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc // Add the non skybox instance modelManifest.instances.insert(instanceID); - modelManifest.instanceSetDirty = true; + _instanceSetEpoch.fetch_add(1, std::memory_order_release); } } @@ -3977,7 +4001,9 @@ void ModelRenderer::SyncToGPU() BindCullingResource(_transparentSkyboxCullingResources); // Rebuild the SVSM dynamic instance mask from the classifier's live set (Update drained the - // raw signals into it earlier this frame) + // raw signals into it earlier this frame). Nothing live now and nothing masked last frame + // means every bit is already zero + if (!_dynamicCasterLiveIDs.empty() || !_dynamicInstanceIDs.empty()) { ZoneScopedN("Sync Dynamic Instance Mask"); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h index 247bf44..65a4ab5 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h @@ -110,7 +110,6 @@ class ModelRenderer : CulledRenderer robin_hood::unordered_set skyboxInstances; robin_hood::unordered_set originallyTransparentDrawIDs; - bool instanceSetDirty = false; // Set under the instances mutex on add/remove, consumed by the animated-caster scan cache }; struct InstanceManifest @@ -412,6 +411,7 @@ class ModelRenderer : CulledRenderer void ComputeInstanceShadowAABB(u32 instanceID, const mat4x4& transformMatrix, vec3& outMin, vec3& outMax); void QueueShadowInvalidation(u32 instanceID, const mat4x4& transformMatrix); + void QueueShadowInvalidation(const vec3& aabbMin, const vec3& aabbMax); // For callers holding a cached AABB void CompactInstanceRefs(); void SyncToGPU(); @@ -486,8 +486,16 @@ class ModelRenderer : CulledRenderer // last signal so brief pauses don't churn the static cache; entering pulls the baked pose out // of the static pages, expiring bakes it back. A despawned instance's stale entry expires // within the grace and costs one spurious page refresh at worst (RemoveInstance already - // invalidated its real footprint) - robin_hood::unordered_map _dynamicCasterLastSignal; + // invalidated its real footprint). + // The shadow AABB is cached in the entry and recomputed only when it can change (classifier + // entry, transform update) — the per-frame tick reads it instead of redoing the glm math + struct DynamicCasterState + { + f32 lastSignal = 0.0f; + vec3 aabbMin = vec3(0.0f); + vec3 aabbMax = vec3(0.0f); + }; + robin_hood::unordered_map _dynamicCasterStates; std::vector _dynamicCasterLiveIDs; // This frame's live set, feeds the mask sync std::vector _dynamicCasterAABBs; // World (min, max) pairs of the live set, rebuilt each frame f32 _dynamicCasterTime = 0.0f; @@ -521,11 +529,17 @@ class ModelRenderer : CulledRenderer f32 lastPushTime = 0.0f; // Accumulated seconds of the last bone push vec3 scanCameraPos = vec3(0.0f); // Camera position the cached subset was scanned from bool scanned = false; + u32 lastSeenInstanceEpoch = 0; // _instanceSetEpoch value the cached subset was scanned at std::vector nearInstances; // Placements within leaveDist at scan time }; moodycamel::ConcurrentQueue _uninstancedAnimatedModelQueue; robin_hood::unordered_map _animatedModelLastPushTime; // Keyed by modelID + // Bumped on every instance add/remove/re-model (any model, any thread). The animated-caster + // scan caches compare against it so the steady-state tick never takes a manifest mutex; an + // epoch bump from an unrelated model costs the few in-grace models one spare rescan + std::atomic _instanceSetEpoch = 0; + // GPU-only workbuffers Renderer::BufferID _occluderArgumentBuffer; Renderer::BufferID _argumentBuffer; diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp index af80599..38e122b 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp @@ -33,19 +33,17 @@ void RenderUtils::Init(Renderer::Renderer* renderer, GameRenderer* gameRenderer) } } -void RenderUtils::SVSMGeometryProfiler::operator()(const char* stageName, u32 viewIndex, const std::function& work) const +Renderer::TimeQueryID RenderUtils::SVSMGeometryProfiler::BeginStage(const char* stageName, u32 viewIndex) const { - if (!_enabled) - { - work(); - return; - } - Renderer::TimeQueryDesc timeQueryDesc; timeQueryDesc.name = _namePrefix + " " + stageName + " v" + std::to_string(viewIndex); Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc); _commandList->BeginTimeQuery(timeQuery); - work(); + return timeQuery; +} + +void RenderUtils::SVSMGeometryProfiler::EndStage(Renderer::TimeQueryID timeQuery) const +{ _commandList->EndTimeQuery(timeQuery); } diff --git a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h index 641fc92..046e2cb 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h +++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h @@ -5,9 +5,9 @@ #include #include #include +#include #include -#include #include namespace Renderer @@ -161,9 +161,24 @@ class RenderUtils { } - void operator()(const char* stageName, u32 viewIndex, const std::function& work) const; + template + void operator()(const char* stageName, u32 viewIndex, Work&& work) const + { + if (!_enabled) + { + work(); + return; + } + + Renderer::TimeQueryID timeQuery = BeginStage(stageName, viewIndex); + work(); + EndStage(timeQuery); + } private: + Renderer::TimeQueryID BeginStage(const char* stageName, u32 viewIndex) const; + void EndStage(Renderer::TimeQueryID timeQuery) const; + Renderer::Renderer* _renderer = nullptr; Renderer::CommandList* _commandList = nullptr; std::string _namePrefix; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index a76747e..86f6b72 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -190,7 +190,7 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render // Fill the occluders to draw FillDrawCallsParams fillParams; - fillParams.passName = "Occluders"; + fillParams.markerName = "Occluders Fill"; fillParams.cellCount = cellCount; fillParams.viewIndex = 0; fillParams.diffAgainstPrev = false; @@ -398,7 +398,7 @@ void TerrainRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Render // Fill the cells to draw FillDrawCallsParams fillParams; - fillParams.passName = "Geometry"; + fillParams.markerName = "Geometry Fill"; fillParams.cellCount = cellCount; fillParams.viewIndex = 0; fillParams.diffAgainstPrev = true; @@ -626,6 +626,17 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re // created pool binds per frame (image binds write the descriptor immediately) data.svsmSet.Bind("_pagePool"_h, data.pagePool); + // View-invariant fill params, cvar reads and marker strings — the loop body runs per clipmap + const bool svsmClipRects = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; + + FillDrawCallsParams fillParams; + fillParams.markerName = "SVSM Geometry Fill"; + fillParams.cellCount = cellCount; + fillParams.diffAgainstPrev = false; + fillParams.currentBitmaskIndex = frameIndex; + fillParams.fillSet = data.fillSet; + fillParams.fillArgsBuffer = data.svsmFillArgsBuffer; + for (u32 i = 1; i < numClipmaps + 1; i++) { std::string markerName = "Clipmap " + std::to_string(i - 1); @@ -640,16 +651,8 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re if (CVAR_TerrainGeometryEnabled.Get()) { - FillDrawCallsParams fillParams; - fillParams.passName = "SVSM Geometry"; - fillParams.cellCount = cellCount; - fillParams.viewIndex = i; - fillParams.diffAgainstPrev = false; - fillParams.currentBitmaskIndex = frameIndex; - fillParams.fillSet = data.fillSet; - // Finalize zeroed the group count for rings with no dirty pages this frame - fillParams.fillArgsBuffer = data.svsmFillArgsBuffer; + fillParams.viewIndex = i; fillParams.fillArgsByteOffset = (i - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + ShadowRenderer::SVSM_FILL_ARGS_TERRAIN_OFFSET; Profiled("Fill", i, [&] { FillDrawCalls(frameIndex, graphResources, commandList, fillParams); }); @@ -678,7 +681,6 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re drawParams.drawDescriptorSet = data.geometryPassSet; drawParams.svsmDescriptorSet = data.svsmSet; - const bool svsmClipRects = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmClipRects"_h) != 0; Profiled("Draw", i, [&] { RenderUtils::DrawSVSMClipRects(svsmClipRects, drawParams, [&](DrawParams& rectDrawParams) { Draw(resources, frameIndex, graphResources, commandList, rectDrawParams); }); }); commandList.PopMarker(); @@ -1411,7 +1413,7 @@ void TerrainRenderer::Draw(const RenderResources& resources, u8 frameIndex, Rend void TerrainRenderer::FillDrawCalls(u8 frameIndex, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, FillDrawCallsParams& params) { - commandList.PushMarker(params.passName + " Fill", Color::White); + commandList.PushMarker(params.markerName, Color::White); Renderer::ComputePipelineID pipeline = _fillDrawCallsPipeline; commandList.BeginPipeline(pipeline); diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h index fba4cd0..5739921 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.h @@ -108,7 +108,7 @@ class TerrainRenderer struct FillDrawCallsParams { public: - std::string passName; + std::string markerName; // Full marker string, prebuilt by the caller (the SVSM pass fills per view) u32 cellCount; u32 viewIndex; From 73f28430b5a67ee8cd362ac36a259d5799a797eb Mon Sep 17 00:00:00 2001 From: Pursche Date: Mon, 20 Jul 2026 22:41:15 +0200 Subject: [PATCH 23/24] Slight ShadowRenderer improvement --- .../Rendering/Shadow/ShadowRenderer.cpp | 672 ++++++++++-------- .../Rendering/Shadow/ShadowRenderer.h | 5 +- 2 files changed, 380 insertions(+), 297 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp index 25b8db3..da3ac91 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp @@ -480,352 +480,432 @@ bool ShadowRenderer::IsSVSMActive() const return CVAR_ShadowEnabled.Get() != 0 && !_svsmNightActive; } -void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +struct ShadowRenderer::SVSMUpdatePassData { - struct Data + Renderer::DepthImageResource depth; + + Renderer::BufferMutableResource cameras; + Renderer::BufferMutableResource svsmDataBuffer; + Renderer::BufferMutableResource pageTableBuffer; + Renderer::BufferMutableResource freeListBuffer; + Renderer::BufferResource dirtyAABBBuffer; + Renderer::BufferMutableResource clearListBuffer; + Renderer::BufferMutableResource dynamicPageTableBuffer; + Renderer::BufferMutableResource dynamicFreeListBuffer; + Renderer::BufferMutableResource dynamicClearListBuffer; + Renderer::BufferResource dynamicAABBBuffer; + Renderer::BufferMutableResource svsmDataReadBackBuffer; + Renderer::BufferMutableResource dynamicValidateReadBackBuffer; + Renderer::ImageMutableResource pagePool; + Renderer::ImageMutableResource dynamicPagePool; + + Renderer::DescriptorSetResource prepareSet; + Renderer::DescriptorSetResource invalidateSet; + Renderer::DescriptorSetResource updateASet; + Renderer::DescriptorSetResource markSet; + Renderer::DescriptorSetResource updateBSet; + Renderer::DescriptorSetResource dynamicMarkSet; + Renderer::DescriptorSetResource dynamicUpdateSet; + Renderer::DescriptorSetResource finalizeSet; + Renderer::DescriptorSetResource clearSet; + Renderer::DescriptorSetResource dynamicClearSet; + + bool DeclareResources(ShadowRenderer& owner, RenderResources& resources, Renderer::RenderGraphBuilder& builder) + { + using BufferUsage = Renderer::BufferPassUsage; + + depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); + cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE); + svsmDataBuffer = builder.Write(owner._svsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + pageTableBuffer = builder.Write(owner._svsmPageTableBuffer, BufferUsage::COMPUTE); + freeListBuffer = builder.Write(owner._svsmFreeListBuffer, BufferUsage::COMPUTE); + dirtyAABBBuffer = builder.Read(owner._svsmDirtyAABBBuffer, BufferUsage::COMPUTE); + clearListBuffer = builder.Write(owner._svsmClearListBuffer, BufferUsage::COMPUTE); + dynamicPageTableBuffer = builder.Write(owner._svsmDynamicPageTableBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + dynamicFreeListBuffer = builder.Write(owner._svsmDynamicFreeListBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); + dynamicClearListBuffer = builder.Write(owner._svsmDynamicClearListBuffer, BufferUsage::COMPUTE); + dynamicAABBBuffer = builder.Read(owner._svsmDynamicAABBBuffer, BufferUsage::COMPUTE); + svsmDataReadBackBuffer = builder.Write(owner._svsmDataReadBackBuffer, BufferUsage::TRANSFER); + dynamicValidateReadBackBuffer = builder.Write(owner._svsmDynamicValidateReadBackBuffer, BufferUsage::TRANSFER); + builder.Write(owner._svsmFillArgsBuffer, BufferUsage::COMPUTE); + pagePool = builder.Write(owner._svsmPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); + dynamicPagePool = builder.Write(owner._svsmDynamicPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); + + prepareSet = builder.Use(owner._svsmPrepareDescriptorSet); + invalidateSet = builder.Use(owner._svsmInvalidateAABBsDescriptorSet); + updateASet = builder.Use(owner._svsmPageUpdateADescriptorSet); + markSet = builder.Use(owner._svsmPageMarkDescriptorSet); + updateBSet = builder.Use(owner._svsmPageUpdateBDescriptorSet); + dynamicMarkSet = builder.Use(owner._svsmDynamicMarkDescriptorSet); + dynamicUpdateSet = builder.Use(owner._svsmDynamicUpdateDescriptorSet); + finalizeSet = builder.Use(owner._svsmFinalizeDescriptorSet); + clearSet = builder.Use(owner._svsmPageClearDescriptorSet); + dynamicClearSet = builder.Use(owner._svsmDynamicPageClearDescriptorSet); + + return true; + } +}; + +struct ShadowRenderer::SVSMUpdateRecorder +{ + struct Constants { - Renderer::DepthImageResource depth; - - Renderer::BufferMutableResource cameras; - Renderer::BufferMutableResource svsmDataBuffer; - Renderer::BufferMutableResource pageTableBuffer; - Renderer::BufferMutableResource freeListBuffer; - Renderer::BufferResource dirtyAABBBuffer; - Renderer::BufferMutableResource clearListBuffer; - Renderer::BufferMutableResource dynamicPageTableBuffer; - Renderer::BufferMutableResource dynamicFreeListBuffer; - Renderer::BufferMutableResource dynamicClearListBuffer; - Renderer::BufferResource dynamicAABBBuffer; - Renderer::BufferMutableResource svsmDataReadBackBuffer; - Renderer::BufferMutableResource dynamicValidateReadBackBuffer; - Renderer::ImageMutableResource pagePool; - Renderer::ImageMutableResource dynamicPagePool; - - Renderer::DescriptorSetResource prepareSet; - Renderer::DescriptorSetResource invalidateSet; - Renderer::DescriptorSetResource updateASet; - Renderer::DescriptorSetResource markSet; - Renderer::DescriptorSetResource updateBSet; - Renderer::DescriptorSetResource dynamicMarkSet; - Renderer::DescriptorSetResource dynamicUpdateSet; - Renderer::DescriptorSetResource finalizeSet; - Renderer::DescriptorSetResource clearSet; - Renderer::DescriptorSetResource dynamicClearSet; + vec4 lightDirection; + u32 numClipmaps; + u32 pageTableSize; + u32 pageSize; + u32 poolPagesPerRow; + f32 clipmap0Extent; + f32 markBorderTexels; + u32 evictAge; + u32 invalidateAll; + u32 numDirtyAABBs; + u32 allocClipmap; + f32 casterMargin; + f32 zHalfRange; + u32 fillDrawCallCount; + f32 padding1; + f32 resolutionScale; + u32 dynamicPoolPagesPerRow; + u32 renderBudget; + u32 dynamicPhase; + u32 fillInstanceCount; + u32 fillCellCount; }; - const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); - const bool enabled = CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get() && !_svsmNightActive; - if (!enabled || _svsmPagePool == Renderer::ImageID::Invalid()) - return; + ShadowRenderer& owner; + SVSMUpdatePassData& data; + Renderer::RenderGraphResources& graphResources; + Renderer::CommandList& commandList; + u8 frameIndex; + u32 numClipmaps; + u32 numDirtyAABBs; + u32 numDynamicAABBs; + bool dynamicSplit; + SVSMDerivedConfig config; + u32 tableCapacity; + Constants* constants; + + SVSMUpdateRecorder(ShadowRenderer& owner, SVSMUpdatePassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, + u8 frameIndex, u32 numClipmaps, const vec3& lightDirection, u32 invalidateCause, u32 numDirtyAABBs, bool dynamicSplit, u32 numDynamicAABBs); + + void Record(); + void ClearPhysicalPoolsIfNeeded(); + void PrepareFrameStateAndClipmapAnchors(); + void InvalidatePagesTouchedByChangedCasters(); + void RecycleStaleStaticPages(); + void MarkPagesNeededByVisibleReceivers(); + void AllocateAndQueueStaticPages(); + void RebuildDynamicPageSet(); + void MarkPagesTouchedByDynamicCasters(); + void BuildClipmapCamerasAndGeometryDispatches(); + void ClearPagesQueuedForRendering(); + void CaptureDiagnostics(); +}; - // Record-time inputs, the shadow sun steps in discrete intervals so cached pages stay valid - // between steps - entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; - auto& dayNightCycle = registry->ctx().get(); - const f32 shadowTimeOfDay = ECS::Systems::GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32()); - const vec3 lightDirection = ECS::Systems::UpdateAreaLights::GetLightDirection(shadowTimeOfDay); +ShadowRenderer::SVSMUpdateRecorder::SVSMUpdateRecorder(ShadowRenderer& owner, SVSMUpdatePassData& data, + Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList, u8 frameIndex, u32 numClipmaps, + const vec3& lightDirection, u32 invalidateCause, u32 numDirtyAABBs, bool dynamicSplit, u32 numDynamicAABBs) + : owner(owner) + , data(data) + , graphResources(graphResources) + , commandList(commandList) + , frameIndex(frameIndex) + , numClipmaps(numClipmaps) + , numDirtyAABBs(numDirtyAABBs) + , numDynamicAABBs(numDynamicAABBs) + , dynamicSplit(dynamicSplit) + , config(DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE)) + , tableCapacity(SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE) + , constants(graphResources.FrameNew()) +{ + constants->lightDirection = vec4(lightDirection, 0.0f); + constants->numClipmaps = numClipmaps; + constants->pageTableSize = config.pageTableSize; + constants->pageSize = config.pageSize; + constants->poolPagesPerRow = config.poolPagesPerRow; + constants->clipmap0Extent = CVAR_SVSMClipmap0Extent.GetFloat(); + constants->markBorderTexels = CVAR_SVSMMarkBorderTexels.GetFloat(); + constants->evictAge = static_cast(glm::clamp(CVAR_SVSMPageEvictAge.Get(), 1, 254)); + constants->invalidateAll = invalidateCause; + constants->numDirtyAABBs = numDirtyAABBs; + constants->allocClipmap = 0; + constants->casterMargin = CVAR_ShadowCasterMargin.GetFloat(); + constants->zHalfRange = CVAR_SVSMZHalfRange.GetFloat(); + constants->padding1 = 0.0f; + constants->resolutionScale = CVAR_SVSMResolutionScale.GetFloat(); + constants->dynamicPoolPagesPerRow = dynamicSplit ? config.dynamicPoolPagesPerRow : 0; + constants->renderBudget = static_cast(glm::max(CVAR_SVSMRenderBudget.Get(), 0)); + constants->dynamicPhase = 0; + + // Finalize turns these record-time counts into same-frame per-view indirect dispatch args. + constants->fillInstanceCount = owner._modelRenderer->GetOpaqueCullingResources().GetNumInstances(); + constants->fillCellCount = owner._terrainRenderer->GetNumDrawCalls(); + constants->fillDrawCallCount = owner._modelRenderer->GetOpaqueCullingResources().GetDrawCallCount(); +} - u32 invalidateCause = 0; - if (CVAR_SVSMInvalidateAll.Get() != 0 || _svsmForceInvalidateAll) - { - invalidateCause |= SVSMCause::Manual; - CVAR_SVSMInvalidateAll.Set(0); - _svsmForceInvalidateAll = false; - } - if (_svsmDirtyAABBOverflow) +void ShadowRenderer::SVSMUpdateRecorder::Record() +{ + GPU_SCOPED_PROFILER_ZONE(commandList, SVSMUpdate); + + ClearPhysicalPoolsIfNeeded(); + PrepareFrameStateAndClipmapAnchors(); + InvalidatePagesTouchedByChangedCasters(); + RecycleStaleStaticPages(); + MarkPagesNeededByVisibleReceivers(); + AllocateAndQueueStaticPages(); + + if (dynamicSplit) { - invalidateCause |= SVSMCause::AABBOverflow; + RebuildDynamicPageSet(); } - const u32 numDirtyAABBs = static_cast(_svsmDirtyAABBs.size() / 2); - const bool dynamicSplit = CVAR_SVSMDynamicSplit.Get() == 1; - const u32 numDynamicAABBs = dynamicSplit ? static_cast(_svsmDynamicAABBs.size() / 2) : 0; - - renderGraph->AddPass("SVSM Update", - [this, &resources](Data& data, Renderer::RenderGraphBuilder& builder) - { - using BufferUsage = Renderer::BufferPassUsage; - - data.depth = builder.Read(resources.depth, Renderer::PipelineType::COMPUTE); - - data.cameras = builder.Write(resources.cameras.GetBuffer(), BufferUsage::COMPUTE); - data.svsmDataBuffer = builder.Write(_svsmDataBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); - data.pageTableBuffer = builder.Write(_svsmPageTableBuffer, BufferUsage::COMPUTE); - data.freeListBuffer = builder.Write(_svsmFreeListBuffer, BufferUsage::COMPUTE); - data.dirtyAABBBuffer = builder.Read(_svsmDirtyAABBBuffer, BufferUsage::COMPUTE); // CPU-uploaded input, the shaders only read it - data.clearListBuffer = builder.Write(_svsmClearListBuffer, BufferUsage::COMPUTE); - data.dynamicPageTableBuffer = builder.Write(_svsmDynamicPageTableBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); // TRANSFER: the svsmValidateDynamic snapshot copies from it - data.dynamicFreeListBuffer = builder.Write(_svsmDynamicFreeListBuffer, BufferUsage::COMPUTE | BufferUsage::TRANSFER); - data.dynamicClearListBuffer = builder.Write(_svsmDynamicClearListBuffer, BufferUsage::COMPUTE); - data.dynamicAABBBuffer = builder.Read(_svsmDynamicAABBBuffer, BufferUsage::COMPUTE); // CPU-uploaded input, the shaders only read it - data.svsmDataReadBackBuffer = builder.Write(_svsmDataReadBackBuffer, BufferUsage::TRANSFER); - data.dynamicValidateReadBackBuffer = builder.Write(_svsmDynamicValidateReadBackBuffer, BufferUsage::TRANSFER); - builder.Write(_svsmFillArgsBuffer, BufferUsage::COMPUTE); // Finalize writes the per-view fill dispatch args - data.pagePool = builder.Write(_svsmPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); - data.dynamicPagePool = builder.Write(_svsmDynamicPagePool, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD); - - data.prepareSet = builder.Use(_svsmPrepareDescriptorSet); - data.invalidateSet = builder.Use(_svsmInvalidateAABBsDescriptorSet); - data.updateASet = builder.Use(_svsmPageUpdateADescriptorSet); - data.markSet = builder.Use(_svsmPageMarkDescriptorSet); - data.updateBSet = builder.Use(_svsmPageUpdateBDescriptorSet); - data.dynamicMarkSet = builder.Use(_svsmDynamicMarkDescriptorSet); - data.dynamicUpdateSet = builder.Use(_svsmDynamicUpdateDescriptorSet); - data.finalizeSet = builder.Use(_svsmFinalizeDescriptorSet); - data.clearSet = builder.Use(_svsmPageClearDescriptorSet); - data.dynamicClearSet = builder.Use(_svsmDynamicPageClearDescriptorSet); + BuildClipmapCamerasAndGeometryDispatches(); + ClearPagesQueuedForRendering(); + CaptureDiagnostics(); +} - return true; // Return true from setup to enable this pass, return false to disable it - }, - [this, frameIndex, numClipmaps, lightDirection, invalidateCause, numDirtyAABBs, dynamicSplit, numDynamicAABBs](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) - { - GPU_SCOPED_PROFILER_ZONE(commandList, SVSMUpdate); +void ShadowRenderer::SVSMUpdateRecorder::ClearPhysicalPoolsIfNeeded() +{ + if (!owner._svsmPoolNeedsClear) + return; - struct SVSMConstants - { - vec4 lightDirection; - u32 numClipmaps; - u32 pageTableSize; - u32 pageSize; - u32 poolPagesPerRow; - f32 clipmap0Extent; - f32 markBorderTexels; - u32 evictAge; - u32 invalidateAll; - u32 numDirtyAABBs; - u32 allocClipmap; - f32 casterMargin; - f32 zHalfRange; - u32 fillDrawCallCount; - f32 padding1; - f32 resolutionScale; - u32 dynamicPoolPagesPerRow; - u32 renderBudget; - u32 dynamicPhase; - u32 fillInstanceCount; - u32 fillCellCount; - }; - - const SVSMDerivedConfig config = DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE); - const u32 pageTableSize = config.pageTableSize; - - SVSMConstants* constants = graphResources.FrameNew(); - constants->lightDirection = vec4(lightDirection, 0.0f); - constants->numClipmaps = numClipmaps; - constants->pageTableSize = config.pageTableSize; - constants->pageSize = config.pageSize; - constants->poolPagesPerRow = config.poolPagesPerRow; - constants->clipmap0Extent = CVAR_SVSMClipmap0Extent.GetFloat(); - constants->markBorderTexels = CVAR_SVSMMarkBorderTexels.GetFloat(); - constants->evictAge = static_cast(glm::clamp(CVAR_SVSMPageEvictAge.Get(), 1, 254)); - constants->invalidateAll = invalidateCause; - constants->numDirtyAABBs = numDirtyAABBs; - constants->allocClipmap = 0; - constants->casterMargin = CVAR_ShadowCasterMargin.GetFloat(); - constants->zHalfRange = CVAR_SVSMZHalfRange.GetFloat(); - constants->padding1 = 0.0f; - constants->resolutionScale = CVAR_SVSMResolutionScale.GetFloat(); - constants->dynamicPoolPagesPerRow = dynamicSplit ? config.dynamicPoolPagesPerRow : 0; - constants->renderBudget = static_cast(glm::max(CVAR_SVSMRenderBudget.Get(), 0)); - constants->dynamicPhase = 0; - // The same record-time counts the geometry passes size their fills from, Finalize - // turns them into per-view indirect dispatch args gated on this frame's page stats - constants->fillInstanceCount = _modelRenderer->GetOpaqueCullingResources().GetNumInstances(); - constants->fillCellCount = _terrainRenderer->GetNumDrawCalls(); - constants->fillDrawCallCount = _modelRenderer->GetOpaqueCullingResources().GetDrawCallCount(); - - if (_svsmPoolNeedsClear) - { - // One-shot zero of the fresh pools: uninitialized VRAM must never be sampled, and - // depth atomics against garbage would keep the garbage - commandList.Clear(data.pagePool, uvec4(0, 0, 0, 0)); - commandList.ImageBarrier(data.pagePool); - commandList.Clear(data.dynamicPagePool, uvec4(0, 0, 0, 0)); - commandList.ImageBarrier(data.dynamicPagePool); - _svsmPoolNeedsClear = false; - } + // Fresh VRAM must be zero before sampling or reversed-depth atomics can preserve garbage. + commandList.Clear(data.pagePool, uvec4(0, 0, 0, 0)); + commandList.ImageBarrier(data.pagePool); + commandList.Clear(data.dynamicPagePool, uvec4(0, 0, 0, 0)); + commandList.ImageBarrier(data.dynamicPagePool); + owner._svsmPoolNeedsClear = false; +} - // The AABB lists upload from Update through the frame-synced staging ring, the render - // graph issues a global upload barrier before any pass executes +void ShadowRenderer::SVSMUpdateRecorder::PrepareFrameStateAndClipmapAnchors() +{ + // Detect global invalidation, snap clipmap windows and reset frame statistics/list headers. + commandList.BeginPipeline(owner._svsmPreparePipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.prepareSet, frameIndex); + commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(owner._svsmPreparePipeline); + + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + // Clear-list header resets must be visible to the later atomic appenders. Barriers are + // per-buffer, so publishing svsmData alone does not cover either list. + commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); +} - // Prepare: invalidation detection, window anchors, per-frame stat reset - commandList.BeginPipeline(_svsmPreparePipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); +void ShadowRenderer::SVSMUpdateRecorder::InvalidatePagesTouchedByChangedCasters() +{ + if (numDirtyAABBs == 0) + return; - commandList.BindDescriptorSet(data.prepareSet, frameIndex); + commandList.BeginPipeline(owner._svsmInvalidateAABBsPipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.invalidateSet, frameIndex); + commandList.Dispatch((numDirtyAABBs + 63) / 64, numClipmaps, 1); + commandList.EndPipeline(owner._svsmInvalidateAABBsPipeline); - commandList.Dispatch(1, 1, 1); - commandList.EndPipeline(_svsmPreparePipeline); + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); +} - commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - // The clear-list header resets must be visible to UpdateB / DynamicUpdate's atomic - // appends; barriers are per-buffer, the svsmData one above does not cover them - commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); +void ShadowRenderer::SVSMUpdateRecorder::RecycleStaleStaticPages() +{ + // Apply toroidal/global invalidation, age entries and return stale physical pages. The full + // capacity is visited so entries orphaned by configuration changes also age out. + commandList.BeginPipeline(owner._svsmPageUpdateAPipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.updateASet, frameIndex); + commandList.Dispatch(tableCapacity / 256, 1, 1); + commandList.EndPipeline(owner._svsmPageUpdateAPipeline); + + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE); +} - // Moved and animated instances invalidate the pages under them - if (numDirtyAABBs > 0) - { - commandList.BeginPipeline(_svsmInvalidateAABBsPipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); - commandList.BindDescriptorSet(data.invalidateSet, frameIndex); +void ShadowRenderer::SVSMUpdateRecorder::MarkPagesNeededByVisibleReceivers() +{ + commandList.BeginPipeline(owner._svsmPageMarkPipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + data.markSet.Bind("_depth"_h, data.depth); + commandList.BindDescriptorSet(data.markSet, frameIndex); - commandList.Dispatch((numDirtyAABBs + 63) / 64, numClipmaps, 1); - commandList.EndPipeline(_svsmInvalidateAABBsPipeline); + const uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); + commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); + commandList.EndPipeline(owner._svsmPageMarkPipeline); - commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - } + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); +} - // Update A: toroidal and global invalidation, aging, eviction. Visits the full table - // capacity so entries orphaned by config changes still age out - const u32 tableCapacity = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; +void ShadowRenderer::SVSMUpdateRecorder::AllocateAndQueueStaticPages() +{ + // Allocate finest-to-coarsest so pool starvation lands on the coarsest fallback rings. This + // also re-dirties invalid pages and builds the dirty rectangles and physical clear list. + commandList.BeginPipeline(owner._svsmPageUpdateBPipeline); + commandList.BindDescriptorSet(data.updateBSet, frameIndex); - commandList.BeginPipeline(_svsmPageUpdateAPipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); - commandList.BindDescriptorSet(data.updateASet, frameIndex); + for (u32 clipmapIndex = 0; clipmapIndex < numClipmaps; clipmapIndex++) + { + Constants* allocConstants = graphResources.FrameNew(); + *allocConstants = *constants; + allocConstants->allocClipmap = clipmapIndex; - commandList.Dispatch(tableCapacity / 256, 1, 1); - commandList.EndPipeline(_svsmPageUpdateAPipeline); + commandList.PushConstant(allocConstants, 0, sizeof(Constants)); + commandList.Dispatch((config.pageTableSize * config.pageTableSize) / 256, 1, 1); - commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + if (clipmapIndex + 1 < numClipmaps) + { commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE); + } + } - // Mark: pages touched by visible depth samples - commandList.BeginPipeline(_svsmPageMarkPipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); - - data.markSet.Bind("_depth"_h, data.depth); - commandList.BindDescriptorSet(data.markSet, frameIndex); + commandList.EndPipeline(owner._svsmPageUpdateBPipeline); - uvec2 depthDimensions = graphResources.GetImageDimensions(data.depth); - commandList.Dispatch((depthDimensions.x + 15) / 16, (depthDimensions.y + 15) / 16, 1); + commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); +} - commandList.EndPipeline(_svsmPageMarkPipeline); +void ShadowRenderer::SVSMUpdateRecorder::MarkPagesTouchedByDynamicCasters() +{ + if (numDynamicAABBs == 0) + return; - commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + Constants* dynamicMarkConstants = graphResources.FrameNew(); + *dynamicMarkConstants = *constants; + dynamicMarkConstants->numDirtyAABBs = numDynamicAABBs; - // Update B: allocation, re-dirtying, dirty rects. One dispatch per clipmap, finest - // first with a free list barrier between, so pool starvation always lands on the - // coarsest ring where the sampler fallback costs the least - commandList.BeginPipeline(_svsmPageUpdateBPipeline); - commandList.BindDescriptorSet(data.updateBSet, frameIndex); + commandList.BeginPipeline(owner._svsmDynamicMarkPipeline); + commandList.PushConstant(dynamicMarkConstants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.dynamicMarkSet, frameIndex); - for (u32 clipmapIndex = 0; clipmapIndex < numClipmaps; clipmapIndex++) - { - SVSMConstants* allocConstants = graphResources.FrameNew(); - *allocConstants = *constants; - allocConstants->allocClipmap = clipmapIndex; + commandList.Dispatch((numDynamicAABBs + 63) / 64, numClipmaps, 1); + commandList.EndPipeline(owner._svsmDynamicMarkPipeline); - commandList.PushConstant(allocConstants, 0, sizeof(SVSMConstants)); - commandList.Dispatch((pageTableSize * pageTableSize) / 256, 1, 1); + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); +} - if (clipmapIndex + 1 < numClipmaps) - { - commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE); - } - } +void ShadowRenderer::SVSMUpdateRecorder::RebuildDynamicPageSet() +{ + MarkPagesTouchedByDynamicCasters(); + + // Release before acquire. Combining free-list pushes and pops in one dispatch can expose a + // slot before its physical page index is written, aliasing one page under two table entries. + commandList.BeginPipeline(owner._svsmDynamicUpdatePipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.dynamicUpdateSet, frameIndex); + commandList.Dispatch(tableCapacity / 256, 1, 1); + + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); + + Constants* dynamicAcquireConstants = graphResources.FrameNew(); + *dynamicAcquireConstants = *constants; + dynamicAcquireConstants->dynamicPhase = 1; + + commandList.PushConstant(dynamicAcquireConstants, 0, sizeof(Constants)); + commandList.Dispatch(tableCapacity / 256, 1, 1); + commandList.EndPipeline(owner._svsmDynamicUpdatePipeline); + + commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); +} - commandList.EndPipeline(_svsmPageUpdateBPipeline); +void ShadowRenderer::SVSMUpdateRecorder::BuildClipmapCamerasAndGeometryDispatches() +{ + commandList.BeginPipeline(owner._svsmFinalizePipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); + commandList.BindDescriptorSet(data.finalizeSet, frameIndex); - commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE); + commandList.Dispatch(1, 1, 1); + commandList.EndPipeline(owner._svsmFinalizePipeline); - // Caster split: mark pages under dynamic casters (visible ones only), then run the - // transient dynamic page lifecycle. Must precede Finalize, which unions the rects - if (dynamicSplit) - { - if (numDynamicAABBs > 0) - { - SVSMConstants* dynamicMarkConstants = graphResources.FrameNew(); - *dynamicMarkConstants = *constants; - dynamicMarkConstants->numDirtyAABBs = numDynamicAABBs; - - commandList.BeginPipeline(_svsmDynamicMarkPipeline); - commandList.PushConstant(dynamicMarkConstants, 0, sizeof(SVSMConstants)); - commandList.BindDescriptorSet(data.dynamicMarkSet, frameIndex); + commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); +} - commandList.Dispatch((numDynamicAABBs + 63) / 64, numClipmaps, 1); - commandList.EndPipeline(_svsmDynamicMarkPipeline); +void ShadowRenderer::SVSMUpdateRecorder::ClearPagesQueuedForRendering() +{ + commandList.BeginPipeline(owner._svsmPageClearPipeline); + commandList.PushConstant(constants, 0, sizeof(Constants)); - commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); - } + data.clearSet.Bind("_pagePool"_h, data.pagePool); + commandList.BindDescriptorSet(data.clearSet, frameIndex); - // Two dispatches: release then acquire. Free-list pushes and pops in one dispatch - // race (a pop can read a slot before the pushed phys index lands), aliasing one - // physical page under two table entries — the ghost-shadow bug - commandList.BeginPipeline(_svsmDynamicUpdatePipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); // dynamicPhase 0 - commandList.BindDescriptorSet(data.dynamicUpdateSet, frameIndex); - commandList.Dispatch(tableCapacity / 256, 1, 1); - - commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); - - SVSMConstants* dynamicAcquireConstants = graphResources.FrameNew(); - *dynamicAcquireConstants = *constants; - dynamicAcquireConstants->dynamicPhase = 1; - - commandList.PushConstant(dynamicAcquireConstants, 0, sizeof(SVSMConstants)); - commandList.Dispatch(tableCapacity / 256, 1, 1); - commandList.EndPipeline(_svsmDynamicUpdatePipeline); - - commandList.BufferBarrier(data.dynamicPageTableBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.dynamicFreeListBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.dynamicClearListBuffer, Renderer::BufferPassUsage::COMPUTE); - commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE); - } + commandList.DispatchIndirect(data.clearListBuffer, 0); + commandList.EndPipeline(owner._svsmPageClearPipeline); - // Finalize: clipmap render cameras from the anchors and this frame's dirty rects - commandList.BeginPipeline(_svsmFinalizePipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + if (!dynamicSplit) + return; - commandList.BindDescriptorSet(data.finalizeSet, frameIndex); + Constants* dynamicClearConstants = graphResources.FrameNew(); + *dynamicClearConstants = *constants; + dynamicClearConstants->poolPagesPerRow = constants->dynamicPoolPagesPerRow; - commandList.Dispatch(1, 1, 1); - commandList.EndPipeline(_svsmFinalizePipeline); + commandList.BeginPipeline(owner._svsmPageClearPipeline); + commandList.PushConstant(dynamicClearConstants, 0, sizeof(Constants)); - commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE); + data.dynamicClearSet.Bind("_pagePool"_h, data.dynamicPagePool); + commandList.BindDescriptorSet(data.dynamicClearSet, frameIndex); - // Clear this frame's dirty pages, one workgroup per page from the list UpdateB built - commandList.BeginPipeline(_svsmPageClearPipeline); - commandList.PushConstant(constants, 0, sizeof(SVSMConstants)); + commandList.DispatchIndirect(data.dynamicClearListBuffer, 0); + commandList.EndPipeline(owner._svsmPageClearPipeline); +} - data.clearSet.Bind("_pagePool"_h, data.pagePool); - commandList.BindDescriptorSet(data.clearSet, frameIndex); +void ShadowRenderer::SVSMUpdateRecorder::CaptureDiagnostics() +{ + commandList.CopyBuffer(data.svsmDataReadBackBuffer, 0, data.svsmDataBuffer, 0, sizeof(u32) * SVSM_DATA_UINT_COUNT); - commandList.DispatchIndirect(data.clearListBuffer, 0); - commandList.EndPipeline(_svsmPageClearPipeline); + if (!dynamicSplit || owner._svsmValidatePending || CVAR_SVSMValidateDynamic.Get() == 0) + return; - // Caster split: every live dynamic page clears (and later re-renders) each frame. - // Same shader, dynamic list + pool, pool row count overridden - if (dynamicSplit) - { - SVSMConstants* dynamicClearConstants = graphResources.FrameNew(); - *dynamicClearConstants = *constants; - dynamicClearConstants->poolPagesPerRow = constants->dynamicPoolPagesPerRow; + const u32 tableUints = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; + commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, 0, data.dynamicPageTableBuffer, 0, sizeof(u32) * tableUints); + commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, sizeof(u32) * tableUints, data.dynamicFreeListBuffer, 0, sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES)); + owner._svsmValidatePending = true; +} - commandList.BeginPipeline(_svsmPageClearPipeline); - commandList.PushConstant(dynamicClearConstants, 0, sizeof(SVSMConstants)); +void ShadowRenderer::AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex) +{ + const u32 numClipmaps = static_cast(glm::clamp(CVAR_SVSMNumClipmaps.Get(), 1, static_cast(SVSM_MAX_CLIPMAPS))); + const bool enabled = CVAR_ShadowEnabled.Get() && !CVAR_SVSMFreeze.Get() && !_svsmNightActive; + if (!enabled || _svsmPagePool == Renderer::ImageID::Invalid()) + return; - data.dynamicClearSet.Bind("_pagePool"_h, data.dynamicPagePool); - commandList.BindDescriptorSet(data.dynamicClearSet, frameIndex); + // Record-time inputs, the shadow sun steps in discrete intervals so cached pages stay valid + // between steps + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + auto& dayNightCycle = registry->ctx().get(); + const f32 shadowTimeOfDay = ECS::Systems::GetShadowTimeOfDay(dayNightCycle.GetTimeInSecondsF32()); + const vec3 lightDirection = ECS::Systems::UpdateAreaLights::GetLightDirection(shadowTimeOfDay); - commandList.DispatchIndirect(data.dynamicClearListBuffer, 0); - commandList.EndPipeline(_svsmPageClearPipeline); - } + u32 invalidateCause = 0; + if (CVAR_SVSMInvalidateAll.Get() != 0 || _svsmForceInvalidateAll) + { + invalidateCause |= SVSMCause::Manual; + CVAR_SVSMInvalidateAll.Set(0); + _svsmForceInvalidateAll = false; + } + if (_svsmDirtyAABBOverflow) + { + invalidateCause |= SVSMCause::AABBOverflow; + } - commandList.CopyBuffer(data.svsmDataReadBackBuffer, 0, data.svsmDataBuffer, 0, sizeof(u32) * SVSM_DATA_UINT_COUNT); + const u32 numDirtyAABBs = static_cast(_svsmDirtyAABBs.size() / 2); + const bool dynamicSplit = CVAR_SVSMDynamicSplit.Get() == 1; + const u32 numDynamicAABBs = dynamicSplit ? static_cast(_svsmDynamicAABBs.size() / 2) : 0; - // One-shot dynamic pool validation snapshot, Update maps and checks it next frame - if (dynamicSplit && !_svsmValidatePending && CVAR_SVSMValidateDynamic.Get() != 0) - { - const u32 tableUints = SVSM_MAX_CLIPMAPS * SVSM_MAX_PAGE_TABLE_SIZE * SVSM_MAX_PAGE_TABLE_SIZE; - commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, 0, data.dynamicPageTableBuffer, 0, sizeof(u32) * tableUints); - commandList.CopyBuffer(data.dynamicValidateReadBackBuffer, sizeof(u32) * tableUints, data.dynamicFreeListBuffer, 0, sizeof(u32) * (4 + SVSM_MAX_POOL_PAGES)); - _svsmValidatePending = true; - } + renderGraph->AddPass("SVSM Update", + [this, &resources](SVSMUpdatePassData& data, Renderer::RenderGraphBuilder& builder) + { + return data.DeclareResources(*this, resources, builder); + }, + [this, frameIndex, numClipmaps, lightDirection, invalidateCause, numDirtyAABBs, dynamicSplit, numDynamicAABBs](SVSMUpdatePassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) + { + SVSMUpdateRecorder recorder(*this, data, graphResources, commandList, frameIndex, numClipmaps, + lightDirection, invalidateCause, numDirtyAABBs, dynamicSplit, numDynamicAABBs); + recorder.Record(); }); } @@ -1292,4 +1372,4 @@ void ShadowRenderer::BindCameraBuffers(RenderResources& resources) _svsmPrepareDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); _svsmPageMarkDescriptorSet.Bind("_srcCameras"_h, camerasBuffer); _svsmFinalizeDescriptorSet.Bind("_rwCameras"_h, camerasBuffer); -} \ No newline at end of file +} diff --git a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h index 5ad5b65..4b5852e 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h +++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h @@ -122,6 +122,9 @@ class ShadowRenderer Renderer::ImageID GetSVSMDynamicPagePoolOrPlaceholder() const { return _svsmDynamicPagePool != Renderer::ImageID::Invalid() ? _svsmDynamicPagePool : _svsmPagePoolPlaceholder; } private: + struct SVSMUpdatePassData; + struct SVSMUpdateRecorder; + void CreatePermanentResources(RenderResources& resources); void ResetSVSMPoolState(RenderResources& resources); @@ -206,4 +209,4 @@ class ShadowRenderer // threshold must not flicker the cache), left immediately with a full re-bake bool _svsmNightActive = false; f32 _svsmNightTimer = 0.0f; -}; \ No newline at end of file +}; From 528da1d6b2934a5fdb56209b6785a8a6376a2dce Mon Sep 17 00:00:00 2001 From: Pursche Date: Thu, 23 Jul 2026 19:10:04 +0200 Subject: [PATCH 24/24] Fade the sun below the horizon, always-filtered SVSM fills, indirect-read barriers, per-area multiplicative shadow tint --- .../Game-Lib/ECS/Systems/UpdateAreaLights.cpp | 6 +-- .../Game-Lib/Rendering/CulledRenderer.cpp | 17 +++++-- .../Rendering/Material/MaterialRenderer.cpp | 2 +- .../Rendering/Model/ModelRenderer.cpp | 47 ++++++++++--------- .../Rendering/Terrain/TerrainRenderer.cpp | 5 +- .../Shaders/Include/Lighting.inc.slang | 14 ++++-- 6 files changed, 56 insertions(+), 35 deletions(-) diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp index 1732d06..67de43e 100644 --- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp +++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp @@ -19,7 +19,7 @@ #include #include -AutoCVar_Int CVAR_SunDebugFullRotation(CVarCategory::Client | CVarCategory::Rendering, "sunDebugFullRotation", "debug: sun does a full rotation per day instead of the authored wobble", 0, CVarFlags::EditCheckbox); +AutoCVar_Int CVAR_SunFullRotation(CVarCategory::Client | CVarCategory::Rendering, "sunFullRotation", "sun does a full rotation per day instead of the authored wobble, the sun sets at night", 1, CVarFlags::EditCheckbox); AutoCVar_Float CVAR_ShadowSunUpdateInterval(CVarCategory::Client | CVarCategory::Rendering, "shadowSunUpdateInterval", "game seconds between shadow sun direction updates, a continuously rotating sun re-renders every shadow texel each frame and shimmers", 120.0f); namespace ECS::Systems @@ -329,7 +329,7 @@ namespace ECS::Systems const vec3& ambientColor = areaLightInfo.finalColorData.ambientColor; vec3 groundAmbientColor = ambientColor * 1.0f; vec3 skyAmbientColor = ambientColor * 1.0f; - vec3 shadowColor = vec3(77.f/255.f, 77.f/255.f, 77.f/255.f); + const vec3& shadowColor = areaLightInfo.finalColorData.shadowColor; // Per-area authored tint, multiplied onto the shadowed directional term constexpr f32 ambientIntensity = 1.0f; if (!materialRenderer->SetDirectionalLight(0, direction, diffuseColor, 1.0f, groundAmbientColor, ambientIntensity, skyAmbientColor, ambientIntensity, shadowColor)) @@ -365,7 +365,7 @@ namespace ECS::Systems f32 progressDayAndNight = timeOfDay / 86400.0f; - if (CVAR_SunDebugFullRotation.Get()) + if (CVAR_SunFullRotation.Get()) { // Full rotation per day, midnight (progress 0) puts the sun straight down, noon straight up phiValue = progressDayAndNight * glm::two_pi(); diff --git a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp index b5d2844..b8237fa 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp @@ -574,7 +574,10 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi // Reset the counters. The per-drawcall instance counts skip their FillBuffer in the gated // path: CreateIndirect clears each count after consuming it, so the buffer is always zero - // between uses (and a gated-off CreateIndirect means nothing read the counts this view) + // between uses (and a gated-off CreateIndirect means nothing read the counts this view). + // The previous view's draws consumed the culled args at DRAW_INDIRECT — this view's + // CreateIndirect rewrite needs the execution dependency against that read (WAR) + params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::GRAPHICS); params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER); params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER); if (!gatedSetup) @@ -593,7 +596,9 @@ void CulledRenderer::RunInstancedGeometryFill(GeometryPassParams& params, u32 vi params.commandList->BufferBarrier(params.culledInstanceCountsBuffer, Renderer::BufferPassUsage::COMPUTE); - // The draws consume this count, a zero-work view must still draw zero + // The draws consume this count, a zero-work view must still draw zero. The previous view's + // draws read it at DRAW_INDIRECT (and CreateIndirect wrote it in compute) — wait on both + params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::GRAPHICS | Renderer::BufferPassUsage::COMPUTE); params.commandList->FillBuffer(params.culledDrawCallCountBuffer, 0, sizeof(u32), 0); params.commandList->BufferBarrier(params.culledDrawCallCountBuffer, Renderer::BufferPassUsage::TRANSFER); @@ -684,8 +689,12 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params) else if (params.cullingResources->IsInstanced() && params.cullingResources->HasSupportForTwoStepCulling() && i > 0) { // The main view (i == 0) consumes the culling pass's output directly, cascades rebuild the - // shared instance counts/lookup/argument buffers from their bitmask slice before drawing - Profiled("Fill", i, [&] { RunInstancedGeometryFill(params, i, params.svsmSplitFills, false, fillMarkerName, createIndirectMarkerName); }); + // shared instance counts/lookup/argument buffers from their bitmask slice before drawing. + // Always the FILTERED fill: the geometry fill set's Vk layout is the union of both fill + // permutations (it includes the dynamic mask binding), and the unfiltered pipeline's own + // layout is incompatible with it (VUID-00358). keepDynamic 0 against an all-zero mask + // keeps every instance, so this is the unfiltered behavior whenever the split is idle + Profiled("Fill", i, [&] { RunInstancedGeometryFill(params, i, true, false, fillMarkerName, createIndirectMarkerName); }); } if (!params.cullingEnabled) diff --git a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp index ca6fe36..4087836 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Material/MaterialRenderer.cpp @@ -365,7 +365,7 @@ void MaterialRenderer::CreatePermanentResources() samplerDesc.enabled = true; samplerDesc.filter = Renderer::SamplerFilter::ANISOTROPIC; samplerDesc.maxAnisotropy = 16; - samplerDesc.mipLODBias = -0.5f; + //samplerDesc.mipLODBias = -0.5f; samplerDesc.addressU = Renderer::TextureAddressMode::WRAP; samplerDesc.addressV = Renderer::TextureAddressMode::WRAP; samplerDesc.addressW = Renderer::TextureAddressMode::CLAMP; diff --git a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp index ee4fbaf..68a0d15 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.cpp @@ -1224,11 +1224,11 @@ void ModelRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Rend data.svsmData = builder.Read(shadowRenderer->GetSVSMDataBuffer(), BufferUsage::GRAPHICS); data.pageTable = builder.Read(shadowRenderer->GetSVSMPageTableBuffer(), BufferUsage::GRAPHICS); data.svsmFillArgsBuffer = builder.Read(shadowRenderer->GetSVSMFillArgsBuffer(), BufferUsage::COMPUTE); // Consumed read-only by DispatchIndirect in the per-view fills + builder.Read(_dynamicInstanceMask.GetBuffer(), BufferUsage::COMPUTE); // The static fills always run the filtered pipeline against the mask, split live or not if (splitFills) { data.dynamicPagePool = builder.Write(shadowRenderer->GetSVSMDynamicPagePool(), Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::LOAD); data.dynamicPageTable = builder.Read(shadowRenderer->GetSVSMDynamicPageTableBuffer(), BufferUsage::GRAPHICS); - builder.Read(_dynamicInstanceMask.GetBuffer(), BufferUsage::COMPUTE); data.svsmDynamicDrawCountReadBackBuffer = builder.Write(_svsmDynamicDrawCountReadBackBuffer, BufferUsage::TRANSFER); data.svsmDynamicSet = builder.Use(_svsmDynamicDrawDescriptorSet); } @@ -4112,9 +4112,9 @@ void ModelRenderer::SyncToGPU() } // Rebuild the SVSM dynamic instance mask from the classifier's live set (Update drained the - // raw signals into it earlier this frame). Nothing live now and nothing masked last frame - // means every bit is already zero - if (!_dynamicCasterLiveIDs.empty() || !_dynamicInstanceIDs.empty()) + // raw signals into it earlier this frame). The buffer always exists and stays bound — the + // SVSM fills always run the filtered pipeline against it (an all-zero mask with keepDynamic 0 + // keeps everything); only the bit updates skip when nothing is live and nothing was masked { ZoneScopedN("Sync Dynamic Instance Mask"); @@ -4129,27 +4129,30 @@ void ModelRenderer::SyncToGPU() } } - // Clear last frame's bits, then set this frame's. The bit test doubles as dedupe - for (u32 instanceID : _dynamicInstanceIDs) + if (!_dynamicCasterLiveIDs.empty() || !_dynamicInstanceIDs.empty()) { - u32 word = instanceID / 32; - _dynamicInstanceMask[word] &= ~(1u << (instanceID & 31)); - _dynamicInstanceMask.SetDirtyElement(word); - } - _dynamicInstanceIDs.clear(); - - for (u32 instanceID : _dynamicCasterLiveIDs) - { - u32 word = instanceID / 32; - if (word >= wordsNeeded) - continue; - - u32 bit = 1u << (instanceID & 31); - if ((_dynamicInstanceMask[word] & bit) == 0) + // Clear last frame's bits, then set this frame's. The bit test doubles as dedupe + for (u32 instanceID : _dynamicInstanceIDs) { - _dynamicInstanceMask[word] |= bit; + u32 word = instanceID / 32; + _dynamicInstanceMask[word] &= ~(1u << (instanceID & 31)); _dynamicInstanceMask.SetDirtyElement(word); - _dynamicInstanceIDs.push_back(instanceID); + } + _dynamicInstanceIDs.clear(); + + for (u32 instanceID : _dynamicCasterLiveIDs) + { + u32 word = instanceID / 32; + if (word >= wordsNeeded) + continue; + + u32 bit = 1u << (instanceID & 31); + if ((_dynamicInstanceMask[word] & bit) == 0) + { + _dynamicInstanceMask[word] |= bit; + _dynamicInstanceMask.SetDirtyElement(word); + _dynamicInstanceIDs.push_back(instanceID); + } } } diff --git a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp index bbad712..4fe4f9b 100644 --- a/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp +++ b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp @@ -644,9 +644,10 @@ void TerrainRenderer::AddSVSMGeometryPass(Renderer::RenderGraph* renderGraph, Re std::string markerName = "Clipmap " + std::to_string(i - 1); commandList.PushMarker(markerName, Color::White); - // Reset the counters + // Reset the counters. The previous view's draw consumed the args at DRAW_INDIRECT + // and the fill wrote them in compute — the reset must wait on both (WAR/WAW) { - commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); + commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::GRAPHICS | Renderer::BufferPassUsage::COMPUTE | Renderer::BufferPassUsage::TRANSFER); commandList.FillBuffer(data.argumentBuffer, 4, 16, 0); // Reset everything but indexCount to 0 commandList.BufferBarrier(data.argumentBuffer, Renderer::BufferPassUsage::TRANSFER); } diff --git a/Source/Shaders/Shaders/Include/Lighting.inc.slang b/Source/Shaders/Shaders/Include/Lighting.inc.slang index 4655e0f..5e8eb58 100644 --- a/Source/Shaders/Shaders/Include/Lighting.inc.slang +++ b/Source/Shaders/Shaders/Include/Lighting.inc.slang @@ -61,12 +61,20 @@ float3 ApplyLighting(float3 materialColor, PixelVertexData pixelVertexData, uint // Directional Light float nDotL = saturate(dot(pixelVertexData.worldNormal, light.direction.xyz)); // Dot product between normal and light direction - float3 lightColor = light.color.rgb * light.color.a * nDotL; - // Directional Light Shadows + // The directional light fades out with the sun's elevation (shadowSettings.strength is + // clamp(sunElevationSin / 0.1), same ramp the shadow fade rides): below the horizon a + // Lambert term would light down-facing surfaces from underneath, ambient/skyband carry + // the night look instead. Like the shadow lerp below, this assumes light 0 is the sun + float3 lightColor = light.color.rgb * light.color.a * nDotL * shadowSettings.strength; + + // Directional Light Shadows: shadowColor is a multiplicative tint on the directional + // term (fully shadowed keeps shadowColor's fraction of it), never a replacement — a + // constant floor exceeds the Lambert term at low sun/grazing angles and inverts the + // shadows. Shadowed can never exceed lit, and ambient carries shadowed areas if (shadowSettings.enableShadows) { - lightColor = lerp(light.shadowColor.rgb, lightColor, shadowFactor); + lightColor *= lerp(light.shadowColor.rgb, float3(1.0f, 1.0f, 1.0f), shadowFactor); } directionalColor += lightColor;