Skip to content

fix(HDR): fix UI and blackscreens when HDR unloaded#2221

Merged
alandtse merged 13 commits into
community-shaders:devfrom
SkrubbySkrubInAShrub:HDR-swapchain-fixes-
Apr 28, 2026
Merged

fix(HDR): fix UI and blackscreens when HDR unloaded#2221
alandtse merged 13 commits into
community-shaders:devfrom
SkrubbySkrubInAShrub:HDR-swapchain-fixes-

Conversation

@SkrubbySkrubInAShrub
Copy link
Copy Markdown
Collaborator

@SkrubbySkrubInAShrub SkrubbySkrubInAShrub commented Apr 27, 2026

fixes #2215

Summary by CodeRabbit

  • Bug Fixes
    • UI routing now consistently redirects UI only when the compositor will read it, preserving correct presentation across modes.
    • Consolidated compositing eligibility logic to avoid mixed behavior; foreground compositing gating is consistent.
    • Swap-chain updated to a modern flip model with proper buffering for more reliable presentation.
    • Color format handling adjusted when HDR is off to ensure correct non‑HDR output and intact HDR fallback.

@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

Adds HDRDisplay::IsFGCompositingThisFrame() and centralizes FG UI-compositing checks; updates SetUIBuffer to route UI at runtime between the wrapped swap-chain back buffer and the dedicated UI buffer based on HDR/FG compositor availability; swap-chain uses FLIP_DISCARD with BufferCount >= 2 and non-HDR sRGB formats map to non-sRGB when HDR is disabled.

Changes

Cohort / File(s) Summary
HDR UI compositing & routing
src/Features/HDRDisplay.cpp, src/Features/HDRDisplay.h
Add IsFGCompositingThisFrame() and replace duplicated FG gating. Rework SetUIBuffer, ScaleUIBrightnessForFG, and UpdateHDRData to use the helper; D3D12 routing now binds UI to uiBufferWrapped only when a compositor will read it, otherwise binds swap-chain back buffer so UI survives Present; keep ApplyHDR fallback when HDR shader missing.
Swap-chain / Upscaling
src/Features/Upscaling.cpp
Swap-chain creation switched to DXGI_SWAP_EFFECT_FLIP_DISCARD and enforces BufferCount >= 2; when HDR disabled, map *_UNORM_SRGB formats to non-sRGB equivalents; HDR-specific upgrade to R10G10B10A2_UNORM preserved.

Sequence Diagram

sequenceDiagram
    participant Caller as SetUIBuffer
    participant HDR as HDRCompositor
    participant FG as FrameGenCompositor
    participant Swap as SwapChainBuffer/RTV

    rect rgba(200,100,0,0.5)
        Note over Caller,HDR: D3D12 flip-model runtime routing
        Caller->>HDR: Query "Will HDR composite UI?"
        Caller->>FG: Query IsFGCompositingThisFrame()
        alt HDR or FG will composite UI
            Caller->>Swap: Clear & bind `uiBufferWrapped` (RTV)
        else No compositor reads UI
            Caller->>Swap: Clear & bind `swapChainBufferWrapped` (RTV) so UI survives Present
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • davo0411
  • alandtse

Poem

🐰
I hop where pixels gently glow,
I check if HDR or FG will show,
If they sip the UI I leave it be,
If not I hide it in swap-chain’s lea,
Hops and frames — a rabbit’s glee.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 directly addresses the main focus of the changeset: fixing UI rendering and blackscreen issues related to HDR unloading.
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

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

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

🤖 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/Features/HDRDisplay.cpp`:
- Around line 858-861: The ffxWillComposite predicate in HDRDisplay.cpp is
missing the same main/loading-menu guard used by its sister predicates, which
can route UI into uiBufferWrapped during menus and reintroduce the black-UI bug;
update the ffxWillComposite expression (the line that computes ffxWillComposite
using globals::game::ui and upscaling.settings.frameGenerationMode) to include
the same checks as HDRDisplay::UpdateHDRData and
HDRDisplay::ScaleUIBrightnessForFG—specifically add && !isMainOrLoadingMenu &&
!isVR—so its semantics match the other fg-compositing checks.
🪄 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: e1375e4e-302b-4bfb-97c4-23353b2255dc

📥 Commits

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

📒 Files selected for processing (2)
  • src/Features/HDRDisplay.cpp
  • src/Features/Upscaling.cpp

Comment thread src/Features/HDRDisplay.cpp Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

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

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 (2)
src/Features/HDRDisplay.cpp (2)

853-856: Minor: validation on uiBufferWrapped short-circuits the fallback path unnecessarily.

The early return at lines 853–854 bails out when uiBufferWrapped/rtv is missing, even when needsUIBuffer will be false and only swapChainBufferWrapped is actually used. In practice these two wrappers are created/torn down together in dx12SwapChain, so this is effectively a no-op today, but it does couple the safe fallback path to a resource it doesn’t consume — if uiBufferWrapped is ever made optional/lazy, the HDR-unloaded + FG-off case (the scenario this PR fixes) would silently regress to the previous black-screen.

Consider checking each wrapper only on the path that actually uses it:

-        if (!upscaling.dx12SwapChain.uiBufferWrapped || !upscaling.dx12SwapChain.uiBufferWrapped->rtv)
-            return;
         if (!upscaling.dx12SwapChain.swapChainBufferWrapped || !upscaling.dx12SwapChain.swapChainBufferWrapped->rtv)
             return;
@@
-        bool needsUIBuffer = hdrWillComposite || ffxWillComposite;
+        bool needsUIBuffer = hdrWillComposite || ffxWillComposite;
+        if (needsUIBuffer &&
+            (!upscaling.dx12SwapChain.uiBufferWrapped || !upscaling.dx12SwapChain.uiBufferWrapped->rtv))
+            return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Features/HDRDisplay.cpp` around lines 853 - 856, The current early
returns unconditionally bail when upscaling.dx12SwapChain.uiBufferWrapped or its
rtv is missing, which prevents the fallback path when needsUIBuffer is false;
change the logic so you only validate uiBufferWrapped/rtv when needsUIBuffer is
true (e.g., check needsUIBuffer before inspecting
upscaling.dx12SwapChain.uiBufferWrapped), and keep the validation for
upscaling.dx12SwapChain.swapChainBufferWrapped/rtv as-is so the swap-chain path
still fails fast; update the checks around uiBufferWrapped, needsUIBuffer, and
swapChainBufferWrapped to perform per-path validation instead of the
unconditional early returns.

858-862: Optional: extract the FG-compositing predicate to a single helper.

The “FFX FG will composite UI this frame” condition is now expressed in three places with subtly different shapes:

  • HDRDisplay::SetUIBuffer (this block) — implicitly relies on the VR early-return and the surrounding d3d12SwapChainActive branch.
  • HDRDisplay::ScaleUIBrightnessForFG (line ~1230).
  • HDRDisplay::UpdateHDRData (line ~1333).

They must agree exactly or the UI routing in SetUIBuffer will desync from what the FG/HDR compositors actually consume. A single private helper (e.g. bool HDRDisplay::IsFGCompositingThisFrame() const) would centralize the rule and eliminate the risk of one site drifting from the others in future changes.

♻️ Sketch
+bool HDRDisplay::IsFGCompositingThisFrame() const
+{
+    auto& upscaling = globals::features::upscaling;
+    auto* ui = globals::game::ui;
+    const bool isMainOrLoadingMenu =
+        globals::state->isMainMenuOpen || globals::state->isLoadingMenuOpen;
+    return upscaling.d3d12SwapChainActive &&
+           upscaling.settings.frameGenerationMode &&
+           ui && !ui->GameIsPaused() &&
+           !isMainOrLoadingMenu &&
+           !globals::game::isVR;
+}

Then SetUIBuffer, ScaleUIBrightnessForFG, and UpdateHDRData all call this helper instead of re-deriving the condition.

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

In `@src/Features/HDRDisplay.cpp` around lines 858 - 862, Extract the
FG-compositing predicate into a single private helper (e.g. bool
HDRDisplay::IsFGCompositingThisFrame() const) that returns the exact condition
currently computed in SetUIBuffer: upscaling.settings.frameGenerationMode is
true, globals::game::ui exists and !gameUi->GameIsPaused(), and globals::state
is not main or loading menu (and preserve any VR/d3d12SwapChainActive
preconditions by documenting/handling them where helper is called). Replace the
duplicated logic in HDRDisplay::SetUIBuffer, HDRDisplay::ScaleUIBrightnessForFG,
and HDRDisplay::UpdateHDRData to call IsFGCompositingThisFrame() so all three
sites use the single canonical rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Features/HDRDisplay.cpp`:
- Around line 853-856: The current early returns unconditionally bail when
upscaling.dx12SwapChain.uiBufferWrapped or its rtv is missing, which prevents
the fallback path when needsUIBuffer is false; change the logic so you only
validate uiBufferWrapped/rtv when needsUIBuffer is true (e.g., check
needsUIBuffer before inspecting upscaling.dx12SwapChain.uiBufferWrapped), and
keep the validation for upscaling.dx12SwapChain.swapChainBufferWrapped/rtv as-is
so the swap-chain path still fails fast; update the checks around
uiBufferWrapped, needsUIBuffer, and swapChainBufferWrapped to perform per-path
validation instead of the unconditional early returns.
- Around line 858-862: Extract the FG-compositing predicate into a single
private helper (e.g. bool HDRDisplay::IsFGCompositingThisFrame() const) that
returns the exact condition currently computed in SetUIBuffer:
upscaling.settings.frameGenerationMode is true, globals::game::ui exists and
!gameUi->GameIsPaused(), and globals::state is not main or loading menu (and
preserve any VR/d3d12SwapChainActive preconditions by documenting/handling them
where helper is called). Replace the duplicated logic in
HDRDisplay::SetUIBuffer, HDRDisplay::ScaleUIBrightnessForFG, and
HDRDisplay::UpdateHDRData to call IsFGCompositingThisFrame() so all three sites
use the single canonical rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a55f7d7-9223-4e6c-9804-ed2146b606fb

📥 Commits

Reviewing files that changed from the base of the PR and between db00688 and 9f2a42a.

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

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

🤖 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/Features/HDRDisplay.cpp`:
- Around line 871-873: The hdr routing allows hdrWillComposite to be true even
when the HDR compute shader is missing, causing UI to be sent to uiBufferWrapped
but never copied back; change the hdrWillComposite/needsUIBuffer check to also
require that GetHDROutputCS() returns non-null (i.e., shader available) before
routing UI to uiBufferWrapped (symbols: hdrWillComposite, needsUIBuffer,
GetHDROutputCS(), SetUIBuffer(), uiBufferWrapped, outputTexture, hdrDataCB,
loaded, ffxWillComposite), and update the ApplyHDR() fallback path (where only
kFRAMEBUFFER is copied) to also copy the uiBufferWrapped contents back when the
HDR shader is not present so UI remains visible in degraded shader-missing runs.
🪄 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: f8429479-09d4-4800-95dc-41bde58a0e25

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2a42a and da4847d.

📒 Files selected for processing (2)
  • src/Features/HDRDisplay.cpp
  • src/Features/HDRDisplay.h

Comment thread src/Features/HDRDisplay.cpp Outdated
SkrubbySkrubInAShrub and others added 2 commits April 27, 2026 21:12
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.

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

872-877: Avoid eager shader lookup when HDR runtime is inactive.

GetHDROutputCS() is queried even when HDR runtime state is not ready (loaded/hdrDataCB/outputTexture false). Short-circuiting avoids unnecessary shader compile attempts/log churn in HDR-unloaded sessions.

Proposed patch
-		bool ffxWillComposite = IsFGCompositingThisFrame();
-		bool hdrShaderAvailable = GetHDROutputCS() != nullptr;
-		bool hdrWillComposite = loaded && hdrDataCB && outputTexture && hdrShaderAvailable;
+		bool ffxWillComposite = IsFGCompositingThisFrame();
+		const bool hdrRuntimeReady = loaded && hdrDataCB && outputTexture;
+		const bool hdrShaderAvailable = hdrRuntimeReady && (GetHDROutputCS() != nullptr);
+		bool hdrWillComposite = hdrRuntimeReady && hdrShaderAvailable;
 		bool needsUIBuffer = hdrWillComposite || ffxWillComposite;
-		bool hdrWillFallbackCopy = loaded && hdrDataCB && outputTexture && !hdrShaderAvailable;
+		bool hdrWillFallbackCopy = hdrRuntimeReady && !hdrShaderAvailable;
As per coding guidelines: `src/Features/**/*.{h,hpp,cpp,hlsl}` — "Consider GPU workload and performance impact when implementing graphics features, with special attention to shader compilation and runtime performance".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Features/HDRDisplay.cpp` around lines 872 - 877, Currently
GetHDROutputCS() is called unconditionally which can trigger shader lookup when
HDR runtime isn't ready; change the logic so you first check the HDR readiness
(loaded && hdrDataCB && outputTexture) and only then call GetHDROutputCS() to
set hdrShaderAvailable and hdrWillComposite. Concretely, test ready = (loaded &&
hdrDataCB && outputTexture); if ready then hdrShaderAvailable = GetHDROutputCS()
!= nullptr and set hdrWillComposite = ready && hdrShaderAvailable and
hdrWillFallbackCopy = ready && !hdrShaderAvailable; otherwise set
hdrShaderAvailable/hdrWillComposite/hdrWillFallbackCopy to false; keep
ffxWillComposite computed as before using IsFGCompositingThisFrame().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Features/HDRDisplay.cpp`:
- Around line 872-877: Currently GetHDROutputCS() is called unconditionally
which can trigger shader lookup when HDR runtime isn't ready; change the logic
so you first check the HDR readiness (loaded && hdrDataCB && outputTexture) and
only then call GetHDROutputCS() to set hdrShaderAvailable and hdrWillComposite.
Concretely, test ready = (loaded && hdrDataCB && outputTexture); if ready then
hdrShaderAvailable = GetHDROutputCS() != nullptr and set hdrWillComposite =
ready && hdrShaderAvailable and hdrWillFallbackCopy = ready &&
!hdrShaderAvailable; otherwise set
hdrShaderAvailable/hdrWillComposite/hdrWillFallbackCopy to false; keep
ffxWillComposite computed as before using IsFGCompositingThisFrame().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16ad8526-a1e9-48c6-a66e-aeedf1a6d619

📥 Commits

Reviewing files that changed from the base of the PR and between da4847d and d5ae43d.

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

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 (1)
src/Features/HDRDisplay.cpp (1)

867-895: Routing logic is correct; minor optional cleanup on the readiness booleans.

The three-way routing (uiBufferWrapped when a compositor will read it, kFRAMEBUFFER for the ApplyHDR shader-missing fallback, otherwise the wrapped back buffer) correctly fixes the HDR-unloaded blackscreen path and preserves the existing fallback in ApplyHDR (lines 1021-1024). The loaded && hdrDataCB && outputTexture && GetHDROutputCS() chain also resolves the previously-flagged shader-availability concern.

Tiny optional simplification: hdrShaderAvailable already implies hdrReady, so hdrReady && hdrShaderAvailable on line 875 is redundant.

♻️ Optional simplification
 		bool ffxWillComposite = IsFGCompositingThisFrame();
 		bool hdrReady = loaded && hdrDataCB && outputTexture;
-		bool hdrShaderAvailable = hdrReady && GetHDROutputCS() != nullptr;
-		bool hdrWillComposite = hdrReady && hdrShaderAvailable;
+		bool hdrWillComposite = hdrReady && GetHDROutputCS() != nullptr;
 		bool needsUIBuffer = hdrWillComposite || ffxWillComposite;
-		bool hdrWillFallbackCopy = hdrReady && !hdrShaderAvailable;
+		bool hdrWillFallbackCopy = hdrReady && !hdrWillComposite;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Features/HDRDisplay.cpp` around lines 867 - 895, The variable
hdrWillComposite is computed redundantly as "hdrReady && hdrShaderAvailable"
even though hdrShaderAvailable is already defined as "hdrReady &&
GetHDROutputCS() != nullptr"; simplify by changing hdrWillComposite to just use
hdrShaderAvailable (i.e., set hdrWillComposite = hdrShaderAvailable) and remove
the redundant use of hdrReady in that expression; update references to
hdrWillComposite if needed and ensure hdrShaderAvailable remains defined as
currently (using GetHDROutputCS()) to preserve the readiness check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Features/HDRDisplay.cpp`:
- Around line 867-895: The variable hdrWillComposite is computed redundantly as
"hdrReady && hdrShaderAvailable" even though hdrShaderAvailable is already
defined as "hdrReady && GetHDROutputCS() != nullptr"; simplify by changing
hdrWillComposite to just use hdrShaderAvailable (i.e., set hdrWillComposite =
hdrShaderAvailable) and remove the redundant use of hdrReady in that expression;
update references to hdrWillComposite if needed and ensure hdrShaderAvailable
remains defined as currently (using GetHDROutputCS()) to preserve the readiness
check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f660339a-db06-43be-9da3-0d9c4c58a4d6

📥 Commits

Reviewing files that changed from the base of the PR and between d5ae43d and 67534bc.

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

@alandtse alandtse merged commit 733df4f into community-shaders:dev Apr 28, 2026
14 checks passed
ParticleTroned added a commit to ParticleTroned/skyrim-community-shaders that referenced this pull request May 2, 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.

Black screen after unloading HDR feature

3 participants