fix(oot): on Shadow resolution Highest, the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link shimmer less #836
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!836
Loading…
Reference in a new issue
No description provided.
Delete branch "Jiro113/Longshot:fix/sun-cascade-stable-fit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: medium · Tested on: Windows 11, RTX 4090; Linux compile and checks (Ubuntu 22.04) · Needs: a by-eye check in motion (this PC)
Depends on #773 (the Shadow resolution setting): this branch is built on it, so its first four commits are #773's.
Review and merge #773 first; this PR's own change is its last four commits (the steady fit, its measured runs, the
Highest-only rule with the menu's note, and that rule's runs).
Both branches were rebased on main
69ff5bebwith no change to their commits (git range-diff): this PR's 0f0d2155 and4f0a1707, named below where they were measured and tested, are now
4bbb9781and1eb6fff1.Why
When the camera turns, or Link walks, the shadows of thin things near Link (grass, the drawbridge's chains, fences)
shimmer and crawl. The game refits the sun's nearest shadow map to the view every frame: the map turns with the camera
and its box slides and changes size by fractions of a shadow-map pixel, so each frame the thin shadows land on
different pixels. On Shadow resolution Highest (3072), this PR keeps that map still under the scene while the camera
moves, and the menu says so.
A still map has to cover the view whichever way the camera turns, so it is a little coarser standing still than the
game's own fit at the same size. On Highest that leaves the nearest shadows about as sharp as the game's fit on Game's
(1600); below Highest it would make them visibly softer, so there the game's fit is kept.
Searched the tracker: #601 (sun shadows on Link break depending on the camera angle, in Castle Town's market) may be
the same fit seen from another side, but nobody has checked it with this PR, so it is linked, not closed.
How to reproduce
Seen on: Windows 11, RTX 4090, NVIDIA 621.24
Settings and switches: defaults; Shadow resolution Highest (3072), as the Balanced, High and Ultra presets set it
Steps: Hyrule Field at the castle's drawbridge as a child, by day; stand still and turn the camera slowly with the
right stick, looking at the chains' and the grass's shadows a few metres ahead
Expected: the shadows stay put on the ground while the view turns
Actual: their edges shimmer and crawl as the camera turns, and flicker when Link walks
What changed
On Shadow resolution Highest, for the sun's nearest shadow cascade only, the light's basis no longer follows the
camera and the box around the view is a square of steady size placed on whole shadow-map pixels, so a point fixed in
the world stays in its shadow-map pixel while the camera turns or Link walks. Below Highest the game's fit is kept; the
other cascades, the other lights and the game's depth range are untouched. The menu's Shadow resolution row says that
Highest also makes the shadows shimmer less.
nx2/oot_shadow_fit.pyhooks two sites of the cascade fit, checking the build id and the instruction wordsat each (and at the box fit's other mode, which rejoins the second site): after the light basis is built
(main+0x713620's caller, x19 the cascade), and in the box fit (main+0x713be0) where its two modes meet, before the
projection is made from the slice's bounds. The box fit runs from main+0x712b9c while playing, so the second hook
acts on its own: it needs nothing from the first hook's call.
host/oot_shadow_fit.c.game's. A change of size (the menu's row applies at once) starts the steady fit afresh, and the log says which fit
runs at that size.
HOS_SHADOW_STABLE=0keeps the game's fit at every size,=1the steady fit at every size.hos_shadow_fit_basis(): row 2 stays the game's sun direction (and its origin, so the depth range is the game's).Rows 0 and 1 are carried from fit to fit by the smallest turn that follows the sun: rows rebuilt from the world's
up each fit spin about the sun as its bearing changes, and slid the map under the scene faster than the game's own
fit while Link stood still. When the sun turns more than a degree between two fits, the rows start again from the
world's up. They measure from an anchor kept at the middle of the slice's depth along the view, which moves by
whole texels along rows 0 and 1.
hos_shadow_fit_box(): only on a cascade whose rows 0 and 1 are exactly the ones the basis hook last stored. Thebox becomes a square holding the slice's bounding sphere and the game's own box, in steps of 0.5 m; it grows at
once and shrinks only after 90 fits that could use one a fifth smaller, and its centre sits on whole texels.
gpu/vk/vk_menu.cpp, Shadow resolution's row: Highest's line adds "and the shadows near you shimmer less whilethe camera moves", and the row's text "Highest also makes them shimmer less while the camera moves." In the 13
languages of
gpu/vk/menu_i18n.h.docs/development.md; the README's Shadow resolution row and its paragraph; nine measured runs indocs/tested/.tests/test_oot_shadow_fit.pychecks the install against the build id and every instruction word, and runsboth hooks' real C on a fixture of the game's cascade: standing still, turning the camera, walking, a moving sun (no
spin about it, a point at the anchor keeps its texel) and a sun that jumps 5 degrees (the rows start level again)
with
HOS_SHADOW_STABLE=1; with the switch unset, that nothing is written at 1600 and the steady square comes at3072; with
=0, that nothing is written.Measurements
Verdict: on Highest (3072) it helps. Turning the camera, the share of samples whose shadow-map position jumps by
more than a tenth of a pixel from one sample to the next drops from 44 to 49 % to 4 to 6 % at 32:9 (r7, r3) and from
49 % to 14 to 15 % at 16:9 (r5, r6); walking, from 19 to 21 % to 0 %. What is left while turning comes from the sun
moving across the sky, not from the camera: turning again once the day had stopped gives no move at all (r6).
Standing still while the sun moves, the steady fit is not better: its jumps over a tenth of a pixel are 4 to 9 %
against 1 to 4 %. Below Highest nothing changes: on Game's (1600) the default build measures the game's fit (r8). The
cost is sharpness standing still, below the table.
Hardware: Windows 11 Home 10.0.26300, RTX 4090 24 GB, NVIDIA 621.24, Core i9-13900K, 64 GB RAM.
Scene: Hyrule Field at the castle's drawbridge as a child, by day. DLSS on, preset K; frame generation Off; V-Sync Auto
(mailbox); Quality preset Custom. 32:9: output 2560x1440 with ultrawide on, presented 5120x1440 SDR, DLSS 2560x720 to
5120x1440. 16:9: ultrawide off, swapchain 1280x720, DLSS 1280x720 to 2560x1440.
Method: starts of the same save, in pairs, one with
HOS_SHADOW_STABLE=0(the game's fit) and one with the steady fit:=0; r8, Game's (1600) at 32:9, the defaultalone, to show the game's fit is kept there.
=1now, its fit'scode unchanged since): r3, Highest at 32:9; r5 and r6, Highest at 16:9; r1, r2 and r4 at Game's (1600), below.
In each start, 80 samples about 217 ms apart standing still, then turning the camera (right stick held), then walking
(left stick held), reading cascade 0's light basis and box from guest memory (
HOS_CONTROL_MEM=ro). For 35 worldpoints spread 2 to 14 m ahead and up to 8 m to each side, the move in shadow-map pixels along each axis from one sample
to the next, whole pixels left out (a whole-pixel step moves no edge). "Jumps" is the change of that move from one
sample to the next, what shows as shimmer; "slide" is the move itself.
Baseline: the game's fit is this PR's build with
HOS_SHADOW_STABLE=0, where the hooks write nothing: main's fit(
tests/test_oot_shadow_fit.pychecks the switch).How to reproduce: a pack of this branch over a base, the save at the drawbridge,
HOS_CONTROL_MEM=ro; the readout is alocal script that reads the cascade (MAIN+0x4e3e258 down to cascade 0's +0x158 basis and +0x268 projection), not part of
this PR.
=0in r1 and r2 belowThe jumps left while turning are about 1 mm in the world (90th percentile times the pixel size: 0.9 mm at 32:9 and at
16:9), from the sun's own movement.
The cost, and why only on Highest. The square holds the whole slice in any direction the camera turns, so standing
still it is coarser than the game's box:
close to the game's fit on Game's 1600 (about 2.0 x 1.1 cm). The still screenshots below show the chains' shadow
about as sharp as on Game's with the game's fit.
along one axis, visibly softer standing still. The motion gain there was as large (r1 and r2 at 162faed7, with the
steady fit at 1600: turning 42 and 44 % against 2 and 0 %, walking 18 and 19 % against 0 %), but not worth that
softness by default, so below Highest the game's fit is kept;
HOS_SHADOW_STABLE=1still gives it at every size.44 % against 2 %), close to the game's. The rule goes by the map's size alone, so 16:9 on Game's keeps the game's fit
too.
162faed7 is this branch's steady fit before its commits were squashed and before the Highest-only rule: its fit's code
is the same as this PR's (0f0d2155 adds only the rule that chooses it, the menu's lines and the docs).
Flags and settings
One new switch; the default chooses by Shadow resolution.
HOS_SHADOW_STABLE1the steady fit at every size;0the game's at every size0to compare or if a scene's shadows go wrong;1to try it below HighestTesting
Tested:
python -I -B tests/run_all.pyon Windows 11 (llvm-mingw) at this PR's head on main69ff5beb(1eb6fff1): 501passed, 21 failed, 74 skipped. The 21st, mesh_args_cleanup, hit its 10 s limit once while #773's suite ran beside it,
and passes run alone (three times out of three). The other 20 also fail at #773's rebased head (
ab099100) on this PC,and failed the same before the rebase (4f0a1707: 501 passed, 20 failed), none in a file this PR touches: appimage_player_build,
bug_report, cpm_discovery, cpp_preflight, crash_dialog, env_on (main's
RX_VK_PRESENT_AFTER_SIGNAL), fpx_sticky,lift_cleanup_error, linux_preflight, make_game_cache, make_game_prepare, missing_nup, oot_state_tap,
options_budget_once, readback_late, spawn_emit, tested_configurations (a pipe check that gets CRLF on this PC, as on
main; its format checks pass with the nine new files), vk_pgo, walk_key, work_lock.
tests/test_oot_shadow_fit.pypasses; with the rule's size lowered to 1600 its Highest-only check fails, as it should.
host/oot_shadow_fit.ccompiles with-std=gnu11 -O2 -fPIC -Wall -Wextra -Werror, with tools.lock's clang 23 and with gcc 11, against the generated runtime headers;tests/test_oot_shadow_fit.pypasses with both. A whole Linux player build needs the game, so it was not run.cascade went from the game's box to a 38 m square and back each time, with the log's line at each change;
In every start DLSS's NGX started, with no device lost and no Vulkan error.
tests/test_oot_shadow_fit.pyfails when the rows are rebuilt from the world's up each fit (themoving-sun check), and passes with the carried rows.
Not tested:
it depends on the GPU maker.
standing still.
menu_i18n.h; only English was shown).Must be tested:
camera and walking, one start with
HOS_SHADOW_STABLE=0and one without, expecting the thin shadows a few metresahead to crawl and flicker in the first and stay put in the second.
Risks
What could break:
the game draws should fall outside it; the fixture checks that while turning and walking.
pixel against about 1.05 x 0.58 cm at 32:9), about as sharp as the game's fit on Game's (1600).
Turning it off:
HOS_SHADOW_STABLE=0, or a Shadow resolution below Highest.Compatibility: the game is translated again at the next build, as for any lifter change. No change to saves,
longshot_options.ini, the shader or pipeline caches, orMKWVK_API_VERSION. Two menu strings change, in English andin the 13 translations. The
gpu/vk/vk_api.hlines in this PR's diff are #773's (an option number, no change toMkwVkHostorMkwVkRenderer); this PR's own commits don't touch it.Screenshots
The drawbridge's chains standing still at 32:9, the sun at the same direction in each, left to right:
Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0(main's fit) on Highest;After: this PR @ 0f0d2155on Highest (the steady fit);this PR @ 0f0d2155 on Game's (1600), the game's fit, kept below Highest. On Highest the steady fit is a little softer
standing still than the game's own fit there, about as sharp as the game's fit on Game's. A still can't show the
shimmer, which needs motion.
The menu's Shadow resolution row on Highest, this PR @ 0f0d2155, with its new lines:
The first screenshot comment's pair was taken while the sun was still moving and the upscaler settling, so it showed
more difference than there is; these pictures replace it.
Issues
See #601
Checklist
Everyone
type(scope): description) of the status line's type.main(69ff5beb).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).mainand passing here.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
Screenshots of the same spot: Hyrule Field at the castle's drawbridge as a child, by day, standing still. Windows 11, RTX 4090, NVIDIA 621.24; 32:9 (2560x720 to 5120x1440 with DLSS, preset K), Shadow resolution Game's (1600). The captures are scaled to 1280x720.
Before, HOS_SHADOW_STABLE=0: this PR @ 162faed7
After: this PR @ 162faed7
The same shadows, the chains' included; the steady fit's are a little coarser standing still (2.4 cm a shadow-map pixel against about 2.0 x 1.1 cm, see Measurements). Stills can't show the shimmer, which needs the camera or Link moving: that is checked in Measurements, and by eye under Must be tested.
🤖 Generated with Claude Code
Linked the screenshots comment from the description.
New screenshots at this PR's 0f0d2155 (now
4bbb9781after the rebase on main, the same commit), replacing the first comment's pair: that pair was taken while the sun was still moving and the upscaler had not settled, so it showed more difference than there is.The drawbridge's chains at 32:9, standing still, the sun at the same direction in each, left to right:
Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0(main's fit), Shadow resolution HighestAfter: this PR @ 0f0d2155, Shadow resolution Highest (the steady fit)This PR @ 0f0d2155, Shadow resolution Game's (1600): the game's fit, kept below HighestOn Highest the steady fit is a little softer standing still than the game's own fit, about as sharp as the game's fit on Game's (1600). The shimmer itself needs motion and doesn't show in a still.
The menu's Shadow resolution row on Highest, with its new lines:
🤖 Generated with Claude Code
66def1fcfc1eb6fff131WIP: fix(oot): the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link stop shimmeringto WIP: fix(oot): on Shadow resolution Highest, the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link shimmer lessThe steady fit now runs on Shadow resolution Highest (3072) only, and the menu says so (0f0d2155, measured in 4f0a1707).
Why: standing still, the steady square is coarser than the game's box. On Highest that leaves the nearest shadows about as sharp as the game's fit on Game's (1600); on Game's at 32:9 it made them up to twice as coarse along one axis. Below Highest the game's fit is kept;
HOS_SHADOW_STABLE=1still gives the steady fit at every size,=0never.docs/development.mdupdated.=0: turning 4 % against 49 %, walking 0 % against 21 %), r8 (Game's 1600: the game's fit is kept), r9 (Shadow resolution changed while playing: the fit follows each change).tests/test_oot_shadow_fit.pychecks the rule: nothing written at 1600 with the switch unset, the steady square at 3072. The Linux compiles (gcc 11, clang 23) rerun at 4f0a1707.69ff5beb, with no change to the commits (0f0d2155 and 4f0a1707 are now4bbb9781and1eb6fff1); run_all rerun at the new head. The new screenshots are in the description. Still WIP: a by-eye check of the shimmer in motion is left.🤖 Generated with Claude Code
1eb6fff13188e561c1b3Rebased onto
main@bbe6fdf2. No tests were run here; the test boxes stay yours. No conflicts.WIP: fix(oot): on Shadow resolution Highest, the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link shimmer lessto fix(oot): on Shadow resolution Highest, the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link shimmer lessRead the diff of host/oot_shadow_fit.c, host/oot_shadow.c, nx2/oot_shadow*.py, the menu/preset/options-file changes and the tests; code only, nothing run. I found nothing that must change before merge, so this is a comment, not an approval, for these reasons:
Otherwise the logic reads right: the box hook only acts when the basis rows are bit-for-bit the ones the basis hook stored, so a game-made basis keeps the game's box; the sun-overhead and bad-input cases fall back to the game's fit; a size change restarts the fit; options files from before the row follow the preset they came from.
@ -0,0 +45,4 @@#define FIT_MIN_SIZE 3072 /* Shadow resolution Highest's map: the smallest the steady fit runs at by default *//* Kept between cascade 0's fits. The trace shows one box fit of cascade 0 after another, never two at once. */static double held; /* the box's half size kept, in the light's units (metres); 0 before the first fit */These statics are read and written by both hooks without a lock or atomics, on whichever worker thread the game fits cascade 0 from. The header relies on a trace showing no two fits at once; if the game ever fits it from two threads (a second sun view, a threaded-rendering path) the carried rows and the held size would tear. A small mutex around each hook's body would make the assumption safe at little cost; at least say in the comment that nothing enforces it.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.