Skip to content

fix: fix normals precision#2233

Closed
doodlum wants to merge 3 commits into
devfrom
fix-normals-precision
Closed

fix: fix normals precision#2233
doodlum wants to merge 3 commits into
devfrom
fix-normals-precision

Conversation

@doodlum
Copy link
Copy Markdown
Collaborator

@doodlum doodlum commented Apr 29, 2026

Normal precision improvements now apply to the base game as well (partially).

Summary by CodeRabbit

  • Refactor
    • Switched to octahedral normal encoding for deferred rendering (improved normal precision/consistency).
    • Second render target now carries decoded view-space normals for composite passes.
    • Blend state adjusted so the normal render target writes to full color channels.
    • Added creation/path handling for dedicated high-precision normal render targets.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

Refactors GBuffer normal encoding from sqrt-based to octahedral, adds an OctWrap helper and EncodeNormalVanilla, updates DeferredComposite to output normals (R10G10B10A2) via EncodeNormalVanilla, adjusts blend state to write all channels on RT1, and adds hooks creating normals RTs using R10G10B10A2 format.

Changes

Cohort / File(s) Summary
GBuffer / Normal encoding
package/Shaders/Common/GBuffer.hlsli
Replaces previous sqrt-based normal encode/decode with octahedral encoding; adds OctWrap and EncodeNormalVanilla; changes EncodeNormal/DecodeNormal implementations and parameter name.
Deferred composite shader
package/Shaders/DeferredCompositePS.hlsl
Renames PS output NormalRoughnessNormal; writes encoded view-space normal using EncodeNormalVanilla; removes DEBUG-only re-decode fallback and reorders conditional compilation for normal calculation.
Render target / blend state changes
src/Deferred.cpp
Blend state for render target index 1 modified to write all color channels instead of only blue.
Render target creation hooks
src/Hooks.cpp
Adds CreateRenderTarget_Normals and CreateRenderTarget_NormalsSwap hooks that clone RenderTargetProperties, force kR10G10B10A2_UNORM format, and install detours at two new relocation offsets adjacent to motion vectors hook.

Sequence Diagram(s)

sequenceDiagram
    participant Engine
    participant Hook as RT Hook
    participant GPU as Render Targets
    participant Shader as DeferredCompositePS
    Engine->>Hook: Request create render target (normals)
    Hook->>Engine: Modify properties (format = R10G10B10A2) and forward
    Engine->>GPU: Allocate RT with R10G10B10A2
    Shader->>GPU: Encode normals via GBuffer::EncodeNormalVanilla and write to RT1
    Shader->>Shader: Later composite reads RT1 for lighting passes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • soda3000
  • alandtse

Poem

🐰🎨 I hop through bytes with careful art,
Octahedral normals, a brand new start.
RTs gleam in ten-bit light,
Blend states set — all channels write.
A rabbit cheers for cleaner sight! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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 'fix: fix normals precision' directly aligns with the main objective of improving normal precision handling across the codebase, as evidenced by changes to normal encoding/decoding in GBuffer, shader outputs, blend states, and render target formats.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

@github-actions
Copy link
Copy Markdown

No actionable suggestions for changed features.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package/Shaders/DeferredCompositePS.hlsl (1)

121-128: ⚠️ Potential issue | 🔴 Critical

Declare NormalRoughnessTexture for the base permutation.

output.Normal now always depends on normalVS, but normalVS still comes from NormalRoughnessTexture, which is only declared behind SSGI || DYNAMIC_CUBEMAPS || DEBUG. Deferred::GetCompositePS() still builds a permutation with none of those defines, so the base-game variant will fail to compile. Since t2 is already bound unconditionally on the C++ side, this texture declaration should be unconditional too.

Suggested fix
-#if defined(SSGI) || defined(DYNAMIC_CUBEMAPS) || defined(DEBUG)
 Texture2D<float4> NormalRoughnessTexture : register(t2);
-#endif

Also applies to: 339-342

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

In `@package/Shaders/DeferredCompositePS.hlsl` around lines 121 - 128, The shader
fails to compile for the base permutation because NormalRoughnessTexture is only
declared under SSGI||DYNAMIC_CUBEMAPS||DEBUG while normalVS and output.Normal
always use it; make the NormalRoughnessTexture declaration unconditional (remove
the preprocessor guards) so it is available for the base permutation that
Deferred::GetCompositePS() builds (t2 is already bound on the C++ side), and
apply the same unconditional declaration fix to the other occurrence around
lines 339-342.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Hooks.cpp`:
- Around line 591-596: The thunk copies a_properties into a local properties,
mutates properties.format, then calls func(...) but fails to run the shared
mutation in globals::state->ModifyRenderTarget, so normals targets lose common
flags (e.g., UAV). Fix: after setting properties.format and before calling
func(This, a_target, &properties), call
globals::state->ModifyRenderTarget(a_target, properties) (or the correct
overload that mutates the struct in-place) and then pass the resulting/modified
properties pointer to func; apply the same change to the other similar thunk
implementations that set the normal buffer formats (the other thunk blocks that
set kR10G10B10A2_UNORM and the other normal-format thunk around the other
reported ranges).

---

Outside diff comments:
In `@package/Shaders/DeferredCompositePS.hlsl`:
- Around line 121-128: The shader fails to compile for the base permutation
because NormalRoughnessTexture is only declared under
SSGI||DYNAMIC_CUBEMAPS||DEBUG while normalVS and output.Normal always use it;
make the NormalRoughnessTexture declaration unconditional (remove the
preprocessor guards) so it is available for the base permutation that
Deferred::GetCompositePS() builds (t2 is already bound on the C++ side), and
apply the same unconditional declaration fix to the other occurrence around
lines 339-342.
🪄 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: f6a567db-15e4-432e-9b80-160c0daf7e41

📥 Commits

Reviewing files that changed from the base of the PR and between 651426e and ade3d0f.

📒 Files selected for processing (4)
  • package/Shaders/Common/GBuffer.hlsli
  • package/Shaders/DeferredCompositePS.hlsl
  • src/Deferred.cpp
  • src/Hooks.cpp

Comment thread src/Hooks.cpp
Comment on lines 591 to +596
static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
{
globals::state->ModifyRenderTarget(a_target, a_properties);
func(This, a_target, a_properties);
RE::BSGraphics::RenderTargetProperties properties = *a_properties;
properties.format.set(RE::BSGraphics::Format::kR10G10B10A2_UNORM);
func(This, a_target, &properties);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve the shared render-target mutation for the normals targets.

These two hooks now override the format and call func(...) directly, but unlike CreateRenderTarget_Main they never pass the copied properties through globals::state->ModifyRenderTarget(...). That drops the common render-target adjustments from src/State.cpp for both normal buffers, so they no longer inherit flags like UAV support. Please apply the global mutation to the copied struct before calling the original function.

Suggested fix
 struct CreateRenderTarget_Normals
 {
 	static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
 	{
 		RE::BSGraphics::RenderTargetProperties properties = *a_properties;
 		properties.format.set(RE::BSGraphics::Format::kR10G10B10A2_UNORM);
+		globals::state->ModifyRenderTarget(a_target, &properties);
 		func(This, a_target, &properties);
 	}
 	static inline REL::Relocation<decltype(thunk)> func;
 };
 
 struct CreateRenderTarget_NormalsSwap
 {
 	static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
 	{
 		RE::BSGraphics::RenderTargetProperties properties = *a_properties;
 		properties.format.set(RE::BSGraphics::Format::kR10G10B10A2_UNORM);
+		globals::state->ModifyRenderTarget(a_target, &properties);
 		func(This, a_target, &properties);
 	}
 	static inline REL::Relocation<decltype(thunk)> func;
 };

Also applies to: 602-606, 967-968

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

In `@src/Hooks.cpp` around lines 591 - 596, The thunk copies a_properties into a
local properties, mutates properties.format, then calls func(...) but fails to
run the shared mutation in globals::state->ModifyRenderTarget, so normals
targets lose common flags (e.g., UAV). Fix: after setting properties.format and
before calling func(This, a_target, &properties), call
globals::state->ModifyRenderTarget(a_target, properties) (or the correct
overload that mutates the struct in-place) and then pass the resulting/modified
properties pointer to func; apply the same change to the other similar thunk
implementations that set the normal buffer formats (the other thunk blocks that
set kR10G10B10A2_UNORM and the other normal-format thunk around the other
reported ranges).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

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

doodlum and others added 2 commits April 29, 2026 21:41
Automated formatting by clang-format, prettier, and other hooks.
See https://pre-commit.ci for details.
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 (1)
src/Hooks.cpp (1)

599-617: ⚠️ Potential issue | 🟠 Major

Preserve the shared render-target mutation.

These two hooks still bypass globals::state->ModifyRenderTarget(...), so the new normals targets will miss the common render-target adjustments applied elsewhere in the pipeline. Please run the shared mutation on the copied properties before calling func(...).

Suggested fix
 static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties)
 {
 	RE::BSGraphics::RenderTargetProperties properties = *a_properties;
 	properties.format.set(RE::BSGraphics::Format::kR10G10B10A2_UNORM);
+	globals::state->ModifyRenderTarget(a_target, &properties);
 	func(This, a_target, &properties);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Hooks.cpp` around lines 599 - 617, Both CreateRenderTarget_Normals::thunk
and CreateRenderTarget_NormalsSwap::thunk copy a_properties and change
properties.format but never call globals::state->ModifyRenderTarget, so the
shared render-target mutations are skipped; after creating the local properties
copy (the properties variable) and before calling func(This, a_target,
&properties), call globals::state->ModifyRenderTarget(a_target, properties) (or
the correct signature used elsewhere) to apply the common adjustments to
properties, then pass the mutated properties to func; update both thunk
functions accordingly so they run the shared mutation on the copied properties
prior to invoking func.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Hooks.cpp`:
- Around line 599-617: Both CreateRenderTarget_Normals::thunk and
CreateRenderTarget_NormalsSwap::thunk copy a_properties and change
properties.format but never call globals::state->ModifyRenderTarget, so the
shared render-target mutations are skipped; after creating the local properties
copy (the properties variable) and before calling func(This, a_target,
&properties), call globals::state->ModifyRenderTarget(a_target, properties) (or
the correct signature used elsewhere) to apply the common adjustments to
properties, then pass the mutated properties to func; update both thunk
functions accordingly so they run the shared mutation on the copied properties
prior to invoking func.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3febf2c-e911-4cf9-a4fc-e5e06904447e

📥 Commits

Reviewing files that changed from the base of the PR and between ade3d0f and cfd4a2b.

📒 Files selected for processing (1)
  • src/Hooks.cpp

@alandtse
Copy link
Copy Markdown
Collaborator

This does not appear to handle the regression reported for reflection jittering unfortunately.

@alandtse
Copy link
Copy Markdown
Collaborator

Closed in favor of #2232. This should be reopened when we try to get #2150 back in.

@alandtse alandtse closed this Apr 30, 2026
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.

2 participants