Skip to content

[mod:dragonage_veilguard] Replace final output pass for HDR10 tone mapping - #716

Open
Pedro-A-D-S wants to merge 14 commits into
clshortfuse:mainfrom
Pedro-A-D-S:mod/dragonage_veilguard
Open

Pedro-A-D-S wants to merge 14 commits into
clshortfuse:mainfrom
Pedro-A-D-S:mod/dragonage_veilguard

Conversation

@Pedro-A-D-S

@Pedro-A-D-S Pedro-A-D-S commented Oct 23, 2026 •

Copy link
Copy Markdown

What

New mod for Dragon Age: The Veilguard (Frostbite, D3D12, Steam 1845910). With in-game HDR on, the game already writes BT.2020 PQ into an r10g10b10a2_unorm swapchain, so this mod replaces only the final output pass and does not upgrade the swapchain or any resource. The game draws that pass through one of five HDR10 shaders depending on the render scale, the window mode and DLSS Frame Generation, and the mod covers all five:

Hash When Scene input
0x4564BB42 Render scale 100% r11g11b10_float, read directly
0x3998EA61 Render scale below 100% (seen with DLSS Ultra Performance at 66%) r10g10b10a2 with packed half-float bits, 4x4 Lanczos resample
0x23DC66B2 100% with DLSS Frame Generation on r11g11b10_float, read directly, with the same two render targets as the next row
0x22E9F0A5 Below 100% (seen at 66% and 88%) with DLSS Frame Generation on Same as above, with two render targets: the final image with the UI, and the same image without the UI (the HUD-less buffer frame generation reads)
0xE779DCD3 Below 100% with DLSS Frame Generation on (seen windowed at 1920x1080, 70% to 92%) Packed r10g10b10a2 like 0x22E9F0A5, read with the bilinear sampler, no Lanczos texture; two render targets
  • Vanilla keeps the original path: UI composite, accessibility, watermark, display-mapping LUT (t9), PQ encode and dither.
  • None / ACES / RenoDRT tone map the graded scene to Peak Brightness with Game Brightness as paper white, then composite the UI at UI Brightness. The default is RenoDRT, which keeps the original look (midtones and shadows within 5% of Vanilla down to 0.005 of diffuse white, and saturated glows keep their hue); ACES adds contrast but darkens the shadows (a 0.01 scene value gives 0.11 nits against 1.12 for Vanilla) and turns bright coloured glows whiter; Preset Off restores Vanilla.
  • 0x23DC66B2 and 0x22E9F0A5 write both outputs through the same tone mapping (FinalPassWithHudLess()), so the buffer frame generation interpolates matches the displayed image. FinalPass() takes with_ui for that, and the Lanczos read is shared in lanczos.hlsli.
  • Peak Brightness has a reset button like the other settings.
  • Settings are injected as b13, space 0, matching the SM5.0 shaders. addon.cpp sets expected_constant_buffer_index = 13, expected_constant_buffer_space = 0, allow_multiple_push_constants and force_pipeline_cloning (same combination as Dead Space).
  • on_create_pipeline_layout skips root signatures with a descriptor table of more than two ranges. The game creates those for the upscaler's compute passes when the Upsample Method changes; injecting b13 into them ended in a GPU device-removed error. The final-pass root signatures only have tables of one or two ranges.

Why

  • The default injection index put the settings buffer at the first free root slot of each root signature, so the replacement shader read zeros from b13 and stayed on the Vanilla branch. Fixing the index made every setting reach the shader.

  • Below 100% render scale the game uses 0x3998EA61, and with DLSS Frame Generation on it uses 0x23DC66B2 (100%) or 0x22E9F0A5 (below 100%). Without replacements the Tone Mapper had no effect there (with Frame Generation on, switching Vanilla, None, ACES and RenoDRT changed nothing on screen).

  • Switching the Upsample Method in the game's settings with the mod loaded hung the GPU (3 of 3 tries, with and without the DevKit, Frame Generation on and off; never without the mod). A log of the layouts created at the switch showed only compute root signatures with tables of 3 to 5 ranges.

  • A fifth pass, 0xE779DCD3, showed up in windowed 1920x1080 tests at 70%, 85% and 92% with Frame Generation on; it was not replaced, so the Tone Mapper did nothing there. It is the 0x23DC66B2 body with the scene read from a packed r10g10b10a2 texture through the bilinear sampler (UnpackScene() is shared with the Lanczos variants).

How it was verified

Build: cmake --build --preset clang-x64-release --target dragonage_veilguard (the DevKit addon built with the same preset; a Debug/Release mix of the two crashes at load). git diff --check and clang-format --dry-run are clean.

In game (HDR on, Minrathous streets, RT off, DLSS Ultra Performance 66%, DevKit also loaded), ReShade HDR screenshots measured in nits:

  • ReShade.log shows cbvIndex:13 in the injected layouts. Vanilla, None, ACES and RenoDRT produce different frames, with Frame Generation off and on.
  • Frame Generation off, RenoDRT, Peak 1000: peak 828 and 862 nits.
  • Frame Generation on at 100% render scale (0x23DC66B2): one menu screenshot with ACES, Peak 1000 has a peak of 963 nits.
  • Frame Generation on, before the 0x22E9F0A5 replacement: peak about 5000 nits (Vanilla). After it, with Peak 1000: ACES 949 to 986 nits, RenoDRT 859 to 873 nits.
  • Black floor (p1) is 0.17 to 0.19 nits for RenoDRT (the default) and Vanilla and 0.004 for ACES; median 6.3 (RenoDRT), 6.1 (Vanilla), 3.3 nits (ACES). In the Minrathous night capture 61% of the pixels sit below 0.08 of diffuse white, where ACES is 40% darker than Vanilla or more. In a Solas cutscene frame (scene values up to 128) ACES turns 1.76% of the pixels near white against 0.01% for RenoDRT. The README states this.
  • Moving the camera with Frame Generation on showed the usual Frame Generation artefacts and nothing new (visual check by the author).
  • Ray tracing (Reflections, AO, Ultra) and Frame Generation do not raise the black floor (Vanilla p1 stays at about 0.2 nits or lower in every state tested).
  • Only the mod loaded (no DevKit): ReShade.log registers only RenoDX, ACES peak 904 to 968 nits.
  • Vanilla against an untouched game (mod not loaded), main menu, three screenshots each: median 3.48 to 3.50 against 3.46 to 3.50 nits, black p1 0.543 to 0.549 against 0.548 to 0.555. Same distribution within the menu animation, not bit for bit.
  • 0xE779DCD3, windowed 1920x1080, Frame Generation on, Render Scale 92%: DevKit reports the add-on shader, and the output peak is 0.725 with RenoDRT against 0.892 with Vanilla (PQ code values).
  • Daily setup of the author (fullscreen LG TV, 3840x1600 and 3440x1440 at 120 Hz, High preset, ray tracing on, Frame Generation on): 100% uses 0x23DC66B2, and 94% and 71% (Performance and Ultra Performance) use 0x22E9F0A5; both are replaced.
  • With the layout filter: ACES peak 970 and 923 nits, and DLSS to XeSS to None to FSR to DLSS in the settings with no error.

Not verified yet

  • Frame Generation multipliers other than the in-game On (x2 or x3 may use another pass), and 3440x1440 at 100%.
  • Vanilla bit for bit against an untouched capture; the Upsample Method switch during gameplay (tested in the main menu).
  • Render scale above 100%, HDR off (the SDR variants stay vanilla), cinematics, photo mode and pre-rendered video.
  • That the generated frames themselves carry the mod's tone mapping (only a visual check; the HUD-less output was not read back).
  • The three scRGB variants (0xA0D84CD4, 0xAA75CF5D, 0xD45D81E7) were not drawn in any case tested.

metadata.json status is testing.

🤖 Generated with Claude Code

@Pedro-A-D-S
Pedro-A-D-S marked this pull request as ready for review October 23, 2026 22:41
@Pedro-A-D-S

Copy link
Copy Markdown
Author

The Action required runs on this PR are the usual approval gate for workflows from a first-time fork contributor. A maintainer can release them from the Actions tab (Approve and run workflows). Nothing on the branch blocks them.

So the builds can be checked without waiting, I ran the same builds locally on ba5735e0:

  • clang-x64-release, target dragonage_veilguard: builds and links renodx-dragonage_veilguard.addon64.
  • vs-x64-release (MSVC), same target: builds and links; the only warnings come from shared headers.
  • x86 workflows: metadata.json lists only x64 in deploy.architecture, so the mod is skipped there.

The follow-up commit only touches the mod README, which the build workflows ignore (paths-ignore: **/*.md).

🤖 Generated with Claude Code

Pedro-A-D-S and others added 12 commits October 23, 2026 19:57
State that the default is RenoDRT tone mapped to the display peak, that Preset Off
restores Vanilla, and that midtones and shadows match the original with Gamma
Correction Off.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nder scale

With the render scale under 100% the game draws the final output through
0x3998EA61 instead of 0x4564BB42: a 4x4 Lanczos resample of a packed
r10g10b10a2 scene, then the same UI composite, display mapping LUT, BT.2020 PQ
and dither. Move the shared tail into common.hlsli and add a replacement for
the new variant, so the Tone Mapper applies at 66% (DLSS Ultra Performance).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Generation

With DLSS Frame Generation on the game draws the final output through
0x22E9F0A5: the same Lanczos pass as 0x3998EA61 with two render targets, the
final image with the UI and the same image without it (the HUD-less colour
buffer frame generation reads). Without a replacement the Tone Mapper had no
effect at all with frame generation on.

Replace it writing both outputs through the same tone mapping. FinalPass()
takes with_ui to skip the UI composite, and the Lanczos scene read moves to
lanczos.hlsli so both variants share it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ACES gives more contrast than the original (deeper shadows, stronger
highlights); RenoDRT stays available for the original look. Peak Brightness
gets the same reset button as the other settings. The README documents the
three final-pass variants and the measured effect of the ACES default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ith DLSS Frame Generation

With DLSS Frame Generation on and the render scale at 100% the game draws the final
output through 0x23DC66B2: the same scene read as 0x4564BB42 with two render targets,
the final image with the UI and the same image without it. Replace it through
FinalPassWithHudLess(), shared with 0x22E9F0A5, so both frame generation variants go
through the same tone mapping. The README lists the four HDR10 variants.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d in game

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mple method

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…outs

Switching the Upsample Method in the game's settings made it create compute root
signatures with descriptor tables of three or more ranges, and injecting the settings
buffer into them hung the GPU (device removed, reproduced with and without the DevKit,
frame generation on and off, not without the mod). The final pass root signatures only
have tables of up to two ranges, so skip the others in on_create_pipeline_layout. The
Tone Mapper still applies, and switching between DLSS, XeSS, None and FSR works.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eplay

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bove 100%

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Solas cutscene frame ACES turns 1.76% of the pixels near white against
0.01% for RenoDRT, which keeps the hue of saturated glows and the original midtones.
ACES stays selectable.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With DLSS Frame Generation on and Render Scale 70% or 85% the game uses a final pass
that reads the packed half-float scene with the bilinear sampler and no Lanczos. It was
not replaced, so the Tone Mapper did nothing there. UnpackScene() is shared with the
Lanczos variants.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

A brand-new mod with five hand-decompiled shader variants, a GPU device-removal workaround via pipeline-layout filtering, and several explicitly unverified scenarios warrants human validation beyond the one minor Preset Off reset gap.

1 open issue
What changed in this PR

New RenoDX mod for Dragon Age: The Veilguard (Frostbite, D3D12). The game natively outputs HDR10 (BT.2020 PQ into an r10g10b10a2_unorm swapchain), so the mod replaces only the final output pass — it performs no swapchain or resource upgrade. For non-Vanilla tone mappers it tone-maps the graded, pre-display-mapping HDR scene directly to the display peak with independent UI brightness, covering all five HDR10 variants of the final pass (direct read, Lanczos-resampled, and Frame-Generation two-target forms). This is a sound approach that preserves the vanilla path and does not inverse-tonemap a completed SDR frame.

Changes:

  • Adds the mod scaffold: addon.cpp, shared.h, metadata.json, and README.md with tone-mapping/color-grade settings injected at b13 space 0, plus a Preset Off path back to Vanilla.
  • Adds five decompiled final-pass shaders sharing common.hlsli (scene+UI → display mapping → BT.2020 PQ) and lanczos.hlsli (4×4 Lanczos resample); non-Vanilla paths replace display mapping with ToneMapPass.
  • Configures injection (expected_constant_buffer_index = 13, space 0, allow_multiple_push_constants, force_pipeline_cloning) and an on_create_pipeline_layout filter that skips upscaler compute root signatures with descriptor tables of >2 ranges to avoid a GPU device-removed hang.

RenoDX review checklist

  • PR scope/commits tidy: Pass — changes are confined to the new game folder.
  • Review comments helpful: Pass.
  • Defaults preserve vanilla look: Pass — default RenoDRT documented within ~5% of Vanilla; Preset Off → Vanilla.
  • Preset Off / Vanilla-0 behavior clear: Needs comment — OnPresetOff() leaves four settings unreset (see comment).
  • No final-frame inverse tonemapping: Pass — tonemaps the pre-display-mapping HDR scene, not an SDR backbuffer.
  • Tonemap signals proven: Pass — single-arg ToneMapPass on a genuinely graded HDR scene (no separate SDR grade).
  • Linear input to tonemappers: Pass — scene is linear HDR before ToneMapPass.
  • Vanilla LUT path domain/sampling: Pass — vanilla display-mapping LUT preserved verbatim in the Vanilla branch.
  • Hard clips/saturate don't destroy HDR: Pass — only max(0,...)/vanilla packing on reads.
  • SwapChainPass not hiding tonemap: N/A — not used.
  • Output prefers HDR10/PQ: Pass — native HDR10 PQ output retained.
File Description
src/​games/​dragonage_veilguard/​shared.h Injection cbuffer layout (b13) and RenoDX tone-map/grade defines.
src/​games/​dragonage_veilguard/​common.hlsli Shared final-pass body: Vanilla path, tonemap path, PQ encode + dither, two-target helper.
src/​games/​dragonage_veilguard/​lanczos.hlsli Shared 4×4 Lanczos scene resample for below-100% render scale.
src/​games/​dragonage_veilguard/​final_0x4564BB42.ps_5_0.hlsl 100% direct-read final pass.
src/​games/​dragonage_veilguard/​final_0x3998EA61.ps_5_0.hlsl Below-100% Lanczos final pass.
src/​games/​dragonage_veilguard/​final_0x23DC66B2.ps_5_0.hlsl 100% Frame-Generation two-target pass.
src/​games/​dragonage_veilguard/​final_0x22E9F0A5.ps_5_0.hlsl Below-100% Frame-Generation two-target Lanczos pass.
src/​games/​dragonage_veilguard/​final_0xE779DCD3.ps_5_0.hlsl Bilinear packed-read two-target pass.
src/​games/​dragonage_veilguard/​addon.cpp Settings, injection config, layout filter, and Preset Off handling.
src/​games/​dragonage_veilguard/​metadata.json Mod metadata (Steam 1845910, d3d12, testing).
src/​games/​dragonage_veilguard/​README.md Documentation of passes, behavior, and manual verification.

Review effort: Balanced

Comment thread src/games/dragonage_veilguard/addon.cpp Resolved
Added new tone mapping settings for improved visuals.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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