feat: unlock shadow limit#1941
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR overhauls shadow handling: it moves shadow-data assembly from a GPU compute pass to CPU-populated structured buffers, adds a ShadowCasterManager for slot scheduling/budgeting, extends shader shadow-sampling APIs (PCF/PCSS), and wires directional/point shadow uploads and debug/visualization into the renderer and LightLimitFix feature. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App / Game
participant SCM as ShadowCasterManager
participant Deferred as Deferred Renderer
participant GPU as GPU Shaders (HLSL)
App->>SCM: ForEachShadowLight() / assign slots
activate SCM
SCM-->>App: shadowMapIndex per light
deactivate SCM
App->>Deferred: EarlyPrepass()
activate Deferred
Deferred->>Deferred: Build DirectionalShadowData (CPU)
Deferred->>GPU: Bind StructuredBuffer t98/t99 and ShadowMaps t101
deactivate Deferred
App->>GPU: Draw frame (Lighting)
activate GPU
GPU->>GPU: ShadowSampling::GetLightingShadow(DirectionalShadows[0], ...)
GPU->>GPU: GetShadowLightShadow(shadowMapIndex, ...)
GPU-->>App: Shaded pixels with PCF/PCSS and LLF debug data
deactivate GPU
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. |
With this PR: - community-shaders/skyrim-community-shaders#1941 We're getting close to the shadow limit being increased. The only reason LPO doesn't already include shadow casting lights everywhere is because of the shadow limit. Add an option for shadow lights to the fomod installer, and add shadow light configs. This prepares LPO for the impending shadow light limit increase.
3f3eb91 to
952dae6
Compare
This comment was marked as resolved.
This comment was marked as resolved.
efc619d to
ed692a5
Compare
This comment was marked as resolved.
This comment was marked as resolved.
ed692a5 to
e6ba2b7
Compare
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/Globals.h (1)
221-221:⚠️ Potential issue | 🟡 MinorDuplicate declaration of
tes.
RE::TES* tesis already declared at line 215. This duplicate declaration at line 221 should be removed.🧹 Proposed fix
extern RE::Setting* shadowMaskQuarter; extern REL::Relocation<ID3D11Buffer**> perFrame; extern REL::Relocation<RE::BSGraphics::BSShaderAccumulator**> currentAccumulator; - extern RE::TES* tes;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Globals.h` at line 221, Remove the duplicate extern declaration of RE::TES* tes in Globals.h by deleting the second occurrence (the redundant line declaring extern RE::TES* tes) so only the original declaration remains; ensure no other code depends on two externs and rebuild to confirm no linker changes are necessary.features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli (1)
153-166:⚠️ Potential issue | 🟠 MajorRemove transpose from
GetVSMShadow2D()matrix multiplication.Lines 154 and 166 use
mul(transpose(shadowProj), ...)whileGetVSMShadow3D()(lines 92, 108) andShadowSampling::GetLightingShadow()both usemul(shadowProj, ...)with the sameDirectionalShadowData.ShadowProj. This inconsistency causes incorrect light space transformation.Proposed fix
- float3 positionLS = mul(transpose(shadowProj), float4(position, 1)).xyz; + float3 positionLS = mul(shadowProj, float4(position, 1)).xyz;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/Volumetric` Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 153 - 166, The matrix multiplication in GetVSMShadow2D uses mul(transpose(shadowProj), float4(position, 1)) for both primary and secondary cascades which is inconsistent with GetVSMShadow3D and ShadowSampling::GetLightingShadow; change both occurrences to use mul(shadowProj, float4(position, 1)) so the same DirectionalShadowData.ShadowProj convention is used (affects the calls around SampleVSMCascade2D, the secondary cascade block using secondaryCascade = 1 - primaryCascade, and the positionLS computation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 12-15: GetShadowDepth and the other shadow-depth code are
incorrectly hard-coding FrameBuffer::CameraPosAdjust[0]; update all uses to
index with the provided eyeIndex (e.g., FrameBuffer::CameraPosAdjust[eyeIndex])
so the camera-adjusted depth, cascade selection, and ray transforms use the
correct eye. Locate GetShadowDepth(float3 positionWS, uint eyeIndex = 0) and the
nearby functions that accept eyeIndex (the shadow depth / cascade selection path
around the other occurrences) and replace any CameraPosAdjust[0] accesses with
CameraPosAdjust[eyeIndex], keeping the rest of the logic intact.
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 45-46: The two SRV declarations conflict when GRASS_COLLISION is
enabled because ShadowSampling.hlsli declares StructuredBuffer<ShadowData>
Shadows : register(t100) while GrassCollision.hlsli declares Texture2D<float4>
Collision : register(t100); update the ShadowSampling.hlsli resource bindings to
avoid t100 (e.g., shift Shadows and ShadowMaps to unused consecutive SRV
registers such as t102/t103 or a dedicated shadow SRV range), and ensure
RunGrass.hlsl/any includes using those new registers; adjust any shader code
that references Shadows or ShadowMaps to the new register names so the
grass+shadow-light variant has no binding overlap.
- Line 21: SharedShadowMap currently unguarded binds to register(t80) colliding
with TRUE_PBR's TexLandDisplacement0Sampler; locate the declaration of
Texture2D<float2> SharedShadowMap in ShadowSampling.hlsli and either wrap it
with `#if` defined(VOLUMETRIC_SHADOWS) / `#endif` so it only exists when volumetric
shadows are enabled, or move its register to an unused slot (e.g., change
register(t80) to register(t98) or register(t99)) to avoid the conflict with
Lighting.hlsl's TexLandDisplacement0Sampler under TRUE_PBR.
In `@package/Shaders/Lighting.hlsl`:
- Around line 2627-2652: The debug code treats overflowed lights as unshadowed
because UpdateLights() strips LightFlags::Shadow; change the logic to detect
overflow by inspecting light.shadowMapIndex before depending on
light.lightFlags: first check if light.shadowMapIndex >=
SharedData::lightLimitFixSettings.ShadowMapSlots and increment
debugOverflowCount (and handle as overflow), else proceed to test
(light.lightFlags & LightLimitFix::LightFlags::Shadow) to increment
debugPLShadowCount / debugUnshadowedPLCount and use shadowMapIndex to classify
types; update the block around debugOverflowCount, debugPLShadowCount,
debugUnshadowedPLCount and the shadow-type classification that currently reads
Shadows[light.shadowMapIndex].ShadowParam.x so overflowed indices are never
dereferenced.
In `@src/Deferred.cpp`:
- Around line 118-131: The code leaves shadowMapSlots unchanged when the
kSHADOWMAPS SRV is null or not a Texture2DArray/has ArraySize==0, causing later
code to treat stale non-zero slots as valid; in SetupResources (Deferred.cpp)
after checking
renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS].depthSRV,
set shadowMapSlots = 0 on both failure branches (null SRV and the else branch
where desc.ViewDimension != D3D11_SRV_DIMENSION_TEXTURE2DARRAY or ArraySize ==
0) and update the corresponding logger messages to reflect the reset so later
code using shadowMapSlots no longer assumes an array exists.
- Around line 172-189: The perDirectionalShadow pointer is overwritten in
SetupResources without releasing the previous GPU Buffer, leaking DirectX
resources; before assigning a new Buffer instance in the block that constructs
sbDesc/srvDesc, release or delete the existing perDirectionalShadow (or
preferably switch perDirectionalShadow to a smart pointer like
std::unique_ptr/ComPtr) and null-check it, then create the new Buffer and call
CreateSRV; update usages of perDirectionalShadow accordingly (Buffer,
perDirectionalShadow, CreateSRV) to ensure no leaks and safe reinitialization
when render targets are recreated.
In `@src/Features/LightLimitFix.cpp`:
- Around line 598-614: The loop currently uses bufferIndex (per-light count) to
decide shadowing against globals::deferred->shadowMapSlots, which is wrong for
multi-slice lights; change the logic to track slicesUsed (start 0) and for each
light (shadowSceneNode->GetRuntimeData().shadowLightsAccum[mapIndex]) compute
canShadow = (slicesUsed + light->shadowMapCount) <=
(int)globals::deferred->shadowMapSlots, pass canShadow to addShadowLight instead
of bufferIndex check, then if canShadow do slicesUsed += light->shadowMapCount;
still increment mapIndex by light->shadowMapCount and bufferIndex only if you
need the original per-light index (or remove bufferIndex entirely).
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 64-86: FormulaHelper currently holds a raw pointer _ptr to a
dynamically allocated FormulaWrapper and is copyable by default, which causes
shallow copies and double-free; make FormulaHelper non-copyable by explicitly
deleting the copy constructor and copy assignment operator (e.g., declare
FormulaHelper(const FormulaHelper&) = delete; and FormulaHelper& operator=(const
FormulaHelper&) = delete;) so copies cannot be made; leave or implement move
operations only if ownership transfer is needed, but at minimum delete copy
operations in the FormulaHelper declaration to prevent double-delete.
- Around line 103-120: Validate and clamp the combined slot count when loading
or initializing settings: in LoadSettings() or Init() (not just Install()),
compute total = Settings::ShadowLightCount + Settings::ConvertedShadowSlots + 1
(for sun) and clamp it to the renderer's supported maximum before allocating the
LightContainer or related DX11 resources; update Settings::ShadowLightCount
and/or Settings::ConvertedShadowSlots (or derive effective counts) so subsequent
code uses the capped values, and ensure allocation failures degrade gracefully
(release any partially created DirectX11 resources and log an error) to prevent
crashes from malformed JSON.
In `@src/Features/LightLimitFix/ShadowRenderer.cpp`:
- Around line 25-38: SetupShadowResources currently recreates the D3D11 sampler
without releasing the previous COM object; before calling
globals::d3d::device->CreateSamplerState(&cmpDesc, &shadowCmpSampler) in
LightLimitFix::SetupShadowResources, check if shadowCmpSampler is non-null and
call shadowCmpSampler->Release() (and set it to nullptr) to avoid leaking the
old sampler, and ensure that on CreateSamplerState failure the member is left
null to prevent dangling references.
- Around line 102-129: The current guard uses "plCount < slots" to allow writes
into sd[depthSlot], but for dual-paraboloid lights (shadowMapCount==2) depthSlot
can index past sd; change the condition to validate the actual depthSlot range
before writing — e.g., compute depthSlot using light->GetVRRuntimeData() /
GetRuntimeData() as done, then ensure (depthSlot + light->shadowMapCount) <=
slots (or depthSlot < slots and depthSlot + light->shadowMapCount - 1 < slots)
before touching sd[depthSlot] and calling SetShadowParameters /
ShadowCasterManager::RecordSlot so you never perform out-of-bounds writes for
lights with shadowMapCount == 2 (references: sd, plCount, slots, depthSlot,
shadowMapCount, shadowAccum, ShadowCasterManager, GetRuntimeData,
GetVRRuntimeData, SetShadowParameters).
In `@src/Features/VolumetricShadows.cpp`:
- Around line 276-279: The current binding logic prefers shadowCopySRV and
therefore rebinds a previous-frame SRV when the current source (shadowView) is
missing; change the selection to only bind a valid current-frame SRV and bind
null otherwise. Specifically, in the block that sets PS slot 18 (using
shadowCopySRV, shadowView and context->PSSetShaderResources), choose shadowView
if present and bind nullptr when shadowView is null (do not fall back to
shadowCopySRV), so the VSM path is effectively disabled for this frame; ensure
you pass a null ID3D11ShaderResourceView* to PSSetShaderResources when no
current SRV is available.
---
Outside diff comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 153-166: The matrix multiplication in GetVSMShadow2D uses
mul(transpose(shadowProj), float4(position, 1)) for both primary and secondary
cascades which is inconsistent with GetVSMShadow3D and
ShadowSampling::GetLightingShadow; change both occurrences to use
mul(shadowProj, float4(position, 1)) so the same
DirectionalShadowData.ShadowProj convention is used (affects the calls around
SampleVSMCascade2D, the secondary cascade block using secondaryCascade = 1 -
primaryCascade, and the positionLS computation).
In `@src/Globals.h`:
- Line 221: Remove the duplicate extern declaration of RE::TES* tes in Globals.h
by deleting the second occurrence (the redundant line declaring extern RE::TES*
tes) so only the original declaration remains; ensure no other code depends on
two externs and rebuild to confirm no linker changes are necessary.
🪄 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
Run ID: ac942958-f423-4cdd-a37d-aebe012b1bdb
📒 Files selected for processing (26)
CMakeLists.txtextern/CommonLibSSE-NGfeatures/Light Limit Fix/Shaders/LightLimitFix/Common.hlslifeatures/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlslifeatures/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlslfeatures/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlslipackage/Shaders/Common/ShadowSampling.hlslipackage/Shaders/Common/SharedData.hlslipackage/Shaders/Lighting.hlslpackage/Shaders/RunGrass.hlslsrc/Deferred.cppsrc/Deferred.hsrc/Features/LightLimitFix.cppsrc/Features/LightLimitFix.hsrc/Features/LightLimitFix/ShadowCasterManager.cppsrc/Features/LightLimitFix/ShadowCasterManager.hsrc/Features/LightLimitFix/ShadowRenderer.cppsrc/Features/RenderDoc.cppsrc/Features/VolumetricShadows.cppsrc/Features/VolumetricShadows.hsrc/Globals.cppsrc/Globals.hsrc/Menu/FeatureListRenderer.cppsrc/State.cppsrc/XSEPlugin.cppvcpkg.json
💤 Files with no reviewable changes (3)
- features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli
- src/Features/VolumetricShadows.h
- features/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlsl
b1089cd to
5f27c69
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (8)
src/Deferred.cpp (2)
172-189:⚠️ Potential issue | 🟠 MajorRelease the old
perDirectionalShadowbuffer before recreating it.
Deferred::SetupResources()can rerun when render targets are recreated. Line 187 overwrites the raw pointer without deleting the previousBuffer, so each reset leaks one GPU resource.Proposed fix
- perDirectionalShadow = new Buffer(sbDesc); + delete perDirectionalShadow; + perDirectionalShadow = nullptr; + perDirectionalShadow = new Buffer(sbDesc); perDirectionalShadow->CreateSRV(srvDesc);As per coding guidelines, "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Deferred.cpp` around lines 172 - 189, Deferred::SetupResources currently overwrites the raw perDirectionalShadow pointer without releasing the previous GPU resource, leaking a Buffer; before assigning a new Buffer(sbDesc) call delete (or release) on the existing perDirectionalShadow if it's non-null and set it to nullptr, then recreate it and call perDirectionalShadow->CreateSRV(srvDesc); update error paths to leave a valid state if creation fails and ensure the Buffer destructor/release path frees the underlying SRV and D3D resources via the Buffer class.
118-131:⚠️ Potential issue | 🟠 MajorReset
shadowMapSlotswhenkSHADOWMAPSdiscovery fails.Keeping the previous value here lets later passes keep treating a stale shadow-array capacity as valid after a target recreation or setup failure.
Proposed fix
if (desc.ViewDimension == D3D11_SRV_DIMENSION_TEXTURE2DARRAY && desc.Texture2DArray.ArraySize > 0) { shadowMapSlots = desc.Texture2DArray.ArraySize; logger::info("[Deferred] kSHADOWMAPS ArraySize = {}, effective shadowMapSlots = {}", desc.Texture2DArray.ArraySize, shadowMapSlots); } else { - logger::warn("[Deferred] kSHADOWMAPS SRV not a Texture2DArray or ArraySize=0; keeping shadowMapSlots = {}", shadowMapSlots); + shadowMapSlots = 0; + logger::warn("[Deferred] kSHADOWMAPS SRV not a Texture2DArray or ArraySize=0; resetting shadowMapSlots to 0"); } } else { - logger::warn("[Deferred] kSHADOWMAPS depthSRV is null at SetupResources; keeping shadowMapSlots = {}", shadowMapSlots); + shadowMapSlots = 0; + logger::warn("[Deferred] kSHADOWMAPS depthSRV is null at SetupResources; resetting shadowMapSlots to 0"); }As per coding guidelines, "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Deferred.cpp` around lines 118 - 131, When kSHADOWMAPS discovery fails in SetupResources (i.e., depthSRV is null or desc.ViewDimension != D3D11_SRV_DIMENSION_TEXTURE2DARRAY or desc.Texture2DArray.ArraySize == 0), reset shadowMapSlots to 0 instead of preserving the prior value; update the branches that currently log warnings (the else branches around renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS].depthSRV and the ViewDimension/ArraySize check) to assign shadowMapSlots = 0 before logging so downstream passes don’t use a stale capacity.features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli (1)
12-15:⚠️ Potential issue | 🟠 MajorUse
eyeIndexconsistently in the camera-adjusted depth path.Lines 14, 65-66, and 69 hard-code
FrameBuffer::CameraPosAdjust[0]. In VR the right eye will pick cascades and ray endpoints from the left-eye origin, which is a direct stereo mismatch near split boundaries.Proposed fix
float GetShadowDepth(float3 positionWS, uint eyeIndex = 0) { - return length(positionWS - FrameBuffer::CameraPosAdjust[0].xyz); + return length(positionWS - FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } @@ - startPosition += FrameBuffer::CameraPosAdjust[0].xyz; - endPosition += FrameBuffer::CameraPosAdjust[0].xyz; + startPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + endPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; @@ - float shadowMapDepth = length(midPosition - FrameBuffer::CameraPosAdjust[0].xyz); + float shadowMapDepth = length(midPosition - FrameBuffer::CameraPosAdjust[eyeIndex].xyz);Also applies to: 63-70
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/Volumetric` Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 12 - 15, GetShadowDepth and nearby camera-adjusted depth/cascade/ray-endpoint code use FrameBuffer::CameraPosAdjust[0] instead of the provided eyeIndex, causing stereo mismatch; update GetShadowDepth to index CameraPosAdjust with the eyeIndex parameter and change the other hard-coded CameraPosAdjust[0] accesses in the same block (cascade selection and ray endpoint calculations) to use the eyeIndex variable (or pass the correct eyeIndex through helpers) so each eye uses its own CameraPosAdjust entry.src/Features/LightLimitFix.cpp (1)
598-614:⚠️ Potential issue | 🟠 MajorTrack consumed shadow-map slices, not shadowed lights.
shadowMapSlotsis array-slice capacity. Comparing it tobufferIndexlets a multi-slice light (shadowMapCount == 2) keep its Shadow flag after the array is already full, so the shader can sample a stale or out-of-rangeshadowMapIndex.Proposed fix
- int bufferIndex = 0; - int mapIndex = 0; + uint32_t mapIndex = 0; while (true) { RE::BSShadowLight* light = shadowSceneNode->GetRuntimeData().shadowLightsAccum[mapIndex]; if (!light) break; - // Only set Shadow flag for lights with a valid written slot. - // Overflow lights still use addShadowLight for correct color/radius setup, - // but without the Shadow flag so the HLSL does not do a shadow map lookup - // with a stale or out-of-range shadowMapIndex. - addShadowLight(light, bufferIndex < (int)globals::deferred->shadowMapSlots); - - mapIndex += light->shadowMapCount; - bufferIndex++; + const uint32_t shadowMapCount = light->shadowMapCount; + const bool hasWrittenSlots = mapIndex + shadowMapCount <= globals::deferred->shadowMapSlots; + + // Only set Shadow for lights whose slices fit inside the written array. + addShadowLight(light, hasWrittenSlots); + mapIndex += shadowMapCount; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix.cpp` around lines 598 - 614, The loop currently compares bufferIndex (counting lights) against globals::deferred->shadowMapSlots, which allows multi-slice lights (shadowMapCount>1) to be marked Shadow even when the slice capacity is exhausted; change the logic to track consumed shadow-map slices instead: introduce a consumedSlices (or reuse bufferIndex as slice counter) and for each RE::BSShadowLight* light from shadowSceneNode->GetRuntimeData().shadowLightsAccum use consumedSlices and light->shadowMapCount to decide the flag passed to addShadowLight (true only if consumedSlices + light->shadowMapCount <= (int)globals::deferred->shadowMapSlots), then increment consumedSlices by light->shadowMapCount; keep mapIndex += light->shadowMapCount to advance the source array.src/Features/VolumetricShadows.cpp (1)
276-279:⚠️ Potential issue | 🟠 MajorBind slot 18 only when this frame has a valid source shadow SRV.
Line 278 falls back to
shadowCopySRVwhenshadowViewis null, so a frame with no current source keeps sampling the previous frame’s blurred VSM texture instead of disabling the VSM path for that frame.Proposed fix
- ID3D11ShaderResourceView* srv = shadowCopySRV ? shadowCopySRV : shadowView; + ID3D11ShaderResourceView* srv = shadowView ? shadowCopySRV : nullptr; context->PSSetShaderResources(18, 1, &srv);As per coding guidelines, "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/VolumetricShadows.cpp` around lines 276 - 279, The current binding falls back to shadowCopySRV when shadowView is null causing stale VSM sampling; change the logic around the PSSetShaderResources call so you only bind a VSM SRV to slot 18 when the current-frame source SRV (shadowView) is valid — if shadowView is null, bind a null/empty SRV to slot 18 (do not use shadowCopySRV as fallback) while leaving the shadow data structured buffer binding (Deferred::CopyShadowData()) unchanged; update the code around the shadowView/shadowCopySRV variables and the context->PSSetShaderResources(18, 1, ...) call accordingly.package/Shaders/Common/ShadowSampling.hlsli (1)
45-46:⚠️ Potential issue | 🔴 Critical
Shadowsstill conflicts with the grass-collision SRV slot.
features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlslialready bindsCollisiontot100. KeepingShadowsatt100makes the grass + shadow-light permutation invalid, so the shadow-light SRVs need a dedicated range and the matching CPU binds updated with it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/Common/ShadowSampling.hlsli` around lines 45 - 46, The Shadow SRV binding for StructuredBuffer<ShadowData> named Shadows is conflicting with the grass collision SRV (Collision bound to t100); move the shadow SRV range away from t100 (e.g., choose an unused register range for Shadows and ShadowMaps instead of t100/t101) and update the HLSL declarations (Shadows and ShadowMaps) to the new registers, then update the matching CPU-side bind code that sets these SRVs so the new register indices are used; ensure you also update any permutation or binding table that assumes t100 to avoid the grass+shadow-light permutation failure (refer to Shadows, ShadowMaps, and Collision to locate all usages).src/Features/LightLimitFix/ShadowCasterManager.h (2)
116-120:⚠️ Potential issue | 🟠 MajorClamp the combined shadow-slot budget before allocation.
These two fields share the same renderer limit, so malformed JSON can still push
ShadowLightCount + ConvertedShadowSlots + 1past the supported slot count even if each field is individually "valid". Please cap the combined total in the load/init path before allocating the light container or DX11 resources. As per coding guidelines, "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix/ShadowCasterManager.h` around lines 116 - 120, Clamp the combined shadow-slot budget before any allocation: in the manager's load/init code where ShadowLightCount and ConvertedShadowSlots are parsed/validated, compute total = ShadowLightCount + ConvertedShadowSlots + 1, then cap total to the renderer's max slots and reduce ConvertedShadowSlots (or ShadowLightCount) accordingly so allocations for the light container and DX11 resources never request more than the supported slot count; ensure the capped values are used for subsequent resource creation and add a safe fallback path that logs the adjustment and avoids creating oversized DX11 resources.
64-67: 🛠️ Refactor suggestion | 🟠 MajorDelete
FormulaHelpercopy operations.
_ptris owning state torn down in~FormulaHelper(), so the compiler-generated copy members would shallow-copy it and double-delete on the first accidental copy. Please delete copy/move here unless you implement real transfer semantics.🛠️ Example fix
struct FormulaHelper { FormulaHelper(); ~FormulaHelper(); + FormulaHelper(const FormulaHelper&) = delete; + FormulaHelper& operator=(const FormulaHelper&) = delete; + FormulaHelper(FormulaHelper&&) = delete; + FormulaHelper& operator=(FormulaHelper&&) = delete; bool Parse(const std::string& input); double Calculate(); @@ private: void* _ptr; };Also applies to: 84-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix/ShadowCasterManager.h` around lines 64 - 67, FormulaHelper owns `_ptr` and defines a destructor, so you must prevent implicit shallow copies; declare the copy constructor and copy-assignment operator as deleted (FormulaHelper(const FormulaHelper&) = delete; FormulaHelper& operator=(const FormulaHelper&) = delete;) and also delete the move constructor and move-assignment operator unless you implement transfer semantics (FormulaHelper(FormulaHelper&&) = delete; FormulaHelper& operator=(FormulaHelper&&) = delete;). Apply the same deletion to the other owning struct in this file that holds `_ptr`.
🧹 Nitpick comments (1)
package/Shaders/RunGrass.hlsl (1)
598-599: Compute the PCF rotation matrix lazily.Both pixel shader variants build this before they know whether the tile contains any shadowed point lights. In grass-heavy scenes that adds per-fragment work even when the clustered-light loop exits early or every clustered light is unshadowed. Compute it on the first
LightLimitFix::LightFlags::Shadowhit instead.Also applies to: 873-874
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/RunGrass.hlsl` around lines 598 - 599, Currently the rotationMatrix is computed unconditionally with ShadowSampling::GetPCFRotationMatrix(input.WorldPosition.xyz); move that call so it is computed lazily the first time a light with LightLimitFix::LightFlags::Shadow is encountered in the per-pixel clustered-light loop: introduce a local flag (e.g. bool pcfComputed) and a float2x2 pcfRotation local, then on the first shadow-flag hit call ShadowSampling::GetPCFRotationMatrix(input.WorldPosition.xyz) to populate pcfRotation and set pcfComputed, and use pcfRotation thereafter instead of the eagerly-created rotationMatrix; apply the same change in both pixel shader variants (also update the other occurrence around lines 873-874).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 156-157: The current fade uses the full range (fade =
saturate(shadowMapDepth / shadow.EndSplitDistances.y)), causing early fading;
change it to a narrow edge-only ramp: compute a small window near
shadow.EndSplitDistances.y (e.g., margin either a small constant or a fraction
of the cascade size using shadow.StartSplitDistances.y and
shadow.EndSplitDistances.y) and remap shadowMapDepth into that window using a
smooth ramp (smoothstep or equivalent) so fading only starts inside that narrow
boundary; update the calculation that produces fade (referencing fade,
shadowMapDepth, shadow.StartSplitDistances.y, and shadow.EndSplitDistances.y)
accordingly.
In `@src/Deferred.cpp`:
- Around line 580-589: The early return guarded by shadowMapSlots causes
directional shadow cascade upload to be skipped and leaves stale SRV bindings in
t98/t99; instead, stop gating the sun cascade upload on shadowMapSlots and gate
on the presence of sunShadowLight and the cascade SRVs (e.g., check
globals::game::smState->shadowSceneNode,
shadowSceneNode->GetRuntimeData().sunShadowDirLight and the ESram cascade SRVs),
and on any path where the cascade data is unavailable explicitly bind null to
shader slots t98 and t99 to clear previous-frame bindings before returning;
update the logic around shadowMapSlots, shadowSceneNode, sunShadowLight and the
code paths referenced (lines handling t98/t99 binding at the end of the block)
to implement these checks and null-bind clears.
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 76-82: The Validate/SetParam/GetParam trio currently shares a
global parameter table allowing light-scoped symbols in RedrawBudgetFormula;
update Validate(const std::string& input, std::string& errorOut) to enforce a
symbol whitelist when validating expressions for RedrawBudgetFormula (only allow
frametime, frametarget, stableframes, camera/environment params) and reject or
rewrite any light-scoped symbols (lightindex, lightintensity, etc.), or
alternatively split the backing parameter storage into separate tables per
formula type and have Validate check/associate the correct table; also ensure
SetParam and GetParam operate against the per-formula table (or respect the same
symbol restrictions) so runtime reads/writes cannot access stale light-scoped
values for RedrawBudgetFormula.
---
Duplicate comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 12-15: GetShadowDepth and nearby camera-adjusted
depth/cascade/ray-endpoint code use FrameBuffer::CameraPosAdjust[0] instead of
the provided eyeIndex, causing stereo mismatch; update GetShadowDepth to index
CameraPosAdjust with the eyeIndex parameter and change the other hard-coded
CameraPosAdjust[0] accesses in the same block (cascade selection and ray
endpoint calculations) to use the eyeIndex variable (or pass the correct
eyeIndex through helpers) so each eye uses its own CameraPosAdjust entry.
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 45-46: The Shadow SRV binding for StructuredBuffer<ShadowData>
named Shadows is conflicting with the grass collision SRV (Collision bound to
t100); move the shadow SRV range away from t100 (e.g., choose an unused register
range for Shadows and ShadowMaps instead of t100/t101) and update the HLSL
declarations (Shadows and ShadowMaps) to the new registers, then update the
matching CPU-side bind code that sets these SRVs so the new register indices are
used; ensure you also update any permutation or binding table that assumes t100
to avoid the grass+shadow-light permutation failure (refer to Shadows,
ShadowMaps, and Collision to locate all usages).
In `@src/Deferred.cpp`:
- Around line 172-189: Deferred::SetupResources currently overwrites the raw
perDirectionalShadow pointer without releasing the previous GPU resource,
leaking a Buffer; before assigning a new Buffer(sbDesc) call delete (or release)
on the existing perDirectionalShadow if it's non-null and set it to nullptr,
then recreate it and call perDirectionalShadow->CreateSRV(srvDesc); update error
paths to leave a valid state if creation fails and ensure the Buffer
destructor/release path frees the underlying SRV and D3D resources via the
Buffer class.
- Around line 118-131: When kSHADOWMAPS discovery fails in SetupResources (i.e.,
depthSRV is null or desc.ViewDimension != D3D11_SRV_DIMENSION_TEXTURE2DARRAY or
desc.Texture2DArray.ArraySize == 0), reset shadowMapSlots to 0 instead of
preserving the prior value; update the branches that currently log warnings (the
else branches around
renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGET_DEPTHSTENCIL::kSHADOWMAPS].depthSRV
and the ViewDimension/ArraySize check) to assign shadowMapSlots = 0 before
logging so downstream passes don’t use a stale capacity.
In `@src/Features/LightLimitFix.cpp`:
- Around line 598-614: The loop currently compares bufferIndex (counting lights)
against globals::deferred->shadowMapSlots, which allows multi-slice lights
(shadowMapCount>1) to be marked Shadow even when the slice capacity is
exhausted; change the logic to track consumed shadow-map slices instead:
introduce a consumedSlices (or reuse bufferIndex as slice counter) and for each
RE::BSShadowLight* light from
shadowSceneNode->GetRuntimeData().shadowLightsAccum use consumedSlices and
light->shadowMapCount to decide the flag passed to addShadowLight (true only if
consumedSlices + light->shadowMapCount <=
(int)globals::deferred->shadowMapSlots), then increment consumedSlices by
light->shadowMapCount; keep mapIndex += light->shadowMapCount to advance the
source array.
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 116-120: Clamp the combined shadow-slot budget before any
allocation: in the manager's load/init code where ShadowLightCount and
ConvertedShadowSlots are parsed/validated, compute total = ShadowLightCount +
ConvertedShadowSlots + 1, then cap total to the renderer's max slots and reduce
ConvertedShadowSlots (or ShadowLightCount) accordingly so allocations for the
light container and DX11 resources never request more than the supported slot
count; ensure the capped values are used for subsequent resource creation and
add a safe fallback path that logs the adjustment and avoids creating oversized
DX11 resources.
- Around line 64-67: FormulaHelper owns `_ptr` and defines a destructor, so you
must prevent implicit shallow copies; declare the copy constructor and
copy-assignment operator as deleted (FormulaHelper(const FormulaHelper&) =
delete; FormulaHelper& operator=(const FormulaHelper&) = delete;) and also
delete the move constructor and move-assignment operator unless you implement
transfer semantics (FormulaHelper(FormulaHelper&&) = delete; FormulaHelper&
operator=(FormulaHelper&&) = delete;). Apply the same deletion to the other
owning struct in this file that holds `_ptr`.
In `@src/Features/VolumetricShadows.cpp`:
- Around line 276-279: The current binding falls back to shadowCopySRV when
shadowView is null causing stale VSM sampling; change the logic around the
PSSetShaderResources call so you only bind a VSM SRV to slot 18 when the
current-frame source SRV (shadowView) is valid — if shadowView is null, bind a
null/empty SRV to slot 18 (do not use shadowCopySRV as fallback) while leaving
the shadow data structured buffer binding (Deferred::CopyShadowData())
unchanged; update the code around the shadowView/shadowCopySRV variables and the
context->PSSetShaderResources(18, 1, ...) call accordingly.
---
Nitpick comments:
In `@package/Shaders/RunGrass.hlsl`:
- Around line 598-599: Currently the rotationMatrix is computed unconditionally
with ShadowSampling::GetPCFRotationMatrix(input.WorldPosition.xyz); move that
call so it is computed lazily the first time a light with
LightLimitFix::LightFlags::Shadow is encountered in the per-pixel
clustered-light loop: introduce a local flag (e.g. bool pcfComputed) and a
float2x2 pcfRotation local, then on the first shadow-flag hit call
ShadowSampling::GetPCFRotationMatrix(input.WorldPosition.xyz) to populate
pcfRotation and set pcfComputed, and use pcfRotation thereafter instead of the
eagerly-created rotationMatrix; apply the same change in both pixel shader
variants (also update the other occurrence around lines 873-874).
🪄 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
Run ID: 49bb58e4-c1dd-470b-917f-2d1f850315d4
📒 Files selected for processing (26)
CMakeLists.txtextern/CommonLibSSE-NGfeatures/Light Limit Fix/Shaders/LightLimitFix/Common.hlslifeatures/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlslifeatures/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlslfeatures/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlslipackage/Shaders/Common/ShadowSampling.hlslipackage/Shaders/Common/SharedData.hlslipackage/Shaders/Lighting.hlslpackage/Shaders/RunGrass.hlslsrc/Deferred.cppsrc/Deferred.hsrc/Features/LightLimitFix.cppsrc/Features/LightLimitFix.hsrc/Features/LightLimitFix/ShadowCasterManager.cppsrc/Features/LightLimitFix/ShadowCasterManager.hsrc/Features/LightLimitFix/ShadowRenderer.cppsrc/Features/RenderDoc.cppsrc/Features/VolumetricShadows.cppsrc/Features/VolumetricShadows.hsrc/Globals.cppsrc/Globals.hsrc/Menu/FeatureListRenderer.cppsrc/State.cppsrc/XSEPlugin.cppvcpkg.json
💤 Files with no reviewable changes (3)
- features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli
- src/Features/VolumetricShadows.h
- features/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlsl
✅ Files skipped from review due to trivial changes (7)
- extern/CommonLibSSE-NG
- vcpkg.json
- src/State.cpp
- src/Menu/FeatureListRenderer.cpp
- src/XSEPlugin.cpp
- src/Globals.h
- package/Shaders/Lighting.hlsl
🚧 Files skipped from review as they are similar to previous changes (5)
- src/Features/RenderDoc.cpp
- features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli
- src/Globals.cpp
- package/Shaders/Common/SharedData.hlsli
- src/Features/LightLimitFix/ShadowRenderer.cpp
5f27c69 to
d373e24
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/Features/VolumetricShadows.cpp (1)
276-279:⚠️ Potential issue | 🟠 MajorBind null when the current shadow SRV is missing.
If Line 83 returns
nullptr, this still rebinds the previousshadowCopySRV, so slot18samples stale shadow data from an earlier frame/scene instead of disabling the VSM path for this frame.Suggested fix
- ID3D11ShaderResourceView* srv = shadowCopySRV ? shadowCopySRV : shadowView; + ID3D11ShaderResourceView* srv = shadowView ? (shadowCopySRV ? shadowCopySRV : shadowView) : nullptr; context->PSSetShaderResources(18, 1, &srv);As per coding guidelines, "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/VolumetricShadows.cpp` around lines 276 - 279, The current binding logic uses ID3D11ShaderResourceView* srv = shadowCopySRV ? shadowCopySRV : shadowView; which can rebind a stale shadowCopySRV when both are null; change it so that srv is explicitly nullptr if neither shadowCopySRV nor shadowView is valid and then call context->PSSetShaderResources(18, 1, &srv) with that nullptr to unbind slot 18; update the code path around shadowCopySRV, shadowView and the PSSetShaderResources call to ensure slot 18 is explicitly cleared when no current SRV exists (use a local ID3D11ShaderResourceView* nullSrv = nullptr and pass its address).
🧹 Nitpick comments (3)
src/Features/LightLimitFix.h (1)
228-228: Partial array initialization.
uint clusterSize[3] = { 16 }initializes only the first element; the remaining two elements are zero-initialized. If the intent is{16, 16, 16}, make it explicit. If{16, 0, 0}is intended, a comment would clarify.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix.h` at line 228, The array clusterSize defined as "uint clusterSize[3] = { 16 }" only initializes the first element and leaves the other two as zero; update the initializer to explicitly set all three entries (e.g., change the initializer to {16, 16, 16} if that's intended) or keep {16, 0, 0} but add a clarifying comment next to clusterSize to document the intended shape so future readers know whether all three dimensions should be 16 or zeros are deliberate.features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli (1)
120-122: Fade curve difference between 2D and 3D paths.
GetVSMShadow3Dusespow(fade * fade, 8)(line 120), whileGetVSMShadow2Dusespow(fade, 8)(line 174). The 3D path effectively appliespow(fade, 16)— a much sharper falloff. If this is intentional for volumetric vs surface shadowing, consider adding a comment; otherwise align them.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/Volumetric` Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 120 - 122, GetVSMShadow3D uses pow(fade * fade, 8) producing an effective pow(fade,16) which differs from GetVSMShadow2D's pow(fade,8); either align the 3D path by replacing pow(fade * fade, 8) with pow(fade, 8) (and update fadeFactor/surfaceShadow usage accordingly) or add a brief comment in VolumetricShadows.hlsli next to GetVSMShadow3D/fadeFactor explaining the intentional stronger falloff for volumetric shadows so reviewers know the discrepancy is deliberate.package/Shaders/Common/ShadowSampling.hlsli (1)
380-387: Magic sentinel values for slot state could use named constants.
ShadowParam.y == 0(unwritten),< 0(suppressed),> 0(valid) are checked inline. Consider extracting these as named constants for clarity, though the comments document the semantics adequately.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/Common/ShadowSampling.hlsli` around lines 380 - 387, Replace the inline magic checks against shadow.ShadowParam.y with well-named constants to clarify slot states: define symbolic names for the unwritten sentinel (== 0), the suppressed sentinel (< 0) and the valid-positive case (> 0) and use those constants in the comparisons where ShadowParam.y is tested; update the nearby comment to reference the constants (and their meanings) so future readers see intent instead of raw numbers. Ensure the constants are visible to the same shader scope (e.g., top of ShadowSampling.hlsli or shared header) and keep the comparison logic in the same function using the new names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 153-154: GetVSMShadow2D is incorrectly using
mul(transpose(shadowProj), float4(position,1)) while GetVSMShadow3D and other
shadow code (in ShadowSampling.hlsli) consistently use mul(shadowProj,
float4(...,1)) for the column_major ShadowProj matrix; update GetVSMShadow2D to
remove transpose and use mul(shadowProj, float4(position, 1)) (and the analogous
change at the other occurrence) so both 2D and 3D paths use the same matrix
multiplication convention with ShadowProj/primaryCascade.
In `@package/Shaders/Lighting.hlsl`:
- Around line 2433-2435: The SharedShadowMap register t80 declared in
Common/ShadowSampling.hlsli collides with TexLandDisplacement0Sampler (t80–t85)
used when LANDSCAPE and TRUE_PBR are defined; update the shader declarations so
they don't overlap: either wrap the SharedShadowMap (and any related Shared*
resources) register assignment in a conditional (`#if` !defined(LANDSCAPE) ||
!defined(TRUE_PBR)) or pick a nonconflicting register when both LANDSCAPE and
TRUE_PBR are defined (e.g., move SharedShadowMap to a free register and update
its declaration in Common/ShadowSampling.hlsli and any call sites like
ShadowSampling::GetLightingShadow), ensuring all references in Lighting.hlsl and
ShadowSampling.hlsli use the matching symbol names (SharedShadowMap,
ShadowSampling::GetLightingShadow) so permutations compile without register
collisions.
In `@src/Features/LightLimitFix.cpp`:
- Around line 206-208: LightLimitFix::LoadSettings currently assigns the
incoming json directly ("settings = o_json") which throws on malformed or
type-mismatched user config; change LoadSettings to validate and merge instead:
keep the existing defaults in the member "settings", check for presence and
correct types on keys in "o_json" (use contains and is_* checks), only overwrite
validated fields, and wrap the parse/merge in a try/catch to log errors and
leave defaults intact on failure so startup does not crash.
In `@src/Features/LightLimitFix.h`:
- Around line 134-135: previousEnableLightsVisualisation and
currentEnableLightsVisualisation are being initialized from
settings.EnableLightsVisualisation before Settings settings is declared, causing
a forward-reference to uninitialized data; fix by ensuring settings is
initialized before those members—either move the declaration of Settings
settings so it appears before
previousEnableLightsVisualisation/currentEnableLightsVisualisation, or stop
using member initializer expressions and instead set
previousEnableLightsVisualisation and currentEnableLightsVisualisation from
settings.EnableLightsVisualisation inside the class constructor (use Settings
settings as the source and assign to
previousEnableLightsVisualisation/currentEnableLightsVisualisation there).
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 40-51: ForEachShadowLight currently assumes accum is
null-terminated and can read out of bounds; update the loop to check the array
length via accum.size() (or its equivalent) before accessing accum[idx] and stop
when idx >= accum.size(); also handle the case where accum[idx] is nullptr by
breaking or continuing as intended, and ensure idx increments by
light->shadowMapCount only after confirming light is non-null—make these changes
in the ForEachShadowLight template to prevent OOB reads.
In `@src/Features/VolumetricShadows.cpp`:
- Around line 77-79: Update the two outdated comments that refer to slot 19 to
reflect the actual PS register bindings: change mentions of "t19" / "slot 19" to
"t98/t99" or "slots 98 and 99" as appropriate; specifically update the comment
near the top that references Deferred::CopyShadowData() and the later comment
referencing the shadow data structured buffer so they state that
Deferred::CopyShadowData() binds the directional shadow structured buffer to PS
slot 98 and the cascade texture to PS slot 99 (matching ShadowSampling.hlsli and
the DirectionalShadows/DirectionalShadowCascades declarations).
---
Duplicate comments:
In `@src/Features/VolumetricShadows.cpp`:
- Around line 276-279: The current binding logic uses ID3D11ShaderResourceView*
srv = shadowCopySRV ? shadowCopySRV : shadowView; which can rebind a stale
shadowCopySRV when both are null; change it so that srv is explicitly nullptr if
neither shadowCopySRV nor shadowView is valid and then call
context->PSSetShaderResources(18, 1, &srv) with that nullptr to unbind slot 18;
update the code path around shadowCopySRV, shadowView and the
PSSetShaderResources call to ensure slot 18 is explicitly cleared when no
current SRV exists (use a local ID3D11ShaderResourceView* nullSrv = nullptr and
pass its address).
---
Nitpick comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 120-122: GetVSMShadow3D uses pow(fade * fade, 8) producing an
effective pow(fade,16) which differs from GetVSMShadow2D's pow(fade,8); either
align the 3D path by replacing pow(fade * fade, 8) with pow(fade, 8) (and update
fadeFactor/surfaceShadow usage accordingly) or add a brief comment in
VolumetricShadows.hlsli next to GetVSMShadow3D/fadeFactor explaining the
intentional stronger falloff for volumetric shadows so reviewers know the
discrepancy is deliberate.
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 380-387: Replace the inline magic checks against
shadow.ShadowParam.y with well-named constants to clarify slot states: define
symbolic names for the unwritten sentinel (== 0), the suppressed sentinel (< 0)
and the valid-positive case (> 0) and use those constants in the comparisons
where ShadowParam.y is tested; update the nearby comment to reference the
constants (and their meanings) so future readers see intent instead of raw
numbers. Ensure the constants are visible to the same shader scope (e.g., top of
ShadowSampling.hlsli or shared header) and keep the comparison logic in the same
function using the new names.
In `@src/Features/LightLimitFix.h`:
- Line 228: The array clusterSize defined as "uint clusterSize[3] = { 16 }" only
initializes the first element and leaves the other two as zero; update the
initializer to explicitly set all three entries (e.g., change the initializer to
{16, 16, 16} if that's intended) or keep {16, 0, 0} but add a clarifying comment
next to clusterSize to document the intended shape so future readers know
whether all three dimensions should be 16 or zeros are deliberate.
🪄 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
Run ID: 60805a5b-faf6-43f6-80a4-ce9ed0678bdd
📒 Files selected for processing (27)
.github/configs/shader-validation-vr.yamlCMakeLists.txtextern/CommonLibSSE-NGfeatures/Light Limit Fix/Shaders/LightLimitFix/Common.hlslifeatures/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlslifeatures/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlslfeatures/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlslipackage/Shaders/Common/ShadowSampling.hlslipackage/Shaders/Common/SharedData.hlslipackage/Shaders/Lighting.hlslpackage/Shaders/RunGrass.hlslsrc/Deferred.cppsrc/Deferred.hsrc/Features/LightLimitFix.cppsrc/Features/LightLimitFix.hsrc/Features/LightLimitFix/ShadowCasterManager.cppsrc/Features/LightLimitFix/ShadowCasterManager.hsrc/Features/LightLimitFix/ShadowRenderer.cppsrc/Features/RenderDoc.cppsrc/Features/VolumetricShadows.cppsrc/Features/VolumetricShadows.hsrc/Globals.cppsrc/Globals.hsrc/Menu/FeatureListRenderer.cppsrc/State.cppsrc/XSEPlugin.cppvcpkg.json
💤 Files with no reviewable changes (3)
- features/Light Limit Fix/Shaders/LightLimitFix/LightLimitFix.hlsli
- src/Features/VolumetricShadows.h
- features/Volumetric Shadows/Shaders/VolumetricShadows/CopyShadowDataCS.hlsl
✅ Files skipped from review due to trivial changes (6)
- src/State.cpp
- vcpkg.json
- extern/CommonLibSSE-NG
- src/XSEPlugin.cpp
- .github/configs/shader-validation-vr.yaml
- src/Menu/FeatureListRenderer.cpp
🚧 Files skipped from review as they are similar to previous changes (6)
- CMakeLists.txt
- src/Features/RenderDoc.cpp
- src/Globals.cpp
- src/Deferred.cpp
- src/Deferred.h
- src/Features/LightLimitFix/ShadowRenderer.cpp
|
✅ A pre-release build is available for this PR: |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/Features/LightLimitFix/ShadowCasterManager.h (3)
41-50:⚠️ Potential issue | 🟡 MinorBound
ForEachShadowLight()byaccum.size().This helper only stops on a null sentinel. If
shadowLightsAccumis truncated or malformed,accum[idx]is read out of bounds before the break condition runs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix/ShadowCasterManager.h` around lines 41 - 50, ForEachShadowLight reads accum[idx] until a null sentinel and can index past the array if accum is malformed; change the loop in ForEachShadowLight to check idx against accum.size() (use size_t) before accessing accum[idx], e.g., loop while idx < accum.size() and inside break if accum[idx] is null, then call fn and advance idx by accum[idx]->shadowMapCount; ensure shadowMapCount is validated (>=1) before using it to avoid infinite loops or zero-advance.
145-157:⚠️ Potential issue | 🟠 MajorReserve the sun slot when sizing and clamping the pool.
ShadowLightCountexcludes the directional sun, butGetShadowSlot()reserves slot0for it whileLightContainer::Sizeis documented as onlyShadowLightCount + ConvertedShadowSlots. IfInit()allocates or clamps from that total directly, the pool is one slot short before the renderer cap is even considered. Verify that the effective size isShadowLightCount + ConvertedShadowSlots + 1and that malformed JSON is clamped before any allocation path. As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."#!/bin/bash set -euo pipefail hdr="$(fd '^ShadowCasterManager\.h$' src)" cpp="$(fd '^ShadowCasterManager\.cpp$' src)" printf 'Header contract\n' sed -n '145,157p' "$hdr" sed -n '288,289p' "$hdr" sed -n '393,408p' "$hdr" printf '\nInit / allocation paths\n' rg -n -C4 'Init\s*\(|Install\s*\(|ShadowLightCount|ConvertedShadowSlots|FindFreeIndex|GetShadowSlot|new\s+LightEntry|resize\(|\bSize\b' "$cpp"Also applies to: 288-289, 393-396, 405-408
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix/ShadowCasterManager.h` around lines 145 - 157, The pool sizing and clamping logic must reserve the sun slot: ensure all allocation and clamp code (Init(), Install(), any place that builds or resizes LightContainer::Size, and code path that reads ShadowLightCount or ConvertedShadowSlots) computes effectiveSize = ShadowLightCount + ConvertedShadowSlots + 1 (the extra +1 reserves slot 0 used by GetShadowSlot()); validate and clamp malformed JSON values for ShadowLightCount/ConvertedShadowSlots before any allocation or new LightEntry/reserve/resize calls so you never allocate one slot too few, and update any places that compare against the renderer cap or call FindFreeIndex to use effectiveSize instead of just ShadowLightCount + ConvertedShadowSlots.
106-119:⚠️ Potential issue | 🟠 Major
RedrawBudgetFormulastill needs formula-scoped validation.The header documents
RedrawBudgetFormulaas frame-scoped, butValidate/SetParam/GetParamexpose one untyped symbol surface. Unless the implementation filters identifiers by formula kind, users can still referencelight*variables here and get stale per-light state. Verify that the parser rejects light-scoped symbols for the budget formula; if it does not, this is still a correctness bug. As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations."#!/bin/bash set -euo pipefail hdr="$(fd '^ShadowCasterManager\.h$' src)" cpp="$(fd '^ShadowCasterManager\.cpp$' src)" printf 'Header API\n' sed -n '106,120p' "$hdr" printf '\nFormulaHelper implementation\n' rg -n -C4 'FormulaHelper::(Validate|SetParam|GetParam|Reparse)|register_symbol|symbol_table|kFormulaParam_' "$cpp" printf '\nFormula call sites\n' rg -n -C4 'RedrawBudgetFormula|ScoreFormula|RedrawIntervalFormula|Validate\s*\(|Reparse\s*\(' src/Features/LightLimitFixAlso applies to: 200-202
package/Shaders/Common/ShadowSampling.hlsli (2)
21-21:⚠️ Potential issue | 🔴 CriticalGuard or move
SharedShadowMap.This still binds to
t80, which overlaps the TRUE_PBR landscape SRV range whenLighting.hlslpulls this header in.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/Common/ShadowSampling.hlsli` at line 21, The Texture2D declaration SharedShadowMap currently binds to register(t80) which conflicts with the TRUE_PBR landscape SRV range when included by Lighting.hlsl; fix by either moving the binding to a non-conflicting register or removing the hardcoded register and guarding the declaration with a macro so callers (e.g., Lighting.hlsl) can supply a safe register; update the SharedShadowMap declaration in ShadowSampling.hlsli to use a guarded/nullable binding (or a different unique t-slot) to avoid overlapping the TRUE_PBR SRV range.
45-46:⚠️ Potential issue | 🔴 CriticalMove the shadow-light SRVs out of
t100.The GRASS_COLLISION permutation includes both this header and
GrassCollision.hlsli, soShadows : register(t100)still collides with that SRV block.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/Common/ShadowSampling.hlsli` around lines 45 - 46, The SRV bindings for Shadows and ShadowMaps collide with the GrassCollision SRV at t100; update the StructuredBuffer<ShadowData> Shadows and Texture2DArray<float> ShadowMaps register declarations to use unused registers (e.g., move from register(t100)/t101 to a non-conflicting pair like register(t110)/t111 or another free range) and ensure any shader references to Shadows and ShadowMaps are adjusted accordingly so the permutation no longer has SRV register collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 156-159: The fade window should be computed from the far-cascade
span instead of the full shadow range: compute cascadeSpan =
shadow.EndSplitDistances.y - shadow.StartSplitDistances.y, set FadeWindow =
cascadeSpan * 0.1 (or a named fraction), compute fadeStart =
shadow.EndSplitDistances.y - FadeWindow, and then compute fade =
saturate((shadowMapDepth - fadeStart) / FadeWindow); update uses of FadeWindow,
fadeStart and the final saturate call (referencing FadeWindow, fadeStart,
shadow.EndSplitDistances.y, shadow.StartSplitDistances.y, and shadowMapDepth)
accordingly.
---
Duplicate comments:
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Line 21: The Texture2D declaration SharedShadowMap currently binds to
register(t80) which conflicts with the TRUE_PBR landscape SRV range when
included by Lighting.hlsl; fix by either moving the binding to a non-conflicting
register or removing the hardcoded register and guarding the declaration with a
macro so callers (e.g., Lighting.hlsl) can supply a safe register; update the
SharedShadowMap declaration in ShadowSampling.hlsli to use a guarded/nullable
binding (or a different unique t-slot) to avoid overlapping the TRUE_PBR SRV
range.
- Around line 45-46: The SRV bindings for Shadows and ShadowMaps collide with
the GrassCollision SRV at t100; update the StructuredBuffer<ShadowData> Shadows
and Texture2DArray<float> ShadowMaps register declarations to use unused
registers (e.g., move from register(t100)/t101 to a non-conflicting pair like
register(t110)/t111 or another free range) and ensure any shader references to
Shadows and ShadowMaps are adjusted accordingly so the permutation no longer has
SRV register collisions.
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 41-50: ForEachShadowLight reads accum[idx] until a null sentinel
and can index past the array if accum is malformed; change the loop in
ForEachShadowLight to check idx against accum.size() (use size_t) before
accessing accum[idx], e.g., loop while idx < accum.size() and inside break if
accum[idx] is null, then call fn and advance idx by accum[idx]->shadowMapCount;
ensure shadowMapCount is validated (>=1) before using it to avoid infinite loops
or zero-advance.
- Around line 145-157: The pool sizing and clamping logic must reserve the sun
slot: ensure all allocation and clamp code (Init(), Install(), any place that
builds or resizes LightContainer::Size, and code path that reads
ShadowLightCount or ConvertedShadowSlots) computes effectiveSize =
ShadowLightCount + ConvertedShadowSlots + 1 (the extra +1 reserves slot 0 used
by GetShadowSlot()); validate and clamp malformed JSON values for
ShadowLightCount/ConvertedShadowSlots before any allocation or new
LightEntry/reserve/resize calls so you never allocate one slot too few, and
update any places that compare against the renderer cap or call FindFreeIndex to
use effectiveSize instead of just ShadowLightCount + ConvertedShadowSlots.
🪄 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
Run ID: aed07b78-0e5c-46b3-9fa0-8ea8b5c1aa40
📒 Files selected for processing (4)
package/Shaders/Common/ShadowSampling.hlslipackage/Shaders/Lighting.hlslsrc/Deferred.cppsrc/Features/LightLimitFix/ShadowCasterManager.h
🚧 Files skipped from review as they are similar to previous changes (2)
- src/Deferred.cpp
- package/Shaders/Lighting.hlsl
There was a problem hiding this comment.
♻️ Duplicate comments (3)
features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli (1)
153-166:⚠️ Potential issue | 🟠 MajorDrop the secondary-cascade transpose.
The primary 2D path here and the other directional shadow paths all use
mul(shadowProj, ...). Transposing only the secondary cascade changes the matrix convention at the blend seam, so the secondary sample won't line up with the primary one.Suggested fix
- positionLS = mul(transpose(shadowProj), float4(position, 1)).xyz; + positionLS = mul(shadowProj, float4(position, 1)).xyz;Run this to compare the remaining
ShadowProjmultiplications. Expected result: this is the onlytranspose(shadowProj)left in the directional paths.#!/bin/bash rg -n -C2 'transpose\(shadowProj\)|mul\(shadowProj,\s*float4\(' \ "features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli" \ "package/Shaders/Common/ShadowSampling.hlsli"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@features/Volumetric` Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli around lines 153 - 166, The secondary-cascade path incorrectly applies transpose(shadowProj) when computing positionLS, causing a matrix-convention mismatch vs the primary path and misaligned samples; change the secondary cascade multiplication to use mul(shadowProj, float4(position, 1)) (same as the primary) so shadowProj, positionLS, SampleVSMCascade2D, primaryCascade and secondaryCascade use consistent matrix multiplication and remove the transpose call.package/Shaders/Common/ShadowSampling.hlsli (1)
49-50:⚠️ Potential issue | 🔴 Critical
Shadowsstill collides with grass collision att100.
RunGrass.hlslpulls in both this header andfeatures/Grass Collision/Shaders/GrassCollision/GrassCollision.hlslifor theGRASS_COLLISIONpermutation, and that header already bindsCollisionatt100. LeavingShadowsatt100makes that combined variant impossible to bind correctly. Move the LLF shadow SRV pair to a non-conflicting range and keep the runtime bindings aligned.Run this to confirm the binding collision and include path. Expected result: both headers declare
t100, andRunGrass.hlslincludes both.#!/bin/bash rg -n -C2 'register\(t100\)' \ "package/Shaders/Common/ShadowSampling.hlsli" \ "features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli" rg -n -C3 'GrassCollision\.hlsli|ShadowSampling\.hlsli' \ "package/Shaders/RunGrass.hlsl"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package/Shaders/Common/ShadowSampling.hlsli` around lines 49 - 50, The ShadowSampling.hlsli file binds StructuredBuffer<ShadowData> Shadows and Texture2DArray<float> ShadowMaps to register(t100/t101) which collides with Collision in GrassCollision.hlsli; change the LLF shadow SRV pair to a non-conflicting register range (e.g., move Shadows and ShadowMaps from t100/t101 to an unused pair such as t110/t111) and update any runtime binding tables or descriptor indices that reference these symbols so the new registers are used at runtime; ensure RunGrass.hlsl variant that includes both ShadowSampling.hlsli and GrassCollision.hlsli compiles with the new non-conflicting registers.src/Features/LightLimitFix/ShadowCasterManager.h (1)
57-91:⚠️ Potential issue | 🟠 MajorBudget formulas need their own symbol whitelist.
RedrawBudgetFormulais documented as frame-scoped, butValidate()/SetParam()/GetParam()have no formula kind and expose the full parameter set. That lets light-scoped symbols validate here and then depend on whatever light values were last written into the shared table at budget-evaluation time. Please validate/bind budget formulas against a budget-only symbol set.Run this to confirm whether budget validation/evaluation is formula-type aware. Expected result: the current implementation uses the same
FormulaHelperentry points for all three formulas and does not pass a budget-specific symbol set.#!/bin/bash rg -n -C3 'FormulaHelper::Validate|FormulaHelper::SetParam|FormulaHelper::GetParam|RedrawBudgetFormula|ScoreFormula|RedrawIntervalFormula' \ "src/Features/LightLimitFix/ShadowCasterManager.cpp" \ "src/Features/LightLimitFix/ShadowCasterManager.h"As per coding guidelines "Include proper resource management and graceful degradation for DirectX 11 resources and user input validation to prevent crashes from malformed configurations".
Also applies to: 117-120, 201-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Features/LightLimitFix/ShadowCasterManager.h` around lines 57 - 91, The budget formulas (RedrawBudgetFormula/ScoreFormula/RedrawIntervalFormula) are being validated and bound against the full FormulaParams enum via FormulaHelper::Validate/SetParam/GetParam, allowing light-scoped symbols to leak into frame-scoped budget evaluation; add a budget-only symbol whitelist and make formula validation/evaluation formula-type aware by changing the formula API to accept a symbol set or formula kind. Update RedrawBudgetFormula (and ScoreFormula/RedrawIntervalFormula) to call FormulaHelper::Validate/SetParam/GetParam with a new budgetSymbols set (a reduced subset of FormulaParams containing only frame-scoped/budget-safe names), or overload the FormulaHelper methods to accept an enum FormulaKind and internally switch to the correct whitelist; ensure Validate() signature and any callers in ShadowCasterManager.cpp use the budget-specific set so light-scoped params cannot be validated for budget formulas.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@features/Volumetric`
Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli:
- Around line 153-166: The secondary-cascade path incorrectly applies
transpose(shadowProj) when computing positionLS, causing a matrix-convention
mismatch vs the primary path and misaligned samples; change the secondary
cascade multiplication to use mul(shadowProj, float4(position, 1)) (same as the
primary) so shadowProj, positionLS, SampleVSMCascade2D, primaryCascade and
secondaryCascade use consistent matrix multiplication and remove the transpose
call.
In `@package/Shaders/Common/ShadowSampling.hlsli`:
- Around line 49-50: The ShadowSampling.hlsli file binds
StructuredBuffer<ShadowData> Shadows and Texture2DArray<float> ShadowMaps to
register(t100/t101) which collides with Collision in GrassCollision.hlsli;
change the LLF shadow SRV pair to a non-conflicting register range (e.g., move
Shadows and ShadowMaps from t100/t101 to an unused pair such as t110/t111) and
update any runtime binding tables or descriptor indices that reference these
symbols so the new registers are used at runtime; ensure RunGrass.hlsl variant
that includes both ShadowSampling.hlsli and GrassCollision.hlsli compiles with
the new non-conflicting registers.
In `@src/Features/LightLimitFix/ShadowCasterManager.h`:
- Around line 57-91: The budget formulas
(RedrawBudgetFormula/ScoreFormula/RedrawIntervalFormula) are being validated and
bound against the full FormulaParams enum via
FormulaHelper::Validate/SetParam/GetParam, allowing light-scoped symbols to leak
into frame-scoped budget evaluation; add a budget-only symbol whitelist and make
formula validation/evaluation formula-type aware by changing the formula API to
accept a symbol set or formula kind. Update RedrawBudgetFormula (and
ScoreFormula/RedrawIntervalFormula) to call
FormulaHelper::Validate/SetParam/GetParam with a new budgetSymbols set (a
reduced subset of FormulaParams containing only frame-scoped/budget-safe names),
or overload the FormulaHelper methods to accept an enum FormulaKind and
internally switch to the correct whitelist; ensure Validate() signature and any
callers in ShadowCasterManager.cpp use the budget-specific set so light-scoped
params cannot be validated for budget formulas.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 56ee20a2-87b1-4948-b085-4e715f0e4228
📒 Files selected for processing (5)
features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlslipackage/Shaders/Common/ShadowSampling.hlslisrc/Features/LightLimitFix.hsrc/Features/LightLimitFix/ShadowCasterManager.cppsrc/Features/LightLimitFix/ShadowCasterManager.h
e81b0e2 to
89a8dfa
Compare
soda3000
left a comment
There was a problem hiding this comment.
The scope of this specific PR is huge, so I recommend trying to break it down into smaller self-contained PRs if possible.
be6fef2 to
7d2fdf6
Compare
This comment was marked as outdated.
This comment was marked as outdated.
124c1ff to
0243c7c
Compare
Two hardening guards in the focus shadow sampling loop added in 76ac692: - ShadowMapSlots upper bound on focusSlice. When ShadowLightCount is configured below 8 the texture array has fewer than 8 slices, so GatherRed(slice = 4 + fi) can read past the end. Break before the sample if the slice would be out of range. - Epsilon check on focusClip.w before perspective divide. The focus matrices are zero until the engine's focus accumulate populates lightTransform; without the guard the divide produces NaN that poisons the shadow min() and crashes on some drivers. Diagnosed from a scene-load CTD without crashlog after the focus sampling went in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EnableLight's focus-shadow block was gated on s_settings.ShadowLightCount <= 4, leaving focus_shadow matrices zero in extended mode and starving the LLF directional sampler of focus data. Replace with s_focusShadowSlots > 0: the per-frame slot reservation added in 5497ffb already carves out the [kFocusShadowBaseSlotIndex .. +focusCount) range from the point light pool, so the original safety net (suppress focus to avoid texture-array contention with point lights) is now redundant. With the gate lifted, GameAccumulate(sun) populates focusShadowmapDescriptors[i].lightTransform, Deferred uploads the matrices to DirectionalShadowLightData::FocusShadowProj, and the LLF shader's focus sampling loop (76ac692, guarded in c6fa469) actually contributes occlusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the magic epsilon in the focus shadow w-guard with the named EPSILON_DIVISION from Common/Math.hlsli. Same value, same behavior.
Project convention is QueryPerformanceCounter (see State.h); std::chrono is disfavored. The 1Hz throttle on the per-frame shadow count debug log now uses QPC directly. Same behavior, matches the rest of the codebase.
Re-applies cb78895 (reverted in 3f2954a) under the same rationale: GetDirectionalShadow only carries cascades 0/1 in its DirectionalShadowLightData, so depth past EndSplitDistances.y had no LLF data. Returning 1.0 (fully lit) there caused brightness regressions in scenes where geometry extends past cascade 1 -- visible as missing distant shadows. Pass the engine's pre-rendered 4-cascade mask sample through to GetDirectionalShadow as engineMaskShadow. Past EndSplitDistances.y the function returns it; within cascade 1's far edge it blends LLF's PCF toward it. Convenience overloads default engineMaskShadow=1.0 for callers without TexShadowMaskSampler bound (e.g. Particle.hlsl), preserving prior behavior on those paths. The original revert was for a VR HMD-rotation artifact in the engineMaskShadow sampling itself; that artifact is separate from the brightness regression and not made worse here.
Mirrors Intellightent's two-layer mitigation. In extended mode (ShadowLightCount > 4), parabolic point/spot lights occupy kSHADOWMAPS slots [4..7] -- the same range g_focusShadowBaseSlotIndex reserves for focus shadows. A stale drawFocusShadows flag on a parabolic light in those slots sends BSShadowParabolicLight::Render into its focus loop on a non-directional light and CTDs with no crashlog at scene load. Two layers of defense (the per-frame scrub alone suffices; the byte patches harden against any engine path that bypasses the per-light flag): - Restore the three byte patches at SetupResources that zero the engine's NumFocusShaderLightsPositive gate and stub the focus count setters (RIDs 10209/10247, 10207/10245, 513201/390932). - Scrub drawFocusShadows = false on every activeShadowLights entry and on sunShadowDirLight at the top of ScheduleShadowCasters. Matches Intellightent main.cpp:1411-1420 verbatim. Accepts the documented Intellightent-era limitation that focus shadows (high-res player + tracked NPCs) are off in extended mode.
Address 7 actionable Copilot review comments on PR #35: - CMakeLists.txt: mark `find_path(EXPRTK_INCLUDE_DIRS)` REQUIRED. Without it, a missing exprtk silently propagates `-NOTFOUND` into target_include_directories and the build fails later with a confusing diagnostic instead of at configure time. - ShadowCasterManager.h: correct GetShadowSlot's docstring. The previous text claimed "0 = sun, 1+ = point lights" but the implementation returns -1 for both the sun and inactive lights, and 1..N for points. The sun lives in kSHADOWMAPS_ESRAM (separate texture from kSHADOWMAPS), so it has no slot in the point-light array. - LightLimitFix.cpp: extend NLOHMANN serialization to include EnableContactShadows, EnableLightsVisualisation, and LightsVisualisationMode. These debug/UI fields previously didn't persist across sessions even though they have JSON-loadable defaults. - LightLimitFix.hlsli: bounds-check shadowIndex against ShadowMapSlots in GetShadowLightShadow before indexing the Shadows StructuredBuffer. Overflowed slots (modelled explicitly elsewhere in the PR) would otherwise read invalid records or slices; the new guard falls back to "unshadowed + no coverage" cleanly. - ShadowRenderer.cpp: when GetInstalledSlotCount() returns 0, clear per-frame slot metadata, counters, and PS bindings at t100/t101 before early-returning. Previously the stale state lingered across zero-slot frames -- the overlay kept showing the previous frame's shadow rows and shaders kept sampling the previous frame's records. - LightLimitFix.cpp: validate shadowmapIndex against ShadowCasterManager::GetInstalledSlotCount before setting LightFlags::Shadow in BSLightingShader_SetupGeometry. A stale or corrupted descriptor mid-frame could otherwise carry an out-of-range index and the shader's Shadows[] / ShadowMaps[] reads would sample OOB (UB, GPU crash on some drivers). Three threads resolved without code changes: - Effect.hlsl GetShadowLightShadow world-space arg: already passes the camera-relative -> world-space conversion. Outdated against an earlier revision of the file. - ShadowRenderer.cpp <chrono> include: the file no longer uses std::chrono (refactored to QPC per project convention). - Globals.cpp engine-global pointers: critical relocations via 2-arg RelocationID intentionally abort at boot on failure rather than silently corrupting -- defense-in-depth wrapper would mask real load failures, not catch them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous review-fix commit added EnableLightsVisualisation and LightsVisualisationMode to the NLOHMANN serialization alongside the real user setting EnableContactShadows. Those two are debug visualisations -- they should reset to defaults on each session so users don't accidentally ship debug overlays in their persisted config, and the corresponding shader-cache rebuild on toggle would fire at every cold-start otherwise. Keep EnableContactShadows in the macro (real persistent setting) and add an explanatory comment so the omission is intentional, not something a future contributor "fixes" by re-adding the debug fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shadowmapIndex bounds check added in the PR #35 review-feedback commit gated `LightFlags::Shadow` on `idx > 0 && idx < installedSlots`. The `> 0` lower bound was overreach -- slot 0 of kSHADOWMAPS is a legitimate slice for a point/spot light when the sun isn't in the SCM pool (per GetShadowSlot's behavior: pool index maps 1:1 to kSHADOWMAPS slot, and the sun's pool slot returns the -1 sentinel, freeing slot 0 for actual point lights). Excluding slot 0 silently dropped shadow rendering for any light occupying that slice -- effectively reverting a working code path. The review comment only flagged the upper-bound OOB risk (sampling past the allocated slice count). Keep that check; drop `> 0`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more Copilot review comments on PR #35 after the initial fix push: - Effect.hlsl: gate strict lights behind `inWorld` the same way clustered lights are gated. Previously totalLightCount included NumStrictLights regardless of inWorld, which meant non-world effect passes (UI, blood splatter on screen-space surfaces, etc.) would iterate strict lights from the world-space CB and pick up unrelated lighting. The clustered branch was already gated; strict matches for symmetry now. - ShadowRenderer.cpp: replace per-frame `std::vector<ShadowLightData> sd(slots)` ctor with a `static` vector + `assign(slots, {})` to reuse the backing storage. The previous shape heap-allocated every frame in CopyShadowLightData's hot path; slot count is stable outside resolution / settings reconfigures so reusing storage is free win. - LightLimitFix.cpp: `shadowLightPtrs.reserve(GetInstalledSlotCount() + 1)` before the ForEachShadowLight insert loop. The unordered_set was constructed empty and grew via inserts each frame, causing multiple rehash allocations under high shadow counts. Upper bound is the configured kSHADOWMAPS slot count (+1 belt-and-braces for the sun's logical entry). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more Copilot review comments on PR #35: - Deferred.cpp focus shadow upload: preserve descriptor->slice correspondence by writing FocusShadowProj[i] for descriptor[i] instead of compacting densely via dd.FocusShadowCount. The LLF shader samples kSHADOWMAPS slice (4 + fi) using fi as the matrix index, so packing past disabled holes would pair matrix N with the wrong shadow slice. Disabled descriptors now leave their slot at the default-zero matrix; the shader's existing `focusClip.w <= EPSILON_DIVISION continue` guard treats zero matrices as "no actor in this slice" and skips sampling. FocusShadowCount is the upper iteration bound (last enabled index + 1). - LightLimitFix.cpp BSLightingShader_SetupGeometry strict-light setup: read the kSHADOWMAPS slice from ShadowCasterManager::GetShadowSlot() instead of shadowmapDescriptors[0].shadowmapIndex. The descriptor field can be corrupted mid-frame by ReturnShadowmaps() (called via Hook_DisableColorMask) after ScheduleShadowCasters fixed it but before this strict-light setup runs -- a stale-but-in-range index would still pass the upper-bound check yet point strict-light shader sampling at the wrong slice. GetShadowSlot reads from the SCM's stable s_lights pool (set in ScheduleShadowCasters, never touched by ReturnShadowmaps), aligning with how CopyShadowLightData and UpdateLights already key off it. - ShadowCasterManager.h GetShadowSlot docstring: corrected the advertised range from "1..ShadowLightCount-1" to the actual "0..GetInstalledSlotCount()-1" raw kSHADOWMAPS slice. The previous text was off by one and would mislead a future caller into skipping slice 0 or adding bogus bias logic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two regressions converged to leave the directional sun unshadowed and either fully bright or fully dark depending on cell type: 1. SCM's Hook_DisableColorMask in ShadowCasterManager.cpp was patching out a 5-byte CALL inside Main::RenderShadowmasks (100422/107140) under the assumption that the call targeted a "DrawColorMask" routine. Ghidra-verified on SE 1.5.97 (+0x90 -> 0x1412e3b80) and SkyrimVR (+0x9E -> 0x141323740): the call is to RenderShadowLightsWithUtilityShader (100423/107141) -- the function that emits every screen-space shadow mask draw. The hook therefore killed the entire kSHADOW_MASK RT every frame; the "Shadowmasks" engine marker was empty in RenderDoc. Replaced with a detour on 100423/107141 that writes a null sentinel into shadowLightsAccum at the 4-slice boundary, runs vanilla, then restores. The engine's iteration terminates cleanly at the sentinel without OOB-reading the 4-entry DAT_141861380 blend-mode table for extended-mode slots, while slots 0..3 (sun cascades + first vanilla point lights) get their mask draws as before. 2. Lighting.hlsl and Particle.hlsl gated the LLF directional shadow path on a bare !SharedData::InInterior check, so Interior Sun cells (where the engine has valid directional data) couldn't reach the cascade + engine-mask sampling. Mirrors #2319's fix on the VOLUMETRIC_SHADOWS branch by switching to ShadowSampling::HasDirectionalShadows() which evaluates !IsInterior() || InteriorSun::IsActive. Stale comments referencing Hook_DisableColorMask updated in LightLimitFix.cpp and ShadowRenderer.cpp; the defensive code that reads the SCM-assigned slot rather than shadowmapDescriptors[0] is still correct, only its rationale shifted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 147063a72 (Interior Sun directional gate fix). Two changes:
1. ShadowCasterManager.cpp: replace the screen-space mask wrapper body
with a no-op. Vanilla RenderShadowLightsWithUtilityShader has three
OOB failure modes that all converge on the same crash site
(107141+0x319, `mov eax, [rbp+rdx*4]` on the 4-entry per-slot blend-
mode table DAT_141861380):
- extended slots whose maskIndex >= 4
- shadowLightsAccum array reads past .size() (vanilla advances by
light->shadowMapCount and dereferences whatever's at that index)
- entries that bypass SLF's EnableLight (engine GameAccumulate path)
and never get a maskIndex assigned
Bounded-call wrappers (slice-count cutoff, maskIndex clamp, BSTArray
padding) each closed one mode but at least one always slipped through.
Verified crash logs:
crash-2026-05-25-15-16-25.log RDX=0x3B1F3023
crash-2026-05-25-15-26-59.log RDX=0x3AA96F53
crash-2026-05-25-15-28-04.log RDX=0x3A4A3190
crash-2026-05-25-15-36-15.log RDX=0x3A4B11F5
Skipping vanilla outright eliminates the call site. Unlike the old
Hook_DisableColorMask this version does NOT call ReturnShadowmaps;
that side-effect cleared shadowmapDescriptors and broke
Deferred::CopyShadowLightData's cascade matrix upload, producing the
original "no sun shadow + brighter scene" symptom. With
shadowmapDescriptors preserved, LLF::GetDirectionalShadow samples
DirectionalShadowCascades directly and the screen-space mask is no
longer needed under LIGHT_LIMIT_FIX.
2. RunGrass.hlsl: swap `!SharedData::InInterior` for
`ShadowSampling::HasDirectionalShadows()` on the engine-mask and
screen-space-shadow gates in both PS_OUTPUT main variants. Mirrors
the Lighting.hlsl / Particle.hlsl swap so Interior Sun cells receive
directional shadow attenuation on grass too. World-shadow gates (and
Effect.hlsl) untouched because GetWorldShadow self-gates to 1.0 in
interiors.
Trade-off: builds with LIGHT_LIMIT_FIX boot-disabled lose point-light
mask shadows (slots 1-3 in vanilla's RGBA mask). Same trade as the
original Hook_DisableColorMask. Acceptable because the fork ships LLF
loaded by default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real findings from the Copilot review pass, the rest either already addressed in earlier review rounds or intentionally not-fixed with documented rationale. 1. FormulaHelper::Parse leaked state on compile failure. The previous sequence assigned `_ptr = new FormulaWrapper()` BEFORE calling parser.compile(), so a failed compile left the helper in a "parsed" state: subsequent Parse() calls returned false via the early `if (_ptr) return false` guard, and Calculate() would evaluate the uncompiled wrapper. Defer the _ptr assignment until compile succeeds, and delete the wrapper on failure. 2. ShadowBitMask was dead work. The field is declared in the LLF cbuffer struct but no shader reads it (the bit-mask-based IsLightIgnored branch was removed when per-light shadowMapIndex sampling replaced it). The C++ side was building it in a per-pass hot loop over numShadowLights and using it in the change-detect that drives strictLightDataCB->Update(). Drop the build loop and remove the change-detect comparison; the field stays initialized to 0 above for cbuffer ABI stability. Also removes the previousShadowBitMask member. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CLAUDE.md comment guideline -- the prior block was descriptive ("X was
removed when ..."). Reframe as the regression warning the project's
exception clause sanctions: name the absent code so a future maintainer
knows not to re-add the build loop. Net -1 line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit review 4359346476 caught a pre-existing bug surfaced by the Interior Sun gate fix: dirSoftShadow and dirDetailedShadow are computed in the VOLUMETRIC_SHADOWS / LIGHT_LIMIT_FIX paths but never multiplied into dirLightColor before it accumulates into propertyColor. Particles in shadow were rendering with full directional light contribution in both exterior daylight and Interior Sun cells. Multiply both factors into the accumulation. Only one path writes a non-1.0 value (the VOLUMETRIC_SHADOWS / LIGHT_LIMIT_FIX branches are mutually exclusive via #if/#elif), so the result equals whichever path fired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three CodeRabbit findings posted inside review bodies as "Outside diff range" / "Nitpick" sections rather than as inline file:line comments, so the initial gh api pulls/35/comments sweep missed them. 1. LightLimitFix.hlsli GetDirectionalShadow: short-circuit the focus- shadow loop when accumulated shadow reaches 0. Once fully occluded, remaining focus actors can only multiply by zero; the extra 8-tap GatherRed work was wasted on every shadowed pixel. 2. Deferred.h: add sizeof() static_asserts to DirectionalShadowLight- Data and ShadowLightData. STATIC_ASSERT_ALIGNAS_16 only verified alignment; silent size drift between the C++ structs and their HLSL mirrors (ShadowSampling.hlsli / LightLimitFix.hlsli) would corrupt every uploaded shadow record. Symbolic expressions (8*float4x4 + 2*float4 / 2*float4x4 + float4) document the layout so additions land in the right place. 3. ShadowRenderer.cpp DrawOverlay: extend the inner table gate to honour showOverlay and hasOverrides. The "Show Shadow Overlay" toggle opens the window and its tooltip promises the debug controls are reachable any time -- previously visualization modes 0-3 left shadowRelatedMode false and the table stayed hidden despite the user explicitly requesting it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #35 CI surfaced 360 new X4000 "use of potentially uninitialized variable" warnings, all from LightLimitFix.hlsli:396 (the closing brace of the shadowIndex>=ShadowMapSlots overflow guard). The previous pattern was: hasCoverage = true; if (overflow) { hasCoverage = false; return 1.0; } ... rest sets nothing, all returns leave hasCoverage = true. Semantically correct -- the entry assignment dominates -- but FXC's dataflow analysis flagged the post-if merge as a potential uninitialized read on the `out bool hasCoverage` parameter. Restructure so each return path writes hasCoverage exactly once. The overflow branch writes false; everything past it writes true (paraboloid lights always sample, including the safe / suppressed sentinels). The 0 new warnings limit is restored. Also initialise the caller's `bool shadowCoverage` in Effect.hlsl for parity with the other call sites (Lighting.hlsl, RunGrass.hlsl x2), which all use `= false`. Defensive only; FXC doesn't flag the caller. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4000) Previous attempt (3b4159d) reordered the writes inside the function so every return path explicitly writes hasCoverage. FXC still emitted 360 new X4000 "use of potentially uninitialized variable" warnings -- the merge point following the overflow-guard if-block trips FXC's flow analysis regardless of write ordering when the parameter is `out`. Change the parameter to `inout`. Every call site already initialises the bool to false at declaration (Lighting.hlsl, RunGrass.hlsl x2, Effect.hlsl), so the inout contract is satisfied with no caller changes. FXC's must-write-before-read check on `out` doesn't apply to `inout`, so the false-positive at the if-merge stops firing. Semantics unchanged: every return path still overwrites hasCoverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shader compile (FXC) emitted 360 X4000 "use of potentially uninitialized variable" warnings at the GetShadowLightShadow overflow-guard. Both `out` and `inout` signatures tripped the same false positive at the post-if merge point. Eliminate the early-return path entirely: write hasCoverage once at function entry from the bounds predicate, and rely on the D3D11 StructuredBuffer OOB-returns-zero spec to make Shadows[shadowIndex] safe even when shadowIndex >= ShadowMapSlots (the zero record falls into the ShadowLightParam.y == 0 branch and returns 1.0, with hasCoverage already false so the caller still discards the sample). Also address review feedback: - ShadowRenderer.cpp: shadowSceneNode-null branch now mirrors the slots==0 cleanup (BeginSlotFrame(0), reset counters, unbind t100/t101) so a null scene node doesn't leave stale shadow data bound to PS. - LightLimitFix.cpp: switch shadowLightPtrs from std::unordered_set to ankerl::unordered_dense::set to avoid per-frame node allocations in the UpdateLights hot path. - ShadowCasterManager.h: Update() doc now correctly describes that scheduling lives in the hooked CalculateActiveShadowCasters path, not in Update() itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dev merge dropped the "// Cluster config (computed)" and "// Debug (last)" markers from PerFrame / LightLimitFixSettings and placed the visualization fields BEFORE ShadowMapSlots / ClusterSize, breaking the long-standing "Debug fields trail the cbuffer" convention this PR's prior history established. Restore the original ordering: contact-shadow + ShadowMapSlots payload first, then `// Cluster config (computed)` + ClusterSize, then `// Debug (last)` + EnableLightsVisualisation / LightsVisualisationMode, then final pad. Total still 64 bytes (sizeof assertion holds): 8 packed uint/float + ClusterSize[4] + 2 uint visualization + pad[2] = 64. Matched in the HLSL mirror (SharedData.hlsli) so the field-for-field contract documented by the size-lock comment continues to hold. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five issues surfaced by a CLAUDE.md-driven review of the in-session work:
- ShadowSampling.hlsli GetWorldShadow: collapse the `#if/#else/#endif` +
trailing `return worldShadow;` pattern that had two returns where only
one was reachable per defined permutation. Single trailing return.
- LightLimitFix.cpp DrawSettings: replace `ImGui::Combo("...",
(int*)&LightsVisualisationMode, ...)` with a temp int round-trip
clamped to the combo option count. The `(int*)&uint` cast violates
strict aliasing (the same UB the contact-shadow SliderScalar block
immediately above explicitly calls out and avoids), and a corrupted
persisted value could otherwise index past the option array.
- LightLimitFix.cpp GetCommonBufferData: clamp LightsVisualisationMode
at the cbuffer boundary too. The DrawSettings clamp is UI-only; JSON
persistence or remote-control writes can land out-of-range and the
shader's switch would silently treat it as mode 0.
- Particle.hlsl: tighten the dirSoftShadow/dirDetailedShadow comment to
one line that names the invariant ("paths are mutually exclusive,
exactly one stays at 1.0") instead of describing what the next line
does. Per CLAUDE.md "Concise Comments" rule.
- Effect.hlsl: drop the "out-param init for parity" comment on
`bool shadowCoverage = false;`. The init is literal next-line code,
and the other callsites (Lighting.hlsl / RunGrass.hlsl) use the same
pattern without commentary, so the "parity" framing was self-referential.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EnableLightsVisualisation and LightsVisualisationMode were in Settings but excluded from the NLOHMANN serializer. That contract was fragile: anyone adding the macro field list could re-include them and a stale config edit could still trip the LLFDEBUG shader recompile on load. Move them onto the LightLimitFix instance as plain non-serializable members alongside the existing previous/currentEnableLightsVisualisation trackers. The Settings struct now contains only persistable user-facing configuration; debug toggles are local instance state that resets on each session. Touches: - LightLimitFix.h: drop fields from Settings; add instance members with a comment explaining the placement. - LightLimitFix.cpp: DrawSettings, GetCommonBufferData, IsOverlayVisible read the instance fields directly; serializer comment updated. - ShadowRenderer.cpp: drop `settings.` prefix on the three viz reads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A local set's buckets get destroyed and recreated every frame, so the reserve() call was paying for an allocation every frame instead of amortising one. Promote to a function-local static: clear() preserves the bucket array, reserve() is then a no-op once steady state is reached. Same dense layout, no per-insert node allocations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
t100 is already taken by Grass Collision's Collision texture (features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli), and RunGrass.hlsl includes both features, so binding LLF's shadow StructuredBuffer at the same register would have caused an HLSL compilation failure whenever both features were enabled. Move LLF's `Shadows` to t102 and `ShadowMaps` to t103 (both free), and update the matching `PSSetShaderResources(102, ...)` / `(103, ...)` calls in ShadowRenderer.cpp (PSSetShaderResources with count=2 at t102 still binds the pair contiguously in the unbind paths). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Release comment
Current state.
CS from this branch.
coc intellightest(note this test esp will break blackreach so not for playthrough). Grab from release assets.Working generally.
Other ways to generate shadows:
Defaults to 16 shadow lights and uses an auto budget system to try to maintain your target fps (but always allows 4 shadow casters to be drawn). If shadows aren't updating every frame, you may be exhausting the budget.
Summary by CodeRabbit
New Features
Other