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

Draft
mooki wants to merge 2 commits from mooki/Longshot:fix/shader-branch-past-end into main
Member

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

What is measured: a game-free test compiles parse_code from the source over shader packages it builds:

  • main fails 33 of its 39 checks and all 5 capture loops; the previous head of this PR fails the 6 new checks;
  • this PR passes all 39 and the 5 loops;
  • 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.

Also measured: vk_shader.cpp object compiles for arm64 and x86_64, and the full game-free suite on an Apple M4 Max
(macOS 26.7.1), compared by test name with main.

Not measured: a game run of this head, so not yet 79116b29 translating in the game; Windows and Linux; the far
(JMP/JCAL/JMX) forms going through the Maxwell frontend and the SPIR-V emitter.

Why

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

Problem

  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:

    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…
    

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

Flags and settings

None.

Measurements

main: 5e072fc8c2adadba5ea610148be1c47800ba0cca (2026-11-14)
this PR: fix/shader-branch-past-end @ d2111b26a566829f32cb15270d7c9c68692c9201 (previous head 3c1b5551)
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>.

main @ 5e072fc8 previous head @ 3c1b5551 this PR @ d2111b26
tests/test_capture_past_end.py exit 1: 33 of 39 checks and 5 of 5 loops fail exit 1: the 6 new checks 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 the same
0x104680 bytes, a BRA at 16404 to its end past 1 MiB refused refused, as in run 18 (below) translated; both BRAs land on their targets
12.8 MB, branches and a call more than 8 MiB of lowered code apart refused refused translated: JMP, JMP, JCAL
vk_shader.cpp object, arm64 / x86_64 exit 0 / 0 (with a one-line environ declaration, which main lacks on Darwin) exit 0 / 0 (the same) exit 0 / 0 (the same)
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 @ d2111b26:
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

Testing

Tested:

  • the new test both ways, and on macos/integrated 1c53cff1 with this PR's second commit applied (exit 0);
  • vk_shader.cpp objects on arm64 and x86_64, on this head and 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).

The whole suite, tests/run_all.py, 6 at once, on the same Mac (the runner labels macOS linux):

main @ 5e072fc8:     525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once)
this PR @ d2111b26:  526 passed, 19 failed, 17 skipped in 174 s (linux, 6 at once)
  • By test name, the only difference is the new tests/test_capture_past_end.py (pass); no other check changed status.
    The previous head, 3c1b5551, gave the same totals.
  • The 19 failures are the same on both. They are macOS portability failures of main, which the macOS port's own
    pull requests address:
    • test_appimage_package_retry, test_collect_wide, test_control, test_framegen_resources, test_game_layout;
    • test_low_memory_notice, test_mem_gate, test_mem_gate_children, test_mirror_trim, test_module_ids;
    • test_oot_state_tap, test_pad_layout, test_pcache_header_read, test_pipeline_cache_file;
    • test_pipeline_cache_save, test_pipeline_play_cache, test_prof_symbols, test_program_cache_cleanup,
      test_program_cache_one_variant.
  • 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.

Not tested:

  • A game run of this head, so whether 79116b29 now translates. 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. 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.

Needs other testers:

  • Windows or Linux, NVIDIA: a start through the shader preparation. Expect no not translated line for
    79116b290d46542f (request to open).

Screenshots

Nothing on screen.

Risk and rollback

  • 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 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.
  • The stamp: vk_shader.cpp and the eden headers are hashed into LONGSHOT_SHADER_STAMP, so the program cache is
    rebuilt once.
  • Rollback: revert the commits. There is no switch.

Reviewer notes

  • Start with 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.

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

Issues

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

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. Based on 5e072fc8; main has moved since, and it is rebased in one sweep before ready.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
    526 passed, 17 skipped, and the same 19 failures as main on this Mac, named under Testing.
  • Tested in the game, or what wasn't is named under Testing. Run 18 is the game evidence for the cause. A run of this
    head is named under Testing, and the PR stays WIP until it is done.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The test
    request is still to open.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each,
    the median reported. A failing-on-main test and run 18's line, not a game measurement.
  • 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.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.

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

**What 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. **What is measured:** a game-free test compiles `parse_code` from the source over shader packages it builds: - `main` fails 33 of its 39 checks and all 5 capture loops; the previous head of this PR fails the 6 new checks; - this PR passes all 39 and the 5 loops; - 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. Also measured: `vk_shader.cpp` object compiles for arm64 and x86_64, and the full game-free suite on an Apple M4 Max (macOS 26.7.1), compared by test name with `main`. **Not measured:** a game run of this head, so not yet 79116b29 translating in the game; Windows and Linux; the far (JMP/JCAL/JMX) forms going through the Maxwell frontend and the SPIR-V emitter. ## Why A shader program the game uses is left out on every platform: the shader reader refuses it before translating it. ## Problem 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: ``` 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… ``` 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 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. ## Flags and settings None. ## Measurements main: `5e072fc8c2adadba5ea610148be1c47800ba0cca` (2026-11-14) this PR: `fix/shader-branch-past-end` @ `d2111b26a566829f32cb15270d7c9c68692c9201` (previous head `3c1b5551`) 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>`. | | main @ 5e072fc8 | previous head @ 3c1b5551 | this PR @ d2111b26 | |---|---|---|---| | `tests/test_capture_past_end.py` | exit 1: 33 of 39 checks and 5 of 5 loops fail | exit 1: the 6 new checks 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 | the same | | 0x104680 bytes, a BRA at 16404 to its end past 1 MiB | refused | refused, as in run 18 (below) | translated; both BRAs land on their targets | | 12.8 MB, branches and a call more than 8 MiB of lowered code apart | refused | refused | translated: JMP, JMP, JCAL | | `vk_shader.cpp` object, arm64 / x86_64 | exit 0 / 0 (with a one-line `environ` declaration, which `main` lacks on Darwin) | exit 0 / 0 (the same) | exit 0 / 0 (the same) | ``` 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 @ d2111b26: 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 ``` ## Testing **Tested:** - the new test both ways, and on `macos/integrated` 1c53cff1 with this PR's second commit applied (exit 0); - `vk_shader.cpp` objects on arm64 and x86_64, on this head and 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). **The whole suite**, `tests/run_all.py`, 6 at once, on the same Mac (the runner labels macOS `linux`): ``` main @ 5e072fc8: 525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once) this PR @ d2111b26: 526 passed, 19 failed, 17 skipped in 174 s (linux, 6 at once) ``` - **By test name**, the only difference is the new `tests/test_capture_past_end.py` (pass); no other check changed status. The previous head, 3c1b5551, gave the same totals. - **The 19 failures are the same on both.** They are macOS portability failures of `main`, which the macOS port's own pull requests address: - `test_appimage_package_retry`, `test_collect_wide`, `test_control`, `test_framegen_resources`, `test_game_layout`; - `test_low_memory_notice`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_module_ids`; - `test_oot_state_tap`, `test_pad_layout`, `test_pcache_header_read`, `test_pipeline_cache_file`; - `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`. - **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`. **Not tested:** - **A game run of this head**, so whether 79116b29 now translates. 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.** 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. **Needs other testers:** - Windows or Linux, NVIDIA: a start through the shader preparation. Expect no `not translated` line for `79116b290d46542f` (request to open). ## Screenshots Nothing on screen. ## Risk and rollback - **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 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. - **The stamp:** `vk_shader.cpp` and the eden headers are hashed into `LONGSHOT_SHADER_STAMP`, so the program cache is rebuilt once. - **Rollback:** revert the commits. There is no switch. ## Reviewer notes - Start with `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. - **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. ## Issues See #420 (found in the macOS port's start runs; this fix is for every platform). ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [ ] Rebased on current `main`. Based on 5e072fc8; main has moved since, and it is rebased in one sweep before ready. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). 526 passed, 17 skipped, and the same 19 failures as `main` on this Mac, named under Testing. - [x] Tested in the game, or what wasn't is named under Testing. Run 18 is the game evidence for the cause. A run of this head is named under Testing, and the PR stays WIP until it is done. - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The test request is still to open. - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. A failing-on-main test and run 18's line, not a game measurement. - [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. ### If applicable - [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. 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.
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
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/shader-branch-past-end:mooki-fix/shader-branch-past-end'
git switch 'mooki-fix/shader-branch-past-end'
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!427
No description provided.