diff --git a/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp b/Source/Game-Lib/Game-Lib/ECS/Scheduler.cpp
index fbf2ea3d..65c82a20 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"
@@ -109,7 +108,6 @@ namespace ECS
Systems::OrbitalCamera::Update(gameRegistry, clampedDeltaTime);
Systems::CalculateCameraMatrices::Update(gameRegistry, clampedDeltaTime);
Systems::CharacterControllerInput::UpdateHoveredUnit(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 e368a428..00000000
--- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.cpp
+++ /dev/null
@@ -1,294 +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
-#include
-
-AutoCVar_Int CVAR_ShadowsStable(CVarCategory::Client | CVarCategory::Rendering, "shadowStable", "stable shadows", 1, 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);
-
-namespace ECS::Systems
-{
- inline vec4 EncodePlane(vec3 position, vec3 normal)
- {
- vec3 normalizedNormal = glm::normalize(normal);
- vec4 result = vec4(normalizedNormal, glm::dot(normalizedNormal, position));
- return result;
- }
-
- 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;
-
- // Initialize any new shadow cascades
- if (numCascades != renderResources.shadowDepthCascades.size())
- {
- u32 numCameras = renderResources.cameras.Count();
- u32 numCamerasToAdd = numCascades - numCameras;
- renderResources.cameras.AddCount(numCamerasToAdd);
-
- 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);
- }
- }
-
- entt::registry::context& ctx = registry.ctx();
- auto& dayNightCycle = ctx.get();
-
- // Get light settings
- vec3 lightDirection = UpdateAreaLights::GetLightDirection(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;
-
- f32 minZ = nearClip;
- f32 maxZ = nearClip + clipRange;
-
- 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
- upDir = vec3(0.0f, 1.0f, 0.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
- 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;
- }
-
- vec3 cascadeExtents = maxExtents - minExtents;
-
- // Get postion of the shadow camera
- 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
-
- 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);
- 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 b036e206..00000000
--- a/Source/Game-Lib/Game-Lib/ECS/Systems/CalculateShadowCameraMatrices.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma once
-#include
-#include
-
-namespace ECS::Systems
-{
- 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 c652ab9e..67de43e6 100644
--- a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp
+++ b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.cpp
@@ -17,9 +17,24 @@
#include
#include
+#include
+
+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
{
+ // 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;
@@ -314,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))
@@ -324,8 +339,14 @@ 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 points toward the sun
+
+ // 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;
- *CVarSystem::Get()->GetVecFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "fogColor"_h) = vec4(areaLightInfo.finalColorData.fogColor, 1.0f);
+ *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;
}
@@ -343,24 +364,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_SunFullRotation.Get())
{
- f32 currentTimestamp = currentPhiIndex * 0.25f;
- f32 nextTimestamp = nextPhiIndex * 0.25f;
+ // Full rotation per day, midnight (progress 0) puts the sun straight down, noon straight up
+ phiValue = progressDayAndNight * glm::two_pi();
+ }
+ else
+ {
+ 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;
- phiValue = glm::mix(currentPhiValue, nextPhiValue, transitionProgress);
+ f32 transitionTime = 0.25f;
+ f32 transitionProgress = (progressDayAndNight / 0.25f) - currentPhiIndex;
+
+ f32 currentPhiValue = phiTable[currentPhiIndex];
+ f32 nextPhiValue = phiTable[nextPhiIndex];
+
+ phiValue = glm::mix(currentPhiValue, nextPhiValue, transitionProgress);
+ }
}
// Convert from Spherical Position to Cartesian coordinates
@@ -374,6 +404,8 @@ namespace ECS::Systems
f32 lightDirZ = sinPhi * sinTheta;
f32 lightDirY = cosPhi;
+ // Points toward the sun (the shading convention); SVSM consumers negate this to get the
+ // direction the light travels
return vec3(lightDirX, -lightDirY, -lightDirZ);
}
}
\ No newline at end of file
diff --git a/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h b/Source/Game-Lib/Game-Lib/ECS/Systems/UpdateAreaLights.h
index c48f4ec7..2985c768 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 a468f2ba..d4e453f5 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
@@ -79,7 +80,141 @@ 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);
+ const u32 numClipmaps = static_cast(*CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmNumClipmaps"_h));
+
+ // SVSM stat block
+ {
+ GameRenderer* gameRenderer = ServiceLocator::GetGameRenderer();
+ ShadowRenderer* shadowRenderer = gameRenderer->GetShadowRenderer();
+
+ // 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)
+ {
+ 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))
+ {
+ std::string csv;
+ char line[512];
+
+ {
+ 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 transitionsIn = 0;
+ u32 transitionsOut = 0;
+ u32 droppedAABBs = 0;
+ 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());
+ csv += line;
+
+ if (ModelRenderer* statsModelRenderer = gameRenderer->GetModelRenderer())
+ {
+ std::string dynInstances = "SVSMDynInstances";
+ for (u32 view = 1; view <= numClipmaps && 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 < numClipmaps; 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());
+ }
+ }
+
+ // SVSM page table state, one frame old
+ if (shadowRenderer)
+ {
+ u32 freePages = 0;
+ u32 totalPages = 0;
+ u32 overflow = 0;
+ 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%s", totalPages - glm::min(freePages, totalPages), totalPages, overflow > 0 ? " (request overflow!)" : "");
+
+ u32 dynamicLive = 0;
+ u32 dynamicTotal = 0;
+ u32 dynamicOverflow = 0;
+ shadowRenderer->GetSVSMDynamicStats(dynamicLive, dynamicTotal, dynamicOverflow);
+
+ u32 dynamicCasters = 0;
+ u32 transitionsIn = 0;
+ u32 transitionsOut = 0;
+ u32 droppedAABBs = 0;
+ shadowRenderer->GetSVSMCasterStats(dynamicCasters, transitionsIn, transitionsOut, 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())
+ {
+ 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());
+ }
+
+ i32 renderBudget = *CVarSystem::Get()->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmRenderBudget"_h);
+ if (renderBudget > 0)
+ {
+ 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)
+ {
+ _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++)
+ {
+ 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();
@@ -131,10 +266,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);
@@ -180,10 +315,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);
@@ -241,9 +376,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);
@@ -268,8 +403,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);
@@ -321,7 +456,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;
@@ -356,7 +491,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);
}
@@ -369,7 +504,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;
@@ -404,7 +539,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);
}
@@ -480,6 +615,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++)
@@ -574,7 +732,7 @@ namespace Editor
std::string viewName = "Main View Instances";
if (viewID > 0)
{
- viewName = "Shadow Cascade " + std::to_string(viewID - 1) + " Drawcalls";
+ viewName = "Clipmap " + std::to_string(viewID - 1) + " Drawcalls";
}
if (!_drawCallStatsOnlyForMainView)
@@ -594,11 +752,11 @@ namespace Editor
ImGui::Text("%s", rightHeaderText.c_str());
ImGui::Separator();
}
-
+
u32 viewDrawCalls = 0;
u32 viewDrawCallsSurvived = 0;
- bool viewSupportsTerrainOcclusionCulling = true;
+ bool viewSupportsTerrainOcclusionCulling = viewID == 0; // Clipmap views draw their full surviving set in one phase, no occluders
bool viewSupportsModelsOcclusionCulling = viewID == 0;
bool viewRendersTerrainCulling = true;
@@ -683,7 +841,7 @@ namespace Editor
std::string viewName = "Main View Triangles";
if (viewID > 0)
{
- viewName = "Shadow Cascade " + std::to_string(viewID - 1) + " Triangles";
+ viewName = "Clipmap " + std::to_string(viewID - 1) + " Triangles";
}
if (!_drawCallStatsOnlyForMainView)
@@ -707,7 +865,7 @@ namespace Editor
u32 viewTriangles = 0;
u32 viewTrianglesSurvived = 0;
- bool viewSupportsTerrainOcclusionCulling = true;
+ 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 69fd0676..d321842f 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);
@@ -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/CulledRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp
index cdbf3b95..b8237fa1 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.cpp
@@ -4,8 +4,13 @@
#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 = clipmaps, -1 = off", -1);
+
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
@@ -36,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);
}
@@ -62,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(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, 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(markerName, 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");
@@ -83,101 +191,28 @@ 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);
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)
{
- 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);
}
- // 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;
- };
-
- 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
-
- 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, 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
- {
- 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.passName + " Create Indirect", params.createIndirectDescriptorSet, nullptr, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0);
params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE);
@@ -188,11 +223,10 @@ 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;
- drawParams.depth = params.depth[0];
+ drawParams.depth = params.depth;
drawParams.argumentBuffer = params.culledDrawCallsBuffer;
drawParams.drawCountBuffer = params.culledDrawCallCountBuffer;
drawParams.drawCountIndex = 0;
@@ -223,110 +257,153 @@ 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);
+ params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER);
+ params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER);
- // Reset the counters
- {
- params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER);
- params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER);
+ params.commandList->FillBuffer(params.drawCountBuffer, 0, sizeof(u32), 0);
+ params.commandList->FillBuffer(params.triangleCountBuffer, 0, sizeof(u32), 0);
- // Reset the counters
- 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);
+
+ Renderer::ComputePipelineID pipeline = _fillDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()];
+ params.commandList->BeginPipeline(pipeline);
- if (i == 1)
+ struct FillDrawCallConstants
{
- uvec2 shadowDepthDimensions = params.graphResources->GetImageDimensions(params.depth[1]);
+ u32 numTotalDraws;
+ u32 bitmaskOffset;
+ u32 diffAgainstPrev;
+ u32 currentBitmaskIndex;
+ };
- 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);
+ 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));
- params.commandList->SetDepthBias(params.biasConstantFactor, params.biasClamp, params.biasSlopeFactor);
- }
+ // Bind descriptorset
+ params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex);
+ params.commandList->BindDescriptorSet(params.occluderFillDescriptorSet, params.frameIndex);
- // Fill the occluders to draw
- {
- std::string debugName = params.passName + " Occlusion Fill";
- params.commandList->PushMarker(debugName, Color::White);
+ params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1);
- Renderer::ComputePipelineID pipeline = _fillDrawCallsFromBitmaskPipeline[params.cullingResources->IsIndexed()];
- params.commandList->BeginPipeline(pipeline);
+ params.commandList->EndPipeline(pipeline);
- struct FillDrawCallConstants
- {
- u32 numTotalDraws;
- u32 bitmaskOffset;
- u32 diffAgainstPrev;
- u32 currentBitmaskIndex;
- };
+ params.commandList->PopMarker();
+ }
- 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));
+ params.commandList->BufferBarrier(params.culledDrawCallsBuffer, Renderer::BufferPassUsage::COMPUTE);
+ params.commandList->BufferBarrier(params.drawCountBuffer, Renderer::BufferPassUsage::COMPUTE);
+ params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::COMPUTE);
- // Bind descriptorset
- params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex);
- params.commandList->BindDescriptorSet(params.occluderFillDescriptorSet, params.frameIndex);
+ if (params.enableDrawing)
+ {
+ // Draw Occluders
+ params.commandList->PushMarker(params.passName + " Occlusion Draw " + std::to_string(numDrawCalls), Color::White);
- params.commandList->Dispatch((numDrawCalls + 31) / 32, 1, 1);
+ 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;
- params.commandList->EndPipeline(pipeline);
+ params.drawCallback(drawParams);
+ }
- 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));
- 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)
+ {
+ params.commandList->PopMarker();
+ }
+ }
+}
- 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.shadowPass = i > 0;
- 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;
+// 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();
- params.drawCallback(drawParams);
- }
+ Renderer::ComputePipelineID pipeline = _cullingInstancedPipeline[useBitmasks];
+ params.commandList->BeginPipeline(pipeline);
- // 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));
+ vec2 viewportSize = _renderer->GetRenderSize();
- if (params.enableDrawing)
- {
- params.commandList->PopMarker();
- }
+ 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->PopMarker();
- }
+ params.commandList->BindDescriptorSet(params.debugDescriptorSet, params.frameIndex);
+ params.commandList->BindDescriptorSet(params.globalDescriptorSet, params.frameIndex);
+ params.commandList->BindDescriptorSet(params.cullingDescriptorSet, params.frameIndex);
- // Finish by resetting the viewport, scissor and depth bias
- 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);
- }
+ params.commandList->Dispatch((numInstances + 31) / 32, 1, 1);
+
+ params.commandList->EndPipeline(pipeline);
}
void CulledRenderer::CullingPass(CullingPassParams& params)
@@ -340,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
@@ -360,104 +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();
+ RunInstancedCullingDispatch(params, !params.disableTwoStepCulling, true);
- 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;
- };
- 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;
- 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);
-
- 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.passName + " Create Indirect", params.createIndirectAfterCullSet, ¶ms.debugDescriptorSet, params.baseInstanceLookupOffset, params.drawCallDataSize, Renderer::BufferResource::Invalid(), 0);
params.commandList->PopMarker();
}
@@ -484,7 +470,7 @@ void CulledRenderer::CullingPass(CullingPassParams& params)
u32 viewportSizeX;
u32 viewportSizeY;
u32 maxDrawCount;
- u32 numCascades;
+ u32 numShadowViews;
u32 occlusionCull;
u32 instanceIDOffset;
u32 modelIDOffset;
@@ -499,7 +485,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;
@@ -544,33 +530,123 @@ void CulledRenderer::CullingPass(CullingPassParams& params)
}
}
+void CulledRenderer::ClipmapCullingPass(CullingPassParams& params)
+{
+ 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.numShadowViews == 0)
+ return;
+
+ // 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);
+
+ params.occlusionCull = false;
+ params.cullMainView = false;
+ params.debugDrawColliders = false;
+
+ // 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->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 std::string& fillMarkerName, const std::string& createIndirectMarkerName)
+{
+ const u32 numDrawCalls = params.cullingResources->GetDrawCallCount();
+
+ // 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).
+ // 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)
+ {
+ 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.drawCountBuffer, Renderer::BufferPassUsage::TRANSFER);
+ params.commandList->BufferBarrier(params.triangleCountBuffer, Renderer::BufferPassUsage::TRANSFER);
+
+ // Fill the instances visible in this view
+ 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);
+
+ // 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);
+
+ // Create indirect argument buffer
+ 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);
+ 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();
- for (u32 i = 0; i < params.numCascades + 1; i++)
+ // svsmProfileGeometry: per-view fill/draw GPU timings, they surface in the perf editor's
+ // render pass list
+ 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" : "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)
+ {
+ // 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
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 +686,16 @@ 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.
+ // 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)
{
@@ -621,13 +707,14 @@ 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();
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;
@@ -665,7 +752,8 @@ void CulledRenderer::GeometryPass(GeometryPassParams& params)
}
- params.drawCallback(drawParams);
+ const bool svsmClipRects = svsmClipRectsEnabled && i > 0;
+ Profiled("Draw", i, [&] { RenderUtils::DrawSVSMClipRects(svsmClipRects, drawParams, [&](DrawParams& rectDrawParams) { params.drawCallback(rectDrawParams); }); });
}
// Copy from our draw count buffer to the readback buffer
@@ -677,14 +765,44 @@ 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. 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);
+
+ Profiled("DynFill", i, [&] { RunInstancedGeometryFill(params, i, true, true, fillMarkerName, createIndirectMarkerName); });
+
+ DrawParams drawParams;
+ drawParams.cullingEnabled = params.cullingEnabled;
+ 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();
}
- // 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)
@@ -723,22 +841,33 @@ void CulledRenderer::CreatePipelines()
// Fill Drawcalls From Bitmask pipelines
Renderer::ComputePipelineDesc pipelineDesc;
{
- pipelineDesc.debugName = "FillInstancedDrawcallsFromBitmask";
-
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);
+ pipelineDesc.debugName = (filtered == 0) ? "FillInstancedDrawcallsFromBitmask" : "FillInstancedDrawcallsFromBitmaskFiltered";
- Renderer::ComputeShaderDesc shaderDesc;
- shaderDesc.shaderEntry = shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Utils/FillInstancedDrawCallsFromBitmask.cs");
+ std::vector permutationFields =
+ {
+ { "IS_INDEXED", std::to_string(i) },
+ { "DYNAMIC_FILTER", std::to_string(filtered) }
+ };
+ u32 shaderEntryNameHash = Renderer::GetShaderEntryNameHash("Utils/FillInstancedDrawCallsFromBitmask.cs", permutationFields);
- pipelineDesc.computeShader = _renderer->LoadShader(shaderDesc);
+ Renderer::ComputeShaderDesc shaderDesc;
+ shaderDesc.shaderEntry = _gameRenderer->GetShaderEntry(shaderEntryNameHash, "Utils/FillInstancedDrawCallsFromBitmask.cs");
+
+ 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 f845033b..8601ea92 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h
+++ b/Source/Game-Lib/Game-Lib/Rendering/CulledRenderer.h
@@ -37,14 +37,17 @@ 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
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;
@@ -125,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;
@@ -149,12 +152,6 @@ class CulledRenderer
u32 baseInstanceLookupOffset = 0;
u32 drawCallDataSize = 0;
- 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;
};
@@ -224,7 +221,8 @@ class CulledRenderer
Renderer::DescriptorSetResource cullingDescriptorSet;
Renderer::DescriptorSetResource createIndirectAfterCullSet;
- u32 numCascades = 0;
+ u32 numShadowViews = 0;
+ bool cullMainView = true;
bool occlusionCull = true;
bool disableTwoStepCulling = false;
@@ -246,15 +244,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 +289,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,20 +298,61 @@ class CulledRenderer
Renderer::DescriptorSetResource globalDescriptorSet;
Renderer::DescriptorSetResource fillDescriptorSet;
+ Renderer::DescriptorSetResource createIndirectDescriptorSet; // Instanced only
Renderer::DescriptorSetResource drawDescriptorSet;
std::function drawCallback;
- u32 numCascades = 0;
+ u32 baseInstanceLookupOffset = 0; // Instanced only
+ u32 drawCallDataSize = 0; // Instanced only
- f32 biasConstantFactor = 0.0f;
- f32 biasClamp = 0.0f;
- f32 biasSlopeFactor = 0.0f;
+ u32 firstViewIndex = 0; // 0 = main view first, 1 = clipmap views only
+ u32 numShadowViews = 0;
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
+
+ // 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::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, 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
+ // 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.
+ // 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);
@@ -329,6 +370,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/CullingResources.cpp b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.cpp
index cbd4d622..759ce266 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/CullingResources.h b/Source/Game-Lib/Game-Lib/Rendering/CullingResources.h
index 3199dbad..e58a8754 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 b9c1ddb5..77bd099a 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/Debug/JoltDebugRenderer.cpp
@@ -111,7 +111,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
{
@@ -173,7 +173,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;
@@ -246,7 +246,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;
@@ -291,7 +291,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
{
@@ -363,7 +363,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;
@@ -424,7 +424,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;
@@ -523,7 +523,7 @@ void JoltDebugRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, Rend
params.enableDrawing = CVAR_JoltDebugDrawGeometry.Get();
params.cullingEnabled = cullingEnabled;
- params.numCascades = 0;
+ params.numShadowViews = 0;
GeometryPass(params);
@@ -603,7 +603,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 1fbb3920..a6edb369 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.cpp
@@ -229,7 +229,7 @@ GameRenderer::GameRenderer()
_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);
@@ -317,7 +317,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
@@ -390,8 +393,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);
@@ -458,6 +459,21 @@ f32 GameRenderer::Render()
_liquidRenderer->AddCullingPass(&renderGraph, _resources, _frameIndex);
_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.
+ // 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);
+ 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);
_materialRenderer->AddPreEffectsPass(&renderGraph, _resources, _frameIndex);
@@ -474,6 +490,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);
@@ -702,6 +719,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/GameRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h
index 3df7cf70..3bc4b12e 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h
+++ b/Source/Game-Lib/Game-Lib/Rendering/GameRenderer.h
@@ -78,6 +78,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/Liquid/LiquidRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp
index 241593bd..54f6f597 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/Liquid/LiquidRenderer.cpp
@@ -272,7 +272,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
{
@@ -344,7 +344,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
@@ -500,7 +500,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 75d4cfeb..40878363 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 - 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);
@@ -129,6 +130,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;
@@ -153,6 +157,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);
@@ -181,20 +195,44 @@ 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: // 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)
{
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
@@ -205,6 +243,7 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende
Color patchEdgeColor;
Color vertexColor;
Color brushColor;
+ vec4 shadowSettings; // x = Shadow Strength, y = Normal Offset Bias, z = SVSM Constant Bias (world meters), w = UNUSED
};
Constants* constants = graphResources.FrameNew();
@@ -213,9 +252,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 u32 shadowEnabled = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled"));
- constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), 0, numCascades, shadowEnabled);
+ const f32 shadowStrength = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowStrength"));
+ // 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 shadowsReady = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled")) != 0
+ && shadowStrength > 0.0f
+ && shadowRenderer != nullptr && shadowRenderer->GetSVSMPagePool() != Renderer::ImageID::Invalid();
+ constants->lightInfo = uvec4(static_cast(_directionalLights.Count()), shadowsReady ? 1 : 0, 0, 0);
constants->tileInfo = uvec4(_lightRenderer->CalculateNumTiles2D(outputSize), 0, 0);
@@ -237,6 +281,10 @@ void MaterialRenderer::AddMaterialPass(Renderer::RenderGraph* renderGraph, Rende
constants->vertexColor = terrainTools->GetVertexColor();
constants->brushColor = terrainTools->GetBrushColor();
+ f32 shadowNormalOffsetBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowNormalOffsetBias"));
+ f32 svsmConstantBias = static_cast(*cvarSystem->GetFloatCVar(CVarCategory::Client | CVarCategory::Rendering, "svsmConstantBias"));
+ constants->shadowSettings = vec4(shadowStrength, shadowNormalOffsetBias, svsmConstantBias, 0.0f);
+
commandList.PushConstant(constants, 0, sizeof(Constants));
}
@@ -306,11 +354,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);
@@ -322,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;
@@ -353,12 +396,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 1209ae31..68a0d152 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"
@@ -15,6 +16,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"
@@ -63,10 +65,15 @@ ModelRenderer::ModelRenderer(Renderer::Renderer* renderer, GameRenderer* gameRen
, _renderer(renderer)
, _gameRenderer(gameRenderer)
, _debugRenderer(debugRenderer)
+ , _svsmDrawDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS)
+ , _svsmDynamicDrawDescriptorSet(Renderer::DescriptorSetSlot::PER_PASS)
, _ownerThreadID(std::this_thread::get_id())
{
ZoneScoped;
+ _dynamicInstanceMask.SetDebugName("ModelDynamicInstanceMask");
+ _dynamicInstanceMask.SetUsage(Renderer::BufferUsage::STORAGE_BUFFER);
+
CreatePermanentResources();
if (CVAR_ModelValidateTransfers.Get())
@@ -119,11 +126,244 @@ 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 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
+ }
+
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);
});
}
+ 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
+ // 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("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;
+ // 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;
+
+ // 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). 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] = _dynamicCasterStates.try_emplace(instanceID);
+ DynamicCasterState& state = it->second;
+ state.lastSignal = _dynamicCasterTime;
+ if (inserted)
+ {
+ _dynamicCasterTransitionsIn++;
+ ComputeInstanceShadowAABB(instanceID, _instanceMatrices[instanceID], state.aabbMin, state.aabbMax);
+ QueueShadowInvalidation(state.aabbMin, state.aabbMax);
+ }
+ };
+
+ 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))
+ {
+ _animatedModelLastPushTime[modelID].lastPushTime = _dynamicCasterTime;
+ }
+
+ 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;
+ }
+ }
+
+ if (range > 0.0f && hasCamera)
+ {
+ const f32 enterDistSq = range * range;
+ 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();)
+ {
+ AnimatedModelState& state = it->second;
+ if (_dynamicCasterTime - state.lastPushTime > dynamicGraceSeconds)
+ {
+ it = _animatedModelLastPushTime.erase(it);
+ continue;
+ }
+
+ u32 animatedModelID = it->first;
+
+ // The full placement scan only runs when the cached near-camera subset can be
+ // 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 || state.lastSeenInstanceEpoch != instanceSetEpoch)
+ {
+ state.scanned = true;
+ state.scanCameraPos = cameraPos;
+ state.lastSeenInstanceEpoch = instanceSetEpoch;
+ state.nearInstances.clear();
+
+ std::scoped_lock lock(*_modelManifestsInstancesMutexes[animatedModelID]);
+ const ModelManifest& manifest = _modelManifests[animatedModelID];
+ 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);
+
+ // 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)
+ {
+ if (casterIt != _dynamicCasterStates.end())
+ {
+ casterIt->second.lastSignal = _dynamicCasterTime;
+ }
+ else
+ {
+ Stamp(placementID);
+ }
+ }
+ }
+ ++it;
+ }
+ }
+
+ // 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 = _dynamicCasterStates.begin(); it != _dynamicCasterStates.end();)
+ {
+ u32 liveInstanceID = it->first;
+ const DynamicCasterState& state = it->second;
+
+ if (_dynamicCasterTime - state.lastSignal > dynamicGraceSeconds)
+ {
+ _dynamicCasterTransitionsOut++;
+ QueueShadowInvalidation(state.aabbMin, state.aabbMax);
+ it = _dynamicCasterStates.erase(it);
+ continue;
+ }
+
+ if (split)
+ {
+ 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(state.aabbMin, state.aabbMax);
+ }
+ else
+ {
+ _dynamicCasterLiveIDs.push_back(liveInstanceID);
+ _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(state.aabbMin, state.aabbMax);
+ }
+
+ ++it;
+ }
+ }
+
+ // Diagnostics only, nothing to read back while the dynamic draws don't run
+ if (shadowsEnabled && split)
+ {
+ 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);
+
+ _numSvsmDynamicInstancesZeroed = false;
+ }
+ else if (!_numSvsmDynamicInstancesZeroed)
+ {
+ memset(_numSvsmDynamicInstances, 0, sizeof(_numSvsmDynamicInstances));
+ _numSvsmDynamicInstancesZeroed = true;
+ }
+
{
ZoneScopedN("Update Culling Resources");
@@ -448,6 +688,34 @@ void ModelRenderer::Clear()
ChangeSkyboxRequest changeSkyboxRequest;
while (_changeSkyboxRequests.try_dequeue(changeSkyboxRequest)) {}
+ // Queued modelIDs/instanceIDs are invalid after a clear
+ u32 drainedID;
+ while (_uninstancedAnimatedModelQueue.try_dequeue(drainedID)) {}
+ while (_dynamicInstanceQueue.try_dequeue(drainedID)) {}
+ ShadowInvalidation drainedInvalidation;
+ while (_shadowInvalidationQueue.try_dequeue(drainedInvalidation)) {}
+ _animatedModelLastPushTime.clear();
+ _dynamicCasterStates.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
+ // 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);
_instancesDirty = true;
@@ -470,16 +738,10 @@ 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");
- }
-
struct Data
{
Renderer::ImageMutableResource visibilityBuffer;
- Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS];
+ Renderer::DepthImageMutableResource depth;
Renderer::BufferMutableResource culledDrawCallsBuffer;
Renderer::BufferMutableResource culledDrawCallCountBuffer;
@@ -502,16 +764,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);
- 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(resources.cameras.GetBuffer(), BufferUsage::GRAPHICS | BufferUsage::COMPUTE);
builder.Read(_vertices.GetBuffer(), BufferUsage::GRAPHICS);
@@ -536,7 +794,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);
@@ -548,10 +806,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;
@@ -579,12 +834,6 @@ void ModelRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, RenderRe
params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset);
params.drawCallDataSize = sizeof(DrawCallData);
-
- 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();
@@ -672,7 +921,8 @@ 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.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();
@@ -700,11 +950,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 numShadowViews = 0; // Main view only, cascades render in their own block late in the frame
struct Data
{
@@ -727,16 +973,12 @@ 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;
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);
@@ -754,7 +996,12 @@ 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);
+ // 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);
@@ -762,7 +1009,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);
@@ -774,7 +1021,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];
}
@@ -801,11 +1048,10 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe
Draw(resources, frameIndex, graphResources, commandList, drawParams);
};
- params.numCascades = numCascades;
+ params.baseInstanceLookupOffset = offsetof(DrawCallData, DrawCallData::baseInstanceLookupOffset);
+ params.drawCallDataSize = sizeof(DrawCallData);
- 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.numShadowViews = numShadowViews;
params.enableDrawing = CVAR_ModelDrawGeometry.Get();
params.cullingEnabled = cullingEnabled;
@@ -814,6 +1060,288 @@ void ModelRenderer::AddGeometryPass(Renderer::RenderGraph* renderGraph, RenderRe
});
}
+void ModelRenderer::AddClipmapCullingPass(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;
+
+ // The same per-view culling the main view uses, run against the clipmap cameras
+ 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
+ {
+ Renderer::ImageResource depthPyramid;
+
+ Renderer::DescriptorSetResource debugSet;
+ Renderer::DescriptorSetResource globalSet;
+ Renderer::DescriptorSetResource cullingSet;
+ };
+
+ renderGraph->AddPass("Model (O) Clipmap 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, numShadowViews](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute
+ {
+ GPU_SCOPED_PROFILER_ZONE(commandList, ModelClipmapCulling);
+
+ 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.numShadowViews = numShadowViews;
+ params.cullMainView = false;
+ params.occlusionCull = false;
+
+ params.cullingDataIsWorldspace = false;
+
+ params.baseInstanceLookupOffset = offsetof(DrawCallData, baseInstanceLookupOffset);
+ params.modelIDOffset = offsetof(DrawCallData, modelID);
+ params.drawCallDataSize = sizeof(DrawCallData);
+
+ ClipmapCullingPass(params);
+ });
+}
+
+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 (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(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
+ // 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::BufferResource svsmFillArgsBuffer;
+
+ 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);
+ 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);
+ 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.numShadowViews = numClipmaps;
+
+ params.svsmPass = true;
+ params.svsmExtent = uvec2(virtualSize, virtualSize);
+
+ 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;
+ 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;
@@ -824,7 +1352,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
{
@@ -894,7 +1422,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
@@ -1010,7 +1538,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;
@@ -1117,7 +1645,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);
});
@@ -1217,7 +1745,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);
});
@@ -1590,6 +2118,63 @@ u32 ModelRenderer::AddPlacementInstance(entt::entity entityID, u32 modelID, u64
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);
+ 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);
+ _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)
{
ZoneScopedN("ModelRenderer::AddInstance");
@@ -1625,6 +2210,7 @@ u32 ModelRenderer::AddInstance(entt::entity entityID, u32 modelID, Model::Comple
{
std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]);
manifest.instances.insert(instanceOffsets.instanceIndex);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
// Set up InstanceManifest
@@ -1651,6 +2237,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;
@@ -1664,6 +2253,9 @@ void ModelRenderer::RemoveInstance(u32 instanceID)
const u32 removedModelID = instanceData.modelID;
ModelManifest& manifest = _modelManifests[removedModelID];
+ // 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
@@ -1671,6 +2263,7 @@ void ModelRenderer::RemoveInstance(u32 instanceID)
std::scoped_lock lock(*_modelManifestsInstancesMutexes[instanceData.modelID]);
manifest.instances.erase(instanceID);
manifest.skyboxInstances.erase(instanceID);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
std::scoped_lock lock(_instanceOffsetsMutex);
@@ -1702,6 +2295,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];
@@ -1710,6 +2307,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);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
// Deallocate old animation data
@@ -1724,6 +2322,7 @@ void ModelRenderer::ModifyInstance(entt::entity entityID, u32 instanceID, u32 mo
{
std::scoped_lock lock(*_modelManifestsInstancesMutexes[modelID]);
newManifest.instances.insert(instanceID);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
// Modify InstanceData
@@ -1748,6 +2347,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;
@@ -2336,6 +2938,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;
@@ -2464,6 +3070,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;
}
@@ -2513,6 +3123,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;
@@ -2710,54 +3327,61 @@ void ModelRenderer::CreateModelPipelines()
std::vector vertexPermutationFields =
{
{ "EDITOR_PASS", "0" },
- { "SHADOW_PASS", "0"}
+ { "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", "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
+ // 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;
- // Rasterizer state
+ pipelineDesc.debugName = "Model Draw SVSM";
+
+ // Rasterizer state, no depth or color attachments
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", "1"}
};
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 =
+
+ for (u32 dynamic = 0; dynamic < 2; dynamic++)
{
- { "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);
+ 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
{
@@ -2920,6 +3544,16 @@ 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::AssertOwnerThread() const
@@ -3175,6 +3809,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc
// Remove the non skybox instance
modelManifest.instances.erase(instanceID);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
else
{
@@ -3183,6 +3818,7 @@ void ModelRenderer::MakeInstanceSkybox(u32 instanceID, InstanceManifest& instanc
// Add the non skybox instance
modelManifest.instances.insert(instanceID);
+ _instanceSetEpoch.fetch_add(1, std::memory_order_release);
}
}
@@ -3474,6 +4110,57 @@ void ModelRenderer::SyncToGPU()
BindCullingResource(_opaqueSkyboxCullingResources);
BindCullingResource(_transparentSkyboxCullingResources);
}
+
+ // Rebuild the SVSM dynamic instance mask from the classifier's live set (Update drained the
+ // 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");
+
+ 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;
+ }
+ }
+
+ if (!_dynamicCasterLiveIDs.empty() || !_dynamicInstanceIDs.empty())
+ {
+ // 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();
+
+ 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);
+ }
+ }
+ }
+
+ 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)
@@ -3482,29 +4169,41 @@ void ModelRenderer::Draw(const RenderResources& resources, u8 frameIndex, Render
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.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)
+ : _drawPipeline;
commandList.BeginPipeline(pipeline);
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/Model/ModelRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h
index cb9fb105..1195848e 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h
+++ b/Source/Game-Lib/Game-Lib/Rendering/Model/ModelRenderer.h
@@ -22,6 +22,7 @@
class DebugRenderer;
class GameRenderer;
+class ShadowRenderer;
struct RenderResources;
namespace Renderer
@@ -106,6 +107,7 @@ class ModelRenderer : CulledRenderer
robin_hood::unordered_set instances;
robin_hood::unordered_set skyboxInstances;
robin_hood::unordered_set originallyTransparentDrawIDs;
+
};
struct InstanceManifest
@@ -334,6 +336,12 @@ 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 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
+ // (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);
@@ -343,6 +351,20 @@ 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 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; }
@@ -350,6 +372,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; }
@@ -375,6 +398,10 @@ 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 QueueShadowInvalidation(const vec3& aabbMin, const vec3& aabbMax); // For callers holding a cached AABB
+
void CompactInstanceRefs();
void SyncToGPU();
@@ -428,11 +455,81 @@ class ModelRenderer : CulledRenderer
CullingResourcesIndexed _transparentSkyboxCullingResources;
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; 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 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).
+ // 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;
+ u32 _dynamicCastersDropped = 0; // Spilled to static this frame (cap or oversize), never dropped from both pools
+ 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
+ // 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 };
+ 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.
+ // 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;
+ 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/RenderResources.h b/Source/Game-Lib/Game-Lib/Rendering/RenderResources.h
index e4253829..58044a9d 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/RenderUtils.cpp b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp
index ee634559..38e122b3 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.cpp
@@ -33,6 +33,20 @@ void RenderUtils::Init(Renderer::Renderer* renderer, GameRenderer* gameRenderer)
}
}
+Renderer::TimeQueryID RenderUtils::SVSMGeometryProfiler::BeginStage(const char* stageName, u32 viewIndex) const
+{
+ Renderer::TimeQueryDesc timeQueryDesc;
+ timeQueryDesc.name = _namePrefix + " " + stageName + " v" + std::to_string(viewIndex);
+ Renderer::TimeQueryID timeQuery = _renderer->CreateTimeQuery(timeQueryDesc);
+ _commandList->BeginTimeQuery(timeQuery);
+ return timeQuery;
+}
+
+void RenderUtils::SVSMGeometryProfiler::EndStage(Renderer::TimeQueryID timeQuery) const
+{
+ _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 5d751004..046e2cbd 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h
+++ b/Source/Game-Lib/Game-Lib/Rendering/RenderUtils.h
@@ -5,8 +5,11 @@
#include
#include
#include
+#include
#include
+#include
+
namespace Renderer
{
class Renderer;
@@ -143,6 +146,65 @@ 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)
+ {
+ }
+
+ 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;
+ 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 fe13b5ac..9fa35bb8 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp
+++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.cpp
@@ -1,43 +1,229 @@
#include "ShadowRenderer.h"
+#include
+#include
#include
-#include
+#include
+#include
+#include
+
+#include
+#include
#include
#include
#include
#include
+#include
+
+#include
#include
#include
#include
-#include
-#include
#include
-AutoCVar_Int CVAR_ShadowEnabled(CVarCategory::Client | CVarCategory::Rendering, "shadowEnabled", "enable shadows", 0, CVarFlags::EditCheckbox);
+#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);
+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, 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, 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. 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);
+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);
+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);
+
+// 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 = 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
+namespace SVSMCause
+{
+ constexpr u32 Manual = 4;
+ constexpr u32 AABBOverflow = 8;
+}
-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);
+// 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));
-AutoCVar_Int CVAR_ShadowDrawMatrices(CVarCategory::Client | CVarCategory::Rendering, "shadowDrawMatrices", "debug shadow matrices by debug drawing them", 0, CVarFlags::EditCheckbox);
+ const u32 pageTableSize = glm::clamp(std::bit_floor(virtualSize / pageSize), 16u, maxPageTableSize);
+ const u32 correctedVirtualSize = pageTableSize * pageSize;
-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);
+ 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;
+}
-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);
+// 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;
+};
-#define TIMESLICED_CASCADES 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, DebugRenderer* debugRenderer, TerrainRenderer* terrainRenderer, ModelRenderer* modelRenderer, RenderResources& resources)
+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)
+ , _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)
{
ZoneScoped;
CreatePermanentResources(resources);
@@ -51,110 +237,1144 @@ void ShadowRenderer::Update(f32 deltaTime, RenderResources& resources)
{
ZoneScoped;
- CVarSystem* cvarSystem = CVarSystem::Get();
- const u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"));
+ if (SanitizeSVSMConfigCVars(SVSM_MAX_PAGE_TABLE_SIZE))
+ {
+ _svsmForceInvalidateAll = true; // Pages cached under the pre-snap addressing are garbage
+ }
- const bool debugMatrices = CVAR_ShadowDebugMatrices.Get();
- const i32 debugMatrixIndex = CVAR_ShadowDebugMatrixIndex.Get();
- if (debugMatrices && debugMatrixIndex >= 0 && debugMatrixIndex < static_cast(numCascades))
+ // 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
{
- const Camera& debugCascadeCamera = resources.cameras[debugMatrixIndex + 1]; // +1 because the first camera is the main camera
+ 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
+ }
+ }
+ }
- Camera& mainCamera = resources.cameras[0];
+ // 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;
+ }
+ }
- mainCamera = debugCascadeCamera;
- resources.cameras.SetDirtyElement(0);
+ // 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;
+ }
}
- if (CVAR_ShadowDrawMatrices.Get())
+ // 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;
+ _svsmCasterTransitionsIn = 0;
+ _svsmCasterTransitionsOut = 0;
+ _svsmDynamicAABBsDropped = 0;
+ const bool frozen = CVAR_SVSMFreeze.Get() != 0;
+ if (CVAR_ShadowEnabled.Get() && !frozen && !_svsmNightActive)
{
- Color colors[] =
+ // The pools are the SVSM VRAM cost, only allocated once shadows are 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
+ }
+
+ // 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.
+ // 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)
{
- Color::Red,
- Color::Green,
- Color::Blue,
- Color::Yellow,
- Color::Magenta,
- Color::Cyan,
- Color::PastelOrange,
- Color::PastelGreen
- };
+ 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 & SVSMPageEntry::Resident) == 0)
+ continue;
+
+ numResident++;
+ u32 physicalPage = entry & SVSMPageEntry::PhysMask;
+ 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);
+ }
- for (u32 i = 0; i < numCascades; i++)
+ // 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)
{
- const Camera& debugCascadeCamera = resources.cameras[i + 1]; // +1 because the first camera is the main camera
- _debugRenderer->DrawFrustum(debugCascadeCamera.worldToClip, colors[i]);
+ _svsmDirtyAABBOverflow = true;
+ }
+
+ // 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());
+
+ _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
+ 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
+ {
+ // 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;
}
}
}
-void ShadowRenderer::AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex)
+bool ShadowRenderer::IsSVSMActive() const
{
- struct ShadowPassData
+ return CVAR_ShadowEnabled.Get() != 0 && !_svsmNightActive;
+}
+
+struct ShadowRenderer::SVSMUpdatePassData
+{
+ 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)
{
- Renderer::DepthImageMutableResource shadowDepthCascades[Renderer::Settings::MAX_SHADOW_CASCADES];
+ using BufferUsage = Renderer::BufferPassUsage;
- Renderer::DescriptorSetResource lightDescriptorSet;
+ 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
+ {
+ 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;
};
- CVarSystem* cvarSystem = CVarSystem::Get();
- u32 numCascades = static_cast(*cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum"));
- numCascades = std::min(numCascades, static_cast(resources.shadowDepthCascades.size()));
+ 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();
+};
+
+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();
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::Record()
+{
+ GPU_SCOPED_PROFILER_ZONE(commandList, SVSMUpdate);
+
+ ClearPhysicalPoolsIfNeeded();
+ PrepareFrameStateAndClipmapAnchors();
+ InvalidatePagesTouchedByChangedCasters();
+ RecycleStaleStaticPages();
+ MarkPagesNeededByVisibleReceivers();
+ AllocateAndQueueStaticPages();
+
+ if (dynamicSplit)
+ {
+ RebuildDynamicPageSet();
+ }
+
+ BuildClipmapCamerasAndGeometryDispatches();
+ ClearPagesQueuedForRendering();
+ CaptureDiagnostics();
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::ClearPhysicalPoolsIfNeeded()
+{
+ if (!owner._svsmPoolNeedsClear)
+ return;
+
+ // 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;
+}
+
+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);
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::InvalidatePagesTouchedByChangedCasters()
+{
+ if (numDirtyAABBs == 0)
+ return;
+
+ 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.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE);
+ commandList.BufferBarrier(data.svsmDataBuffer, 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);
+}
+
+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);
+
+ 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);
+}
+
+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);
+
+ for (u32 clipmapIndex = 0; clipmapIndex < numClipmaps; clipmapIndex++)
+ {
+ Constants* allocConstants = graphResources.FrameNew();
+ *allocConstants = *constants;
+ allocConstants->allocClipmap = clipmapIndex;
+
+ commandList.PushConstant(allocConstants, 0, sizeof(Constants));
+ commandList.Dispatch((config.pageTableSize * config.pageTableSize) / 256, 1, 1);
+
+ if (clipmapIndex + 1 < numClipmaps)
+ {
+ commandList.BufferBarrier(data.freeListBuffer, Renderer::BufferPassUsage::COMPUTE);
+ }
+ }
+
+ commandList.EndPipeline(owner._svsmPageUpdateBPipeline);
+
+ commandList.BufferBarrier(data.pageTableBuffer, Renderer::BufferPassUsage::COMPUTE);
+ commandList.BufferBarrier(data.svsmDataBuffer, Renderer::BufferPassUsage::COMPUTE);
+ commandList.BufferBarrier(data.clearListBuffer, Renderer::BufferPassUsage::COMPUTE);
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::MarkPagesTouchedByDynamicCasters()
+{
+ if (numDynamicAABBs == 0)
+ return;
+
+ Constants* dynamicMarkConstants = graphResources.FrameNew();
+ *dynamicMarkConstants = *constants;
+ dynamicMarkConstants->numDirtyAABBs = numDynamicAABBs;
+
+ commandList.BeginPipeline(owner._svsmDynamicMarkPipeline);
+ commandList.PushConstant(dynamicMarkConstants, 0, sizeof(Constants));
+ commandList.BindDescriptorSet(data.dynamicMarkSet, frameIndex);
+
+ commandList.Dispatch((numDynamicAABBs + 63) / 64, numClipmaps, 1);
+ commandList.EndPipeline(owner._svsmDynamicMarkPipeline);
- if (numCascades == 0)
+ commandList.BufferBarrier(data.dynamicPageTableBuffer, 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);
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::BuildClipmapCamerasAndGeometryDispatches()
+{
+ commandList.BeginPipeline(owner._svsmFinalizePipeline);
+ commandList.PushConstant(constants, 0, sizeof(Constants));
+ commandList.BindDescriptorSet(data.finalizeSet, frameIndex);
+
+ commandList.Dispatch(1, 1, 1);
+ commandList.EndPipeline(owner._svsmFinalizePipeline);
+
+ commandList.BufferBarrier(data.cameras, Renderer::BufferPassUsage::COMPUTE);
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::ClearPagesQueuedForRendering()
+{
+ commandList.BeginPipeline(owner._svsmPageClearPipeline);
+ commandList.PushConstant(constants, 0, sizeof(Constants));
+
+ data.clearSet.Bind("_pagePool"_h, data.pagePool);
+ commandList.BindDescriptorSet(data.clearSet, frameIndex);
+
+ commandList.DispatchIndirect(data.clearListBuffer, 0);
+ commandList.EndPipeline(owner._svsmPageClearPipeline);
+
+ if (!dynamicSplit)
+ return;
+
+ Constants* dynamicClearConstants = graphResources.FrameNew();
+ *dynamicClearConstants = *constants;
+ dynamicClearConstants->poolPagesPerRow = constants->dynamicPoolPagesPerRow;
+
+ commandList.BeginPipeline(owner._svsmPageClearPipeline);
+ commandList.PushConstant(dynamicClearConstants, 0, sizeof(Constants));
+
+ data.dynamicClearSet.Bind("_pagePool"_h, data.dynamicPagePool);
+ commandList.BindDescriptorSet(data.dynamicClearSet, frameIndex);
+
+ commandList.DispatchIndirect(data.dynamicClearListBuffer, 0);
+ commandList.EndPipeline(owner._svsmPageClearPipeline);
+}
+
+void ShadowRenderer::SVSMUpdateRecorder::CaptureDiagnostics()
+{
+ commandList.CopyBuffer(data.svsmDataReadBackBuffer, 0, data.svsmDataBuffer, 0, sizeof(u32) * SVSM_DATA_UINT_COUNT);
+
+ if (!dynamicSplit || owner._svsmValidatePending || CVAR_SVSMValidateDynamic.Get() == 0)
+ return;
+
+ 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;
+}
+
+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;
- const bool shadowEnabled = CVAR_ShadowEnabled.Get();
+ // 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());
+ // GetLightDirection points toward the sun (the shading convention); SVSM works with the
+ // direction the light travels
+ 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](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();
+ });
+}
+
+void ShadowRenderer::AddSVSMDebugOverlayPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex)
+{
+ struct Data
+ {
+ Renderer::ImageMutableResource target;
+ Renderer::ImageResource pagePool;
+
+ Renderer::DescriptorSetResource debugSet;
+ Renderer::DescriptorSetResource poolDebugSet;
+ };
- renderGraph->AddPass("Shadow Pass",
- [=, &resources](ShadowPassData& data, Renderer::RenderGraphBuilder& builder)
+ 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_ShadowEnabled.Get() || (debugClipmap < 0 && !showPool))
+ return;
+
+ renderGraph->AddPass("SVSM Debug Overlay",
+ [this, &resources, showPool, poolMode](Data& data, Renderer::RenderGraphBuilder& builder)
{
- for (u32 i = 0; i < numCascades; i++)
+ data.target = builder.Write(resources.sceneColor, Renderer::PipelineType::COMPUTE, Renderer::LoadMode::LOAD);
+ builder.Read(_svsmPageTableBuffer, Renderer::BufferPassUsage::COMPUTE);
+ if (showPool)
{
- data.shadowDepthCascades[i] = builder.Write(resources.shadowDepthCascades[i], Renderer::PipelineType::GRAPHICS, Renderer::LoadMode::CLEAR);
+ data.pagePool = builder.Read(poolMode == 2 ? _svsmDynamicPagePool : _svsmPagePool, Renderer::PipelineType::COMPUTE);
}
- data.lightDescriptorSet = builder.Use(resources.lightDescriptorSet);
+ 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
},
- [=](ShadowPassData& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList)
+ [this, frameIndex, debugClipmap, numClipmaps, showPool, poolMode](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList)
{
- Renderer::DepthImageMutableResource cascadeDepthResource;
- for (u32 i = 0; i < Renderer::Settings::MAX_SHADOW_CASCADES; i++)
+ GPU_SCOPED_PROFILER_ZONE(commandList, SVSMDebugOverlay);
+
+ uvec2 targetDimensions = graphResources.GetImageDimensions(data.target);
+
+ if (debugClipmap >= 0)
{
- if (i < numCascades)
+ struct DebugConstants
{
- cascadeDepthResource = data.shadowDepthCascades[i];
- }
+ u32 clipmapIndex;
+ u32 pageTableSize;
+ u32 cellSize;
+ u32 padding0;
+ ivec2 screenOffset;
+ };
+
+ const u32 pageTableSize = DeriveSVSMConfig(SVSM_MAX_PAGE_TABLE_SIZE).pageTableSize;
+ 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);
- data.lightDescriptorSet.BindArray("_shadowCascadeRTs", cascadeDepthResource, i);
+ 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::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];
+}
+
+u32 ShadowRenderer::GetSVSMBudgetUsed() const
+{
+ return _svsmDataReadBack[SVSMDataOffsets::StatsBudgetUsed];
+}
+
+void ShadowRenderer::AddSVSMBindPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex)
+{
+ struct Data
+ {
+ Renderer::ImageResource svsmPagePool;
+ Renderer::ImageResource svsmDynamicPagePool;
+
+ Renderer::DescriptorSetResource lightDescriptorSet;
+ };
+
+ // 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)
+ {
+ 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
+ },
+ [](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList)
+ {
+ data.lightDescriptorSet.Bind("_svsmPagePool"_h, data.svsmPagePool);
+ data.lightDescriptorSet.Bind("_svsmDynamicPagePool"_h, data.svsmDynamicPagePool);
+ });
+}
+
void ShadowRenderer::CreatePermanentResources(RenderResources& resources)
{
ZoneScoped;
- 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.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);
-}
\ No newline at end of file
+ SanitizeSVSMConfigCVars(SVSM_MAX_PAGE_TABLE_SIZE);
+
+ // 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);
+
+ Renderer::BufferDesc bufferDesc;
+ 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);
+
+ 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;
+ });
+
+ // 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("_dirtyAABBs"_h, _svsmDirtyAABBBuffer);
+ _svsmPageUpdateBDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer);
+ _svsmDynamicMarkDescriptorSet.Bind("_dynamicAABBs"_h, _svsmDynamicAABBBuffer);
+ _svsmDynamicUpdateDescriptorSet.Bind("_dynamicClearList"_h, _svsmDynamicClearListBuffer);
+ _svsmPageClearDescriptorSet.Bind("_clearList"_h, _svsmClearListBuffer);
+ _svsmDynamicPageClearDescriptorSet.Bind("_clearList"_h, _svsmDynamicClearListBuffer);
+ // _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 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;
+ 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 * 15; 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;
+ 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);
+
+ 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);
+
+ // 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)
+ 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
+ _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";
+ 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();
+
+ _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 76fb006b..4b5852e3 100644
--- a/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h
+++ b/Source/Game-Lib/Game-Lib/Rendering/Shadow/ShadowRenderer.h
@@ -1,9 +1,16 @@
#pragma once
+#include "Game-Lib/Rendering/Camera.h"
+
#include
#include
#include
#include
+#include
+
+#include
+#include
+#include
#include
#include
@@ -14,7 +21,6 @@ namespace Renderer
}
struct RenderResources;
-class DebugRenderer;
class GameRenderer;
class ModelRenderer;
class TerrainRenderer;
@@ -22,44 +28,185 @@ 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);
- void AddShadowPass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex);
+ // SVSM: page marking, page table lifecycle and allocation
+ void AddSVSMUpdatePass(Renderer::RenderGraph* renderGraph, RenderResources& resources, u8 frameIndex);
-private:
- void CreatePermanentResources(RenderResources& resources);
+ // 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);
-private:
- struct ShadowCascadeDebugInformation
+ struct SVSMClipmapStats
{
- public:
- vec3 frustumCorners[8];
- vec3 frustumPlanePos;
- vec4 frustumPlanes[6];
+ 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
+ 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; // 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
+ // 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. 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(); }
+
+ // 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
+ {
+ outDynamicCasters = _svsmNumDynamicCasters;
+ outTransitionsIn = _svsmCasterTransitionsIn;
+ outTransitionsOut = _svsmCasterTransitionsOut;
+ outDroppedAABBs = _svsmDynamicAABBsDropped;
+ }
+
+ // SVSM resources for the terrain/model page render passes, the pools are created lazily on
+ // the first shadow-enabled 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; }
+
+ // 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 = 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
+ 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;
+
+ // 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; }
+ Renderer::ImageID GetSVSMDynamicPagePoolOrPlaceholder() const { return _svsmDynamicPagePool != Renderer::ImageID::Invalid() ? _svsmDynamicPagePool : _svsmPagePoolPlaceholder; }
+
+private:
+ struct SVSMUpdatePassData;
+ struct SVSMUpdateRecorder;
+
+ 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;
- Renderer::SamplerID _shadowCmpSampler;
- Renderer::SamplerID _shadowPointClampSampler;
+ 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;
+
+ 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::TextureArrayID _shadowDepthTextures;
- u32 _numInitializedShadowDepthImages = 0;
+ 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; // 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 };
- mat4x4 _cascadeProjectionMatrices[Renderer::Settings::MAX_SHADOW_CASCADES];
+ std::vector _svsmDirtyAABBs; // Static invalidation (min, max) pairs, uploaded by the update pass
+ 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 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
+ bool _svsmValidatePending = false; // A validation copy was recorded, Update maps and checks it next frame
+ bool _svsmPoolNeedsClear = false;
+ u32 _svsmPoolPages = 0;
+ u32 _svsmDynamicPoolPages = 0;
- ShadowCascadeDebugInformation _cascadeDebugInformation[Renderer::Settings::MAX_SHADOW_CASCADES];
+ // 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;
- vec3 _boundingBoxMin;
- vec3 _boundingBoxMax;
+ // 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;
- u32 _currentCascade = 0;
-};
\ No newline at end of file
+ // 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;
+};
diff --git a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.cpp
index 177a200c..26efeae7 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)
@@ -74,6 +78,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)
@@ -93,6 +98,12 @@ void SkyboxRenderer::SetSkybandColors(const vec3& skyTopColor, const vec3& skyMi
_skybandColors.horizon = vec4(skyHorizonColor, 0.0f);
}
+void SkyboxRenderer::SetSunDirection(const vec3& directionToSun)
+{
+ // w is stomped from skyboxDrawSun in AddSkyboxPass every frame
+ _skybandColors.sunDirection = vec4(glm::normalize(directionToSun), 0.0f);
+}
+
void SkyboxRenderer::CreatePermanentResources()
{
ZoneScoped;
diff --git a/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h b/Source/Game-Lib/Game-Lib/Rendering/Skybox/SkyboxRenderer.h
index 453a81ab..31b06113 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/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp b/Source/Game-Lib/Game-Lib/Rendering/Terrain/TerrainRenderer.cpp
index fb0d16b3..4fe4f9b7 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"
@@ -42,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)
{
ZoneScoped;
@@ -127,18 +129,10 @@ void TerrainRenderer::AddOccluderPass(Renderer::RenderGraph* renderGraph, Render
const bool disableTwoStepCulling = CVAR_TerrainDisableTwoStepCulling.Get();
- CVarSystem* cvarSystem = CVarSystem::Get();
-
- u32 numCascades = 0;
- if (CVAR_TerrainCastShadow.Get() == 1)
- {
- numCascades = *cvarSystem->GetIntCVar(CVarCategory::Client | CVarCategory::Rendering, "shadowCascadeNum");
- }
-
struct Data
{
Renderer::ImageMutableResource visibilityBuffer;
- Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS];
+ Renderer::DepthImageMutableResource depth;
Renderer::BufferMutableResource culledInstanceBuffer;
Renderer::BufferMutableResource culledInstanceBitMaskBuffer;
@@ -151,16 +145,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);
@@ -180,7 +170,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);
@@ -193,80 +183,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.markerName = "Occluders Fill";
+ 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);
});
}
@@ -284,7 +243,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 numShadowViews = 0; // Main view only, cascades are culled in their own block late in the frame
struct Data
{
@@ -324,7 +283,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);
@@ -361,43 +320,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 numCascades;
- u32 occlusionEnabled;
- u32 bitMaskBufferSizePerView;
- u32 currentBitmaskIndex;
- };
-
- vec2 viewportSize = _renderer->GetRenderSize();
-
- CullConstants* cullConstants = graphResources.FrameNew();
- cullConstants->viewportSizeX = u32(viewportSize.x);
- cullConstants->viewportSizeY = u32(viewportSize.y);
- cullConstants->numCascades = numCascades;
- cullConstants->occlusionEnabled = CVAR_OcclusionCullingEnabled.Get();
- const u32 cellCount = static_cast(_cellDatas.Count());
- cullConstants->bitMaskBufferSizePerView = RenderUtils::CalcCullingBitmaskUints(cellCount);
- cullConstants->currentBitmaskIndex = frameIndex;
-
- 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);
});
}
@@ -413,17 +337,10 @@ 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");
- }
-
struct Data
{
Renderer::ImageMutableResource visibilityBuffer;
- Renderer::DepthImageMutableResource depth[Renderer::Settings::MAX_VIEWS];
+ Renderer::DepthImageMutableResource depth;
Renderer::BufferMutableResource culledInstanceBuffer;
@@ -437,16 +354,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);
@@ -470,71 +383,313 @@ 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++)
+
+ // 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 cells to draw
+ FillDrawCallsParams fillParams;
+ fillParams.markerName = "Geometry Fill";
+ fillParams.cellCount = cellCount;
+ fillParams.viewIndex = 0;
+ 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);
+
+ commandList.PushMarker("Draw", Color::White);
+
+ 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);
+
+ drawParams.globalDescriptorSet = data.globalSet;
+ drawParams.drawDescriptorSet = data.geometryPassSet;
+
+ Draw(resources, frameIndex, graphResources, commandList, drawParams);
+
+ commandList.PopMarker();
+ }
+
+ if (cullingEnabled)
+ {
+ commandList.CopyBuffer(data.drawCountReadBackBuffer, 0, data.argumentBuffer, 4, 4);
+ }
+ });
+}
+
+void TerrainRenderer::AddClipmapCullingPass(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;
+
+ // The same per-view culling the main view uses, run against the clipmap cameras
+ 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
+ {
+ Renderer::ImageResource depthPyramid;
+
+ Renderer::BufferMutableResource currentInstanceBitMaskBuffer;
+
+ Renderer::DescriptorSetResource debugSet;
+ Renderer::DescriptorSetResource globalSet;
+ Renderer::DescriptorSetResource cullingSet;
+ };
+
+ renderGraph->AddPass("Terrain Clipmap 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, numShadowViews](Data& data, Renderer::RenderGraphResources& graphResources, Renderer::CommandList& commandList) // Execute
+ {
+ GPU_SCOPED_PROFILER_ZONE(commandList, TerrainClipmapCulling);
+
+ // 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);
+ });
+}
+
+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);
+
+ struct CullConstants
+ {
+ u32 viewportSizeX;
+ u32 viewportSizeY;
+ u32 numShadowViews;
+ 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->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;
+
+ commandList.PushConstant(cullConstants, 0, sizeof(CullConstants));
+
+ commandList.BindDescriptorSet(debugSet, frameIndex);
+ commandList.BindDescriptorSet(globalSet, frameIndex);
+ commandList.BindDescriptorSet(cullingSet, frameIndex);
+
+ commandList.Dispatch((cellCount + 31) / 32, 1, 1);
+
+ commandList.EndPipeline(pipeline);
+}
+
+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 (shadowRenderer->GetSVSMPagePool() == Renderer::ImageID::Invalid())
+ return;
+
+ 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
+ {
+ Renderer::ImageMutableResource pagePool;
+ Renderer::BufferResource svsmData;
+ Renderer::BufferResource pageTable;
+ Renderer::BufferResource svsmFillArgsBuffer;
+
+ 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);
+ 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);
+ 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;
+ 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)
+ 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 = (i == 0) ? "Main" : "Cascade " + std::to_string(i - 1);
+ 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);
}
if (CVAR_TerrainGeometryEnabled.Get())
{
- const u32 cellCount = static_cast(_cellDatas.Count());
-
- // Fill the occluders to draw
- FillDrawCallsParams fillParams;
- fillParams.passName = "Geometry";
- fillParams.cellCount = cellCount;
+ // Finalize zeroed the group count for rings with no dirty pages this frame
fillParams.viewIndex = i;
- fillParams.diffAgainstPrev = true;
- fillParams.currentBitmaskIndex = frameIndex;
- fillParams.fillSet = data.fillSet;
+ fillParams.fillArgsByteOffset = (i - 1) * ShadowRenderer::SVSM_FILL_ARGS_VIEW_STRIDE + ShadowRenderer::SVSM_FILL_ARGS_TERRAIN_OFFSET;
- FillDrawCalls(frameIndex, graphResources, commandList, fillParams);
+ 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)
{
- 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);
+ // 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