Repository navigation
Snowbreak: fix the displaced DLSS output, and keep the indirect texture format upgrade chain off - #222
Snowbreak: fix the displaced DLSS output, and keep the indirect texture format upgrade chain off#222Clackz wants to merge 1 commit into
Conversation
a1ff12b to
2621aaf
Compare
…re format upgrade chain off
Two independent Snowbreak fixes, both in this game's main.cpp.
1) DLSS output displaced from the real image
When the internal render resolution is not the backbuffer size - the render
precision slider below 100%, or any resolution that is not 1:1 - the DLSS
result was drawn offset: a sharp but recognisably displaced copy of moving
geometry next to the real one.
Root cause: game_device_data.render_resolution was written exactly once, in
OnInitSwapchain, and never again (the two writes in the TAA slot are
commented out). It therefore held whatever the backbuffer size was at
startup for the whole session. That value ends up in
LumaData.GameData.RenderResolution, and the motion-vector decode shader
converts a clip-space motion delta into screen pixels with
screenSpaceDelta = motionDelta * renderRes.xy
so every motion vector was scaled by (frozen resolution / current render
resolution). DLSS then reprojected its history along the wrong vector.
Measured: 1280x720 at 100% (render size == backbuffer, factor 1.0) is fine;
2560x1440 at 90% (factor 1.111) and 1920x1080 at 100% from a 2560x1440
swapchain (factor 1.333) are displaced by exactly that factor.
Fix: mirror device_data.render_resolution into game_device_data
.render_resolution/.viewport_rect every frame in UpdateLumaInstanceDataCB.
That is the live value - the per-view global constant buffer scan refreshes
it with the real internal render resolution - so the MVs, which are
expressed in pixels of that same resolution, and the scale the decode
shader divides by, are consistent by construction.
Supporting changes, all in the same direction (never feed DLSS data whose
size it cannot know):
- The MV decode dispatch uses the size recorded at capture time instead of
the frozen render_resolution, so it covers exactly the pixels the target
has.
- The decode no longer caches its motion-vector ratio in a function-local
static: the ratio is only valid for the resolution it was derived from,
so it is now derived per pass, and a mismatch means "no usable MVs this
frame" rather than "reuse the last one".
- Captures are stamped with the frame they belong to (frame_index /
capture_frame / capture_mv_w / capture_mv_h, atomics: written by the TAA
slot, read by the upscale pass). The upscale pass skips DLSS instead of
reprojecting with MVs from another frame or another resolution, and
resets the history when the render size changed or the jitter is unknown.
- IsViewSizeInvSize takes an optional aspect ratio (negative = accept any).
Validating a candidate against the aspect ratio of the resolution we
happen to be at is self-referential: that ratio is itself only ever
written from this very check, so a switch to a different aspect ratio
could never be discovered again and jitter/near/fov stayed frozen.
- UE4-SRSTATE now prints "mvres WxH", the resolution the decode divides
by. It has to equal the DLSS input size; when it does not, the offset is
mvres / in_w. This is the invariant to check in a log.
Verified in game at 1280x720@100%, 1920x1080@100% and 2560x1440@90%.
2) MainMap loading screen hang with HDR (previously reviewed in this PR)
Kept at "None" instead of the "DirectAndIndirectDependencies" the other
Unreal Engine games use. With the indirect chain enabled, this game hangs
on the MainMap loading screen when leaving a dungeon: it keeps running at
full frame rate underneath, but the world only restarts rendering once the
user presses ESC. Bisected 2026-09-19 by turning the individual HDR
switches off one at a time - the swapchain upgrades, the texture format
upgrades, the 2D size filters and the display composition setting are all
still enabled here.
2621aaf to
6fca7bd
Compare
|
Update: this PR is now rebased onto current About the red
So the remaining red is the file's pre-existing state, not a regression. If you want the linter to Build-wise: I compiled it through my fork's CI (Publishing-Release x64) before pushing, and the |
|
Follow-up on the build: The three projects its summary reports as "failed to build (excluded from this release)" - Need For So the only red check left is the |
Snowbreak: fix the displaced DLSS output, and keep the indirect texture format upgrade chain off
Two independent fixes for Snowbreak: Containment Zone (UE4.26, DX11). Both are game-local and touch
only
Source/Games/Snowbreak Containment Zone/main.cpp.1) DLSS output displaced from the real image
Problem
Whenever the internal render resolution is not the backbuffer size - the "render precision" slider
below 100%, or any resolution that is not 1:1 - the DLSS result was drawn offset: a sharp but
recognisably displaced copy of moving geometry sitting next to the real one.
How to reproduce
Measured A/B, which is what pinned the factor:
The last row starts from a 2560x1440 swapchain at init, so the frozen value is 2560x1440 while the
render resolution is 1920x1080.
Root cause
game_device_data.render_resolutionwas written exactly once, inOnInitSwapchain, and never againhappened to be at startup, for the rest of the session.
That value ends up in
LumaData.GameData.RenderResolution, and the motion-vector decode shaderconverts a clip-space motion delta into screen pixels with
so every motion vector was scaled by
frozen resolution / current render resolution, and DLSSreprojected its history along the wrong vector. The offset is exactly that factor (1.111 and 1.333
in the table above).
Fix
Mirror
device_data.render_resolutionintogame_device_data.render_resolution/viewport_rectevery frame in
UpdateLumaInstanceDataCB. That is the live value: the per-view global constantbuffer scan refreshes it with the real internal render resolution. The MVs are expressed in pixels of
that same resolution, so the scale the decode divides by is correct by construction.
Supporting changes, all in the same direction - never hand DLSS data whose size it cannot know:
render_resolution, so it covers exactly the pixels the target has.valid for the resolution it was derived from, so it is now derived per pass, and a mismatch means
"no usable MVs this frame" rather than "reuse the last one".
frame_index/capture_frame/capture_mv_w/capture_mv_h; atomics, because the TAA slot writes them and the upscale passreads them). The upscale pass skips DLSS rather than reprojecting with MVs from another frame or
another resolution, and resets the history when the render size changed or the jitter is unknown.
IsViewSizeInvSizetakes an optional aspect ratio (negative = accept any). Validating a candidateagainst the aspect ratio of the resolution we happen to be at is self-referential - that ratio is
itself only ever written from this very check - so a switch to a different aspect ratio could never
be discovered again, and jitter / near / fov stayed frozen at their previous values.
UE4-SRSTATEnow printsmvres WxH, the resolution the decode divides by. It has to equal theDLSS input size
in; when it does not, the offset ismvres / in_w. That makes the invariantvisible in a log instead of something that has to be inferred.
Verification
displaced before it.
mvresequals the DLSS input size in every state line logged since.Scope / risk
skip_srnow also fires when the MV capture is not from the current frame or does not match therender size. In that state the game's own bilinear blit is used for that frame instead of DLSS. It
is transient - the TAA slot re-captures on the next frame that has a velocity pass - but it does
mean DLSS pauses on frames without a velocity pass (menus, loading, camera cuts) instead of
upscaling with data that belongs to another frame.
2) MainMap loading screen hang with Luma HDR
Problem
With Luma HDR enabled in Snowbreak: Containment Zone (UE4.26, DX11), leaving a dungeon and
returning to the main lobby could leave the game stuck on the MainMap loading screen
indefinitely. The game is not frozen: it keeps rendering at full frame rate underneath, but the
world only starts drawing again after the user presses ESC back out to the "main" lobby screen
(about a second later).
How to reproduce
EnableHDRin the ReShade config).the world never resumes rendering until ESC is pressed.
Two caveats that matter when A/B testing this:
different UI teardown paths, so keep the stage type fixed between the "before" and "after" runs.
as its own config key rather than by rebuilding between runs.
Root cause
The HDR path in this game's
main.cppturns on several things at once. Turning them off one at atime, with
EnableHDRas the only other variable, isolates the indirect texture format upgradechain:
2(AllowedEnabled)1(scRGB)0/10(None)122932(DirectAndIndirectDependencies)0(None)enable_chain_indirect_texture_format_upgradeswas set toChainTextureFormatUpgradesType::DirectAndIndirectDependencies, the highest of the three levelsand the only one that additionally pulls in the compute/dispatch side resources
(
core.hpp:if ((ChainTextureFormatUpgradesType)cmd_list_data.enable_chain_indirect_texture_format_upgrades >= ChainTextureFormatUpgradesType::DirectAndIndirectDependencies)).Only two other games sit at that level (Final Fantasy XV and Middle-earth: Shadow of Mordor);
every other game uses
DirectDependencies.I have not traced which specific resource ends up stuck — the evidence stops at "the indirect chain
at the
DirectAndIndirectlevel causes it". Happy to dig further if you want the exact resource.Fix
Set the indirect chain to
Nonefor this game only.Noneis also the core default(
ResourceUpgradeManager::enable_chain_indirect_texture_format_upgrades). Everything else the HDRpath sets is kept: the swapchain format upgrade, the scRGB swapchain upgrade, the texture format
upgrades, the 2D size filters and display composition.
Note that the per command list value is separately pinned to
DirectDependenciesbyOnPresent();this change only touches the device wide setting.
Verification
9,431,552bytes, SHA-25611d3176a5ee205d156a4a3d5bcedd8ba0bc245be5e5a51a0fb127d3da4c06adf.game still gets its scRGB swapchain, and switching Display Mode between SDR and HDR still changes
the image on screen.
ScenePeakWhitereads400.Scope / risk
Source/Games/Snowbreak Containment Zone/main.cpp.thorough on resources that are only reached indirectly. HDR output itself is unchanged in the
cases tested.
first boot when the primary display reports HDR support. That is upstream behaviour, but it means
users on an HDR display who want SDR have to untick "Enable Luma HDR" once.
Notes for the reviewer
Source/Games/Unreal Engine/*, so there isno file overlap with this change.
build on the rebased branch and post the artifact.