Skip to content

feat(volumetric-shadows): EVSM shadow mapping with global depth normalization#2493

Merged
doodlum merged 4 commits into
devfrom
volumetric-shadows
Jun 9, 2026
Merged

feat(volumetric-shadows): EVSM shadow mapping with global depth normalization#2493
doodlum merged 4 commits into
devfrom
volumetric-shadows

Conversation

@doodlum

@doodlum doodlum commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces Moment Shadow Maps (MSM) with Exponential Variance Shadow Maps (EVSM) for volumetric shadow filtering. EVSM uses dual Chebyshev upper bounds (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.
  • Adds globally-consistent orthographic depth normalization across both shadow cascades. Shadow map depth (already linear under orthographic projection) is remapped through world units to a shared [0,1] range (globalNear/globalFar), ensuring exponent application is consistent regardless of cascade frustum size or camera angle.
  • Implements configurable Gaussian blur with dynamic radius for soft shadow filtering on the downsampled EVSM moments.
  • Stores EVSM moments in RGBA16F (was RG16_UNORM for MSM) to accommodate exponential warping values.
  • Passes cascade and global depth parameters through DirectionalShadowLightData structured buffer so the sampling shader matches the downsample shader's depth space exactly.

Key changes

File What changed
VolumetricShadows.h/cpp EVSM settings (exponents, blur radius), EVSMLinearizeCB with global range, cascade extraction, GetCascadeDepthParams()/GetGlobalDepthParams()
DownsampleShadowCS.hlsl Orthographic NormalizeDepth + WarpDepth into EVSM moments using global range
VolumetricShadows.hlsli NormalizeDepth + ComputeEVSM with dual Chebyshev, light bleed reduction, 3D ray march and 2D point sample paths
BlurShadowCS.hlsl Separable Gaussian blur operating on float4 EVSM moments
ShadowSampling.hlsli Added CascadeDepthParams and GlobalDepthParams to DirectionalShadowLightData struct
Deferred.h/cpp Populates depth params from VolumetricShadows feature, fixes render target resource leak in SetupRenderTarget
Hooks.cpp Removed old shadow copy hook (replaced by compute-based downsample)

EVSM algorithm overview

  1. Downsample: Gather shadow depth → orthographic normalize to global [0,1] → warp to (e^(c*d), e^(2c*d), e^(-c*d), e^(-2c*d)) → average
  2. Blur: Separable Gaussian on float4 moments (preserves statistical properties)
  3. Sample: Normalize receiver depth to same global space → warp → dual Chebyshev upper bound → min(positive, negative) → light bleed reduction remap

Test plan

  • Build with ALL preset — verified locally ✅
  • In-game: verify shadow quality on particles, decals, and volumetric effects
  • Test cascade transitions (walk from near to far) for consistent shadow sharpness
  • Verify no fp16 overflow artifacts at default exponents (positive=40, negative=5)
  • Test with different shadow quality settings and cascade distances
  • Compare performance vs previous MSM implementation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Upgraded shadow map filtering algorithm from VSM to EVSM
    • Added runtime-configurable blur radius for shadow maps
    • Introduced global and per-cascade depth normalization parameters
    • Enhanced debug UI with cascade metrics and blur statistics
    • Enabled settings persistence for volumetric shadow configuration
  • Bug Fixes

    • Corrected depth-stencil buffer sizing in volumetric lighting

doodlum and others added 4 commits June 9, 2026 10:58
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>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request converts the volumetric shadow system from VSM to EVSM by updating shader compute kernels to work with float4 moment textures, introducing dynamic blur radius and depth linearization via constant buffers, and extending CPU-side code to extract cascade parameters and manage GPU buffer updates.

Changes

EVSM Pipeline Conversion

Layer / File(s) Summary
Shader data contracts and constant buffers
package/Shaders/Common/ShadowSampling.hlsli, features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl, features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl, src/Deferred.h, src/Features/VolumetricShadows.h
DirectionalShadowLightData gains CascadeDepthParams and GlobalDepthParams fields; BlurShadowCS.hlsl introduces BlurCB constant buffer with runtime BlurRadius; DownsampleShadowCS.hlsl adds EVSMLinearizeCB with depth normalization bounds and exponent parameters; VolumetricShadows.h declares Settings struct (blur radius, exponents) and constant-buffer layout types.
EVSM moment computation and visibility
features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli, features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl
VolumetricShadows.hlsli introduces EVSM constants and ComputeEVSM() function that evaluates positive/negative exponent warps and returns minimum visibility; NormalizeDepth() maps cascade-local depth to global [0,1]; ChebyshevUpperBound() computes variance-biased visibility. DownsampleShadowCS.hlsl replaces VSM moment averaging with WarpDepth() exponential warping of all four gathered cascade depths.
3D and 2D cascade sampling integration
features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli
SampleEVSMCascade3D() and SampleEVSMCascade2D() functions added to read float4 EVSM moments and call ComputeEVSM() with per-cascade and global depth parameters; GetVSMShadow3D() and GetVSMShadow2D() updated to pass cascade/global near/far from DirectionalShadowLightData and call new EVSM samplers instead of VSM moment lookups.
Dynamic-radius separable Gaussian blur
features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl
Replaces fixed 11-tap VSM blur with runtime Gaussian: BlurRadius from constant buffer drives kernel size (capped at MAX_KERNEL_RADIUS=32), weights computed per dispatch via exp(...) and normalized by rcp(totalWeight). Horizontal and vertical passes update shared-memory cache from compile-time KERNEL_RADIUS/float2 to MAX_KERNEL_RADIUS/float4 with expanded overlap loading regions.
CPU depth parameter extraction and constant buffer management
src/Features/VolumetricShadows.cpp, src/Features/VolumetricShadows.h, src/Deferred.cpp, src/Deferred.h
ExtractCascadeNearFar() reads cascade bounds and UV/world projection scale from active sun shadow directional light; GetCascadeDepthParams() and GetGlobalDepthParams() expose depth bounds to compute shaders. SetupResources() creates linearizeCB and blurCB constant buffers with JSON serialization for Settings. CopyShadowLightData() populates DirectionalShadowLightData.CascadeDepthParams/GlobalDepthParams from extracted bounds and uploads to structured buffer.
Blur pass constant buffer updates and compute dispatch
src/Features/VolumetricShadows.cpp
Per-mip blur radius computed from settings.BlurRadius, cascade scale, and fixed shadow texture size, clamped to [1, 32]. linearizeCB updated per downsample dispatch with computed global/cascade near/far and exponent parameters via Map/Unmap. blurCB set per-mip with derived BlurRadius before separable blur passes; constant buffer binding/unbinding managed during dispatch sequence.
Settings persistence and debug UI
src/Features/VolumetricShadows.cpp, src/Features/VolumetricShadows.h
DrawSettings() adds ImGui sliders for blur radius and EVSM exponents with updated tooltips; debug "Info" panel shows cascade scale/near/far and computed blur pixel sizes. LoadSettings()/SaveSettings()/RestoreDefaultSettings() implement JSON persistence. "Buffer Viewer" labels changed from VSM to EVSM. Bug fix: CreateDepthStencil_VolumetricLighting::thunk applies 1024×1024 sizing to local properties struct before onward use.

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

  • alandtse
  • davo0411
  • jiayev

🐰 A quantum leap in shadows deep,
From moments squared to exponents steep,
Each blur now breathes what scale demands,
While Chebyshev keeps the light in lands.
EVSM's glow, a warped delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: migrating from VSM/MSM to EVSM shadow mapping with added global depth normalization, which aligns with the core functionality changes across all modified shader and C++ files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch volumetric-shadows

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
Raised by primitive operation at UTmp.replace_named_pipe_by_regular_file_if_needed in file "libs/commons/UTmp.ml", line 145, characters 8-27
Called from Scan_CLI.replace_target_roots_by_regular_files_where_needed.(fun) in file "src/osemgrep/cli_scan/Scan_CLI.ml", lines 1086-1087, characters 19-65
Called from List_.fast_map in file "libs/commons/List_.ml", line 81, characters 17-20
Called from Scan_CLI.re

🔧 Infer (1.2.0)
src/Features/VolumetricShadows.cpp

Usage Error: Failed to execute compilation command:
'/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/clang/install/bin/clang++'
-c src/Features/VolumetricShadows.cpp -o
/tmp/coderabbit-infer/2fe60b1e49a5926e/file.o

Error message:
clang++: error: no such file or directory: 'src/Features/VolumetricShadows.cpp'

*** Infer needs a working compilation command to run.

src/Deferred.cpp

Usage Error: Failed to execute compilation command:
'/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/clang/install/bin/clang++'
-c src/Deferred.cpp -o /tmp/coderabbit-infer/9aac6da5ffddefdc/file.o

Error message:
clang++: error: no such file or directory: 'src/Deferred.cpp'

*** 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@doodlum doodlum marked this pull request as draft June 9, 2026 15:56
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

No actionable suggestions for changed features.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/Features/VolumetricShadows.cpp (1)

332-333: 💤 Low value

Consider reusing a single null buffer pointer for unbinding.

The code creates a new nullptr local variable (nullCB, nullCB2) each time to unbind constant buffers. While this works correctly, you could declare a single ID3D11Buffer* 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

📥 Commits

Reviewing files that changed from the base of the PR and between 080afc0 and 51629b1.

📒 Files selected for processing (8)
  • features/Volumetric Shadows/Shaders/VolumetricShadows/BlurShadowCS.hlsl
  • features/Volumetric Shadows/Shaders/VolumetricShadows/DownsampleShadowCS.hlsl
  • features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli
  • package/Shaders/Common/ShadowSampling.hlsli
  • src/Deferred.cpp
  • src/Deferred.h
  • src/Features/VolumetricShadows.cpp
  • src/Features/VolumetricShadows.h

Comment on lines +8 to +12
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Comment on lines +39 to 54
// 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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Comment on lines +33 to +34
float4 CascadeDepthParams;
float4 GlobalDepthParams;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 hlsl

Repository: 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 . || true

Repository: 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 . || true

Repository: 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 . || true

Repository: 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 || true

Repository: 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 || true

Repository: 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)" || true

Repository: 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."
fi

Repository: 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" || true

Repository: 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" || true

Repository: 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" || true

Repository: 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" || true

Repository: 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 . || true

Repository: 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" || true

Repository: 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" || true

Repository: 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.35e17 overflows fp16 (~65504) → +inf
  • the 2nd moment uses exp(2*c*d)=exp(80*d), which overflows for d > ln(65504)/80 ≈ 0.139
    There is no clamping / isinf / isnan handling before writing to the RGBA16F UAV, so +inf/NaN moments 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.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

✅ A pre-release build is available for this PR:
Download

@doodlum doodlum merged commit 51629b1 into dev Jun 9, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant