Skip to content

perf: terrain variation & parallax#2220

Open
davo0411 wants to merge 29 commits into
community-shaders:devfrom
davo0411:tv-perf-3
Open

perf: terrain variation & parallax#2220
davo0411 wants to merge 29 commits into
community-shaders:devfrom
davo0411:tv-perf-3

Conversation

@davo0411
Copy link
Copy Markdown
Collaborator

@davo0411 davo0411 commented Apr 27, 2026

  1. refactor to lighting landscape layers to facilitate small compiler wins for tv & pbr not having so many branches inside lighting.hlsl
  2. adopted Alexander (bad_emperor)'s ideas on secant stepping instead of linear, per graphics discord. nice perf bump on parallax.
  3. full terrain variation rework, now just takes 2 samples always, no artifacts.
  4. contains like a billion magic number fixes and general comment/code clean up.
image image

Summary by CodeRabbit

  • New Features

    • Tuned parallax and soft-shadow sampling for terrain for cleaner, more consistent shadows and quality-driven tap counts.
    • Terrain feature metadata version bumped.
  • Refactor

    • Mip selection made deterministic for steadier LOD behavior.
    • Terrain sampling simplified to a 2-sample stochastic model and centralized per-layer landscape blending for more consistent lighting.
  • Chores

    • Removed "Extend Shadows" option; terrain tiling fix is now always enabled in the UI.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors terrain/landscape parallax and stochastic sampling: splits ExtendedMaterials into parallax core and terrain headers, reduces TerrainVariation sampling from 3→2 samples, removes stochastic mip noise from GetMipLevel, adds parallax/shadow tunables and terrain shadow helpers, consolidates per-layer blending, and removes the CPU ExtendShadows setting and its UI wiring.

Changes

Extended Terrain & Parallax Refactor

Layer / File(s) Summary
Data Shape & Constants
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli, package/Shaders/Common/SharedData.hlsli, src/Features/ExtendedMaterials.h, src/Features/ExtendedMaterials.cpp, src/Features/TerrainVariation.h, src/Features/TerrainVariation.cpp, features/Terrain Variation/Shaders/Features/TerrainVariation.ini
Adds parallax/shadow constants and ParallaxShadowTapCount; bumps TerrainVariation feature version; removes CPMSettings::ExtendShadows and TerrainVariationSettings::enableTilingFix; adjusts padding/field types and CPU settings/UI serialization.
Terrain Variation Core Sampling
features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli
Replaces 3-sample barycentric hashing with top-2-corner 2-sample approach; introduces StochasticCorner, jitter/LOD helpers, ComputeStochasticOffsetsLOD, StochasticSampleLODJitter, StochasticHeightFadeFromMip, SampleTerrain; changes StochasticEffect/StochasticEffectParallax/StochasticSampleLOD signatures to remove ddx/ddy and use jitter/mip-bias.
GetMipLevel Change
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
Removes screenNoise parameter and stochastic selection; new deterministic gradient-based mip computation applying PARALLAX/TRUE_PBR/VR adjustments and returning floored mip level.
Parallax Core Implementation
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli
Adds ray-marching/parallax-refinement core: GetParallaxCoords overloads (LANDSCAPE and non-LANDSCAPE), secant refinement, outputs pixelOffset/hasPOM; non-LANDSCAPE GetParallaxSoftShadowMultiplier.
Terrain Height & Shadow Helpers
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli
New terrain include: initialize per-layer mip levels, TerrainParallaxTexSample, ProcessTerrainHeightWeights, FinishTerrainHeightQuadBlend, GetTerrainHeight/GetTerrainHeightQuadRayMarch (PBR and legacy), displacement/blend detection, max-weighted scale, tap-count mapping, and terrain parallax shadow base/soft/directional evaluation helpers.
Lighting Integration & Wiring
package/Shaders/Common/LightingLandscape.hlsli, package/Shaders/Lighting.hlsl
Adds LightingLandscape X-macros and per-layer blend macros; Lighting.hlsl includes the new header for LANDSCAPE, tightens parallax-shadow gating to EnableShadows && parallaxShadowQuality > 0.0, updates GetMipLevel/GetParallaxCoords call-sites, initializes/clamps terrain mip levels, computes shared stochastic offsets, caches directional terrain parallax shadow, and replaces explicit 6-layer blending with macro-driven per-layer blends.
CPU Settings / UI / Serialization
src/Features/ExtendedMaterials.h, src/Features/ExtendedMaterials.cpp, src/Features/TerrainVariation.h, src/Features/TerrainVariation.cpp
Removes ExtendShadows from ExtendedMaterials settings and JSON mapping/UI; TerrainVariation drops enableTilingFix and exposes enableLODTerrainTilingFix (uint/aligned), deletes UpdateShaderSettings() and removes shader-wiring calls that marked vertex-descriptor dirty.
Metadata / Cleanups
features/Terrain Variation/Shaders/Features/TerrainVariation.ini, package/Shaders/*, package/Shaders/Common/*
Feature version bump to 1-1-0; EMAT-only macro undef cleanup in Lighting.hlsl; restructured includes to split parallax core and terrain helpers.

Sequence Diagram(s)

sequenceDiagram
  participant PS as PixelShader (EMAT)
  participant EM as ExtendedMaterials Parallax Core
  participant ET as ExtendedMaterials Terrain
  participant TV as TerrainVariation (stochastic offsets)
  participant Tex as Terrain Textures
  participant CPU as CPU Settings

  PS->>EM: GetMipLevel(uv, parallaxTex)
  EM-->>PS: mipLevel (deterministic)
  PS->>TV: ComputeStochasticOffsets(sharedUV) [if TERRAIN_VARIATION]
  TV-->>PS: sharedOffset
  PS->>ET: InitializeTerrainMipLevels(coords)
  ET-->>PS: mipLevels[6]
  PS->>EM: GetParallaxCoords(..., mipLevels, viewDir, tbn, noise, params, sharedOffset)
  EM->>Tex: sample displacement/color (stochastic or SampleLevel)
  Tex-->>EM: height samples
  EM->>EM: ray-march & secant refine -> pixelOffset, parallax UV
  EM-->>PS: parallax UV, pixelOffset, hasPOM
  PS->>ET: ComputeTerrainParallaxShadowBaseHeight(..., sharedOffset)
  ET->>Tex: sample heights along light dir
  Tex-->>ET: shadow samples
  ET-->>PS: terrain parallax shadow multiplier
  CPU->>PS: shared settings (parallax/shadow quality, EnableShadows)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • alandtse
  • doodlum
  • jiayev

Poem

🐇 Two samples now hop where three once leapt away,
Golden jitter nudges LOD through dusk and day,
Rays march then sharpen, secant finds the line,
Layers blend and shadows sip their quiet wine,
A rabbit cheers: tidy shaders, carrots and hay.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: terrain variation & parallax' clearly summarizes the main objective: performance improvements to terrain variation and parallax systems through the substantial refactoring shown in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Automated formatting by clang-format, prettier, and other hooks.
See https://pre-commit.ci for details.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

No actionable suggestions for changed features.

@davo0411 davo0411 marked this pull request as ready for review April 28, 2026 05:23
Copilot AI review requested due to automatic review settings April 28, 2026 05:23
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
package/Shaders/Common/LightingLandscape.hlsli (1)

126-126: Align closing header-guard comment with macro name

The closing comment references __LANDSCAPE_LAYERS_HLSLI__, but the actual guard is __LIGHTING_LANDSCAPE_HLSLI__. Consider syncing for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/Shaders/Common/LightingLandscape.hlsli` at line 126, The closing
header-guard comment is mismatched: update the comment after `#endif` to match the
actual macro __LIGHTING_LANDSCAPE_HLSLI__ (replace __LANDSCAPE_LAYERS_HLSLI__
with __LIGHTING_LANDSCAPE_HLSLI__), ensuring the comment aligns with the guard
macro used at the top of this file.
features/Terrain Variation/Shaders/Features/TerrainVariation.ini (1)

1-2: Consider adding a linked issue keyword in the PR description

If this PR implements tracked perf/feature work, add a reference like Addresses #<id> for traceability.

As per coding guidelines, feature PRs should include issue references using keywords such as “Addresses #123” / “Implements #123”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Terrain` Variation/Shaders/Features/TerrainVariation.ini around
lines 1 - 2, This PR is missing an issue reference for traceability—update the
PR description to include a keywords-style reference like "Addresses
#<issue-id>" or "Implements #<issue-id>" with the actual issue number;
additionally, add a RelatedIssue field under the [Info] section in
TerrainVariation.ini (e.g., add "RelatedIssue = `#123`" next to "Version = 1-1-0")
so the feature (TerrainVariation.ini / [Info] / Version) explicitly records the
linked issue.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli (1)

23-27: PR metadata follow-up: add scope in title and link tracking issue.

Suggested title: perf(extended-materials): optimize terrain parallax path
If this maps to a tracked task, append Addresses #<id> (or Implements #<id>) in the PR description.

As per coding guidelines, "When reviewing PRs, please provide suggestions for: Conventional Commit Titles ... and Issue References ..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli around lines 23 -
27, Update the PR title to include scope and the suggested conventional-commit
format — e.g. change it to "perf(extended-materials): optimize terrain parallax
path" — and add an issue reference in the PR description like "Addresses #<id>"
(or "Implements #<id>") if this change maps to a tracked task; note this PR
touches constants such as ParallaxCheapDistance, ParallaxNearShadowQuality,
ParallaxFarShadowQuality, and TerrainParallaxShadowMaxMipLevel so include the
issue link and short summary referencing the terrain parallax/extended-materials
change in the description.
🤖 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/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 127-139: Normalize guard: after summing weights[] into wsum, avoid
calling rcp(wsum) when wsum is zero/near-zero by computing invwsum =
rcp(max(wsum, 1e-6)) (or choose a small epsilon) and then multiply weights[k] by
invwsum; update both occurrences that use rcp(wsum) (the block that references
heightBlend, weights, wsum, invwsum and the similar block later in the file) so
degenerate/invalid blends no longer produce INF/NaN.

In `@package/Shaders/Common/LightingLandscape.hlsli`:
- Around line 91-94: The non-full-PBR branch double-applies weight by
multiplying landRMAOS with weight at assignment and again when accumulating into
blendedRMAOS; change the assignment of landRMAOS inside that branch to not
multiply by weight (e.g., set landRMAOS = float4(1 - glossiness.x, 0, 1, 0)) and
keep the accumulation blendedRMAOS += landRMAOS * weight so each layer’s RMAOS
is weighted exactly once (references: landRMAOS, blendedRMAOS, blendedRGB,
landColorRGB, glossiness.x, weight).

---

Nitpick comments:
In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 23-27: Update the PR title to include scope and the suggested
conventional-commit format — e.g. change it to "perf(extended-materials):
optimize terrain parallax path" — and add an issue reference in the PR
description like "Addresses #<id>" (or "Implements #<id>") if this change maps
to a tracked task; note this PR touches constants such as ParallaxCheapDistance,
ParallaxNearShadowQuality, ParallaxFarShadowQuality, and
TerrainParallaxShadowMaxMipLevel so include the issue link and short summary
referencing the terrain parallax/extended-materials change in the description.

In `@features/Terrain` Variation/Shaders/Features/TerrainVariation.ini:
- Around line 1-2: This PR is missing an issue reference for traceability—update
the PR description to include a keywords-style reference like "Addresses
#<issue-id>" or "Implements #<issue-id>" with the actual issue number;
additionally, add a RelatedIssue field under the [Info] section in
TerrainVariation.ini (e.g., add "RelatedIssue = `#123`" next to "Version = 1-1-0")
so the feature (TerrainVariation.ini / [Info] / Version) explicitly records the
linked issue.

In `@package/Shaders/Common/LightingLandscape.hlsli`:
- Line 126: The closing header-guard comment is mismatched: update the comment
after `#endif` to match the actual macro __LIGHTING_LANDSCAPE_HLSLI__ (replace
__LANDSCAPE_LAYERS_HLSLI__ with __LIGHTING_LANDSCAPE_HLSLI__), ensuring the
comment aligns with the guard macro used at the top of this file.
🪄 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: 57ffdc06-34ee-4729-b25a-f3f07a561d70

📥 Commits

Reviewing files that changed from the base of the PR and between c614997 and 2a8cb8f.

📒 Files selected for processing (10)
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
  • features/Terrain Variation/Shaders/Features/TerrainVariation.ini
  • features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli
  • package/Shaders/Common/LightingLandscape.hlsli
  • package/Shaders/Common/SharedData.hlsli
  • package/Shaders/Lighting.hlsl
  • src/Features/ExtendedMaterials.cpp
  • src/Features/ExtendedMaterials.h
  • src/Features/TerrainVariation.cpp
  • src/Features/TerrainVariation.h
💤 Files with no reviewable changes (1)
  • src/Features/ExtendedMaterials.cpp

Comment thread features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli Outdated
Comment thread package/Shaders/Common/LightingLandscape.hlsli Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR restructures landscape/terrain shader paths to reduce branching and improve performance, while reworking Terrain Variation sampling and updating Extended Materials’ parallax/shadow logic (including secant-based refinement).

Changes:

  • Refactors Lighting.hlsl landscape layer blending into a shared include (LightingLandscape.hlsli) and streamlines Terrain Variation integration.
  • Removes ExtendShadows setting and adjusts Extended Materials shader constants/helpers for cheaper parallax + shadow evaluation.
  • Reworks Terrain Variation stochastic sampling to a consistent 2-sample approach and updates feature settings structs/padding accordingly.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Features/TerrainVariation.h Adjusts shader-define gating and aligns Settings layout to 16 bytes.
src/Features/TerrainVariation.cpp Updates UI toggles to write uint32_t settings and removes vertex-desc “dirty” forcing.
src/Features/ExtendedMaterials.h Removes ExtendShadows and fixes Settings padding/alignment.
src/Features/ExtendedMaterials.cpp Drops JSON/UI handling for ExtendShadows.
package/Shaders/Lighting.hlsl Major landscape/parallax/TV integration changes; adds landscape blend helpers and terrain shadow caching.
package/Shaders/Common/SharedData.hlsli Removes ExtendShadows from shader-visible CPM settings and adjusts padding.
package/Shaders/Common/LightingLandscape.hlsli New shared landscape blending macro include to reduce duplicated per-layer code.
features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli Reworks TV stochastic sampling (2-sample blend, distance/minification fallbacks, LOD jitter helpers).
features/Terrain Variation/Shaders/Features/TerrainVariation.ini Bumps Terrain Variation feature version.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli Adds new parallax/shadow helpers, secant refinement, and terrain shadow evaluation helpers.

Comment thread package/Shaders/Common/LightingLandscape.hlsli Outdated
@davo0411
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please be mean like doodlum and no niceties. Point out any magic numbers and sloppily written code. Suggest improvements where you feel appropriate. Suggest ideas to further reduce compile times

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

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

Comment thread features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli Outdated
@doodlum doodlum marked this pull request as draft April 30, 2026 23:33
@davo0411 davo0411 marked this pull request as ready for review May 4, 2026 04:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (7)
features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli (2)

27-33: 💤 Low value

Consider documenting or removing offset3 from StochasticOffsets.

With the transition to a 2-sample model, offset3 is always set to 0 (lines 101, 118). The field is retained for struct compatibility, but this could confuse future maintainers who might expect it to be used.

Consider adding a comment explaining that offset3 is unused (kept for ABI stability), or remove it if no external consumers depend on the struct layout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Terrain` Variation/Shaders/TerrainVariation/TerrainVariation.hlsli
around lines 27 - 33, The struct StochasticOffsets contains an unused field
offset3 (always set to zero under the new 2-sample model); update the
declaration of StochasticOffsets to either (a) add a clear comment above offset3
explaining it is intentionally unused and kept only for ABI/struct
compatibility, referencing the struct name StochasticOffsets and the field
offset3, or (b) remove offset3 entirely if you confirm no external consumers
rely on the existing layout (search for StochasticOffsets usages before
removing). Ensure the chosen change is applied where StochasticOffsets is
defined so future maintainers understand why offset3 is present or has been
removed.

111-113: 💤 Low value

Magic numbers in hash computation.

Lines 111 and 113 use unexplained constants (255437.0, 127.0) for the LOD hash computation. While these are typical in hash functions, brief comments would help future maintainers understand the rationale (e.g., "large prime for uniform distribution" or "offset for second independent sample").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Terrain` Variation/Shaders/TerrainVariation/TerrainVariation.hlsli
around lines 111 - 113, The hash computation uses unexplained magic constants
(255437.0 and 127.0) in the cellID and second sample; update the code around the
cellID calculation and the hashLOD calls to either replace these literals with
named constants (e.g., LARGE_PRIME_FOR_HASH and SECOND_SAMPLE_OFFSET) or add
brief comments above the lines mentioning why the values were chosen (e.g.,
"large prime for uniform distribution" and "offset for independent second
sample"), referencing the variables cellID, landscapeUV and the hashLOD(...)
calls so maintainers can locate and understand the rationale.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli (2)

735-737: 💤 Low value

Incorrect closing comment.

Line 736's comment says // defined(LANDSCAPE) && defined(TERRAIN_VARIATION) but this #endif closes the #if defined(LANDSCAPE) block that started at line 346 (and implicitly includes line 584). The TERRAIN_VARIATION part is not required for this guard.

♻️ Suggested fix
 #	undef TERRAIN_HEIGHT_AT
-#endif  // defined(LANDSCAPE) && defined(TERRAIN_VARIATION)
+#endif  // defined(LANDSCAPE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli around lines 735 -
737, The closing preprocessor comment on the `#endif` after the `#undef`
TERRAIN_HEIGHT_AT is wrong: it references "defined(LANDSCAPE) &&
defined(TERRAIN_VARIATION)" but the block being closed is the `#if`
defined(LANDSCAPE) block (and not dependent on TERRAIN_VARIATION). Update the
comment on that `#endif` to accurately reflect the opening guard (e.g., //
defined(LANDSCAPE)) so the `#endif` matches the `#if` that began the LANDSCAPE
section; refer to the surrounding symbols (`#if` defined(LANDSCAPE),
TERRAIN_VARIATION, `#undef` TERRAIN_HEIGHT_AT) to locate the correct closing
directive.

619-628: 💤 Low value

Inconsistent tap-count thresholds between functions.

TerrainDirectionalShadowTapCount uses thresholds [0.7, 0.25, 0.0] while ParallaxShadowTapCount uses [0.75, 0.5, 0.25]. The inconsistency may be intentional (directional shadows use fewer taps), but the differing threshold values (0.7 vs 0.75, 0.25 vs 0.5) could benefit from a brief comment explaining the rationale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli around lines 619 -
628, TerrainDirectionalShadowTapCount currently uses thresholds (0.7, 0.25, 0.0)
that differ from ParallaxShadowTapCount's (0.75, 0.5, 0.25); either align the
thresholds if this was accidental or add a concise comment above the
TerrainDirectionalShadowTapCount definition explaining the intentional rationale
(e.g., fewer taps for directional shadows, performance/quality tradeoffs and why
0.7/0.25 were chosen vs 0.75/0.5/0.25), referencing the function names
TerrainDirectionalShadowTapCount and ParallaxShadowTapCount so future readers
know the difference is deliberate.
package/Shaders/Lighting.hlsl (3)

2257-2268: 💤 Low value

Variable lightDirectionTS shadows outer declaration.

Line 2261 redeclares lightDirectionTS inside the conditional block, but it's already declared and computed at line 2252 with the same expression. This variable shadowing is unnecessary and could confuse maintainers.

♻️ Suggested fix
 			if (LANDSCAPE_PARALLAX_ENABLED) {
 				if (ExtendedMaterials::TerrainHasSignificantBlend(input.LandBlendWeights1, input.LandBlendWeights2.xy) && ExtendedMaterials::TerrainHasAnyDisplacement()) {
 					float terrainHeightScale = ExtendedMaterials::TerrainMaxWeightedHeightScale(input, displacementParams);
 					if (terrainHeightScale > 0.01) {
-						float3 lightDirectionTS = normalize(mul(refractedLightDirection, tbn).xyz);
 #					if defined(TERRAIN_VARIATION)
 						parallaxShadow = ExtendedMaterials::GetParallaxSoftShadowMultiplierTerrain(input, uv, terrainShadowMipLevels, lightDirectionTS, sh0, terrainDirectionalShadowQuality, screenNoise, displacementParams, sharedOffset);
 #					else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/Shaders/Lighting.hlsl` around lines 2257 - 2268, The variable
lightDirectionTS is being redeclared inside the LANDSCAPE_PARALLAX_ENABLED
branch although it was already computed earlier (using
normalize(mul(refractedLightDirection, tbn).xyz);), causing shadowing; remove
the local re-declaration and reuse the previously declared lightDirectionTS
before calling ExtendedMaterials::GetParallaxSoftShadowMultiplierTerrain (and
keep the existing arguments: input, uv, terrainShadowMipLevels,
lightDirectionTS, sh0, terrainDirectionalShadowQuality, screenNoise,
displacementParams, sharedOffset) so behavior is unchanged.

1025-1029: 💤 Low value

Redundant assignment of terrainDirectionalShadowQuality.

On line 1026, terrainDirectionalShadowQuality is initialized to parallaxShadowQuality, and immediately on lines 1027-1029, it's conditionally reassigned to the same value when LANDSCAPE is defined. The second assignment is a no-op.

♻️ Suggested simplification
 	float parallaxShadowQuality = ExtendedMaterials::ParallaxNearShadowQuality;
-	float terrainDirectionalShadowQuality = parallaxShadowQuality;
-#		if defined(LANDSCAPE)
-	terrainDirectionalShadowQuality = ExtendedMaterials::ParallaxNearShadowQuality;
-#		endif
+	float terrainDirectionalShadowQuality = ExtendedMaterials::ParallaxNearShadowQuality;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/Shaders/Lighting.hlsl` around lines 1025 - 1029, The variable
terrainDirectionalShadowQuality is initialized to parallaxShadowQuality and then
conditionally reassigned to the same
ExtendedMaterials::ParallaxNearShadowQuality inside the LANDSCAPE block, which
is redundant; remove the conditional reassignment (the lines inside the `#if`
defined(LANDSCAPE) / `#endif` that set terrainDirectionalShadowQuality =
ExtendedMaterials::ParallaxNearShadowQuality) so terrainDirectionalShadowQuality
remains initialized from parallaxShadowQuality without a no-op repeat, or
alternatively move the initial assignment into the conditional and drop the
unconditional one—update references to parallaxShadowQuality,
terrainDirectionalShadowQuality, and the LANDSCAPE conditional accordingly.

1031-1036: 💤 Low value

Consider documenting implicit macro dependencies.

The COMPUTE_TERRAIN_SHADOW_BASE and EVAL_TERRAIN_DIR_SHADOW macros capture multiple variables implicitly (input, uv, terrainShadowMipLevels, terrainDirectionalShadowQuality, screenNoise, displacementParams, sharedOffset). While this reduces duplication, it makes the code harder to reason about.

A brief comment listing the expected variables in scope would help maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package/Shaders/Lighting.hlsl` around lines 1031 - 1036, Add a short comment
above the COMPUTE_TERRAIN_SHADOW_BASE and EVAL_TERRAIN_DIR_SHADOW macro
definitions that lists the implicit variables those macros expect in scope
(e.g., input, uv, terrainShadowMipLevels, terrainDirectionalShadowQuality,
screenNoise, displacementParams, sharedOffset) and mention which
ExtendedMaterials functions they map to
(ExtendedMaterials::ComputeTerrainParallaxShadowBaseHeight and
ExtendedMaterials::EvaluateTerrainDirectionalParallaxShadowMultiplier) so future
readers can quickly see required context before the macros are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 735-737: The closing preprocessor comment on the `#endif` after the
`#undef` TERRAIN_HEIGHT_AT is wrong: it references "defined(LANDSCAPE) &&
defined(TERRAIN_VARIATION)" but the block being closed is the `#if`
defined(LANDSCAPE) block (and not dependent on TERRAIN_VARIATION). Update the
comment on that `#endif` to accurately reflect the opening guard (e.g., //
defined(LANDSCAPE)) so the `#endif` matches the `#if` that began the LANDSCAPE
section; refer to the surrounding symbols (`#if` defined(LANDSCAPE),
TERRAIN_VARIATION, `#undef` TERRAIN_HEIGHT_AT) to locate the correct closing
directive.
- Around line 619-628: TerrainDirectionalShadowTapCount currently uses
thresholds (0.7, 0.25, 0.0) that differ from ParallaxShadowTapCount's (0.75,
0.5, 0.25); either align the thresholds if this was accidental or add a concise
comment above the TerrainDirectionalShadowTapCount definition explaining the
intentional rationale (e.g., fewer taps for directional shadows,
performance/quality tradeoffs and why 0.7/0.25 were chosen vs 0.75/0.5/0.25),
referencing the function names TerrainDirectionalShadowTapCount and
ParallaxShadowTapCount so future readers know the difference is deliberate.

In `@features/Terrain` Variation/Shaders/TerrainVariation/TerrainVariation.hlsli:
- Around line 27-33: The struct StochasticOffsets contains an unused field
offset3 (always set to zero under the new 2-sample model); update the
declaration of StochasticOffsets to either (a) add a clear comment above offset3
explaining it is intentionally unused and kept only for ABI/struct
compatibility, referencing the struct name StochasticOffsets and the field
offset3, or (b) remove offset3 entirely if you confirm no external consumers
rely on the existing layout (search for StochasticOffsets usages before
removing). Ensure the chosen change is applied where StochasticOffsets is
defined so future maintainers understand why offset3 is present or has been
removed.
- Around line 111-113: The hash computation uses unexplained magic constants
(255437.0 and 127.0) in the cellID and second sample; update the code around the
cellID calculation and the hashLOD calls to either replace these literals with
named constants (e.g., LARGE_PRIME_FOR_HASH and SECOND_SAMPLE_OFFSET) or add
brief comments above the lines mentioning why the values were chosen (e.g.,
"large prime for uniform distribution" and "offset for independent second
sample"), referencing the variables cellID, landscapeUV and the hashLOD(...)
calls so maintainers can locate and understand the rationale.

In `@package/Shaders/Lighting.hlsl`:
- Around line 2257-2268: The variable lightDirectionTS is being redeclared
inside the LANDSCAPE_PARALLAX_ENABLED branch although it was already computed
earlier (using normalize(mul(refractedLightDirection, tbn).xyz);), causing
shadowing; remove the local re-declaration and reuse the previously declared
lightDirectionTS before calling
ExtendedMaterials::GetParallaxSoftShadowMultiplierTerrain (and keep the existing
arguments: input, uv, terrainShadowMipLevels, lightDirectionTS, sh0,
terrainDirectionalShadowQuality, screenNoise, displacementParams, sharedOffset)
so behavior is unchanged.
- Around line 1025-1029: The variable terrainDirectionalShadowQuality is
initialized to parallaxShadowQuality and then conditionally reassigned to the
same ExtendedMaterials::ParallaxNearShadowQuality inside the LANDSCAPE block,
which is redundant; remove the conditional reassignment (the lines inside the
`#if` defined(LANDSCAPE) / `#endif` that set terrainDirectionalShadowQuality =
ExtendedMaterials::ParallaxNearShadowQuality) so terrainDirectionalShadowQuality
remains initialized from parallaxShadowQuality without a no-op repeat, or
alternatively move the initial assignment into the conditional and drop the
unconditional one—update references to parallaxShadowQuality,
terrainDirectionalShadowQuality, and the LANDSCAPE conditional accordingly.
- Around line 1031-1036: Add a short comment above the
COMPUTE_TERRAIN_SHADOW_BASE and EVAL_TERRAIN_DIR_SHADOW macro definitions that
lists the implicit variables those macros expect in scope (e.g., input, uv,
terrainShadowMipLevels, terrainDirectionalShadowQuality, screenNoise,
displacementParams, sharedOffset) and mention which ExtendedMaterials functions
they map to (ExtendedMaterials::ComputeTerrainParallaxShadowBaseHeight and
ExtendedMaterials::EvaluateTerrainDirectionalParallaxShadowMultiplier) so future
readers can quickly see required context before the macros are used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e416c1af-4478-4a0b-a78b-1dd58c13a03b

📥 Commits

Reviewing files that changed from the base of the PR and between 2a8cb8f and 0e17ca6.

📒 Files selected for processing (6)
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
  • features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli
  • package/Shaders/Common/SharedData.hlsli
  • package/Shaders/Lighting.hlsl
  • src/Features/TerrainVariation.cpp
  • src/Features/TerrainVariation.h

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 89-97: InitializeTerrainMipLevels currently derives mip LODs from
albedo samplers (TexColorSampler, TexLandColor2/3/4/5/6Sampler) but
GetTerrainHeight and TRUE_PBR paths sample TexLandDisplacement*Sampler, causing
wrong mip selection for secant march and terrain-shadow sampling; update
InitializeTerrainMipLevels (or add a variant) to call GetMipLevel with the
corresponding TexLandDisplacement2Sampler..TexLandDisplacement6Sampler (and
TexLandDisplacementSampler instead of TexColorSampler) when the
TRUE_PBR/displacement path is active, and ensure callers in the TRUE_PBR
codepath call the displacement-aware initializer so mip chains match the
heightmap reads.
- Around line 562-572: GetTerrainHeight updates the out parameter weights during
each probe but after computing final parallaxAmount (r = saturate(fNear / (fNear
- fFar)); parallaxAmount = lerp(tNear, tFar, r)) the code returns a UV that was
never resampled, leaving weights stale; before returning from this function
(after computing pixelOffset/hasPOM and before "return viewDirTS.xy * offset +
coords.xy;") call GetTerrainHeight with the final UV to refresh the out weights
so Lighting.hlsl receives the correct, final blended weights (use the same input
params and the final UV used for the return).

In `@package/Shaders/Lighting.hlsl`:
- Around line 2257-2265: The terrain-parallax shadow call is using
terrainDirectionalShadowQuality (which forces near-quality for landscape)
causing distant point lights to use the expensive near tap profile; in the
LANDSCAPE_PARALLAX_ENABLED branch replace the quality parameter passed to
ExtendedMaterials::GetParallaxSoftShadowMultiplierTerrain from
terrainDirectionalShadowQuality to parallaxShadowQuality so the distance-based
parallaxShadowQuality is used for point-light terrain shadows while retaining
terrainDirectionalShadowQuality only for the cached directional path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a31016da-05f9-481b-a7cf-1273b6f7afa9

📥 Commits

Reviewing files that changed from the base of the PR and between 0e17ca6 and b7164bd.

📒 Files selected for processing (2)
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
  • package/Shaders/Lighting.hlsl

Comment thread features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli Outdated
Comment thread features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli Outdated
Comment thread package/Shaders/Lighting.hlsl Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli (2)

6-14: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mip levels derived from albedo textures, but TRUE_PBR samples displacement textures.

InitializeTerrainMipLevels always uses TexColorSampler / TexLandColor*Sampler for mip derivation, but GetTerrainHeight reads from TexLandDisplacement*Sampler under TRUE_PBR. If those textures have different dimensions or mip chains, the secant march and terrain-shadow sampling will use incorrect mip levels.

🛠️ Proposed fix
 void InitializeTerrainMipLevels(float2 coords, out float mipLevels[6])
 {
+#if defined(TRUE_PBR)
+	mipLevels[0] = GetMipLevel(coords, TexLandDisplacement0Sampler);
+	mipLevels[1] = GetMipLevel(coords, TexLandDisplacement1Sampler);
+	mipLevels[2] = GetMipLevel(coords, TexLandDisplacement2Sampler);
+	mipLevels[3] = GetMipLevel(coords, TexLandDisplacement3Sampler);
+	mipLevels[4] = GetMipLevel(coords, TexLandDisplacement4Sampler);
+	mipLevels[5] = GetMipLevel(coords, TexLandDisplacement5Sampler);
+#else
 	mipLevels[0] = GetMipLevel(coords, TexColorSampler);
 	mipLevels[1] = GetMipLevel(coords, TexLandColor2Sampler);
 	mipLevels[2] = GetMipLevel(coords, TexLandColor3Sampler);
 	mipLevels[3] = GetMipLevel(coords, TexLandColor4Sampler);
 	mipLevels[4] = GetMipLevel(coords, TexLandColor5Sampler);
 	mipLevels[5] = GetMipLevel(coords, TexLandColor6Sampler);
+#endif
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli around lines
6 - 14, InitializeTerrainMipLevels currently derives mip levels from albedo
samplers (TexColorSampler / TexLandColor*Sampler) while GetTerrainHeight under
TRUE_PBR samples displacement textures (TexLandDisplacement*Sampler), causing
mismatched mipchain usage; update InitializeTerrainMipLevels to choose the
correct samplers when TRUE_PBR (use TexLandDisplacementSampler and
TexLandDisplacement2/3/4/5/6Sampler) so GetMipLevel is called on the
displacement textures that GetTerrainHeight actually samples (or add a
boolean/compile-time branch parameter to InitializeTerrainMipLevels to select
displacement vs color samplers), ensuring mipLevels[] align with the textures
used by GetTerrainHeight and terrain shadow/secant march code.

52-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard rcp(wsum) against zero-sum blends.

Lines 52 and 76 use rcp(wsum) without a zero guard. Degenerate or invalid blend inputs (all weights zero) can propagate INF/NaN into terrain weights.

🛠️ Proposed fix
-		float invwsum = rcp(wsum);
+		float invwsum = (wsum > 1e-6) ? rcp(wsum) : 0.0;

Apply to both occurrences at lines 52 and 76.

Also applies to: 76-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli at line 52,
The use of rcp(wsum) for invwsum can produce INF/NaN when all blend weights are
zero; update both occurrences (the variable invwsum computed from wsum at the
invwsum assignments) to guard against zero by replacing rcp(wsum) with
rcp(max(wsum, epsilon)) or explicitly set invwsum = 0 when wsum is zero (choose
a small epsilon like 1e-6), and apply the same change at both places where
invwsum is computed so downstream terrain weight math uses a safe reciprocal.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli (1)

196-201: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Weights remain stale after final secant interpolation.

GetTerrainHeight mutates weights during each probe, but after computing the final parallaxAmount via lerp(tNear, tFar, r), the returned UV was never re-sampled. Lighting.hlsl copies these stale weights back into input.LandBlendWeights*, causing height-blended terrain to drift from the actual parallax coordinate.

Consider refreshing weights at the final UV before returning.

🛠️ Proposed fix
 		float offset = (1.0 - parallaxAmount) * -maxHeight + minHeight;
 		pixelOffset = saturate(parallaxAmount);
 `#if` defined(VR_STEREO_OPT)
 		hasPOM = true;
 `#endif`
-		return viewDirTS.xy * offset + coords.xy;
+		float2 finalCoords = viewDirTS.xy * offset + coords.xy;
+#if defined(LANDSCAPE)
+		// Refresh weights at final parallax UV so callers get consistent blend weights.
+		GetTerrainHeight(noise, input, finalCoords, mipLevels, params, blendFactor, w1, w2, sharedOffset, weights);
+#endif
+		return finalCoords;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli around
lines 196 - 201, GetTerrainHeight currently mutates the `weights` during each
secant probe but the code returns the final UV (computed as coords.xy +
viewDirTS.xy * offset after calculating `parallaxAmount = lerp(tNear, tFar, r)`)
without re-sampling the terrain weights at that final UV, leaving `weights`
stale and later copied back into `input.LandBlendWeights*`. Fix: after computing
the final `offset`/final UV (the expression `viewDirTS.xy * offset + coords.xy`)
call the terrain weight sampling path used inside `GetTerrainHeight` (or a
dedicated sampleWeightsAtUV function) to re-evaluate and overwrite the `weights`
array at that UV, then proceed to set `pixelOffset`/hasPOM and return—ensuring
`weights` now reflect the returned parallax coordinate so
`input.LandBlendWeights*` receives the refreshed values.
🧹 Nitpick comments (4)
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli (2)

257-257: 💤 Low value

Consider documenting the bit positions for TERRAIN_DISPLACEMENT_MASK.

The mask (1u << 6u) | ... | (1u << 11u) corresponds to PBR::TerrainFlags::LandTile0-5HasDisplacement. A brief comment would clarify the mapping.

♻️ Suggested documentation
 #	if defined(TRUE_PBR)
+	// Bits 6–11: PBR::TerrainFlags::LandTile[0-5]HasDisplacement
 	static const uint TERRAIN_DISPLACEMENT_MASK = (1u << 6u) | (1u << 7u) | (1u << 8u) | (1u << 9u) | (1u << 10u) | (1u << 11u);
 #	endif
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli at line 257,
Add a brief inline comment above the TERRAIN_DISPLACEMENT_MASK constant that
documents which bit positions it covers and maps them to PBR::TerrainFlags
entries (i.e., bits 6..11 correspond to
PBR::TerrainFlags::LandTile0HasDisplacement through LandTile5HasDisplacement),
so future readers can immediately see the mapping between the mask and
PBR::TerrainFlags::LandTile0-5HasDisplacement.

259-260: 💤 Low value

Macro captures surrounding scope implicitly.

TERRAIN_HEIGHT_AT references noise, input, params, and sharedOffset from the calling scope. This is intentional for conciseness but can be surprising. The comment at line 29 partially documents this pattern; consider adding a brief note here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli around lines
259 - 260, The macro TERRAIN_HEIGHT_AT implicitly captures variables from the
caller (noise, input, params, sharedOffset and uses
SharedData::extendedMaterialSettings.EnableHeightBlending), which can be
surprising; add a brief inline comment above the macro definition calling out
that TERRAIN_HEIGHT_AT forwards those specific external symbols to
GetTerrainHeight (list noise, input, params, sharedOffset and the
EnableHeightBlending usage) so users know it depends on caller scope and what
must be present when using the macro.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli (1)

161-161: 💤 Low value

Consider naming the secant iteration count.

The literal 3 should be a named constant (e.g., ParallaxSecantIterations) for clarity and easier tuning, as noted in PR feedback.

♻️ Suggested change
+static const uint ParallaxSecantIterations = 3;
+
 // ... in GetParallaxCoords:
-[loop] for (uint i = 0; i < 3; i++)
+[loop] for (uint i = 0; i < ParallaxSecantIterations; i++)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli at line
161, Replace the magic literal 3 in the secant iteration loop with a named
constant to improve clarity and tunability: define a constant like
ParallaxSecantIterations (e.g., static const uint ParallaxSecantIterations = 3)
and use it in the loop header where you currently have "for (uint i = 0; i < 3;
i++)" (reference the loop using the loop variable i in
ExtendedMaterialsParallaxCore.hlsli). Ensure any relevant comments mention this
constant so future maintainers know it's the secant iteration count to tune.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli (1)

20-26: 💤 Low value

Consider removing the unused offset3 field from the stub.

Per PR discussion, StochasticOffsets.offset3 is dead. Including it in the stub maintains signature compatibility, but removing it from both the stub and the real struct would clean up the API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli around lines 20 -
26, Remove the dead field offset3 from the StochasticOffsets struct: delete the
float2 offset3 member from the stub in ExtendedMaterials.hlsli and mirror that
change in the real/production StochasticOffsets definition and any associated
helpers; then update all uses of StochasticOffsets (constructors, initializers,
packing/unpacking/serialization functions, shader inputs/CBVs, and any call
sites that read/write .offset3) so they no longer reference offset3 to keep
signatures consistent.
🤖 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/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli:
- Around line 196-201: GetTerrainHeight currently mutates the `weights` during
each secant probe but the code returns the final UV (computed as coords.xy +
viewDirTS.xy * offset after calculating `parallaxAmount = lerp(tNear, tFar, r)`)
without re-sampling the terrain weights at that final UV, leaving `weights`
stale and later copied back into `input.LandBlendWeights*`. Fix: after computing
the final `offset`/final UV (the expression `viewDirTS.xy * offset + coords.xy`)
call the terrain weight sampling path used inside `GetTerrainHeight` (or a
dedicated sampleWeightsAtUV function) to re-evaluate and overwrite the `weights`
array at that UV, then proceed to set `pixelOffset`/hasPOM and return—ensuring
`weights` now reflect the returned parallax coordinate so
`input.LandBlendWeights*` receives the refreshed values.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli:
- Around line 6-14: InitializeTerrainMipLevels currently derives mip levels from
albedo samplers (TexColorSampler / TexLandColor*Sampler) while GetTerrainHeight
under TRUE_PBR samples displacement textures (TexLandDisplacement*Sampler),
causing mismatched mipchain usage; update InitializeTerrainMipLevels to choose
the correct samplers when TRUE_PBR (use TexLandDisplacementSampler and
TexLandDisplacement2/3/4/5/6Sampler) so GetMipLevel is called on the
displacement textures that GetTerrainHeight actually samples (or add a
boolean/compile-time branch parameter to InitializeTerrainMipLevels to select
displacement vs color samplers), ensuring mipLevels[] align with the textures
used by GetTerrainHeight and terrain shadow/secant march code.
- Line 52: The use of rcp(wsum) for invwsum can produce INF/NaN when all blend
weights are zero; update both occurrences (the variable invwsum computed from
wsum at the invwsum assignments) to guard against zero by replacing rcp(wsum)
with rcp(max(wsum, epsilon)) or explicitly set invwsum = 0 when wsum is zero
(choose a small epsilon like 1e-6), and apply the same change at both places
where invwsum is computed so downstream terrain weight math uses a safe
reciprocal.

---

Nitpick comments:
In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 20-26: Remove the dead field offset3 from the StochasticOffsets
struct: delete the float2 offset3 member from the stub in
ExtendedMaterials.hlsli and mirror that change in the real/production
StochasticOffsets definition and any associated helpers; then update all uses of
StochasticOffsets (constructors, initializers, packing/unpacking/serialization
functions, shader inputs/CBVs, and any call sites that read/write .offset3) so
they no longer reference offset3 to keep signatures consistent.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli:
- Line 161: Replace the magic literal 3 in the secant iteration loop with a
named constant to improve clarity and tunability: define a constant like
ParallaxSecantIterations (e.g., static const uint ParallaxSecantIterations = 3)
and use it in the loop header where you currently have "for (uint i = 0; i < 3;
i++)" (reference the loop using the loop variable i in
ExtendedMaterialsParallaxCore.hlsli). Ensure any relevant comments mention this
constant so future maintainers know it's the secant iteration count to tune.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli:
- Line 257: Add a brief inline comment above the TERRAIN_DISPLACEMENT_MASK
constant that documents which bit positions it covers and maps them to
PBR::TerrainFlags entries (i.e., bits 6..11 correspond to
PBR::TerrainFlags::LandTile0HasDisplacement through LandTile5HasDisplacement),
so future readers can immediately see the mapping between the mask and
PBR::TerrainFlags::LandTile0-5HasDisplacement.
- Around line 259-260: The macro TERRAIN_HEIGHT_AT implicitly captures variables
from the caller (noise, input, params, sharedOffset and uses
SharedData::extendedMaterialSettings.EnableHeightBlending), which can be
surprising; add a brief inline comment above the macro definition calling out
that TERRAIN_HEIGHT_AT forwards those specific external symbols to
GetTerrainHeight (list noise, input, params, sharedOffset and the
EnableHeightBlending usage) so users know it depends on caller scope and what
must be present when using the macro.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 174fc57c-f2b8-4963-8506-2ab9ffce7fca

📥 Commits

Reviewing files that changed from the base of the PR and between b7164bd and 806f5f4.

📒 Files selected for processing (4)
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli
  • package/Shaders/Lighting.hlsl
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/Shaders/Lighting.hlsl

@community-shaders community-shaders deleted a comment from coderabbitai Bot May 4, 2026
@davo0411
Copy link
Copy Markdown
Collaborator Author

davo0411 commented May 4, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli (2)

161-164: ⚡ Quick win

Name the secant iteration count.

The literal 5 for iteration count is a tuning parameter. For consistency with other named constants (e.g., ParallaxCheapDistance, ParallaxNearShadowQuality), consider defining a ParallaxSecantIterations constant.

♻️ Suggested refactor

In ExtendedMaterials.hlsli namespace:

static const uint ParallaxSecantIterations = 5;

Then here:

-			[loop] for (uint i = 0; i < 5; i++)
+			[loop] for (uint i = 0; i < ParallaxSecantIterations; i++)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli around
lines 161 - 164, The loop uses a hardcoded iteration count (literal `5`) in the
secant iteration inside ExtendedMaterialsParallaxCore.hlsli; introduce a named
constant (e.g., `ParallaxSecantIterations`) alongside other parallax constants
(like `ParallaxCheapDistance`, `ParallaxNearShadowQuality`) in
ExtendedMaterials.hlsli and replace the literal `5` in the loop (`for (uint i =
0; i < 5; i++)`) with the new `ParallaxSecantIterations` symbol so the iteration
count is configurable and consistent across the codebase.

27-32: 💤 Low value

Consider documenting the view direction flattening constants.

The 0.7 and 0.3 constants are empirical tuning values. A brief comment explaining their derivation (e.g., "0.3 floor prevents extreme foreshortening at grazing angles") would aid future maintainers, as requested in the PR review comments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli around
lines 27 - 32, Add a brief comment explaining the empirical flattening constants
used when computing viewDirTS (the 0.7 multiplier and 0.3 floor) so future
maintainers understand their purpose; update the lines handling viewDirTS (both
the LANDSCAPE branch with params[0].FlattenAmount and the other branch with
params.FlattenAmount) to include a short note like “0.7 scales foreshortening
with angle; 0.3 prevents extreme division at grazing angles” placed directly
above or inline with the expression so it’s obvious why those constants are
used.
features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli (1)

20-26: 💤 Low value

Consider removing unused offset3 field.

Per the PR comments summary, StochasticOffsets.offset3 is identified as a dead/unused field. If it's not consumed anywhere in the terrain sampling paths, removing it would reduce struct size and clarify the API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli around lines 20 -
26, Remove the dead field offset3 from the StochasticOffsets struct to reduce
its size and clarify the API: edit the struct declaration (StochasticOffsets) to
delete the float2 offset3 member, then search for any references to
StochasticOffsets.offset3 (or usages of the third offset) and update
callers/data upload/packing so they no longer expect a third offset (adjust
buffer layouts or any SetConstant/CBV population logic that assumed three
offsets). Ensure any stride/packing calculations and HLSL/C++ interop that
depended on the removed member are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli:
- Around line 20-26: Remove the dead field offset3 from the StochasticOffsets
struct to reduce its size and clarify the API: edit the struct declaration
(StochasticOffsets) to delete the float2 offset3 member, then search for any
references to StochasticOffsets.offset3 (or usages of the third offset) and
update callers/data upload/packing so they no longer expect a third offset
(adjust buffer layouts or any SetConstant/CBV population logic that assumed
three offsets). Ensure any stride/packing calculations and HLSL/C++ interop that
depended on the removed member are updated accordingly.

In `@features/Extended`
Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli:
- Around line 161-164: The loop uses a hardcoded iteration count (literal `5`)
in the secant iteration inside ExtendedMaterialsParallaxCore.hlsli; introduce a
named constant (e.g., `ParallaxSecantIterations`) alongside other parallax
constants (like `ParallaxCheapDistance`, `ParallaxNearShadowQuality`) in
ExtendedMaterials.hlsli and replace the literal `5` in the loop (`for (uint i =
0; i < 5; i++)`) with the new `ParallaxSecantIterations` symbol so the iteration
count is configurable and consistent across the codebase.
- Around line 27-32: Add a brief comment explaining the empirical flattening
constants used when computing viewDirTS (the 0.7 multiplier and 0.3 floor) so
future maintainers understand their purpose; update the lines handling viewDirTS
(both the LANDSCAPE branch with params[0].FlattenAmount and the other branch
with params.FlattenAmount) to include a short note like “0.7 scales
foreshortening with angle; 0.3 prevents extreme division at grazing angles”
placed directly above or inline with the expression so it’s obvious why those
constants are used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c178ad98-bea9-416e-b37a-2fed11f10856

📥 Commits

Reviewing files that changed from the base of the PR and between 806f5f4 and 04297e0.

📒 Files selected for processing (3)
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli
  • features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli
  • package/Shaders/Common/LightingLandscape.hlsli
🚧 Files skipped from review as they are similar to previous changes (1)
  • package/Shaders/Common/LightingLandscape.hlsli

@davo0411
Copy link
Copy Markdown
Collaborator Author

@coderabbitai explore the landscape shaders in depth and evaluate a tangible way to massively improve compilation time. This PR apparently has not helped much with it. The landscape shaders are incredibly slow to compile.

@davo0411
Copy link
Copy Markdown
Collaborator Author

@copilot explore the landscape shaders in depth and evaluate a tangible way to massively improve compilation time. This PR apparently has not helped much with it. The landscape shaders are incredibly slow to compile.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me dig into the actual shader code to count the permutation explosion and find the real compilation bottlenecks.


🏁 Script executed:

#!/bin/bash
# Understand the permutation space - count all `#ifdef/`#if defined() in key files
echo "=== Lighting.hlsl top-level ifdefs ==="
rg '#(ifdef|if defined|if !defined|elif defined|elif !defined)' "package/Shaders/Lighting.hlsl" | wc -l

echo ""
echo "=== Lighting.hlsl unique defines referenced ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "package/Shaders/Lighting.hlsl" -o --pcre2 | grep -oP '\w+$' | sort -u

echo ""
echo "=== ExtendedMaterials.hlsli unique defines ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli" -o --pcre2 | grep -oP '\w+$' | sort -u

echo ""
echo "=== ExtendedMaterialsParallaxCore.hlsli unique defines ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli" -o --pcre2 | grep -oP '\w+$' | sort -u

echo ""
echo "=== ExtendedMaterialsTerrain.hlsli unique defines ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli" -o --pcre2 | grep -oP '\w+$' | sort -u

echo ""
echo "=== LightingLandscape.hlsli unique defines ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "package/Shaders/Common/LightingLandscape.hlsli" -o --pcre2 | grep -oP '\w+$' | sort -u

echo ""
echo "=== TerrainVariation.hlsli unique defines ==="
rg '#(ifdef|if defined|elif defined|if !defined|elif !defined)\s*\(?(\w+)' "features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli" -o --pcre2 | grep -oP '\w+$' | sort -u

Length of output: 1054


🏁 Script executed:

#!/bin/bash
# Read the full content of key new files to understand structure

echo "=== ExtendedMaterialsParallaxCore.hlsli ==="
cat "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli"

echo ""
echo "=== LightingLandscape.hlsli ==="
cat "package/Shaders/Common/LightingLandscape.hlsli"

Length of output: 280


🏁 Script executed:

#!/bin/bash
# Read ExtendedMaterialsTerrain.hlsli and TerrainVariation.hlsli
echo "=== ExtendedMaterialsTerrain.hlsli ==="
cat "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli"

echo ""
echo "=== TerrainVariation.hlsli ==="
cat "features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli"

Length of output: 299


🏁 Script executed:

#!/bin/bash
# Check how shaders are compiled - look for shader compilation code, permutation lists
echo "=== Shader compilation / permutation system ==="
rg -n "LANDSCAPE|TERRAIN_VARIATION|TRUE_PBR|SNOW|VR_STEREO" "src/Features/ExtendedMaterials.cpp" -C2
echo ""
rg -n "LANDSCAPE|TERRAIN_VARIATION|TRUE_PBR|SNOW|VR_STEREO" "src/Features/TerrainVariation.cpp" -C2

echo ""
echo "=== Look for permutation definitions / compilation flags in cpp/h files ==="
rg -rn "ExtraDescriptor|ShaderDescriptor|FeatureDescriptor|GetDescriptor" --type=cpp --type=h -C2 | head -80

echo ""
echo "=== Check if there's a shader cache or precompiled shader system ==="
fd -e hlsl -e hlsli --type f | wc -l
fd -e hlsl --type f | head -30

Length of output: 2490


🏁 Script executed:

#!/bin/bash
# Check how Lighting.hlsl is included and what the full permutation space looks like
echo "=== LANDSCAPE_PARALLAX_ENABLED usage in Lighting.hlsl ==="
rg -n "LANDSCAPE_PARALLAX_ENABLED|TERRAIN_PARALLAX|EnableTerrainParallax|EnableParallax" "package/Shaders/Lighting.hlsl" -C1 | head -60

echo ""
echo "=== All defines that gate EMAT+LANDSCAPE code in Lighting.hlsl ==="
rg -n "EMAT|LANDSCAPE|TERRAIN_VARIATION|TRUE_PBR|SNOW|VR_STEREO_OPT" "package/Shaders/Lighting.hlsl" | grep "^.*:#\(if\|elif\|ifdef\|ifndef\)" | head -80

echo ""
echo "=== Total line count of key files ==="
wc -l "package/Shaders/Lighting.hlsl" \
   "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli" \
   "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsParallaxCore.hlsli" \
   "features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterialsTerrain.hlsli" \
   "package/Shaders/Common/LightingLandscape.hlsli" \
   "features/Terrain Variation/Shaders/TerrainVariation/TerrainVariation.hlsli"

echo ""
echo "=== What defines are passed to shader compiler (look for D_ or /D flags) ==="
rg -rn "\"LANDSCAPE\"|\"TERRAIN_VARIATION\"|\"TRUE_PBR\"|\"SNOW\"|ShaderFlags|permut" --type=cpp --type=h | head -60

Length of output: 3484


🏁 Script executed:

#!/bin/bash
# Use fd to find files - handles spaces better
echo "=== All hlsli files ==="
fd -e hlsli

echo ""
echo "=== ExtendedMaterials feature shaders ==="
fd . "features" -e hlsli -e hlsl

echo ""
echo "=== Terrain variation shaders ==="
fd . -e ini -e hlsli -e hlsl | grep -i terrain

Length of output: 2420


🏁 Script executed:

#!/bin/bash
# Read the Lighting.hlsl to understand full permutation scope and EMAT/LANDSCAPE interaction
echo "=== Full list of `#if/`#ifdef in Lighting.hlsl ==="
grep -n "#if\|#elif\|#ifdef\|#ifndef\|#define\|#endif" "package/Shaders/Lighting.hlsl" | head -150

echo ""
echo "=== EMAT sections in Lighting.hlsl ==="
grep -n "EMAT\|TERRAIN_VARIATION\|TRUE_PBR" "package/Shaders/Lighting.hlsl" | head -80

echo ""
echo "=== Include directives in Lighting.hlsl ==="
grep -n "#include" "package/Shaders/Lighting.hlsl"

Length of output: 4860


🏁 Script executed:

#!/bin/bash
# Look for how permutations are generated in the C++ source
fd -e cpp -e h | xargs grep -l "LANDSCAPE\|permut\|ShaderDescriptor\|ExtraFeature" 2>/dev/null | head -20

echo ""
echo "=== Files containing permutation/descriptor logic ==="
fd -e cpp -e h | xargs grep -ln "ExtraFeatureDescriptor\|ShaderDescriptor\|ShaderPermut" 2>/dev/null

echo ""
echo "=== Permutation descriptor for landscape ==="
fd -e cpp -e h | xargs grep -n "LANDSCAPE\|THLandHas\|ExtraFeatureFlags" 2>/dev/null | head -60

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Read the available Lighting.hlsl PSHADER section - particularly the LANDSCAPE and EMAT blocks
echo "=== Lines 359-500 (PSHADER start, EMAT definition) ==="
sed -n '359,500p' "package/Shaders/Lighting.hlsl"

echo ""
echo "=== Lines 870-960 (EMAT definition and TERRAIN_VARIATION) ==="
sed -n '870,960p' "package/Shaders/Lighting.hlsl"

echo ""
echo "=== Lines 1024-1100 (LANDSCAPE parallax init) ==="
sed -n '1024,1100p' "package/Shaders/Lighting.hlsl"

echo ""
echo "=== Lines 1320-1450 (LANDSCAPE layer blending) ==="
sed -n '1320,1450p' "package/Shaders/Lighting.hlsl"

echo ""
echo "=== Lines 2030-2090 (directional shadow LANDSCAPE) ==="
sed -n '2030,2090p' "package/Shaders/Lighting.hlsl"

echo ""
echo "=== Lines 2420-2500 (LOD_LAND_BLEND and end) ==="
sed -n '2420,2500p' "package/Shaders/Lighting.hlsl"

Length of output: 23811


🏁 Script executed:

#!/bin/bash
# Look at Permutation.hlsli to understand the full permutation space
cat "package/Shaders/Common/Permutation.hlsli"

Length of output: 73


🏁 Script executed:

#!/bin/bash
# Count real permutation axes for LANDSCAPE shaders
echo "=== All defines referenced in PSHADER section of Lighting.hlsl ==="
sed -n '359,2817p' "package/Shaders/Lighting.hlsl" | grep -oP '#\s*(if|ifdef|ifndef|elif)\s+[^(]*\(?defined\s*\(\s*\K\w+|#\s*(ifdef|ifndef)\s+\K\w+' | sort -u

echo ""
echo "=== Count of `#if` blocks in PSHADER section ==="
sed -n '359,2817p' "package/Shaders/Lighting.hlsl" | grep -c "^#\s*if"

echo ""
echo "=== Includes in all Common hlsli files ==="
fd -e hlsli -p "package/Shaders/Common" 2>/dev/null || ls package/Shaders/Common/ 2>/dev/null

echo ""
echo "=== Landscape-specific other includes ==="
ls package/Shaders/Common/ 2>/dev/null
ls package/Shaders/ 2>/dev/null

Length of output: 1927


@davo0411 Here's a deep analysis of the actual compile-time bottlenecks in the landscape shaders, with concrete ranked recommendations.


Root Cause: Permutation Explosion in a Monolithic Shader

Lighting.hlsl is 2,817 lines with 264 #if blocks in the PSHADER section alone, referencing 57 unique preprocessor defines. Every unique combination of active defines is a separate compile job. The LANDSCAPE path is the worst offender because it activates the most cross-cutting feature axes simultaneously.

Active compile-time permutation axes for a LANDSCAPE shader

Axis Values Notes
TRUE_PBR 0/1 Changes texture layouts, blending, RMAOS paths
TERRAIN_VARIATION 0/1 Changes sampling in every terrain texture call
SNOW 0/1 Adds GetLandSnowMaskValue, GetLandNormal
EXTENDED_MATERIALS (EMAT) 0/1 Pulls in 734 new lines of HLSLI
DEFERRED 0/1 Different output path
VR / VR_STEREO_OPT 0/1/2 Eye indexing, POM UAV
WETNESS_EFFECTS 0/1 Porosity/wetting path
TERRAIN_BLENDING 0/1 Depth blend include
SKYLIGHTING 0/1 Include + evaluation
LOD_LAND_BLEND + LODLANDSCAPE 0/1 LOD stochastic path
SCREEN_SPACE_SHADOWS 0/1 SSS evaluation
LIGHT_LIMIT_FIX 0/1 Cluster evaluation

That's 12 binary axes = 4,096 theoretical permutations, of which even a conservative 5–10% surviving game-side pruning means 200–400 LANDSCAPE shader compilations, each of a 2,817-line monolith with 12 headers included per TU.


Recommendations, Ranked by Impact

🔴 #1: Split Lighting.hlsl into separate entry-point files (massive)

This is the single biggest win and requires no runtime behavior change. The game's shader system already compiles shaders by passing defines at the command line — so separate .hlsl entry-point files are natural. Currently every permutation of every material type (LANDSCAPE, PARALLAX, ENVMAP, EYE, FACEGEN, SKINNED…) recompiles the entire monolith.

Create:

package/Shaders/LightingLandscape.hlsl    ← LANDSCAPE entry point
package/Shaders/LightingParallax.hlsl     ← PARALLAX/ENVMAP entry point  
package/Shaders/Lighting.hlsl             ← base (everything else)

LightingLandscape.hlsl can skip including headers that LANDSCAPE never needs (Skinned.hlsli, Triplanar.hlsli, etc.), exclude the non-LANDSCAPE sampler/texture register blocks at lines 395–500, and omit the entire non-landscape PSHADER body that's guarded by #else // Non-landscape code. Conservatively this eliminates 30–40% of the lines-per-TU for landscape, and also shrinks the irrelevant-define namespace so the compiler doesn't speculatively analyze dead #if arms.


🔴 #2: Convert TERRAIN_VARIATION to a runtime cbuffer bool (high)

This is the most actionable change in this PR's scope. The pattern is already established: enableLODTerrainTilingFix is already a uint32_t cbuffer field used as a runtime branch. The core TV path just needs the same treatment.

Currently TERRAIN_VARIATION gates:

  • ComputeStochasticOffsets(input.TexCoord0.zw) vs (StochasticOffsets)0
  • StochasticEffect(...) vs TEX.SampleBias(SAMP, UV, SharedData::MipBias + EXTRA_BIAS) inside SampleTerrain
  • StochasticEffectParallax(...) vs direct SampleLevel inside TerrainParallaxTexSample

The StochasticOffsets sharedOffset is already zero-initialized for the non-TV path, showing the data structure is identical. The only thing preventing runtime dispatch is that StochasticEffect/TerrainParallaxTexSample branch at compile time.

The fix is to add a uint enableTerrainVariation to TerrainVariationSettings (alongside enableLODTerrainTilingFix) and convert:

// Before (compile-time, two permutations):
`#if` defined(TERRAIN_VARIATION)
    StochasticOffsets sharedOffset = ComputeStochasticOffsets(input.TexCoord0.zw);
`#else`
    StochasticOffsets sharedOffset = (StochasticOffsets)0;
`#endif`
// After (runtime, one permutation):
StochasticOffsets sharedOffset = (StochasticOffsets)0;
[branch] if (SharedData::terrainVariationSettings.enableTerrainVariation)
    sharedOffset = ComputeStochasticOffsets(input.TexCoord0.zw);

And inside TerrainParallaxTexSample / SampleTerrain:

inline float4 TerrainParallaxTexSample(Texture2D tex, float2 uv, float mipLevel, StochasticOffsets sharedOffset)
{
    [branch] if (SharedData::terrainVariationSettings.enableTerrainVariation)
        return StochasticEffectParallax(tex, SampTerrainParallaxSampler, uv, mipLevel, sharedOffset);
    return tex.SampleLevel(SampTerrainParallaxSampler, uv, mipLevel);
}

TerrainVariation::HasShaderDefine() would then always return false for Lighting type (the feature is always "installed" but never contributes a compile-time define). This alone cuts all LANDSCAPE permutation counts by 50%.


🟠 #3: Convert SNOW (LANDSCAPE path only) to a runtime bool (high)

For LANDSCAPE, SNOW gates only:

  • GetLandSnowMaskValue(baseColor.w) — a single float multiply/saturate
  • GetLandNormal(...) — a lerp between normals

Both are trivially runtime-branchable. The LandscapeTexture1to4IsSnow constant is already passed as a cbuffer float4 — just add a uint hasSnow alongside it and branch on it:

float landSnowMask = 0.0;
[branch] if (SharedData::landscapeSettings.hasSnow)
    landSnowMask = GetLandSnowMaskValue(baseColor.w);

Combined with #2, this gives a 75% reduction in LANDSCAPE permutation count.


🟠 #4: Replace the 6-layer X-macro unroll with a [unroll] for-loop (medium compile time, large IR reduction)

The LIGHTING_LANDSCAPE_BLEND_ONE_LAYER_PBR / LIGHTING_LANDSCAPE_BLEND_ONE_LAYER macros expand 6 complete inline copies of the blending body. This creates enormous IR for the register allocator, especially since each expansion references different named texture/sampler registers. Consider packing the 6 color, normal, and RMAOS textures into Texture2D arrays (register arrays like Texture2D TexLandColor[6] : register(t0)) and then using:

[unroll] for (uint i = 0; i < 6; ++i) {
    float w = LandBlendWeight(input, i);
    float4 col = SampleTerrain(TexLandColor[i], SampColorSampler, uv, sharedOffset, 0);
    // accumulate...
}

This is a larger refactor because register arrays change how the engine binds textures, but it would dramatically reduce the HLSL IR size per LANDSCAPE permutation and give the compiler's optimizer a much smaller graph to schedule.


🟡 #5: Guard ExtendedMaterialsTerrain.hlsli (377 lines) under EMAT && LANDSCAPE (medium)

Currently ExtendedMaterials.hlsli conditionally includes ExtendedMaterialsTerrain.hlsli whenever LANDSCAPE is defined. But EMAT requires EXTENDED_MATERIALS — LANDSCAPE-without-EMAT should not be paying the 377-line parse cost. Verify the include guard is inside #if defined(EMAT), not just #if defined(LANDSCAPE).


🟡 #6: Precompile the 12 unconditional headers into a single umbrella include (medium)

// LightingCore.hlsli — included exactly once per TU
`#include` "Common/Color.hlsli"
`#include` "Common/FrameBuffer.hlsli"
`#include` "Common/GBuffer.hlsli"
// ... etc.

DXC caches #pragma once includes by content hash. A single stable umbrella include that's listed first allows the compiler to reuse its parse cache across all permutations of the same source tree. Scattering 12 includes at the top of Lighting.hlsl means 12 cache-lookup entries per compilation instead of one.


🟡 #7: Dead-code elimination — EMAT_ENVMAP is unreachable for LANDSCAPE (small but free)

EMAT_ENVMAP requires ENVMAP || MULTI_LAYER_PARALLAX || EYE. None of these are defined for LANDSCAPE materials. However, inside the PSHADER EMAT sections, #elif defined(EMAT_ENVMAP) arms are checked even in LANDSCAPE permutations. Adding && !defined(LANDSCAPE) to those guards removes dead arms the compiler has to process:

// Lines 2055, 2258, etc.
`#elif` defined(EMAT_ENVMAP) && !defined(LANDSCAPE)

Summary Table

Recommendation Permutation reduction Effort
Split Lighting.hlsl by material class 60–70% fewer LANDSCAPE TUs High
TERRAIN_VARIATION → runtime bool 50% Medium
SNOW (LANDSCAPE) → runtime bool 50% additive Low–Medium
Texture array + [unroll] for-loop Smaller IR per TU High
Guard terrain hlsli under EMAT Smaller TU parse Low
Umbrella header PCH Better cache reuse Low
Remove dead EMAT_ENVMAP arms in LANDSCAPE Trivial Trivial

Recommendations #2 + #3 together are the most immediately actionable without restructuring the build system, and would cut LANDSCAPE shader compile jobs by ~75%. Recommendation #1 (splitting the file) is the nuclear option but yields the most sustained benefit as the feature set continues to grow.

@davo0411
Copy link
Copy Markdown
Collaborator Author

@coderabbitai per your review, what if we make terrain variation exclusively defined = on, not defined = off? wouldn't it work correctly then with minimal compiler steps? Just compiling TV ON and vanilla? We also need terrain variation defined separately to allow for it to be installed/uninistalled by the end user no? Question critically and deeply evaluate if i am correct or not.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Tip

For best results, initiate chat on the files or code changes.

@davo0411 — Good instinct, but let me be precise about where you're right, where you're wrong, and what actually matters.


Your Proposal Is Already Exactly How It Works

HasShaderDefine() already implements the model you're describing:

// src/Features/TerrainVariation.h (post-PR)
loaded && shaderType == RE::BSShader::Type::Lighting

When TV is not installed: define absent → vanilla permutations compiled.
When TV is installed: define present → TV permutations compiled.

So yes, TERRAIN_VARIATION defined = ON and not defined = OFF is the current architecture. Your instinct is correct on the semantics.


Where You Are Wrong: This Does NOT Minimize Compile Steps

Here is the critical failure in your reasoning. The install/uninstall model you describe produces two full sets of permutations:

Permutations without TERRAIN_VARIATION = N  (compiled at first launch)
Permutations with TERRAIN_VARIATION    = N  (compiled when TV is installed)
Total = 2N

You are not choosing between "compile TV-ON" or "compile vanilla". The shader cache must contain both, because users can switch. Any LANDSCAPE permutation axis that is a compile-time define doubles the permutation count. This is the exact problem.


The Install/Uninstall Concern Is Real But Solvable Without a Compile-Time Define

You raise a valid concern: TV needs to be installable and uninstallable by end users. But a runtime cbuffer bool handles this just as well — arguably better:

// cbuffer (already exists as TerrainVariationSettings)
uint enableTerrainVariation;  // ← add this

// In shader (runtime branch, warp-coherent)
StochasticOffsets sharedOffset = (StochasticOffsets)0;
[branch] if (SharedData::terrainVariationSettings.enableTerrainVariation)
    sharedOffset = ComputeStochasticOffsets(input.TexCoord0.zw);
// HasShaderDefine() returns false unconditionally for Lighting type
// TV feature just writes enableTerrainVariation = 1 to its cbuffer when loaded

Result:

  • TV installed: enableTerrainVariation = 1, stochastic path taken
  • TV uninstalled/toggled off: enableTerrainVariation = 0, vanilla path taken
  • Zero shader recompilation required on install/uninstall — this is actually a UX improvement over the current model
  • Total permutation count: N (50% reduction)

The One Legitimate Counter-Argument Against Runtime Bool

There is a real cost: every LANDSCAPE shader binary — even for vanilla users who never install TV — would contain the stochastic sampling code. This means:

  1. Slightly larger shader binary (minor)
  2. Higher register pressure — the GPU register allocator must account for the TV code paths even in the non-taken branch, potentially reducing occupancy for vanilla users

Whether (2) matters in practice depends on how register-pressure-bound the LANDSCAPE shader already is. On modern AMD and Nvidia GPUs, [branch] with a uniform (warp-coherent) condition is essentially free at runtime — the warp either uniformly takes or uniformly skips the block. But the register allocator still counts those registers at compile time.

Bottom line: If LANDSCAPE is already register-bound (which, given its complexity, it may well be), the runtime bool costs vanilla users a small occupancy penalty. If it's not register-bound, the runtime bool is strictly better in every dimension.


Verdict

Approach Permutation count Install/uninstall Vanilla perf impact
Current (compile-time define) 2N Requires recompile None
Runtime cbuffer bool N No recompile needed Possible minor occupancy hit

Your reasoning is partially correct — the defined/!defined semantics are right and already implemented. But the claim that it gives "minimal compiler steps" is wrong — it is literally the maximum compile cost because both variants must exist. The runtime bool is the approach that actually minimizes compile steps, and it handles install/uninstall correctly. The only reason to keep the compile-time define is if LANDSCAPE is measurably register-bound and vanilla-without-TV performance is a priority worth protecting at the cost of doubled compile time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants