feat(volumetric-shadows): EVSM shadow mapping with global depth normalization#2493
Conversation
Upgrade from VSM to optimized 4-moment shadow mapping (MSM) with linearized cascade depth. Add dynamic blur radius with configurable kernel, replace fixed 11-tap Gaussian with runtime-variable sigma. Add CascadeDepthParams to DirectionalShadowLightData for per-cascade near/far linearization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 4-moment shadow mapping (Hamburger reconstruction) with Exponential Variance Shadow Maps. EVSM uses Chebyshev upper bound on warped depth (e^cz) for simpler, faster shadow evaluation with better light bleed control via dual positive/negative exponents. Key changes: - DownsampleShadowCS: compute EVSM moments (e^cz, e^2cz, e^-cz, e^-2cz) instead of optimized power moments with 4x4 matrix transform - VolumetricShadows.hlsli: replace ComputeMSM (Cholesky factorization) with ComputeEVSM (dual Chebyshev bounds) - Texture format changed from R16G16B16A16_UNORM to R16G16B16A16_FLOAT to accommodate exponential range - Exponents configurable at runtime via settings (pos=40, neg=5 default) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove globals::game::isVR branch that doesn't exist on dev. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…for EVSM Shadow maps use orthographic projection so depth is already linear within each cascade. Replace the incorrect perspective linearization with orthographic remapping to a global [0,1] range shared by both cascades. This ensures exponent application is consistent regardless of cascade frustum size or camera angle. - Add GlobalDepthParams (globalNear/globalFar) to DirectionalShadowLightData - Update DownsampleShadowCS to normalize via global range before warping - Update VolumetricShadows.hlsli sampling to match downsample normalization - Pass per-cascade near/far and global range through structured buffer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis pull request converts the volumetric shadow system from VSM to EVSM by updating shader compute kernels to work with ChangesEVSM Pipeline Conversion
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The change involves substantial shader logic replacement (VSM → EVSM algorithms), new constant buffer wiring patterns across CPU and GPU sides, multi-file coordination for depth parameter flow, and removal of VSM-specific functions. While individual layer logic is coherent, the broad scope (8 files, 3 major shader types, CPU/GPU coordination) and shader-domain specificity (exponent warping, Chebyshev bounds, moment computation) demand careful cross-file verification of parameter passing, texture format changes, and numeric correctness. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.22.0)OpenGrep fatal error (exit code 2): [00.23][ERROR]: Error: exception Unix_error: No such file or directory stat src/Deferred.cpp 🔧 Infer (1.2.0)src/Features/VolumetricShadows.cppUsage Error: Failed to execute compilation command: Error message: *** Infer needs a working compilation command to run. src/Deferred.cppUsage Error: Failed to execute compilation command: Error message: *** Infer needs a working compilation command to run. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
No actionable suggestions for changed features. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/Features/VolumetricShadows.cpp (1)
332-333: 💤 Low valueConsider reusing a single null buffer pointer for unbinding.
The code creates a new
nullptrlocal variable (nullCB,nullCB2) each time to unbind constant buffers. While this works correctly, you could declare a singleID3D11Buffer* nullCB = nullptr;at the function scope and reuse it for all unbinding calls to avoid redundant variable declarations.♻️ Optional refactor to reuse null pointer
At the start of
CopyShadowLightData(), declare:void VolumetricShadows::CopyShadowLightData() { ZoneScoped; TracyD3D11Zone(globals::state->tracyCtx, "VolumetricShadows::CopyShadowLightData"); auto context = globals::d3d::context; + ID3D11Buffer* nullCB = nullptr;Then reuse it:
- ID3D11Buffer* nullCB = nullptr; context->CSSetConstantBuffers(0, 1, &nullCB); // ... blur passes ... - ID3D11Buffer* nullCB2 = nullptr; - context->CSSetConstantBuffers(0, 1, &nullCB2); + context->CSSetConstantBuffers(0, 1, &nullCB);Also applies to: 434-435
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Features/VolumetricShadows.cpp` around lines 332 - 333, The code repeatedly creates local nullptr buffer variables (e.g., nullCB / nullCB2) when unbinding constant buffers; instead declare a single ID3D11Buffer* nullCB = nullptr at the start of CopyShadowLightData() and reuse that pointer for all CSSetConstantBuffers (and similar unbind) calls (including the sites around the 434-435 region) to remove redundant declarations while leaving the unbind calls unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 39-54: ComputeEVSM currently uses compile-time constants
EVSM_EXPONENT_POS and EVSM_EXPONENT_NEG; update it to accept runtime exponents
so the same values used during moment generation are applied during evaluation.
Modify the ComputeEVSM signature to add two parameters (e.g., float evsmExpPos,
float evsmExpNeg) and replace EVSM_EXPONENT_POS/EVSM_EXPONENT_NEG with those
parameters, then ensure callers (where ComputeEVSM is invoked) forward the
runtime exponents produced in DownsampleShadowCS.hlsl (or read from the same
shader uniform/buffer) so moment generation and shadow evaluation use identical
exponent values. Ensure the parameter names match usages in callers and keep the
rest of the logic (posWarp/negWarp and ChebyshevUpperBound calls) unchanged.
- Around line 8-12: The hardcoded EVSM_EXPONENT_POS/NEG in
VolumetricShadows.hlsli cause sampling to ignore runtime settings — update the
shader to read the exponents from GPU state instead of using compile-time
constants: add fields for exponentPositive/exponentNegative to
DirectionalShadowLightData (or create a small EVSM sampling CB populated from
EVSMLinearizeCB), remove/replace EVSM_EXPONENT_POS and EVSM_EXPONENT_NEG, thread
those exponent values into ComputeEVSM and all call sites that perform EVSM
sampling (ensure DownsampleShadowCS.hlsl and the deferred light upload path
supply the same runtime exponents), and keep
EVSM_VARIANCE_BIAS/EVSM_LIGHT_BLEED_REDUCTION as needed.
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 33-34: The fog shader defines a narrower local
DirectionalShadowLightData that mismatches the shared version in
ShadowSampling.hlsli (which includes CascadeDepthParams and GlobalDepthParams)
causing a fragile t98 layout; fix this by removing the local struct in
features/Exponential Height Fog/.../VolumetricFogLightScatteringCS.hlsl and
instead `#include` or reference the shared DirectionalShadowLightData from
package/Shaders/Common/ShadowSampling.hlsli so DirectionalShadowLights[0]
(accesses ShadowProj, InvShadowProj, EndSplitDistances, StartSplitDistances)
uses the exact same layout (including CascadeDepthParams and GlobalDepthParams)
and preserves correct register(t98) stride.
In `@src/Features/VolumetricShadows.cpp`:
- Line 179: The RGBA16F target (copyDesc.Format =
DXGI_FORMAT_R16G16B16A16_FLOAT) can overflow with current EVSM moments; clamp
the positive/negative warps and their squared moments in the moment generator
(the HLSL function that emits float4(posWarp, posWarp*posWarp, negWarp,
negWarp*negWarp)) to the fp16 max (≈65504) or choose a safe headroom value
before writing to the UAV, and/or switch to R32G32B32A32_FLOAT if you need full
range. Ensure ExponentPositive and ExponentNegative (C++ sliders and the
moment-generation shader) are bounded to the values used in
VolumetricShadows.hlsli (or update the HLSL constants to accept the higher C++
defaults) so generator and evaluator match. Add a debug validation after the
moment-pass (readback or GPU-side assert) that checks the moment texture
contains no inf/NaN for the default settings (ExponentPositive=40,
ExponentNegative=5) across representative cascade depths and fail/clip if
invalid values are detected.
---
Nitpick comments:
In `@src/Features/VolumetricShadows.cpp`:
- Around line 332-333: The code repeatedly creates local nullptr buffer
variables (e.g., nullCB / nullCB2) when unbinding constant buffers; instead
declare a single ID3D11Buffer* nullCB = nullptr at the start of
CopyShadowLightData() and reuse that pointer for all CSSetConstantBuffers (and
similar unbind) calls (including the sites around the 434-435 region) to remove
redundant declarations while leaving the unbind calls unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 279a2fd6-de07-4fe7-901f-f0bd83bd051c
📒 Files selected for processing (8)
features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlslfeatures/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlslfeatures/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlslipackage/Shaders/Common/ShadowSampling.hlslisrc/Deferred.cppsrc/Deferred.hsrc/Features/VolumetricShadows.cppsrc/Features/VolumetricShadows.h
| // EVSM exponents — must match values set in C++ (VolumetricShadows.h) | ||
| static const float EVSM_EXPONENT_POS = 40.0; | ||
| static const float EVSM_EXPONENT_NEG = 5.0; | ||
| static const float EVSM_VARIANCE_BIAS = 0.001; | ||
| static const float EVSM_LIGHT_BLEED_REDUCTION = 0.3; |
There was a problem hiding this comment.
Hardcoded EVSM exponents won't respect runtime settings.
The constants EVSM_EXPONENT_POS = 40.0 and EVSM_EXPONENT_NEG = 5.0 are compile-time values that ignore the user's runtime settings (VolumetricShadows::Settings::ExponentPositive and ExponentNegative). The comment claims they "must match values set in C++," but the C++ values are runtime-configurable, not compile-time constants.
More critically, these hardcoded values are used in ComputeEVSM (lines 44-45) during shadow sampling, while DownsampleShadowCS.hlsl uses the runtime exponents from EVSMLinearizeCB during moment generation. If a user changes the exponent settings, moments will be generated with one exponent but sampled with another, producing completely incorrect shadow results.
🐛 Recommended fix: read exponents from DirectionalShadowLightData
Since the exponents are already uploaded to the GPU via EVSMLinearizeCB during downsample and stored in DirectionalShadowLightData during deferred light upload, you can pass them through to the sampling functions.
Option 1: Add exponent fields to DirectionalShadowLightData and pass them to ComputeEVSM:
In ShadowSampling.hlsli:
struct DirectionalShadowLightData
{
column_major float4x4 ShadowProj[2];
column_major float4x4 InvShadowProj[2];
float2 EndSplitDistances;
float2 StartSplitDistances;
float4 CascadeDepthParams;
- float4 GlobalDepthParams; // x=globalNear, y=globalFar, zw=unused
+ float4 GlobalDepthParams; // x=globalNear, y=globalFar, z=ExponentPositive, w=ExponentNegative
};In VolumetricShadows.hlsli, update ComputeEVSM:
-float ComputeEVSM(float4 moments, float depth, float cascadeNear, float cascadeFar, float globalNear, float globalFar)
+float ComputeEVSM(float4 moments, float depth, float cascadeNear, float cascadeFar, float globalNear, float globalFar, float exponentPos, float exponentNeg)
{
float d = NormalizeDepth(depth, cascadeNear, cascadeFar, globalNear, globalFar);
- float posWarp = exp(EVSM_EXPONENT_POS * d);
- float negWarp = exp(-EVSM_EXPONENT_NEG * d);
+ float posWarp = exp(exponentPos * d);
+ float negWarp = exp(-exponentNeg * d);
// ... rest unchanged
}Then thread the exponents through all call sites.
Option 2: Add a separate constant buffer for EVSM sampling parameters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 8 - 12,
The hardcoded EVSM_EXPONENT_POS/NEG in VolumetricShadows.hlsli cause sampling to
ignore runtime settings — update the shader to read the exponents from GPU state
instead of using compile-time constants: add fields for
exponentPositive/exponentNegative to DirectionalShadowLightData (or create a
small EVSM sampling CB populated from EVSMLinearizeCB), remove/replace
EVSM_EXPONENT_POS and EVSM_EXPONENT_NEG, thread those exponent values into
ComputeEVSM and all call sites that perform EVSM sampling (ensure
DownsampleShadowCS.hlsl and the deferred light upload path supply the same
runtime exponents), and keep EVSM_VARIANCE_BIAS/EVSM_LIGHT_BLEED_REDUCTION as
needed.
| // Compute EVSM shadow from stored moments | ||
| // moments = (E[e^cz], E[e^2cz], E[e^-cz], E[e^-2cz]) | ||
| float ComputeEVSM(float4 moments, float depth, float cascadeNear, float cascadeFar, float globalNear, float globalFar) | ||
| { | ||
| return saturate((shadow - amount) / (1.0 - amount)); | ||
| float d = NormalizeDepth(depth, cascadeNear, cascadeFar, globalNear, globalFar); | ||
| float posWarp = exp(EVSM_EXPONENT_POS * d); | ||
| float negWarp = exp(-EVSM_EXPONENT_NEG * d); | ||
|
|
||
| // Positive exponent test (standard front-face shadow) | ||
| float posShadow = ChebyshevUpperBound(moments.x, moments.y, posWarp); | ||
|
|
||
| // Negative exponent test (back-face light bleed suppression) | ||
| float negShadow = ChebyshevUpperBound(moments.z, moments.w, negWarp); | ||
|
|
||
| return min(posShadow, negShadow); | ||
| } |
There was a problem hiding this comment.
Update ComputeEVSM to use runtime exponents instead of hardcoded constants.
Lines 44-45 use the hardcoded compile-time constants EVSM_EXPONENT_POS and EVSM_EXPONENT_NEG, which will not match the runtime exponents used during moment generation in DownsampleShadowCS.hlsl. See the comment on lines 8-12 for the full explanation and recommended fix.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 39 - 54,
ComputeEVSM currently uses compile-time constants EVSM_EXPONENT_POS and
EVSM_EXPONENT_NEG; update it to accept runtime exponents so the same values used
during moment generation are applied during evaluation. Modify the ComputeEVSM
signature to add two parameters (e.g., float evsmExpPos, float evsmExpNeg) and
replace EVSM_EXPONENT_POS/EVSM_EXPONENT_NEG with those parameters, then ensure
callers (where ComputeEVSM is invoked) forward the runtime exponents produced in
DownsampleShadowCS.hlsl (or read from the same shader uniform/buffer) so moment
generation and shadow evaluation use identical exponent values. Ensure the
parameter names match usages in callers and keep the rest of the logic
(posWarp/negWarp and ChebyshevUpperBound calls) unchanged.
| float4 CascadeDepthParams; | ||
| float4 GlobalDepthParams; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for DirectionalShadowLightData struct definitions across all shader files
rg -n -C5 'struct\s+DirectionalShadowLightData' --type-add 'hlsl:*.{hlsl,hlsli}' --type hlsl
# Search for usage of DirectionalShadowLights structured buffer
rg -n -C3 'DirectionalShadowLights\[' --type-add 'hlsl:*.{hlsl,hlsli}' --type hlslRepository: community-shaders/skyrim-community-shaders
Length of output: 5242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the full local struct in fog shader
echo "---- Fog shader DirectionalShadowLightData struct ----"
sed -n '240,360p' "features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl" | sed -n '1,120p' || true
# Show where fog shader reads End/Start split distances from directionalShadowLightData
echo "---- Fog shader directionalShadowLightData usage (near lines ~150-210) ----"
sed -n '140,220p' "features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl" || true
# Confirm whether fog shader ever reads CascadeDepthParams/GlobalDepthParams
echo "---- Fog shader reads of CascadeDepthParams/GlobalDepthParams ----"
rg -n "CascadeDepthParams|GlobalDepthParams" "features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl" || true
# Show Common/ShadowSampling.hlsli struct
echo "---- ShadowSampling.hlsli DirectionalShadowLightData struct ----"
sed -n '1,120p' "package/Shaders/Common/ShadowSampling.hlsli" || true
# Confirm how VolumetricShadows.hlsli uses DirectionalShadowLightData (near relevant lines)
echo "---- VolumetricShadows.hlsli directionalShadowLightData usage ----"
sed -n '150,230p' "features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli" || true
# Find CPU/shader interface population for DirectionalShadowLights / t98
echo "---- Search C++ for DirectionalShadowLights / t98 ----"
rg -n "DirectionalShadowLights|t98" -S . || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 17485
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FOG="features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl"
COMMON="package/Shaders/Common/ShadowSampling.hlsli"
VOLUMETRIC="features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli"
echo "---- Fog struct location ----"
rg -n "struct\s+DirectionalShadowLightData" "$FOG"
echo "---- ShadowSampling struct location ----"
rg -n "struct\s+DirectionalShadowLightData" "$COMMON"
echo "---- VolumetricShadows directionalShadowLightData reads of members ----"
rg -n "directionalShadowLightData\.(CascadeDepthParams|GlobalDepthParams|EndSplitDistances|StartSplitDistances)" "$VOLUMETRIC" || true
echo "---- Fog shader directionalShadowLightData reads of members ----"
rg -n "directionalShadowLightData\.(CascadeDepthParams|GlobalDepthParams|EndSplitDistances|StartSplitDistances)" "$FOG" || true
echo "---- Fog shader local struct block ----"
# Print a reasonable window around the first match
LINE=$(rg -n "struct\s+DirectionalShadowLightData" "$FOG" | head -n1 | cut -d: -f1)
START=$((LINE-10))
END=$((LINE+60))
sed -n "${START},${END}p" "$FOG"
echo "---- ShadowSampling.hlsli struct block ----"
LINE2=$(rg -n "struct\s+DirectionalShadowLightData" "$COMMON" | head -n1 | cut -d: -f1)
START2=$((LINE2-10))
END2=$((LINE2+60))
sed -n "${START2},${END2}p" "$COMMON"
echo "---- Fog shader usage window around directionalShadowLightData = DirectionalShadowLights[0] ----"
LINE3=$(rg -n "DirectionalShadowLights\[0\]" "$FOG" | head -n1 | cut -d: -f1)
START3=$((LINE3-25))
END3=$((LINE3+40))
sed -n "${START3},${END3}p" "$FOG"
echo "---- VolumetricShadows.hlsli usage window around DirectionalShadowLights[0] ----"
LINE4=$(rg -n "DirectionalShadowLights\[0\]" "$VOLUMETRIC" | head -n1 | cut -d: -f1)
START4=$((LINE4-25))
END4=$((LINE4+80))
sed -n "${START4},${END4}p" "$VOLUMETRIC"
echo "---- Search C++ for DirectionalShadowLights / register(t98) ----"
# Restrict to likely source dirs to reduce noise if repo is huge
rg -n "DirectionalShadowLights|register\(t98\)" -S . || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 15520
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FOG="features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl"
COMMON="package/Shaders/Common/ShadowSampling.hlsli"
VOLUMETRIC="features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli"
echo "---- Fog struct location ----"
rg -n "struct\s+DirectionalShadowLightData" "$FOG"
echo "---- ShadowSampling struct location ----"
rg -n "struct\s+DirectionalShadowLightData" "$COMMON"
echo "---- VolumetricShadows directionalShadowLightData reads of members ----"
rg -n "directionalShadowLightData\.(CascadeDepthParams|GlobalDepthParams|EndSplitDistances|StartSplitDistances)" "$VOLUMETRIC" || true
echo "---- Fog shader directionalShadowLightData reads of members ----"
rg -n "directionalShadowLightData\.(CascadeDepthParams|GlobalDepthParams|EndSplitDistances|StartSplitDistances)" "$FOG" || true
echo "---- Fog shader local struct block ----"
LINE=$(rg -n "struct\s+DirectionalShadowLightData" "$FOG" | head -n1 | cut -d: -f1)
START=$((LINE-10))
END=$((LINE+60))
sed -n "${START},${END}p" "$FOG"
echo "---- ShadowSampling.hlsli struct block ----"
LINE2=$(rg -n "struct\s+DirectionalShadowLightData" "$COMMON" | head -n1 | cut -d: -f1)
START2=$((LINE2-10))
END2=$((LINE2+60))
sed -n "${START2},${END2}p" "$COMMON"
echo "---- Fog shader usage window around DirectionalShadowLights[0] ----"
LINE3=$(rg -n "DirectionalShadowLights\[0\]" "$FOG" | head -n1 | cut -d: -f1)
START3=$((LINE3-25))
END3=$((LINE3+40))
sed -n "${START3},${END3}p" "$FOG"
echo "---- VolumetricShadows.hlsli usage window around DirectionalShadowLights[0] ----"
LINE4=$(rg -n "DirectionalShadowLights\[0\]" "$VOLUMETRIC" | head -n1 | cut -d: -f1)
START4=$((LINE4-25))
END4=$((LINE4+80))
sed -n "${START4},${END4}p" "$VOLUMETRIC"
echo "---- Search C++ for DirectionalShadowLights / register(t98) ----"
rg -n "DirectionalShadowLights|register\(t98\)" -S . || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 15491
Fix redundant/forked DirectionalShadowLightData struct layout between fog and Common for register(t98)
features/Exponential Height Fog/.../VolumetricFogLightScatteringCS.hlsl defines a smaller local DirectionalShadowLightData (only ShadowProj, InvShadowProj, EndSplitDistances, StartSplitDistances) but reads DirectionalShadowLights[0] only for ShadowProj and split-distance members. package/Shaders/Common/ShadowSampling.hlsli (and features/Volumetric Shadows/...) use the extended struct that appends float4 CascadeDepthParams; and float4 GlobalDepthParams;. To avoid fragile layout/stride mismatch on t98 and future breakage, reuse the shared DirectionalShadowLightData definition in the fog shader instead of locally redefining it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package/Shaders/Common/ShadowSampling.hlsli` around lines 33 - 34, The fog
shader defines a narrower local DirectionalShadowLightData that mismatches the
shared version in ShadowSampling.hlsli (which includes CascadeDepthParams and
GlobalDepthParams) causing a fragile t98 layout; fix this by removing the local
struct in features/Exponential Height
Fog/.../VolumetricFogLightScatteringCS.hlsl and instead `#include` or reference
the shared DirectionalShadowLightData from
package/Shaders/Common/ShadowSampling.hlsli so DirectionalShadowLights[0]
(accesses ShadowProj, InvShadowProj, EndSplitDistances, StartSplitDistances)
uses the exact same layout (including CascadeDepthParams and GlobalDepthParams)
and preserves correct register(t98) stride.
| copyDesc.MipLevels = 2; | ||
| copyDesc.ArraySize = 1; | ||
| copyDesc.Format = DXGI_FORMAT_R16G16_UNORM; | ||
| copyDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the referenced area in VolumetricShadows.cpp
FILE="src/Features/VolumetricShadows.cpp"
if [ ! -f "$FILE" ]; then
echo "Missing $FILE"
exit 1
fi
echo "---- File tail/head around line 179 ----"
nl -ba "$FILE" | sed -n '130,240p'
# 2) Search for EVSM / moment / exponent usage (40, 5)
echo "---- Search for EVSM/moments/exponents ----"
rg -n --hidden --no-ignore-vcs -S "EVSM|moments|moment|exp\(|exponent|c\*z|c·z|R16G16B16A16_FLOAT|DXGI_FORMAT_R16G16B16A16_FLOAT" src/Features/VolumetricShadows.cpp src || true
echo "---- Search for explicit exponent constants 40 and 5 ----"
rg -n --hidden --no-ignore-vcs -S "(^|[^0-9])40([^0-9]|$)|(^|[^0-9])5([^0-9]|$)" src/Features/VolumetricShadows.cpp src || true
# 3) Look for depth normalization / clamping before moment exponentiation
echo "---- Search for depth normalization/clamp before exp ----"
rg -n --hidden --no-ignore-vcs -S "(normalize|normalized|depth|z|cascade|far|near|saturate|clamp|min|max)" src/Features/VolumetricShadows.cpp src || true
# 4) Look for any overflow checks mentioned in comments/tests
echo "---- Search for overflow/inf/half/fp16 tests in repo ----"
rg -n --hidden --no-ignore-vcs -S "(overflow|inf|isinf|fp16|half|R16G16B16A16|65504|max.*half|exp\\(.*40|exp\\(.*5)" src || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 189
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/Features/VolumetricShadows.cpp"
test -f "$FILE"
echo "---- Context around line ~179 ----"
awk 'NR>=160 && NR<=210 {print NR ":" $0}' "$FILE"
echo "---- Search for EVSM/moments/exponents/format usage ----"
rg -n --hidden --no-ignore-vcs -S "EVSM|moments?\\b|moment\\b|exp\\(|exponent|c\\*z|DXGI_FORMAT_R16G16B16A16_FLOAT|R16G16B16A16_FLOAT" "$FILE" src || true
echo "---- Search for explicit exponent constants 40 and 5 ----"
rg -n --hidden --no-ignore-vcs -S "(^|[^0-9])40([^0-9]|$)|(^|[^0-9])5([^0-9]|$)" "$FILE" src || true
echo "---- Search for depth normalization/clamping/saturate before exp ----"
rg -n --hidden --no-ignore-vcs -S "(normalize|normalized|depth\\b|z\\b|cascade\\b|far\\b|near\\b|saturate|clamp|min\\b|max\\b)" "$FILE" src || true
echo "---- Search for fp16/half overflow/inf checks ----"
rg -n --hidden --no-ignore-vcs -S "(overflow|inf|isinf|fp16|\\bhalf\\b|65504|max\\.half|exp\\(.*40|exp\\(.*5|\\.isinf\\()" src || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 50399
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focused inspection of EVSM setup and shaders (avoid huge repo-wide output)
FILE_CPP="src/Features/VolumetricShadows.cpp"
FILE_H="src/Features/VolumetricShadows.h"
echo "---- VolumetricShadows.h EVSMLinearizeCB + defaults ----"
awk 'NR>=1 && NR<=140 {print NR ":" $0}' "$FILE_H"
echo "---- VolumetricShadows.cpp around EVSM cbuffer updates (near/far + exponents) ----"
awk 'NR>=240 && NR<=380 {print NR ":" $0}' "$FILE_CPP"
echo "---- List shader files under Data/Shaders/VolumetricShadows ----"
fd -t f "VolumetricShadows" Data 2>/dev/null | head -n 50 || true
# fallback if fd doesn't match directories:
fd -t f "DownsampleShadowCS.hlsl" Data 2>/dev/null || true
fd -t f "BlurShadowCS.hlsl" Data 2>/dev/null || true
fd -t f "*VolumetricShadows*.hlsl" Data 2>/dev/null | head -n 50 || true
echo "---- Search for EVSM/moment exp() in VolumetricShadows shaders ----"
fd -t f "*VolumetricShadows*.hlsl" Data 2>/dev/null \
| xargs -r rg -n -S "EVSM|moment\\b|moments\\b|exp\\(|exponent|ExponentPositive|ExponentNegative" || true
echo "---- Search for any clamping/saturate before exp() in those shaders ----"
fd -t f "*VolumetricShadows*.hlsl" Data 2>/dev/null \
| xargs -r rg -n -S "(saturate|clamp|min\\(|max\\(|half\\b|isinf|isnan|finite|FLT_MAX|65504|halfmax)" || true
echo "---- Search for 'overflow' / 'inf' mentions in those shaders ----"
fd -t f "*VolumetricShadows*.hlsl" Data 2>/dev/null \
| xargs -r rg -n -S "(overflow|isinf|inf|half\\b)" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 11343
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Confirm whether Data/Shaders exists and where VolumetricShadows shader sources are
echo "---- Top-level dirs ----"
ls -la
echo "---- Check for Data/ and shader dirs ----"
ls -la Data 2>/dev/null || true
ls -la Data/Shaders 2>/dev/null || true
echo "---- Locate VolumetricShadows shader folder/files (best-effort) ----"
find Data -type f 2>/dev/null | rg -n "VolumetricShadows|DownsampleShadowCS\.hlsl|BlurShadowCS\.hlsl|EVSM" || true
echo "---- Locate the referenced shader filenames anywhere ----"
find . -type f 2>/dev/null | rg -n "DownsampleShadowCS\.hlsl|BlurShadowCS\.hlsl" || true
# 2) If we find shader files, search them for exp/moment/clamp/overflow
echo "---- Search for exp()/moment code in any located VolumetricShadows shaders ----"
shaders=$(find . -type f 2>/dev/null | rg "VolumetricShadows.*\.hlsl$|DownsampleShadowCS\.hlsl$|BlurShadowCS\.hlsl$" || true)
if [ -n "$shaders" ]; then
echo "$shaders" | head -n 50
echo "---- RG search for EVSM/moment in those files ----"
printf "%s\n" "$shaders" | xargs -r rg -n -S "exp\\(|moment\\b|moments\\b|EVSM|ExponentPositive|ExponentNegative|clamp\\(|saturate\\(|isinf\\(|isnan\\(" || true
else
echo "No matching shader files found for the known names/patterns."
fiRepository: community-shaders/skyrim-community-shaders
Length of output: 3461
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_DS="features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl"
FILE_BL="features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl"
ls -ლა "$FILE_DS" "$FILE_BL"
echo "---- Line counts ----"
wc -l "$FILE_DS" "$FILE_BL"
echo "---- Search for EVSM params + moment exp in DownsampleShadowCS.hlsl ----"
rg -n --fixed-string "ExponentPositive" "$FILE_DS" || true
rg -n --fixed-string "ExponentNegative" "$FILE_DS" || true
rg -n -S "exp\\s*\\(" "$FILE_DS" || true
rg -n -S "moment|moments|EVSM" "$FILE_DS" || true
rg -n -S "clamp\\(|saturate\\(|min\\(|max\\(|isinf\\(|isnan\\(" "$FILE_DS" || true
echo "---- Show relevant section around exp() in DownsampleShadowCS.hlsl (first hits) ----"
# Print ~40 lines around the first exp() occurrence(s)
first_exp_line=$(rg -n -S "exp\\s*\\(" "$FILE_DS" | head -n 1 | cut -d: -f1 || true)
if [ -n "${first_exp_line:-}" ]; then
start=$((first_exp_line-20))
end=$((first_exp_line+60))
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {print NR ":" $0}' "$FILE_DS"
fi
echo "---- Search for exp/clamp/half in BlurShadowCS.hlsl (in case moments are transformed there) ----"
rg -n -S "exp\\s*\\(|moment|EVSM|ExponentPositive|ExponentNegative" "$FILE_BL" || true
rg -n -S "clamp\\(|saturate\\(|min\\(|max\\(|isinf\\(|isnan\\(" "$FILE_BL" || true
rg -n -S "\\bhalf\\b" "$FILE_BL" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 150
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_DS="features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl"
FILE_BL="features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl"
ls -la "$FILE_DS" "$FILE_BL"
echo "---- Search for EVSM params + exp in DownsampleShadowCS.hlsl ----"
rg -n --fixed-string "ExponentPositive" "$FILE_DS" || true
rg -n --fixed-string "ExponentNegative" "$FILE_DS" || true
rg -n -S "exp\\s*\\(" "$FILE_DS" || true
rg -n -S "EVSM|moment|moments" "$FILE_DS" || true
rg -n -S "clamp\\(|saturate\\(|min\\(|max\\(|isinf\\(|isnan\\(" "$FILE_DS" || true
echo "---- Show region around first exp() in DownsampleShadowCS.hlsl ----"
first_exp_line=$(rg -n -S "exp\\s*\\(" "$FILE_DS" | head -n 1 | cut -d: -f1 || true)
if [ -n "${first_exp_line:-}" ]; then
start=$((first_exp_line-25))
end=$((first_exp_line+80))
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {print NR ":" $0}' "$FILE_DS"
fi
echo "---- Search for EVSM/exp/clamp in BlurShadowCS.hlsl ----"
rg -n -S "EVSM|moment|moments|exp\\s*\\(" "$FILE_BL" || true
rg -n -S "clamp\\(|saturate\\(|min\\(|max\\(|isinf\\(|isnan\\(" "$FILE_BL" || true
rg -n -S "\\bhalf\\b" "$FILE_BL" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 5977
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect around where the exponents are configured in VolumetricShadows.cpp
FILE_CPP="src/Features/VolumetricShadows.cpp"
echo "---- ExponentNegative/ExponentPositive settings UI/config around line ~440-520 ----"
awk 'NR>=420 && NR<=520 {print NR ":" $0}' "$FILE_CPP"
echo "---- Search within VolumetricShadows.cpp for fp16/overflow/inf ----"
rg -n -S "(overflow|inf|isinf|fp16|half\\b|65504|ExponentPositive|ExponentNegative)" "$FILE_CPP" || true
echo "---- Search repo for fp16 overflow checks mention ----"
rg -n -S "fp16 overflow|overflow checks|isinf|inf values|R16G16B16A16_FLOAT" . || true
echo "---- Search within VolumetricShadows shaders for clamp/saturate near WarpDepth ----"
FILE_DS="features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl"
rg -n -S "NormalizeDepth\\(|WarpDepth\\(|clamp\\(|saturate\\(" "$FILE_DS" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 12367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_DS="features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl"
echo "---- DownsampleShadowCS.hlsl (top 120 lines) ----"
awk 'NR>=1 && NR<=120 {print NR ":" $0}' "$FILE_DS"
echo "---- Search for OutputTexture declaration/type ----"
rg -n "OutputTexture|RWTexture|RWTexture2D|RWTexture3D|Texture2D<|RWTexture2D<" "$FILE_DS" || true
echo "---- Search for any saturate/clamp around writing moments ----"
rg -n "evsmMoments|WarpDepth|OutputTexture" "$FILE_DS" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 5001
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -S "VolumetricShadows|DownsampleShadowCS|BlurShadowCS|EVSMLinearizeCB" package tests docs src features .github || true
rg -n -S "fp16 overflow|overflow checks|ExponentPositive.*40|exp\\(ExponentPositive|posWarp|negWarp|isinf\\(" . || true
rg -n -S "fp16|R16G16B16A16_FLOAT|half" package/Shaders/Tests tests package docs src/features . || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 50469
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_HLSLI="features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli"
# Print the EVSM helpers section
awk 'NR>=1 && NR<=140 {print NR ":" $0}' "$FILE_HLSLI"
# Search for any clamping/overflow filtering around EVSM moments
rg -n -S "NormalizeDepth\\(|WarpDepth\\(|posWarp|negWarp|EVSM_EXPONENT|EVSMLinearizeCB|clamp\\(|saturate\\(|isinf\\(|isnan\\(" "$FILE_HLSLI" || true
# Search for usage of fp16/inf filtering patterns
rg -n -S "filterInf\\(|isinf\\(|isnan\\(|overflow" "$FILE_HLSLI" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 7558
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_HLSLI="features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli"
rg -n -S "ExponentPositive|ExponentNegative|EVSM_EXPONENT_POS|EVSM_EXPONENT_NEG" "$FILE_HLSLI" || trueRepository: community-shaders/skyrim-community-shaders
Length of output: 281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_CPP="src/Features/VolumetricShadows.cpp"
echo "---- ExtractCascadeNearFar() ----"
awk 'NR>=90 && NR<=170 {print NR ":" $0}' "$FILE_CPP"
echo "---- GetGlobalDepthParams/GetCascadeDepthParams (if any) ----"
awk 'NR>=120 && NR<=220 {print NR ":" $0}' "$FILE_CPP"Repository: community-shaders/skyrim-community-shaders
Length of output: 8218
Verify RGBA16F can represent EVSM moments without overflow.
Texture is now DXGI_FORMAT_R16G16B16A16_FLOAT, but the moment generator stores EVSM moments as float4(posWarp, posWarp*posWarp, negWarp, negWarp*negWarp) with posWarp = exp(ExponentPositive * d) and d globally normalized depth (NormalizeDepth(...)). Because globalNear/globalFar are the min/max across cascades, d can reach ~1.0 (e.g., at the far end of the far cascade), so:
exp(40) ≈ 2.35e17overflows fp16 (~65504) →+inf- the 2nd moment uses
exp(2*c*d)=exp(80*d), which overflows ford > ln(65504)/80 ≈ 0.139
There is no clamping /isinf/isnanhandling before writing to the RGBA16F UAV, so+inf/NaNmoments can corrupt the Chebyshev EVSM filtering.
Additionally, VolumetricShadows.hlsli hardcodes EVSM_EXPONENT_POS=40 / EVSM_EXPONENT_NEG=5 for the shadow evaluation, while C++ exposes sliders up to ExponentPositive=80 / ExponentNegative=40 and passes those to the moment generator—so non-default settings will mismatch stored moments and evaluation.
Please clamp the positive warp/moments (and/or lower defaults) to guarantee fp16-safe writes, and add an explicit test/assert that the moment texture contains no inf/NaN for the default (pos=40, neg=5) across representative cascade depths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Features/VolumetricShadows.cpp` at line 179, The RGBA16F target
(copyDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT) can overflow with current
EVSM moments; clamp the positive/negative warps and their squared moments in the
moment generator (the HLSL function that emits float4(posWarp, posWarp*posWarp,
negWarp, negWarp*negWarp)) to the fp16 max (≈65504) or choose a safe headroom
value before writing to the UAV, and/or switch to R32G32B32A32_FLOAT if you need
full range. Ensure ExponentPositive and ExponentNegative (C++ sliders and the
moment-generation shader) are bounded to the values used in
VolumetricShadows.hlsli (or update the HLSL constants to accept the higher C++
defaults) so generator and evaluator match. Add a debug validation after the
moment-pass (readback or GPU-side assert) that checks the moment texture
contains no inf/NaN for the default settings (ExponentPositive=40,
ExponentNegative=5) across representative cascade depths and fail/clip if
invalid values are detected.
|
✅ A pre-release build is available for this PR: |
Summary
exp(c*z)positive +exp(-c*z)negative) instead of MSM's 4-moment Hamburger/Cholesky reconstruction, yielding simpler evaluation and better light bleed control.[0,1]range (globalNear/globalFar), ensuring exponent application is consistent regardless of cascade frustum size or camera angle.RGBA16F(wasRG16_UNORMfor MSM) to accommodate exponential warping values.DirectionalShadowLightDatastructured buffer so the sampling shader matches the downsample shader's depth space exactly.Key changes
VolumetricShadows.h/cppEVSMLinearizeCBwith global range, cascade extraction,GetCascadeDepthParams()/GetGlobalDepthParams()DownsampleShadowCS.hlslNormalizeDepth+WarpDepthinto EVSM moments using global rangeVolumetricShadows.hlsliNormalizeDepth+ComputeEVSMwith dual Chebyshev, light bleed reduction, 3D ray march and 2D point sample pathsBlurShadowCS.hlslShadowSampling.hlsliCascadeDepthParamsandGlobalDepthParamstoDirectionalShadowLightDatastructDeferred.h/cppSetupRenderTargetHooks.cppEVSM algorithm overview
[0,1]→ warp to(e^(c*d), e^(2c*d), e^(-c*d), e^(-2c*d))→ averageTest plan
ALLpreset — verified locally ✅🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes