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

Open
Jiro113 wants to merge 8 commits from Jiro113/Longshot:fix/sun-cascade-stable-fit into main
First-time contributor

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 69ff5beb with no change to their commits (git range-diff): this PR's 0f0d2155 and
4f0a1707, named below where they were measured and tested, are now 4bbb9781 and 1eb6fff1.

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.

  • lifter: nx2/oot_shadow_fit.py hooks two sites of the cascade fit, checking the build id and the instruction words
    at 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: host/oot_shadow_fit.c.
    • Which fit runs: the cascade's map size, read each fit, decides. At 3072 (Highest) the steady fit; below, the
      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=0 keeps the game's fit at every size, =1 the 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. The
      box 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.
  • menu: gpu/vk/vk_menu.cpp, Shadow resolution's row: Highest's line adds "and the shadows near you shimmer less while
    the 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: the switch in docs/development.md; the README's Shadow resolution row and its paragraph; nine measured runs in
    docs/tested/.
  • tests: tests/test_oot_shadow_fit.py checks the install against the build id and every instruction word, and runs
    both 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 at
    3072; 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:

  • this PR as it is (0f0d2155): r7, Highest at 32:9, the default then =0; r8, Game's (1600) at 32:9, the default
    alone, to show the game's fit is kept there.
  • the steady fit before the Highest-only rule (162faed7, the steady fit at every size: the same as =1 now, its fit's
    code 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 world
points 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.py checks 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 a
local script that reads the cascade (MAIN+0x4e3e258 down to cascade 0's +0x158 basis and +0x268 projection), not part of
this PR.

Cascade 0 on Highest (3072), 80 samples a phase, 35 points Before, HOS_SHADOW_STABLE=0: this PR @ 0f0d2155 (r7), @ 162faed7 (r3, r5) After: this PR @ 0f0d2155 (r7); the steady fit @ 162faed7 (r3, r5, r6)
32:9, r7 / r3
Turning: samples that jump > 0.1 pixel 49 % / 44 % 4 % / 6 %
Turning: jump, 90th percentile 0.417 / 0.405 pixel 0.074 / 0.074 pixel
Walking (the sun stopped): samples that jump > 0.1 pixel 21 % / 20 % 0 % / 0 %
Standing still, the sun moving: slide a sample, median 0.244 / 0.252 pixel 0.182 / 0.181 pixel
Standing still: jumps > 0.1 pixel (90th percentile) 4 % (0.048) / 4 % (0.045) 6 % (0.064) / 4 % (0.046)
The cascade's box 32.3 x 17.9 m standing, changing every sample while turning 38.0 m square throughout
16:9, r5 (r6: the steady fit only)
Turning: samples that jump > 0.1 pixel (90th percentile) 49 % (0.413 pixel) 15 % (0.126 pixel); r6 14 % (0.122 pixel)
Turning again, the sun stopped (r6) not measured 0 % (no move at all)
Walking: samples that jump > 0.1 pixel 19 % 0 %
Standing still: slide a sample, median; jumps > 0.1 pixel (90th percentile) 0.215 pixel; 1 % (0.084 pixel) 0.238 pixel; 9 % (0.092 pixel)
The cascade's box about 19.8 x 14.4 m standing, changing every sample while turning 21.0 m square (the log showed 14.0 m first)
Game's (1600), 32:9, r8: this PR's default the game's fit: turning 45 % (0.407 pixel), walking 9 %, the box about 32.7 x 17.9 m and changing every sample while turning, as with =0 in r1 and r2 below

The 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.

m3on-fstill    slide median 0.182 p90 0.402 | jitter median 0.004 p90 0.064, >0.1 in 6%
m3on-fturn     slide median 0.193 p90 0.405 | jitter median 0.014 p90 0.074, >0.1 in 4%
m3on-fwalk     slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0%
m3off-fstill   slide median 0.244 p90 0.434 | jitter median 0.006 p90 0.048, >0.1 in 4%
m3off-fturn    slide median 0.217 p90 0.440 | jitter median 0.094 p90 0.417, >0.1 in 49%
m3off-fwalk    slide median 0.000 p90 0.287 | jitter median 0.000 p90 0.326, >0.1 in 21%
m16def-fstill  slide median 0.143 p90 0.294 | jitter median 0.002 p90 0.017, >0.1 in 2%
m16def-fturn   slide median 0.194 p90 0.440 | jitter median 0.068 p90 0.407, >0.1 in 45%
m16def-fwalk   slide median 0.000 p90 0.172 | jitter median 0.000 p90 0.074, >0.1 in 9%
s3on-fstill    slide median 0.181 p90 0.401 | jitter median 0.001 p90 0.046, >0.1 in 4%
s3on-fturn     slide median 0.195 p90 0.411 | jitter median 0.013 p90 0.074, >0.1 in 6%
s3on-fwalk     slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0%
s3off-fstill   slide median 0.252 p90 0.436 | jitter median 0.007 p90 0.045, >0.1 in 4%
s3off-fturn    slide median 0.240 p90 0.448 | jitter median 0.054 p90 0.405, >0.1 in 44%
s3off-fwalk    slide median 0.000 p90 0.358 | jitter median 0.000 p90 0.299, >0.1 in 20%
w3on-fstill    slide median 0.238 p90 0.449 | jitter median 0.002 p90 0.092, >0.1 in 9%
w3on-fturn     slide median 0.247 p90 0.454 | jitter median 0.023 p90 0.126, >0.1 in 15%
w3on-fwalk     slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0%
w3off-fstill   slide median 0.215 p90 0.448 | jitter median 0.005 p90 0.084, >0.1 in 1%
w3off-fturn    slide median 0.227 p90 0.444 | jitter median 0.097 p90 0.413, >0.1 in 49%
w3off-fwalk    slide median 0.000 p90 0.378 | jitter median 0.000 p90 0.290, >0.1 in 19%
w3on2-fturn    slide median 0.267 p90 0.459 | jitter median 0.024 p90 0.122, >0.1 in 14%
w3on2-fturn2   slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0%
[hos ModuleSystemWorker3] shadow: the sun's nearest cascade is kept steady (3072 texels)                      (r7)
[hos ModuleSystemWorker2] shadow: the sun's nearest cascade is 38.0 m a side (3072 texels)                    (r7)
[hos ModuleSystemWorker3] shadow: the sun's nearest cascade keeps the game's fit below Shadow resolution Highest (1600 texels)   (r8)

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:

  • On Highest at 32:9, 38 m on 3072 pixels is 1.24 cm a pixel, where the game's fit at 3072 is about 1.05 x 0.58 cm:
    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.
  • On Game's (1600) at 32:9 it would be 2.4 cm a pixel against about 2.0 x 1.1 cm: thin shadows up to twice as coarse
    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=1 still gives it at every size.
  • At 16:9 the view is narrower: at 1600 the square was 21 m, 1.31 cm a pixel against about 1.24 x 0.90 cm (r4, turning
    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.

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
HOS_SHADOW_STABLE which fit the sun's nearest shadow cascade gets: unset, the steady fit on Highest (3072) and the game's below; 1 the steady fit at every size; 0 the game's at every size none: the game's fit unset: steady on Highest only Shadows near Link stay put while the camera turns or Link walks; the square is coarser than the game's box standing still (see Measurements) Helps on Highest: Hyrule Field, RTX 4090, 32:9 and 16:9. Below Highest it helps motion as much but costs sharpness at 32:9 (r1, r2). Untested: other areas, Linux, other makers Unset for everyone; 0 to compare or if a scene's shadows go wrong; 1 to try it below Highest
Shadow resolution (menu row, #773) its Highest value now also chooses the steady fit — Highest's line and the row's text say so — — as #773's presets set it

Testing

Tested:

  • python -I -B tests/run_all.py on Windows 11 (llvm-mingw) at this PR's head on main 69ff5beb (1eb6fff1): 501
    passed, 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.py
    passes; with the rule's size lowered to 1600 its Highest-only check fails, as it should.
  • Linux (Ubuntu 22.04 in WSL 2, at 4f0a1707): host/oot_shadow_fit.c compiles 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.py passes with both. A whole Linux player build needs the game, so it was not run.
  • In the game on Windows 11, RTX 4090, NVIDIA 621.24, Hyrule Field at the drawbridge, a pack of 0f0d2155:
    • r7 and r8 above;
    • r9: Shadow resolution changed while playing (Game's, Highest, Game's, Highest, through the control channel): the
      cascade went from the game's box to a 38 m square and back each time, with the log's line at each change;
    • still screenshots of the same spot with the sun at the same direction (below);
    • the menu's Shadow resolution row on Highest, with its new lines (below).
      In every start DLSS's NGX started, with no device lost and no Vulkan error.
  • Earlier, at 162faed7 (the steady fit at every size): r1 to r6 above.
  • The fixture in tests/test_oot_shadow_fit.py fails when the rows are rebuilt from the world's up each fit (the
    moving-sun check), and passes with the carried rows.

Not tested:

  • Walking while the sun moves: in every start the day stopped before the walk.
  • Other areas (dungeons, Castle Town, Kokiri Forest), the night, Link as an adult.
  • A run in the game on Linux, AMD, Intel or a Steam Deck. The change runs on the CPU in the lifted game, so nothing in
    it depends on the GPU maker.
  • By eye, the shimmer while turning and walking: the numbers above come from guest memory; the screenshots were taken
    standing still.
  • The menu's new lines in the other 12 languages on screen (the strings are in menu_i18n.h; only English was shown).
  • Whether it changes #601.

Must be tested:

  • By eye, on this PC before it leaves WIP (by hand): Hyrule Field at the drawbridge by day on Highest, turning the
    camera and walking, one start with HOS_SHADOW_STABLE=0 and one without, expecting the thin shadows a few metres
    ahead to crawl and flicker in the first and stay put in the second.

Risks

What could break:

  • A scene where the steady square misses part of the view: it is made to hold the game's own box each fit, so nothing
    the game draws should fall outside it; the fixture checks that while turning and walking.
  • Sharpness on Highest: standing still, the nearest shadows are coarser than the game's own fit at 3072 (1.24 cm a
    pixel against about 1.05 x 0.58 cm at 32:9), about as sharp as the game's fit on Game's (1600).
  • Changing Shadow resolution while playing: the steady fit starts afresh at each size change (r9 checks it).
  • A changed game build: the hooks check the build id and the instruction words and install nothing when they differ.

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, or MKWVK_API_VERSION. Two menu strings change, in English and
in the 13 translations. The gpu/vk/vk_api.h lines in this PR's diff are #773's (an option number, no change to
MkwVkHost or MkwVkRenderer); 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 @ 0f0d2155 on 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.

Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0, Highest / After: this PR @ 0f0d2155, Highest / this PR @ 0f0d2155, Game's 1600

The menu's Shadow resolution row on Highest, this PR @ 0f0d2155, with its new lines:

The Shadow resolution row on Highest, this PR @ 0f0d2155

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

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main (69ff5beb).
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named).
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • Screenshots of the same spot, captioned like the Measurements columns, for a change that shows on screen.
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md).
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • Docs updated.

🤖 Generated with Claude Code

**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 69ff5beb with no change to their commits (`git range-diff`): this PR's 0f0d2155 and 4f0a1707, named below where they were measured and tested, are now 4bbb9781 and 1eb6fff1. ## 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. - lifter: `nx2/oot_shadow_fit.py` hooks two sites of the cascade fit, checking the build id and the instruction words at 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: `host/oot_shadow_fit.c`. - Which fit runs: the cascade's map size, read each fit, decides. At 3072 (Highest) the steady fit; below, the 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=0` keeps the game's fit at every size, `=1` the 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. The box 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. - menu: `gpu/vk/vk_menu.cpp`, Shadow resolution's row: Highest's line adds "and the shadows near you shimmer less while the 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: the switch in `docs/development.md`; the README's Shadow resolution row and its paragraph; nine measured runs in `docs/tested/`. - tests: `tests/test_oot_shadow_fit.py` checks the install against the build id and every instruction word, and runs both 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 at 3072; 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: - this PR as it is (0f0d2155): r7, Highest at 32:9, the default then `=0`; r8, Game's (1600) at 32:9, the default alone, to show the game's fit is kept there. - the steady fit before the Highest-only rule (162faed7, the steady fit at every size: the same as `=1` now, its fit's code 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 world points 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.py` checks 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 a local script that reads the cascade (MAIN+0x4e3e258 down to cascade 0's +0x158 basis and +0x268 projection), not part of this PR. | Cascade 0 on Highest (3072), 80 samples a phase, 35 points | Before, HOS_SHADOW_STABLE=0: this PR @ 0f0d2155 (r7), @ 162faed7 (r3, r5) | After: this PR @ 0f0d2155 (r7); the steady fit @ 162faed7 (r3, r5, r6) | |---|---|---| | **32:9, r7 / r3** | | | | Turning: samples that jump > 0.1 pixel | 49 % / 44 % | 4 % / 6 % | | Turning: jump, 90th percentile | 0.417 / 0.405 pixel | 0.074 / 0.074 pixel | | Walking (the sun stopped): samples that jump > 0.1 pixel | 21 % / 20 % | 0 % / 0 % | | Standing still, the sun moving: slide a sample, median | 0.244 / 0.252 pixel | 0.182 / 0.181 pixel | | Standing still: jumps > 0.1 pixel (90th percentile) | 4 % (0.048) / 4 % (0.045) | 6 % (0.064) / 4 % (0.046) | | The cascade's box | 32.3 x 17.9 m standing, changing every sample while turning | 38.0 m square throughout | | **16:9, r5 (r6: the steady fit only)** | | | | Turning: samples that jump > 0.1 pixel (90th percentile) | 49 % (0.413 pixel) | 15 % (0.126 pixel); r6 14 % (0.122 pixel) | | Turning again, the sun stopped (r6) | not measured | 0 % (no move at all) | | Walking: samples that jump > 0.1 pixel | 19 % | 0 % | | Standing still: slide a sample, median; jumps > 0.1 pixel (90th percentile) | 0.215 pixel; 1 % (0.084 pixel) | 0.238 pixel; 9 % (0.092 pixel) | | The cascade's box | about 19.8 x 14.4 m standing, changing every sample while turning | 21.0 m square (the log showed 14.0 m first) | | **Game's (1600), 32:9, r8: this PR's default** | | the game's fit: turning 45 % (0.407 pixel), walking 9 %, the box about 32.7 x 17.9 m and changing every sample while turning, as with `=0` in r1 and r2 below | The 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. ``` m3on-fstill slide median 0.182 p90 0.402 | jitter median 0.004 p90 0.064, >0.1 in 6% m3on-fturn slide median 0.193 p90 0.405 | jitter median 0.014 p90 0.074, >0.1 in 4% m3on-fwalk slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0% m3off-fstill slide median 0.244 p90 0.434 | jitter median 0.006 p90 0.048, >0.1 in 4% m3off-fturn slide median 0.217 p90 0.440 | jitter median 0.094 p90 0.417, >0.1 in 49% m3off-fwalk slide median 0.000 p90 0.287 | jitter median 0.000 p90 0.326, >0.1 in 21% m16def-fstill slide median 0.143 p90 0.294 | jitter median 0.002 p90 0.017, >0.1 in 2% m16def-fturn slide median 0.194 p90 0.440 | jitter median 0.068 p90 0.407, >0.1 in 45% m16def-fwalk slide median 0.000 p90 0.172 | jitter median 0.000 p90 0.074, >0.1 in 9% s3on-fstill slide median 0.181 p90 0.401 | jitter median 0.001 p90 0.046, >0.1 in 4% s3on-fturn slide median 0.195 p90 0.411 | jitter median 0.013 p90 0.074, >0.1 in 6% s3on-fwalk slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0% s3off-fstill slide median 0.252 p90 0.436 | jitter median 0.007 p90 0.045, >0.1 in 4% s3off-fturn slide median 0.240 p90 0.448 | jitter median 0.054 p90 0.405, >0.1 in 44% s3off-fwalk slide median 0.000 p90 0.358 | jitter median 0.000 p90 0.299, >0.1 in 20% w3on-fstill slide median 0.238 p90 0.449 | jitter median 0.002 p90 0.092, >0.1 in 9% w3on-fturn slide median 0.247 p90 0.454 | jitter median 0.023 p90 0.126, >0.1 in 15% w3on-fwalk slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0% w3off-fstill slide median 0.215 p90 0.448 | jitter median 0.005 p90 0.084, >0.1 in 1% w3off-fturn slide median 0.227 p90 0.444 | jitter median 0.097 p90 0.413, >0.1 in 49% w3off-fwalk slide median 0.000 p90 0.378 | jitter median 0.000 p90 0.290, >0.1 in 19% w3on2-fturn slide median 0.267 p90 0.459 | jitter median 0.024 p90 0.122, >0.1 in 14% w3on2-fturn2 slide median 0.000 p90 0.000 | jitter median 0.000 p90 0.000, >0.1 in 0% [hos ModuleSystemWorker3] shadow: the sun's nearest cascade is kept steady (3072 texels) (r7) [hos ModuleSystemWorker2] shadow: the sun's nearest cascade is 38.0 m a side (3072 texels) (r7) [hos ModuleSystemWorker3] shadow: the sun's nearest cascade keeps the game's fit below Shadow resolution Highest (1600 texels) (r8) ``` **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: - On Highest at 32:9, 38 m on 3072 pixels is 1.24 cm a pixel, where the game's fit at 3072 is about 1.05 x 0.58 cm: 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. - On Game's (1600) at 32:9 it would be 2.4 cm a pixel against about 2.0 x 1.1 cm: thin shadows up to twice as coarse 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=1` still gives it at every size. - At 16:9 the view is narrower: at 1600 the square was 21 m, 1.31 cm a pixel against about 1.24 x 0.90 cm (r4, turning 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. | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `HOS_SHADOW_STABLE` | which fit the sun's nearest shadow cascade gets: unset, the steady fit on Highest (3072) and the game's below; `1` the steady fit at every size; `0` the game's at every size | none: the game's fit | unset: steady on Highest only | Shadows near Link stay put while the camera turns or Link walks; the square is coarser than the game's box standing still (see Measurements) | Helps on Highest: Hyrule Field, RTX 4090, 32:9 and 16:9. Below Highest it helps motion as much but costs sharpness at 32:9 (r1, r2). Untested: other areas, Linux, other makers | Unset for everyone; `0` to compare or if a scene's shadows go wrong; `1` to try it below Highest | | Shadow resolution (menu row, #773) | its Highest value now also chooses the steady fit | — | Highest's line and the row's text say so | — | — | as #773's presets set it | ## Testing **Tested:** - `python -I -B tests/run_all.py` on Windows 11 (llvm-mingw) at this PR's head on main 69ff5beb (1eb6fff1): 501 passed, 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.py` passes; with the rule's size lowered to 1600 its Highest-only check fails, as it should. - Linux (Ubuntu 22.04 in WSL 2, at 4f0a1707): `host/oot_shadow_fit.c` compiles 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.py` passes with both. A whole Linux player build needs the game, so it was not run. - In the game on Windows 11, RTX 4090, NVIDIA 621.24, Hyrule Field at the drawbridge, a pack of 0f0d2155: - r7 and r8 above; - r9: Shadow resolution changed while playing (Game's, Highest, Game's, Highest, through the control channel): the cascade went from the game's box to a 38 m square and back each time, with the log's line at each change; - still screenshots of the same spot with the sun at the same direction (below); - the menu's Shadow resolution row on Highest, with its new lines (below). In every start DLSS's NGX started, with no device lost and no Vulkan error. - Earlier, at 162faed7 (the steady fit at every size): r1 to r6 above. - The fixture in `tests/test_oot_shadow_fit.py` fails when the rows are rebuilt from the world's up each fit (the moving-sun check), and passes with the carried rows. **Not tested:** - Walking while the sun moves: in every start the day stopped before the walk. - Other areas (dungeons, Castle Town, Kokiri Forest), the night, Link as an adult. - A run in the game on Linux, AMD, Intel or a Steam Deck. The change runs on the CPU in the lifted game, so nothing in it depends on the GPU maker. - By eye, the shimmer while turning and walking: the numbers above come from guest memory; the screenshots were taken standing still. - The menu's new lines in the other 12 languages on screen (the strings are in `menu_i18n.h`; only English was shown). - Whether it changes #601. **Must be tested:** - By eye, on this PC before it leaves WIP (by hand): Hyrule Field at the drawbridge by day on Highest, turning the camera and walking, one start with `HOS_SHADOW_STABLE=0` and one without, expecting the thin shadows a few metres ahead to crawl and flicker in the first and stay put in the second. ## Risks **What could break:** - A scene where the steady square misses part of the view: it is made to hold the game's own box each fit, so nothing the game draws should fall outside it; the fixture checks that while turning and walking. - Sharpness on Highest: standing still, the nearest shadows are coarser than the game's own fit at 3072 (1.24 cm a pixel against about 1.05 x 0.58 cm at 32:9), about as sharp as the game's fit on Game's (1600). - Changing Shadow resolution while playing: the steady fit starts afresh at each size change (r9 checks it). - A changed game build: the hooks check the build id and the instruction words and install nothing when they differ. **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, or `MKWVK_API_VERSION`. Two menu strings change, in English and in the 13 translations. The `gpu/vk/vk_api.h` lines in this PR's diff are #773's (an option number, no change to `MkwVkHost` or `MkwVkRenderer`); 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 @ 0f0d2155` on 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. ![Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0, Highest / After: this PR @ 0f0d2155, Highest / this PR @ 0f0d2155, Game's 1600](https://git.magicbean.dev/attachments/a0f0a97c-d0ce-42c1-aac9-48d834e3467e) The menu's Shadow resolution row on Highest, this PR @ 0f0d2155, with its new lines: ![The Shadow resolution row on Highest, this PR @ 0f0d2155](https://git.magicbean.dev/attachments/dd793098-d646-4a8e-bb4d-06d1322acccf) 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](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/836#issuecomment-26815) replace it. ## Issues See #601 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main` (69ff5beb). - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] Screenshots of the same spot, captioned like the Measurements columns, for a change that shows on screen. - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`). - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The sun's shadow maps are 1600 pixels a side in the game's normal path, so
the shadows of chains, fences and leaves near Link come out stair-stepped.
The game has larger sizes in its own table (2200, 3072); the new Graphics
row (Detail) raises the size the game rounds each frame to one of them:
Game's (1600), High (2200) or Highest (3072). It is read every frame, so a
change applies at once, and it only ever raises the game's own size.

The hook (nx2/oot_shadow.py) checks the build and the five instructions it
follows, and is left out otherwise. The default is Game's until measured.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Machine-made like the rest of menu_i18n.h, to be checked by speakers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minimum and Low keep the game's 1600 (the 4 and 6 GB cards they are for),
Balanced, High and Ultra choose Highest. An options file from before the
row follows the preset its settings came from (memory_budget), Auto's tier
once the GPU is known; the bug report counts the row among the defaults
that depend on the PC. The run measured on an RTX 4090 is in docs/tested.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(options): review: Shadow resolution's old-file reading runs once, the docs say what was computed
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
608cd6abd5
The flag for an options file from before the row is cleared once Auto has
used it, so a later call can't overwrite what the player chose; the option
id sits in order in vk_api.h; the README's preset paragraph is re-flowed and
says the video memory figures come from the maps' size; the tested run says
what the pass timing and the picture showed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game refits its sun cascades every frame: a light basis whose sideways axis follows the camera (main+0x713620),
then the tightest box around the cascade's slice of the view (main+0x713be0, which makes the frame's box from
main+0x712b9c while playing). Turning the camera turns the shadow map under the scene, and walking slides and resizes
the box by fractions of a texel, so the shadows of grass, chains and fences near Link shimmer and crawl.

For the sun's cascade 0 only (host/oot_shadow_fit.c, nx2/oot_shadow_fit.py):
- the basis no longer follows the camera: row 2 stays the game's sun direction, 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 would spin about the sun as
  its bearing changes and slide the map twice as fast as the game's own fit while Link stands still), restarting from
  the world's up when the sun jumps more than a degree;
- they measure from an anchor kept at the middle of the slice's depth along the view, moving by whole texels;
- the box, where the box fit's two modes meet, is a square around the slice's bounding sphere holding the game's box,
  of steady size (grows at once, shrinks after 90 fits), on whole texels. The box hook acts only on a cascade whose
  rows are the exact ones the basis hook last stored.

HOS_SHADOW_STABLE=0 keeps the game's fit. tests/test_oot_shadow_fit.py runs both sites' C on a fixture: still, turning,
walking, a moving sun and a sun that jumps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(tested): the steady sun cascade against the game's fit on an RTX 4090, at 1600 and 3072, 32:9 and 16:9
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
66def1fcfc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
First-time contributor

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

Before, HOS_SHADOW_STABLE=0: this PR @ 162faed7

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

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** ![Before, HOS_SHADOW_STABLE=0: this PR @ 162faed7](https://git.magicbean.dev/attachments/82461184-abaf-4106-a01f-4302e90c0270) **After: this PR @ 162faed7** ![After: this PR @ 162faed7](https://git.magicbean.dev/attachments/14dee2ce-ca8a-4232-93de-8381e9e64638) 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](https://claude.com/claude-code)
Author
First-time contributor

Linked the screenshots comment from the description.

Linked the screenshots comment from the description.
Author
First-time contributor

New screenshots at this PR's 0f0d2155 (now 4bbb9781 after 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:

  1. Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0 (main's fit), Shadow resolution Highest
  2. After: this PR @ 0f0d2155, Shadow resolution Highest (the steady fit)
  3. This PR @ 0f0d2155, Shadow resolution Game's (1600): the game's fit, kept below Highest

Before: HOS_SHADOW_STABLE=0, Highest / After, Highest / Game's 1600, this PR @ 0f0d2155

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

The Shadow resolution row on Highest, this PR @ 0f0d2155

🤖 Generated with Claude Code

New screenshots at this PR's 0f0d2155 (now 4bbb9781 after 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: 1. `Before: this PR @ 0f0d2155 with HOS_SHADOW_STABLE=0` (main's fit), Shadow resolution Highest 2. `After: this PR @ 0f0d2155`, Shadow resolution Highest (the steady fit) 3. `This PR @ 0f0d2155`, Shadow resolution Game's (1600): the game's fit, kept below Highest ![Before: HOS_SHADOW_STABLE=0, Highest / After, Highest / Game's 1600, this PR @ 0f0d2155](https://git.magicbean.dev/attachments/a0f0a97c-d0ce-42c1-aac9-48d834e3467e) On 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: ![The Shadow resolution row on Highest, this PR @ 0f0d2155](https://git.magicbean.dev/attachments/dd793098-d646-4a8e-bb4d-06d1322acccf) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Jiro113 force-pushed fix/sun-cascade-stable-fit from 66def1fcfc
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to 1eb6fff131
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
2026-11-15 17:54:05 +00:00
Compare
Jiro113 changed title from WIP: fix(oot): the sun's nearest shadow cascade no longer moves with the camera, so thin shadows near Link stop shimmering to 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 less 2026-11-15 17:54:44 +00:00
Author
First-time contributor

The 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=1 still gives the steady fit at every size, =0 never.

  • Highest's line and the row's text in the menu say the shadows near you shimmer less while the camera moves, in English and the 13 translations; README and docs/development.md updated.
  • New runs on an RTX 4090 at this head: r7 (Highest 32:9, default against =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.py checks 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.
  • The title, Why, Measurements, Flags and Testing are rewritten for Highest only. Rebased with #773 on main 69ff5beb, with no change to the commits (0f0d2155 and 4f0a1707 are now 4bbb9781 and 1eb6fff1); 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

The 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=1` still gives the steady fit at every size, `=0` never. - Highest's line and the row's text in the menu say the shadows near you shimmer less while the camera moves, in English and the 13 translations; README and `docs/development.md` updated. - New runs on an RTX 4090 at this head: r7 (Highest 32:9, default against `=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.py` checks 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. - The title, Why, Measurements, Flags and Testing are rewritten for Highest only. Rebased with #773 on main 69ff5beb, with no change to the commits (0f0d2155 and 4f0a1707 are now 4bbb9781 and 1eb6fff1); 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](https://claude.com/claude-code)
HumanBean force-pushed fix/sun-cascade-stable-fit from 1eb6fff131
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Guide screenshots / Guide images and local UI source review (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to 88e561c1b3
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 4s
Third-party parts / As their notes say (pull_request) Successful in 7s
2026-11-15 18:44:52 +00:00
Compare
Owner

Rebased onto main @ bbe6fdf2. No tests were run here; the test boxes stay yours. No conflicts.

Rebased onto `main` @ bbe6fdf2. No tests were run here; the test boxes stay yours. No conflicts.
Jiro113 changed title from 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 less to 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 2026-11-15 18:53:23 +00:00
HumanBean left a comment

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

  • It carries #773's four commits ("Depends on #773", still open). It should merge after #773 or be retargeted; reviewing the whole diff here means reviewing #773 too.
  • The description's own "Must be tested" is a by-eye check of the shimmer in motion, and every game run is on one machine (Windows, RTX 4090). This PR also makes Highest the default for every preset above Low (via #773), so the steady fit and the 3072 maps (about 113 MB) reach everyone by default. That is a reasonable default if it is right, but I would want a second GPU maker or the Test Farm to have seen it first.
  • The fit's state is plain statics shared by the basis hook and the box hook, which the comment says run on "whichever worker thread". It relies on the game never fitting cascade 0 twice at once (traced, not enforced). See the inline note.

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.

Read 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: - It carries #773's four commits ("Depends on #773", still open). It should merge after #773 or be retargeted; reviewing the whole diff here means reviewing #773 too. - The description's own "Must be tested" is a by-eye check of the shimmer in motion, and every game run is on one machine (Windows, RTX 4090). This PR also makes Highest the default for every preset above Low (via #773), so the steady fit and the 3072 maps (about 113 MB) reach everyone by default. That is a reasonable default if it is right, but I would want a second GPU maker or the Test Farm to have seen it first. - The fit's state is plain statics shared by the basis hook and the box hook, which the comment says run on "whichever worker thread". It relies on the game never fitting cascade 0 twice at once (traced, not enforced). See the inline note. 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 */
Owner

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.

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.
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 4s
Third-party parts / As their notes say (pull_request) Successful in 7s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/sun-cascade-stable-fit:Jiro113-fix/sun-cascade-stable-fit'
git switch 'Jiro113-fix/sun-cascade-stable-fit'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!836
No description provided.