fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left out #427

Merged
HumanBean merged 2 commits from mooki/Longshot:fix/shader-branch-past-end into main 2026-11-15 18:10:42 +00:00
Member

Type: fix · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks on arm64 and x86_64; a macOS game run with this PR's commits, run E9a) · Needs: Windows or Linux, NVIDIA GPU

Why

A shader program the game uses is left out on every platform: the shader reader refuses it before translating it.

This is for players on every platform and GPU. Compute shader program 79116b290d46542f is never translated, on any
renderer path. Its stage is refused before translation for "a branch outside the program". Two causes are fixed here:

  • a word after a program's end that only looks like a branch refused the whole program;
  • programs over 1 MiB (65,536 Ampere instructions) were refused outright. 79116b29 is one: its code is 0x104680 bytes.

The GPU has no instruction count of its own. The limit is now the largest code area the renderer reads, 16 MiB, and a
lowered branch beyond Maxwell's 8 MiB relative reach takes its absolute form.

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK: run 18, an integrated build of the macOS port with this PR's first
commit (its longshot_log.txt, below). The capture and the size limits are the same code on every OS and renderer path:
the changed code has no platform branch. Game-free: the new test against main 6c9edffe's files on this Mac.
Settings and switches: defaults.
Steps: start the game through the shader preparation and look for 79116b290d46542f in longshot_log.txt.
Game-free: from this branch's checkout, python -I -B tests/test_capture_past_end.py --cxx clang++ --source <a checkout of main>.
Expected: the program translated: the line for its key, vk: NVN2 program 79116b290d46542f (stage 5) …, with no
not translated line.
Actual: it is refused before translation for "a branch outside the program". Run 18's line:

vk: program 79116b290d46542f not translated: its NVN stage 5 (code 79116b290d46542f) the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0…

The test against main 6c9edffe's files: exit 1, 33 of its 39 checks and all 5 capture loops fail.

Why main refuses it:

  1. The capture reads past a program's end. parse_code (gpu/vk/vk_shader.cpp) reads an NVN2 stage's Ampere
    instructions up to the code length (+0x38). After each one, ProgramCaptureState::Observe decides whether the
    program has ended. Its end rules are narrower than the lowering's, which decides the real end (finish,
    source_size):

    • a loop left only by predicated EXITs ends at its closing branch to itself (lower_to_maxwell.h, "the closing
      self-branch"), which the capture takes as the end only after a computed branch;
    • a library function can end the shader with EXIT, while the capture ends only at a RET.

    So the capture can read past the end. A word there that decodes as a branch with an impossible target (negative,
    misaligned, or past the largest program) refused the whole program. Nothing reaches such a word.

  2. Programs over 1 MiB were refused. The capture refused any target at or past 1 MiB (program_capture.h,
    MaximumInstructions = 65536). So did the lowering (code.size() > 65536, "invalid native program size") and the
    native address map (count > 65536). Run 18, an integrated build with the first commit of this PR, shows that this
    is 79116b29's case (the line above). 0x104680 bytes is 66,664 instructions.

    • The hardware has no instruction count. A Maxwell program is placed by a 32-bit offset in its program region:
      Eden's Maxwell3D::Regs::Pipeline::offset (src/video_core/engines/maxwell_3d.h) and KeplerCompute's
      LaunchParams::program_start (kepler_compute.h). Its relative branches reach 24 bits (±8 MiB) and its absolute
      ones 32 bits: frontend/maxwell/instruction.h, BitField<20, 24, s64> offset and
      BitField<20, 32, u64> absolute. An Ampere BRA's offset has 50 bits, as the capture decodes it.
    • The other decoders have no limit either. yuzu/suyu/Eden look for the end only within 1 MiB (TryFindSize,
      MAXIMUM_SIZE = 0x100000, shader_environment.cpp). When that fails they build the control-flow graph to get the
      real size ("Slow path … Build a control flow graph to get the real shader size", ShaderCache::MakeShaderInfo,
      shader_cache.cpp). Ryujinx sizes a program by its furthest reachable block (Translator.cs: maxEndAddress).

What changed

The capture loop now ends before a word it reads as a branch outside every program, and the lowering decides whether the
code before that word stands on its own. A program's size limit is now the largest code area the renderer reads, 16 MiB,
instead of 1 MiB, and a lowered branch beyond Maxwell's 8 MiB relative reach takes its absolute form.

  • Read first: program_address_map.h (the limit) and the finish lambda in lower_to_maxwell.h (the absolute forms),
    then parse_code's capture loop. The suyu copies and the other four loops are the same change.

  • The capture loop ends before a word it reads as a branch outside every program. This is in parse_code, and in
    the four other copies of the loop:

    • the WebGPU renderer, gpu/render/rx_shader.cpp read_nvn_code;
    • the bake tools, gpu/shaderbatch.cpp build_pkg and nx2/bake_ds/bake_ds.cpp;
    • gpu/shadertool.cpp.

    The lowering then decides:

    • the code before the word lowers: the program is translated, and one line says so (each program once, up to 64):
      vk: NVN2 program K (stage S) ends at +E, before the word at +X that would branch outside it (L code bytes);
    • it does not: the program is refused for that word. The message starts with main's text and adds the code size
      and the lowering's reason.
  • One limit for a program's size, MaximumNativeInstructions (frontend/ampere/program_address_map.h): 16 MiB ÷ 16
    = 1,048,576 instructions. That is as many as fit in the largest code area the renderer reads (control_end refuses
    an area that ends past 16 MiB). The capture's "outside" test, the lowering's size check and the address map's count
    check all use it, in gpu/vk/eden and in the WebGPU renderer's gpu/third_party/suyu copy. lower_to_maxwell.h now
    includes program_address_map.h.

  • Far branches in the lowering (LowerToMaxwellWith), both copies. The lowered Maxwell code holds each branch in the
    24-bit field at bits 20-43. A target more than 8 MiB of lowered code away (about 786,000 lowered instructions) was
    refused ("lowered branch distance", "native computed branch range"). It now takes the absolute form, which the
    frontend reads through IsAbsoluteJump (control_flow.cpp):

    • BRA → JMP and CAL → JCAL, with the target's address in bits 20-51;
    • BRX → JMX, with offset −8, so that AnalyzeBRX's target is the table value itself, as with BRX.

    The predicate and the flow test stay as they were. Branches within reach are emitted exactly as before.

  • tests/test_capture_past_end.py (new) compiles vk_shader.cpp's hash_bytes, Code, code_key and
    parse_code with the recompiler's headers (--source selects a checkout), over packages it builds:

    • stray words after a loop's end: a BRA between two instructions at instruction 16404, a BRA 16 MiB on, a BRA and a
      BSSY before the start, and a CALL between two instructions. Each runs in compute and in graphics, and must translate
      to the same code as the loop alone, with one log line;
    • a program that branches over such a word: still refused, naming it;
    • two programs main parses: unchanged;
    • a program of 0x104680 bytes: a @P0 BRA at instruction 16404 to its EXIT, past 1 MiB, and a @P1 BRA back. It
      translates, each BRA lands on its target, and the address map finds both ends;
    • 12.8 MB programs whose two branches, and a CALL to a function at the end, are more than 8 MiB of lowered code
      apart: the branches become JMPs and the call a JCAL, each holding its target's address, the predicate kept;
    • all five capture loops: each ends before the word.
  • Unchanged:

    • the capture's end rules;
    • third_party/;
    • every program that parsed before. Such a program never met a target past 1 MiB, so it gets the same native; a
      program under 8 MiB of lowered code emits the same words.
  • Sources read:

    • Eden's GitHub mirror eden-emulator/mirror @ 4a2c9234: shader_environment.cpp, shader_cache.cpp,
      maxwell/control_flow.cpp and instruction.h, engines/maxwell_3d.h and kepler_compute.h;
    • alula/Ryujinx @ 4930dced: Decoders/Decoder.cs and Translation/Translator.cs.

    GitHub's yuzu, suyu and Ryujinx repositories are blocked. Written from LS's own source; nothing was copied.

Measurements

Verdict: fixed in the game-free test, not yet seen in the game. The new test fails on main (33 of its 39 checks and
all 5 capture loops) and passes here (39 of 39, 5 of 5); the previous head of this PR failed its 6 new checks. Among them
is a program of 79116b29's size: on the previous head it is refused with run 18's line, word for word as far as that line
was quoted. The full game-free suite changes only by the new test and the load-sensitive test_gpu_hang_watch. No game
run of this head yet.

Commits: main 6c9edffe5d (2026-11-14); this PR fix/shader-branch-past-end @
32d7759026 (rebased from d2111b26; previous head 3c1b5551, on main 5e072fc8).
Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21. No GPU or driver involved.
Scene: none. Game-free test, object compiles and the game-free suite; the game line is run 18's.
Method: the new test at this head, and with --source on main's and the previous head's files (git archive);
vk_shader.cpp objects with the renderer's flags (-mmacosx-version-min=14.4, arm64 and x86_64); tests/run_all.py,
the whole suite, compared by test name with a main run on the same Mac.
How to reproduce: python -I -B tests/test_capture_past_end.py --cxx clang++, then the same with --source <a checkout of main>.

Before: main @ 6c9edffe After: this PR @ 32d77590
tests/test_capture_past_end.py exit 1: 33 of 39 checks and 5 of 5 loops fail exit 0: 39 of 39, 5 of 5 loops
a stray BRA at instruction 16404 after a loop's end refused: goes outside the program translated, the same code as the loop alone
0x104680 bytes, a BRA at 16404 to its end past 1 MiB refused translated; both BRAs land on their targets
12.8 MB, branches and a call more than 8 MiB of lowered code apart refused translated: JMP, JMP, JCAL
vk_shader.cpp object, arm64 / x86_64 exit 0 / 0 (with a compile-only environ declaration, which main lacks on Darwin) exit 0 / 0 (the same)
whole game-free suite on macOS (tests/run_all.py, 6 at once) 531 passed, 32 failed, 17 skipped 531 passed, 33 failed, 17 skipped (test_capture_past_end new and passing; test_gpu_hang_watch load-sensitive, under Testing)

The previous head of this PR, 3c1b5551 (on main 5e072fc8), with the same test (--source):

previous head @ 3c1b5551
tests/test_capture_past_end.py exit 1: the 6 new checks fail
a stray BRA at instruction 16404 after a loop's end translated, the same code as the loop alone
0x104680 bytes, a BRA at 16404 to its end past 1 MiB refused, as in run 18 (below)
12.8 MB, branches and a call more than 8 MiB of lowered code apart refused
vk_shader.cpp object, arm64 / x86_64 exit 0 / 0 (with the same compile-only environ declaration)
run 18 (integrated with 3c1b5551), the game:
vk: program 79116b290d46542f not translated: its NVN stage 5 (code 79116b290d46542f) the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0…

previous head @ 3c1b5551, the test's program of the same size:
FAIL: 0x104680 bytes, a branch at instruction 16404 to past 1 MiB: translates: got the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0x40140: native program has no unconditional EXIT
FAIL: 12.8 MB, branches more than 8 MiB of lowered code apart: translates: got the branch at instruction 0 (+0) goes outside the program (0xc35000 code bytes), and the code before it does not lower at +0: invalid native program size

this PR @ 32d77590:
PASS: 0x104680 bytes, a branch at instruction 16404 to past 1 MiB: translates
PASS: 0x104680 bytes: each BRA lands on its target, the address map finds both ends
PASS: 12.8 MB: each branch is a JMP to its target
PASS: 12.8 MB: the call is a JCAL to the function
PASS: a word past the program's end that reads as a branch outside it ends the capture; programs over 1 MiB translate
PASS: every capture loop ends before such a word

The whole suite's summary lines, on the same Mac (the runner labels macOS linux):

main @ 6c9edffe:     531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once)
this PR @ 32d77590:  531 passed, 33 failed, 17 skipped in 203 s (linux, 6 at once)

Screenshots: none; the game run's evidence is its log lines (Testing).

Flags and settings

None.

Testing

Tested:

  • On an Apple M4 Max with macOS 26.7.1, game-free only.
  • The new test both ways: at 32d77590, and with --source on main 6c9edffe's files, whose 40 FAIL and 6 PASS lines are
    word for word those of main 5e072fc8 before the rebase; and, before the rebase, on macos/integrated 1c53cff1 with
    this PR's second commit applied (exit 0).
  • vk_shader.cpp objects on arm64 and x86_64, at 32d77590 and on main 6c9edffe (with the compile-only environ
    declaration on both), and before the rebase on that integrated tree.
  • The WebGPU renderer's suyu copies of the three Ampere headers, compiled on their own with the lowering instantiated
    (exit 0, at 32d77590: limit 1048576, capture 1, lowered 1 (1 words)).
  • The whole suite, tests/run_all.py, 6 at once, on the same Mac (the runner labels macOS linux), at 32d77590 and
    main 6c9edffe:
    • By test name, against main 6c9edffe's run on the same Mac: 2 statuses changed: test_capture_past_end, absent on
      main, passes here; test_gpu_hang_watch: pass on main, FAIL here.
    • test_gpu_hang_watch is load-sensitive (a time bound, 3.8 s against its 3.4 s; load averages at the run's start
      14.38 22.01 21.80) and is accepted without a rerun. This PR does not touch vk_replay.cpp.
    • These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull
      requests, some are checks main added or changed since 5e072fc8): test_appimage_package_retry,
      test_appimage_player_build, test_collect_wide, test_control, test_cpm_discovery, test_crash_dialog,
      test_crash_record, test_env_on, test_game_layout, test_lift_cleanup_error, test_linux_preflight,
      test_longshot_ctl, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children,
      test_mirror_trim, test_oot_state_tap, test_pcache_header_read, test_pipeline_cache_file,
      test_pipeline_cache_save, test_pipeline_play_cache, test_pipeline_tour, test_prof_symbols,
      test_program_cache_cleanup, test_program_cache_one_variant, test_readback_late, test_report_run,
      test_split_giants, test_vk_pgo, test_vram_budget_row, test_walk_key.
    • The same 17 checks skipped as on main.
    • Crash reports during the run: none (main's run: fault-arm64-2026-11-14-180704.ips,
      recorder_sync_test.exe-2026-11-14-180744.ips).
  • Checks near this change pass:
    • tests/test_bake_mimo.py, which builds the recompiler, gpu/shaderbatch.cpp and the eden Ampere headers, and runs
      them;
    • tests/test_lower_end_at_closing_branch.py, tests/test_bake_ds.py, tests/test_log_levels.py and
      tests/test_compute_local_memory.py.
  • Run 18 (an integrated macOS build with this PR's first commit) is the game evidence for the cause.
  • After the rebase onto main aa95d35f, at 33c427bb (32d77590 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave S1's merge 11a5b4e7, which carries
    this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).
  • A game run with this PR's two commits (5f1c612c, 86f13743, carried in the macOS integration I41 78ec1c47), run
    E9a: arm64, the field-dawn save, a 1,197 s hold, clean end (reports/2026-11-15-mac-perf-E9.md).
    • 79116b290d46542f translated and was made, in two variants of 1,230,352 and 1,230,356 SPIR-V words (lines 45857
      and 46501: vk: slow compile: compute 79116b290d46542f …, 26.2 s: made (0)). There is no not translated line for it.
    • All 30 not translated lines in that run are a network left out on this card, as designed on non-NVIDIA cards.
    • Rosetta (x86_64) run R39 on I42 63fc3d2c also carries both commits and ended clean.

Not tested:

  • A game run of this head on Windows or Linux. The macOS game run above settles the cause and the fix on that
    platform; the request below covers NVIDIA, where the program reaches its driver. The line for its key settles it:
    • vk: NVN2 program 79116b290d46542f (stage 5) … with no not translated line: translated;
    • on cards other than NVIDIA's it may then be [trace] … a network left out on this card, as designed.
  • The absolute forms (JMP, JCAL, JMX) through the Maxwell frontend and the SPIR-V emitter. The test checks the
    lowered words. The frontend's handling was read, not run: control_flow.cpp takes JMP/JCAL/JMX through
    IsAbsoluteJump, and a block ends before its branch. A JMX needs a jump table, which no test builds. A program needs
    more than 8 MiB of lowered code for any of them to appear.
  • Windows and Linux: nothing ran or was built there (llvm-mingw or Linux). The changed code has no platform branch.
  • Not compiled here: rx_shader.cpp (needs wgpu-native's headers), bake_ds.cpp (fmt and boost) and
    shadertool.cpp (MSVC with the Eglantine clone). Their edits mirror vk_shader.cpp's.
  • No timing measurement: a failing-on-main test and run 18's line, not a game measurement.

Must be tested:

  • Windows or Linux, an NVIDIA GPU (RTX 20-50), any driver: build this PR (head 33c427bb) and main aa95d35f the same way,
    start each once through the shader preparation to the title and search longshot_log.txt for 79116b290d46542f,
    expecting no not translated line for it on this PR, where main logs vk: program 79116b290d46542f not translated: … goes outside the program (by hand: test request T-C78E).

Risks

What could break:

  • What can change: only programs that main refuses (a stray word, or a program over 1 MiB) and programs with more
    than 8 MiB of lowered code. Others get the same words. A program main left out now reaches the driver, which no game
    run of this head has shown yet.
  • A stray word: it is dropped only when the code before it lowers on its own, which means every direct branch is
    inside the program. An indirect branch's targets are checked when it translates.
  • The absolute forms are the frontend's existing JMP/JCAL/JMX paths, which Switch 1 code uses.
  • A 16 MiB native program would carry a 16 MiB address map. That is memory per translation, not per frame.
  • Landing order: no conflict with main or with the other macOS-port PRs rebased on 6c9edffe (git merge-tree of the
    new heads, 19:49 CST).

Turning it off: revert the commits. There is no switch.

Compatibility: vk_shader.cpp and the eden Ampere headers (gpu/vk/eden) are hashed into LONGSHOT_SHADER_STAMP
(the translator stamp's files, listed in nx2/translator_files.py since main's #370), so the program cache, the prepare
record and the Vulkan pipeline cache files are rebuilt once: every player translates every program again at the first
start after updating. The WebGPU renderer's gpu/third_party/suyu copy is not in the stamp. Saves, the options file and
the vk ABI are unchanged.

Issues

See #420 (found in the macOS port's start runs; this fix is for every platform).

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: on aa95d35f (#761, 2026-11-15), head 33c427bb, without a conflict; this PR's own
    commits, old → new: 71ed77da → cf23d72e, 32d77590 → 33c427bb; the commit names elsewhere in this description
    are from before the rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). On macOS: 531 passed, 17
    skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing, plus the load-sensitive
    test_gpu_hang_watch timing (Testing). After the rebase onto main aa95d35f (head 33c427bb): wave S1's merge
    11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead). gpu/third_party/suyu's Ampere headers are
    LS's own lowering, changed in step with gpu/vk/eden's (as earlier LS commits did); top-level third_party/ is
    untouched.
  • 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.
  • 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. None needed a change: the
    checks that read the changed files (tests/test_bake_mimo.py, tests/test_bake_ds.py,
    tests/test_lower_end_at_closing_branch.py, tests/test_log_levels.py, tests/test_compute_local_memory.py) pass
    unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 [email protected]

**Type:** fix · **Risk:** medium · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks on arm64 and x86_64; a macOS game run with this PR's commits, run E9a) · **Needs:** Windows or Linux, NVIDIA GPU ## Why A shader program the game uses is left out on every platform: the shader reader refuses it before translating it. This is for players on every platform and GPU. Compute shader program `79116b290d46542f` is never translated, on any renderer path. Its stage is refused before translation for "a branch outside the program". Two causes are fixed here: - a word after a program's end that only looks like a branch refused the whole program; - programs over 1 MiB (65,536 Ampere instructions) were refused outright. 79116b29 is one: its code is 0x104680 bytes. The GPU has no instruction count of its own. The limit is now the largest code area the renderer reads, 16 MiB, and a lowered branch beyond Maxwell's 8 MiB relative reach takes its absolute form. ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, MoltenVK: run 18, an integrated build of the macOS port with this PR's first commit (its `longshot_log.txt`, below). The capture and the size limits are the same code on every OS and renderer path: the changed code has no platform branch. Game-free: the new test against main 6c9edffe's files on this Mac. **Settings and switches:** defaults. **Steps:** start the game through the shader preparation and look for `79116b290d46542f` in `longshot_log.txt`. Game-free: from this branch's checkout, `python -I -B tests/test_capture_past_end.py --cxx clang++ --source <a checkout of main>`. **Expected:** the program translated: the line for its key, `vk: NVN2 program 79116b290d46542f (stage 5) …`, with no `not translated` line. **Actual:** it is refused before translation for "a branch outside the program". Run 18's line: ``` vk: program 79116b290d46542f not translated: its NVN stage 5 (code 79116b290d46542f) the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0… ``` The test against main 6c9edffe's files: exit 1, 33 of its 39 checks and all 5 capture loops fail. Why main refuses it: 1. **The capture reads past a program's end.** `parse_code` (`gpu/vk/vk_shader.cpp`) reads an NVN2 stage's Ampere instructions up to the code length (+0x38). After each one, `ProgramCaptureState::Observe` decides whether the program has ended. Its end rules are narrower than the lowering's, which decides the real end (`finish`, `source_size`): - a loop left only by predicated EXITs ends at its closing branch to itself (`lower_to_maxwell.h`, "the closing self-branch"), which the capture takes as the end only after a computed branch; - a library function can end the shader with EXIT, while the capture ends only at a RET. So the capture can read past the end. A word there that decodes as a branch with an impossible target (negative, misaligned, or past the largest program) refused the whole program. Nothing reaches such a word. 2. **Programs over 1 MiB were refused.** The capture refused any target at or past 1 MiB (`program_capture.h`, `MaximumInstructions = 65536`). So did the lowering (`code.size() > 65536`, "invalid native program size") and the native address map (`count > 65536`). Run 18, an integrated build with the first commit of this PR, shows that this is 79116b29's case (the line above). 0x104680 bytes is 66,664 instructions. - **The hardware has no instruction count.** A Maxwell program is placed by a 32-bit offset in its program region: Eden's `Maxwell3D::Regs::Pipeline::offset` (`src/video_core/engines/maxwell_3d.h`) and `KeplerCompute`'s `LaunchParams::program_start` (`kepler_compute.h`). Its relative branches reach 24 bits (±8 MiB) and its absolute ones 32 bits: `frontend/maxwell/instruction.h`, `BitField<20, 24, s64> offset` and `BitField<20, 32, u64> absolute`. An Ampere BRA's offset has 50 bits, as the capture decodes it. - **The other decoders have no limit either.** yuzu/suyu/Eden look for the end only within 1 MiB (`TryFindSize`, `MAXIMUM_SIZE = 0x100000`, `shader_environment.cpp`). When that fails they build the control-flow graph to get the real size ("Slow path … Build a control flow graph to get the real shader size", `ShaderCache::MakeShaderInfo`, `shader_cache.cpp`). Ryujinx sizes a program by its furthest reachable block (`Translator.cs`: `maxEndAddress`). ## What changed The capture loop now ends before a word it reads as a branch outside every program, and the lowering decides whether the code before that word stands on its own. A program's size limit is now the largest code area the renderer reads, 16 MiB, instead of 1 MiB, and a lowered branch beyond Maxwell's 8 MiB relative reach takes its absolute form. - Read first: `program_address_map.h` (the limit) and the `finish` lambda in `lower_to_maxwell.h` (the absolute forms), then `parse_code`'s capture loop. The suyu copies and the other four loops are the same change. - **The capture loop ends before a word it reads as a branch outside every program.** This is in `parse_code`, and in the four other copies of the loop: - the WebGPU renderer, `gpu/render/rx_shader.cpp` `read_nvn_code`; - the bake tools, `gpu/shaderbatch.cpp` `build_pkg` and `nx2/bake_ds/bake_ds.cpp`; - `gpu/shadertool.cpp`. The lowering then decides: - **the code before the word lowers:** the program is translated, and one line says so (each program once, up to 64): `vk: NVN2 program K (stage S) ends at +E, before the word at +X that would branch outside it (L code bytes)`; - **it does not:** the program is refused for that word. The message starts with `main`'s text and adds the code size and the lowering's reason. - **One limit for a program's size, `MaximumNativeInstructions` (`frontend/ampere/program_address_map.h`):** 16 MiB ÷ 16 = 1,048,576 instructions. That is as many as fit in the largest code area the renderer reads (`control_end` refuses an area that ends past 16 MiB). The capture's "outside" test, the lowering's size check and the address map's count check all use it, in `gpu/vk/eden` and in the WebGPU renderer's `gpu/third_party/suyu` copy. `lower_to_maxwell.h` now includes `program_address_map.h`. - **Far branches in the lowering (`LowerToMaxwellWith`), both copies.** The lowered Maxwell code holds each branch in the 24-bit field at bits 20-43. A target more than 8 MiB of lowered code away (about 786,000 lowered instructions) was refused ("lowered branch distance", "native computed branch range"). It now takes the absolute form, which the frontend reads through `IsAbsoluteJump` (`control_flow.cpp`): - BRA → JMP and CAL → JCAL, with the target's address in bits 20-51; - BRX → JMX, with offset −8, so that `AnalyzeBRX`'s target is the table value itself, as with BRX. The predicate and the flow test stay as they were. Branches within reach are emitted exactly as before. - **`tests/test_capture_past_end.py` (new)** compiles `vk_shader.cpp`'s `hash_bytes`, `Code`, `code_key` and `parse_code` with the recompiler's headers (`--source` selects a checkout), over packages it builds: - **stray words after a loop's end:** a BRA between two instructions at instruction 16404, a BRA 16 MiB on, a BRA and a BSSY before the start, and a CALL between two instructions. Each runs in compute and in graphics, and must translate to the same code as the loop alone, with one log line; - **a program that branches over such a word:** still refused, naming it; - **two programs `main` parses:** unchanged; - **a program of 0x104680 bytes:** a `@P0 BRA` at instruction 16404 to its EXIT, past 1 MiB, and a `@P1 BRA` back. It translates, each BRA lands on its target, and the address map finds both ends; - **12.8 MB programs** whose two branches, and a CALL to a function at the end, are more than 8 MiB of lowered code apart: the branches become JMPs and the call a JCAL, each holding its target's address, the predicate kept; - **all five capture loops:** each ends before the word. - **Unchanged:** - the capture's end rules; - `third_party/`; - every program that parsed before. Such a program never met a target past 1 MiB, so it gets the same `native`; a program under 8 MiB of lowered code emits the same words. - Sources read: - Eden's GitHub mirror `eden-emulator/mirror` @ `4a2c9234`: `shader_environment.cpp`, `shader_cache.cpp`, `maxwell/control_flow.cpp` and `instruction.h`, `engines/maxwell_3d.h` and `kepler_compute.h`; - `alula/Ryujinx` @ `4930dced`: `Decoders/Decoder.cs` and `Translation/Translator.cs`. GitHub's yuzu, suyu and Ryujinx repositories are blocked. Written from LS's own source; nothing was copied. ## Measurements **Verdict:** fixed in the game-free test, not yet seen in the game. The new test fails on main (33 of its 39 checks and all 5 capture loops) and passes here (39 of 39, 5 of 5); the previous head of this PR failed its 6 new checks. Among them is a program of 79116b29's size: on the previous head it is refused with run 18's line, word for word as far as that line was quoted. The full game-free suite changes only by the new test and the load-sensitive `test_gpu_hang_watch`. No game run of this head yet. Commits: main 6c9edffe5da1f5b5383977aeab3f544c0de25d4c (2026-11-14); this PR `fix/shader-branch-past-end` @ 32d775902616d66fe2389ecfd77b819844980912 (rebased from `d2111b26`; previous head `3c1b5551`, on main 5e072fc8). Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21. No GPU or driver involved. Scene: none. Game-free test, object compiles and the game-free suite; the game line is run 18's. Method: the new test at this head, and with `--source` on `main`'s and the previous head's files (`git archive`); `vk_shader.cpp` objects with the renderer's flags (`-mmacosx-version-min=14.4`, arm64 and x86_64); `tests/run_all.py`, the whole suite, compared by test name with a `main` run on the same Mac. How to reproduce: `python -I -B tests/test_capture_past_end.py --cxx clang++`, then the same with `--source <a checkout of main>`. | | Before: main @ 6c9edffe | After: this PR @ 32d77590 | |---|---|---| | `tests/test_capture_past_end.py` | exit 1: 33 of 39 checks and 5 of 5 loops fail | exit 0: 39 of 39, 5 of 5 loops | | a stray BRA at instruction 16404 after a loop's end | refused: `goes outside the program` | translated, the same code as the loop alone | | 0x104680 bytes, a BRA at 16404 to its end past 1 MiB | refused | translated; both BRAs land on their targets | | 12.8 MB, branches and a call more than 8 MiB of lowered code apart | refused | translated: JMP, JMP, JCAL | | `vk_shader.cpp` object, arm64 / x86_64 | exit 0 / 0 (with a compile-only `environ` declaration, which `main` lacks on Darwin) | exit 0 / 0 (the same) | | whole game-free suite on macOS (`tests/run_all.py`, 6 at once) | 531 passed, 32 failed, 17 skipped | 531 passed, 33 failed, 17 skipped (`test_capture_past_end` new and passing; `test_gpu_hang_watch` load-sensitive, under Testing) | The previous head of this PR, `3c1b5551` (on main 5e072fc8), with the same test (`--source`): | | previous head @ 3c1b5551 | |---|---| | `tests/test_capture_past_end.py` | exit 1: the 6 new checks fail | | a stray BRA at instruction 16404 after a loop's end | translated, the same code as the loop alone | | 0x104680 bytes, a BRA at 16404 to its end past 1 MiB | refused, as in run 18 (below) | | 12.8 MB, branches and a call more than 8 MiB of lowered code apart | refused | | `vk_shader.cpp` object, arm64 / x86_64 | exit 0 / 0 (with the same compile-only `environ` declaration) | ``` run 18 (integrated with 3c1b5551), the game: vk: program 79116b290d46542f not translated: its NVN stage 5 (code 79116b290d46542f) the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0… previous head @ 3c1b5551, the test's program of the same size: FAIL: 0x104680 bytes, a branch at instruction 16404 to past 1 MiB: translates: got the branch at instruction 16404 (+0x40140) goes outside the program (0x104680 code bytes), and the code before it does not lower at +0x40140: native program has no unconditional EXIT FAIL: 12.8 MB, branches more than 8 MiB of lowered code apart: translates: got the branch at instruction 0 (+0) goes outside the program (0xc35000 code bytes), and the code before it does not lower at +0: invalid native program size this PR @ 32d77590: PASS: 0x104680 bytes, a branch at instruction 16404 to past 1 MiB: translates PASS: 0x104680 bytes: each BRA lands on its target, the address map finds both ends PASS: 12.8 MB: each branch is a JMP to its target PASS: 12.8 MB: the call is a JCAL to the function PASS: a word past the program's end that reads as a branch outside it ends the capture; programs over 1 MiB translate PASS: every capture loop ends before such a word ``` The whole suite's summary lines, on the same Mac (the runner labels macOS `linux`): ``` main @ 6c9edffe: 531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once) this PR @ 32d77590: 531 passed, 33 failed, 17 skipped in 203 s (linux, 6 at once) ``` Screenshots: none; the game run's evidence is its log lines (Testing). ### Flags and settings None. ## Testing **Tested:** - On an Apple M4 Max with macOS 26.7.1, game-free only. - The new test both ways: at 32d77590, and with `--source` on main 6c9edffe's files, whose 40 FAIL and 6 PASS lines are word for word those of main 5e072fc8 before the rebase; and, before the rebase, on `macos/integrated` 1c53cff1 with this PR's second commit applied (exit 0). - `vk_shader.cpp` objects on arm64 and x86_64, at 32d77590 and on main 6c9edffe (with the compile-only `environ` declaration on both), and before the rebase on that integrated tree. - The WebGPU renderer's suyu copies of the three Ampere headers, compiled on their own with the lowering instantiated (exit 0, at 32d77590: `limit 1048576, capture 1, lowered 1 (1 words)`). - **The whole suite**, `tests/run_all.py`, 6 at once, on the same Mac (the runner labels macOS `linux`), at 32d77590 and main 6c9edffe: - By test name, against main 6c9edffe's run on the same Mac: 2 statuses changed: `test_capture_past_end`, absent on main, passes here; `test_gpu_hang_watch`: pass on main, FAIL here. - `test_gpu_hang_watch` is load-sensitive (a time bound, 3.8 s against its 3.4 s; load averages at the run's start 14.38 22.01 21.80) and is accepted without a rerun. This PR does not touch `vk_replay.cpp`. - These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull requests, some are checks main added or changed since 5e072fc8): `test_appimage_package_retry`, `test_appimage_player_build`, `test_collect_wide`, `test_control`, `test_cpm_discovery`, `test_crash_dialog`, `test_crash_record`, `test_env_on`, `test_game_layout`, `test_lift_cleanup_error`, `test_linux_preflight`, `test_longshot_ctl`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_oot_state_tap`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_pipeline_tour`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_readback_late`, `test_report_run`, `test_split_giants`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`. - The same 17 checks skipped as on main. - Crash reports during the run: none (main's run: fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips). - **Checks near this change pass:** - `tests/test_bake_mimo.py`, which builds the recompiler, `gpu/shaderbatch.cpp` and the eden Ampere headers, and runs them; - `tests/test_lower_end_at_closing_branch.py`, `tests/test_bake_ds.py`, `tests/test_log_levels.py` and `tests/test_compute_local_memory.py`. - Run 18 (an integrated macOS build with this PR's first commit) is the game evidence for the cause. - **After the rebase onto main aa95d35f**, at `33c427bb` (`32d77590` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave S1's merge `11a5b4e7`, which carries this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). - **A game run with this PR's two commits** (`5f1c612c`, `86f13743`, carried in the macOS integration I41 `78ec1c47`), run E9a: arm64, the field-dawn save, a 1,197 s hold, clean end (`reports/2026-11-15-mac-perf-E9.md`). - **79116b290d46542f translated and was made**, in two variants of 1,230,352 and 1,230,356 SPIR-V words (lines 45857 and 46501: `vk: slow compile: compute 79116b290d46542f …, 26.2 s: made (0)`). There is no `not translated` line for it. - All 30 `not translated` lines in that run are `a network left out on this card`, as designed on non-NVIDIA cards. - Rosetta (x86_64) run R39 on I42 `63fc3d2c` also carries both commits and ended clean. **Not tested:** - **A game run of this head on Windows or Linux.** The macOS game run above settles the cause and the fix on that platform; the request below covers NVIDIA, where the program reaches its driver. The line for its key settles it: - `vk: NVN2 program 79116b290d46542f (stage 5) …` with no `not translated` line: translated; - on cards other than NVIDIA's it may then be `[trace] … a network left out on this card`, as designed. - **The absolute forms (JMP, JCAL, JMX) through the Maxwell frontend and the SPIR-V emitter.** The test checks the lowered words. The frontend's handling was read, not run: `control_flow.cpp` takes JMP/JCAL/JMX through `IsAbsoluteJump`, and a block ends before its branch. A JMX needs a jump table, which no test builds. A program needs more than 8 MiB of lowered code for any of them to appear. - **Windows and Linux**: nothing ran or was built there (llvm-mingw or Linux). The changed code has no platform branch. - **Not compiled here:** `rx_shader.cpp` (needs wgpu-native's headers), `bake_ds.cpp` (fmt and boost) and `shadertool.cpp` (MSVC with the Eglantine clone). Their edits mirror `vk_shader.cpp`'s. - No timing measurement: a failing-on-main test and run 18's line, not a game measurement. **Must be tested:** - Windows or Linux, an NVIDIA GPU (RTX 20-50), any driver: build this PR (head 33c427bb) and main aa95d35f the same way, start each once through the shader preparation to the title and search `longshot_log.txt` for `79116b290d46542f`, expecting no `not translated` line for it on this PR, where main logs `vk: program 79116b290d46542f not translated: … goes outside the program` (by hand: test request T-C78E). ## Risks **What could break:** - **What can change:** only programs that `main` refuses (a stray word, or a program over 1 MiB) and programs with more than 8 MiB of lowered code. Others get the same words. A program `main` left out now reaches the driver, which no game run of this head has shown yet. - **A stray word:** it is dropped only when the code before it lowers on its own, which means every direct branch is inside the program. An indirect branch's targets are checked when it translates. - **The absolute forms** are the frontend's existing JMP/JCAL/JMX paths, which Switch 1 code uses. - **A 16 MiB native program** would carry a 16 MiB address map. That is memory per translation, not per frame. - Landing order: no conflict with `main` or with the other macOS-port PRs rebased on 6c9edffe (`git merge-tree` of the new heads, 19:49 CST). **Turning it off:** revert the commits. There is no switch. **Compatibility:** `vk_shader.cpp` and the eden Ampere headers (`gpu/vk/eden`) are hashed into `LONGSHOT_SHADER_STAMP` (the translator stamp's files, listed in `nx2/translator_files.py` since main's #370), so the program cache, the prepare record and the Vulkan pipeline cache files are rebuilt once: every player translates every program again at the first start after updating. The WebGPU renderer's `gpu/third_party/suyu` copy is not in the stamp. Saves, the options file and the vk ABI are unchanged. ## Issues See #420 (found in the macOS port's start runs; this fix is for every platform). ## 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`: on aa95d35f (#761, 2026-11-15), head `33c427bb`, without a conflict; this PR's own commits, old → new: `71ed77da` → `cf23d72e`, `32d77590` → `33c427bb`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). On macOS: 531 passed, 17 skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing, plus the load-sensitive `test_gpu_hang_watch` timing (Testing). After the rebase onto main aa95d35f (head `33c427bb`): wave S1's merge `11a5b4e7`, 568/22/17 against main aa95d35f's 543/33/17, no new failure. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). `gpu/third_party/suyu`'s Ampere headers are LS's own lowering, changed in step with `gpu/vk/eden`'s (as earlier LS commits did); top-level `third_party/` is untouched. - [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] 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. None needed a change: the checks that read the changed files (`tests/test_bake_mimo.py`, `tests/test_bake_ds.py`, `tests/test_lower_end_at_closing_branch.py`, `tests/test_log_levels.py`, `tests/test_compute_local_memory.py`) pass unchanged. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(vk): a shader program is no longer refused for a word after its end that reads as a branch outside it
All checks were successful
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
CI / Conventional Commit title (pull_request) Successful in 5s
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
3c1b555125
parse_code reads an NVN2 stage's Ampere instructions until ProgramCaptureState
says the program ended. The capture's end rules are narrower than the
lowering's (a loop left only by predicated EXITs ends at its closing branch
to itself; a library function can end the shader with EXIT), so it can read
on past the program's end. A word there that decodes as a branch to before
the start, between two instructions or 1 MiB or more on refused the whole
program: compute program 79116b290d46542f, "the branch at instruction 16404
(+0x40140) goes outside the program", on every platform.

Now the capture ends before that word. The lowering, which takes a program
only to its own end with every branch inside it, decides: the program
translates if it ends before the word (one log line says so), and is refused
for the word, as before, if the code before it does not lower. Programs that
parsed before are unchanged: the capture refuses exactly the same words.

The same in the four other copies of the capture loop: the WebGPU renderer
(rx_shader.cpp), the bake tools (shaderbatch.cpp, bake_ds.cpp) and shadertool.

tests/test_capture_past_end.py compiles parse_code from the source over
synthetic packages built in the test.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki changed title from WIP: fix(vk): a word after a shader program's end that reads as a stray branch no longer leaves the program out to WIP: fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left out 2026-11-14 16:12:34 +00:00
fix(vk): shader programs over 1 MiB translate: the native limit is the largest code area, 16 MiB
All checks were successful
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
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
PR description / Description follows the template (pull_request_target) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
d2111b26a5
The capture, the lowering and the native address map took at most 65,536
Ampere instructions (1 MiB). Compute program 79116b290d46542f has 0x104680
bytes of code, and a branch at instruction 16404 goes past 1 MiB: run 18
(integrated with the previous commit) refused it with "... goes outside the
program (0x104680 code bytes), and the code before it does not lower".

The GPU has no instruction count: Maxwell and Ampere address a program by a
32-bit offset in its program region, and an Ampere BRA's offset has 50 bits.
The limit is now MaximumNativeInstructions (program_address_map.h): as many
instructions as fit in the largest code area the renderer reads (control_end
refuses an area past 16 MiB), used by ProgramCaptureState, LowerToMaxwellWith
and NativeProgramAddress, in gpu/vk/eden and in gpu/third_party/suyu.

A lowered branch more than 8 MiB away is past the 24-bit offset of Maxwell's
BRA, CAL and BRX. It now takes their absolute forms, JMP, JCAL (32-bit
address) and JMX (the table value alone), which the frontend reads through
IsAbsoluteJump; it was refused as "lowered branch distance" or "native
computed branch range". Branches within reach are emitted as before.

tests/test_capture_past_end.py: a program of 0x104680 bytes with a branch at
instruction 16404 to its end, and 12.8 MB programs whose branches and call
are more than 8 MiB of lowered code apart.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
First-time contributor

Independent Windows baseline evidence for your shader target: main9c9d69ed, RTX5090 driver617.42, Windows11 build26300, matched API22 build. Preparation logs program79116b290d46542f not translated at NVN stage5 because a branch is outside the program; the summary reports38809 programs and1 failed. This strongly matches the summary failure, although the log does not explicitly pair them. Preparation exits0 and no noted/compute precompile pipeline is reported not made. I have not run this branch, so this is baseline reproduction only; no gameplay-effect or after-fix claim.

Independent Windows baseline evidence for your shader target: main9c9d69ed, RTX5090 driver617.42, Windows11 build26300, matched API22 build. Preparation logs program79116b290d46542f not translated at NVN stage5 because a branch is outside the program; the summary reports38809 programs and1 failed. This strongly matches the summary failure, although the log does not explicitly pair them. Preparation exits0 and no noted/compute precompile pipeline is reported not made. I have not run this branch, so this is baseline reproduction only; no gameplay-effect or after-fix claim.
mooki force-pushed fix/shader-branch-past-end from d2111b26a5
All checks were successful
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
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
PR description / Description follows the template (pull_request_target) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to 32d7759026
All checks were successful
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
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 01:51:44 +00:00
Compare
mooki force-pushed fix/shader-branch-past-end from 32d7759026
All checks were successful
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
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
to 19fd58215e
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 07:01:10 +00:00
Compare
mooki force-pushed fix/shader-branch-past-end from 19fd58215e
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 4s
to 33c427bb8d
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
CI / Conventional Commit title (pull_request) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:02:34 +00:00
Compare
mooki changed title from WIP: fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left out to fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left out 2026-11-15 17:04:48 +00:00
HumanBean approved these changes 2026-11-15 18:04:06 +00:00
HumanBean left a comment

Approving on code. This is a code-only review: I read the diff at 33c427bb and ran nothing here.

Stray branch after the end. The capture now stops before the word and leaves it out of native. Nothing past the program gets read. The lowering only walks native. finish refuses any direct branch or call aimed past the instruction it ends at. The address map is built from source_instruction_offsets, which runs only up to that end, so an indirect target landing on or past the dropped word throws in TranslateProgramAddress; it can't index out of bounds. Env::ReadInstruction is bounds-checked. An empty native (the word at instruction 0) fails as "invalid native program size". The ~0u sentinel can't collide, because i stays under 1M. Programs main already parsed get the same native: they never hit InvalidTarget, and the capture's end rules are unchanged.

The 16 MiB limit. It follows from control_end's 16 MiB area cap, so native is at most 16 MiB and the address map at most 8 MiB. That is per translation, not per frame. I found no fixed buffer or size field on the program path that the larger size could overflow. The new why[256] is snprintf'd, so a long message is cut short, not overrun. code_key hashes the whole area and the control section as before, so the program cache keys don't change. The translator stamp rebuilds the caches once, as the description says.

Far branches. I checked these against the frontend. address_of is in the same address space as Location: it includes the 0x50 header, and graphics and compute both start the CFG at 0x50. So JMP/JCAL with address_of(target) in bits 20-51 lands where the BRA would. JMX with offset -8 gives AnalyzeBRX the table value plus 0 plus 8, the same target as the BRX form, and branch_offset comes out as 0. JMP and JCAL end their block before the branch, as BRA and CAL do, so their throwing translator visitors are never reached. One non-blocking note is inline.

monsterlag's Windows RTX 5090 baseline (main 9c9d69ed: 79116b290d46542f, stage 5, "branch outside the program", 1 of 38,809 programs failed) is the program this PR targets. On main that refusal comes from the real @P0 BRA at instruction 16404 to a target past 1 MiB. Run 18 shows the first commit alone doesn't clear it. The second commit raises the limit, which does. On macOS, run E9a with both commits translated and built it (two variants, about 1.23M SPIR-V words, 26.2 s each), with no not translated line.

Before merge: it still needs one game run on Windows or Linux with an NVIDIA card, as the description's Must be tested says (test request T-C78E). That run should show no not translated line for 79116b290d46542f, and that the driver gets through compiling a program this large without stalling preparation. The macOS run shows the translation is right; only NVIDIA hardware shows this program reaching its driver.

Approving on code. This is a code-only review: I read the diff at `33c427bb` and ran nothing here. **Stray branch after the end.** The capture now stops before the word and leaves it out of `native`. Nothing past the program gets read. The lowering only walks `native`. `finish` refuses any direct branch or call aimed past the instruction it ends at. The address map is built from `source_instruction_offsets`, which runs only up to that end, so an indirect target landing on or past the dropped word throws in `TranslateProgramAddress`; it can't index out of bounds. `Env::ReadInstruction` is bounds-checked. An empty `native` (the word at instruction 0) fails as "invalid native program size". The `~0u` sentinel can't collide, because `i` stays under 1M. Programs `main` already parsed get the same `native`: they never hit `InvalidTarget`, and the capture's end rules are unchanged. **The 16 MiB limit.** It follows from `control_end`'s 16 MiB area cap, so `native` is at most 16 MiB and the address map at most 8 MiB. That is per translation, not per frame. I found no fixed buffer or size field on the program path that the larger size could overflow. The new `why[256]` is snprintf'd, so a long message is cut short, not overrun. `code_key` hashes the whole area and the control section as before, so the program cache keys don't change. The translator stamp rebuilds the caches once, as the description says. **Far branches.** I checked these against the frontend. `address_of` is in the same address space as `Location`: it includes the 0x50 header, and graphics and compute both start the CFG at 0x50. So JMP/JCAL with `address_of(target)` in bits 20-51 lands where the BRA would. JMX with offset -8 gives `AnalyzeBRX` the table value plus 0 plus 8, the same target as the BRX form, and `branch_offset` comes out as 0. JMP and JCAL end their block before the branch, as BRA and CAL do, so their throwing translator visitors are never reached. One non-blocking note is inline. **monsterlag's Windows RTX 5090 baseline** (main 9c9d69ed: `79116b290d46542f`, stage 5, "branch outside the program", 1 of 38,809 programs failed) is the program this PR targets. On `main` that refusal comes from the real `@P0 BRA` at instruction 16404 to a target past 1 MiB. Run 18 shows the first commit alone doesn't clear it. The second commit raises the limit, which does. On macOS, run E9a with both commits translated and built it (two variants, about 1.23M SPIR-V words, 26.2 s each), with no `not translated` line. **Before merge:** it still needs one game run on Windows or Linux with an NVIDIA card, as the description's *Must be tested* says (test request T-C78E). That run should show no `not translated` line for `79116b290d46542f`, and that the driver gets through compiling a program this large without stalling preparation. The macOS run shows the translation is right; only NVIDIA hardware shows this program reaching its driver.
@ -3329,0 +3338,4 @@
// (longshot) BRX goes to the table's value plus its own address plus its offset, which reaches back only 8 MiB
// (24 bits). Past that, JMX: the value plus the offset alone, -8 (the frontend's AnalyzeBRX adds the 8 back)
const s64 back = -s64{address_of(static_cast<u32>(result.instructions.size()))} - 8;
const bool absolute = back < -0x800000;
Owner

Non-blocking. The JMX form reaches the frontend correctly, but in practice only through the IMNMX and MOV32I table bounds. The measured-table path in indirect_branch_table_track.cpp (table_count) accepts an entry only if rel < 0x40000 and target >= block_begin.Offset(), and the second compares a native table value with a lowered block address. At more than 8 MiB of lowered code, block_begin is past 8 MiB, so no entry qualifies. Tracking then throws "Failed to track indirect branch" and the program is left out, as before this PR: a safe refusal, not a wrong translation. It's worth a line in Not tested or a follow-up issue so a far jump table there isn't taken for covered.

Non-blocking. The JMX form reaches the frontend correctly, but in practice only through the IMNMX and MOV32I table bounds. The measured-table path in `indirect_branch_table_track.cpp` (`table_count`) accepts an entry only if `rel < 0x40000` and `target >= block_begin.Offset()`, and the second compares a native table value with a lowered block address. At more than 8 MiB of lowered code, `block_begin` is past 8 MiB, so no entry qualifies. Tracking then throws "Failed to track indirect branch" and the program is left out, as before this PR: a safe refusal, not a wrong translation. It's worth a line in *Not tested* or a follow-up issue so a far jump table there isn't taken for covered.
HumanBean deleted branch fix/shader-branch-past-end 2026-11-15 18:10:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!427
No description provided.