WIP: fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left out #427
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!427
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/shader-branch-past-end"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is for: players on every platform and GPU. Compute shader program
79116b290d46542fis never translated,on any renderer path. Its stage is refused before translation for "a branch outside the program". Two causes are
fixed here:
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_codefrom the source over shader packages it builds:mainfails 33 of its 39 checks and all 5 capture loops; the previous head of this PR fails the 6 new checks;that line was quoted.
Also measured:
vk_shader.cppobject 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
The capture reads past a program's end.
parse_code(gpu/vk/vk_shader.cpp) reads an NVN2 stage's Ampereinstructions up to the code length (+0x38). After each one,
ProgramCaptureState::Observedecides whether theprogram has ended. Its end rules are narrower than the lowering's, which decides the real end (
finish,source_size):lower_to_maxwell.h, "the closingself-branch"), which the capture takes as the end only after a computed branch;
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.
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 thenative address map (
count > 65536). Run 18, an integrated build with the first commit of this PR, shows that thisis 79116b29's case:
0x104680 bytes is 66,664 instructions.
Eden's
Maxwell3D::Regs::Pipeline::offset(src/video_core/engines/maxwell_3d.h) andKeplerCompute'sLaunchParams::program_start(kepler_compute.h). Its relative branches reach 24 bits (±8 MiB) and its absoluteones 32 bits:
frontend/maxwell/instruction.h,BitField<20, 24, s64> offsetandBitField<20, 32, u64> absolute. An Ampere BRA's offset has 50 bits, as the capture decodes it.TryFindSize,MAXIMUM_SIZE = 0x100000,shader_environment.cpp). When that fails they build the control-flow graph to get thereal 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 inthe four other copies of the loop:
gpu/render/rx_shader.cppread_nvn_code;gpu/shaderbatch.cppbuild_pkgandnx2/bake_ds/bake_ds.cpp;gpu/shadertool.cpp.The lowering then decides:
vk: NVN2 program K (stage S) ends at +E, before the word at +X that would branch outside it (L code bytes);main's text and adds the code sizeand 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_endrefusesan 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/edenand in the WebGPU renderer'sgpu/third_party/suyucopy.lower_to_maxwell.hnowincludes
program_address_map.h.Far branches in the lowering (
LowerToMaxwellWith), both copies. The lowered Maxwell code holds each branch in the24-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):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) compilesvk_shader.cpp'shash_bytes,Code,code_keyandparse_codewith the recompiler's headers (--sourceselects a checkout), over packages it builds: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;
mainparses: unchanged;@P0 BRAat instruction 16404 to its EXIT, past 1 MiB, and a@P1 BRAback. Ittranslates, each BRA lands on its target, and the address map finds both ends;
apart: the branches become JMPs and the call a JCAL, each holding its target's address, the predicate kept;
Unchanged:
third_party/;native; aprogram 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 head3c1b5551)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:
--sourceonmain's and the previous head's files (git archive);vk_shader.cppobjects 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 amainrun 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>.5e072fc83c1b5551d2111b26tests/test_capture_past_end.pygoes outside the programvk_shader.cppobject, arm64 / x86_64environdeclaration, whichmainlacks on Darwin)Testing
Tested:
macos/integrated1c53cff1 with this PR's second commit applied (exit 0);vk_shader.cppobjects on arm64 and x86_64, on this head and on that integrated tree;(exit 0).
The whole suite,
tests/run_all.py, 6 at once, on the same Mac (the runner labels macOSlinux):tests/test_capture_past_end.py(pass); no other check changed status.The previous head,
3c1b5551, gave the same totals.main, which the macOS port's ownpull 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.tests/test_bake_mimo.py, which builds the recompiler,gpu/shaderbatch.cppand the eden Ampere headers, and runsthem;
tests/test_lower_end_at_closing_branch.py,tests/test_bake_ds.py,tests/test_log_levels.pyandtests/test_compute_local_memory.py.Not tested:
vk: NVN2 program 79116b290d46542f (stage 5) …with nonot translatedline: translated;[trace] … a network left out on this card, as designed.lowered words. The frontend's handling was read, not run:
control_flow.cpptakes JMP/JCAL/JMX throughIsAbsoluteJump, and a block ends before its branch. A JMX needs a jump table, which no test builds. A program needsmore than 8 MiB of lowered code for any of them to appear.
rx_shader.cpp(needs wgpu-native's headers),bake_ds.cpp(fmt and boost) andshadertool.cpp(MSVC with the Eglantine clone). Their edits mirrorvk_shader.cpp's.Needs other testers:
not translatedline for79116b290d46542f(request to open).Screenshots
Nothing on screen.
Risk and rollback
mainrefuses (a stray word, or a program over 1 MiB) and programs with morethan 8 MiB of lowered code. Others get the same words.
inside the program. An indirect branch's targets are checked when it translates.
vk_shader.cppand the eden headers are hashed intoLONGSHOT_SHADER_STAMP, so the program cache isrebuilt once.
Reviewer notes
Start with
program_address_map.h(the limit) and thefinishlambda inlower_to_maxwell.h(the absoluteforms), then
parse_code's capture loop. The suyu copies and the other four loops are the same change.Sources read:
eden-emulator/mirror@4a2c9234:shader_environment.cpp,shader_cache.cpp,maxwell/control_flow.cppandinstruction.h,engines/maxwell_3d.handkepler_compute.h;alula/Ryujinx@4930dced:Decoders/Decoder.csandTranslation/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
type(scope): description).main. Based on5e072fc8; main has moved since, and it is rebased in one sweep before ready.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).526 passed, 17 skipped, and the same 19 failures as
mainon this Mac, named under Testing.head is named under Testing, and the PR stays WIP until it is done.
request is still to open.
the median reported. A failing-on-main test and run 18's line, not a game measurement.
third_party/untouched (a new folder with aPROVENANCE.mdinstead).gpu/third_party/suyu's Ampere headers areLS's own lowering, changed in step with
gpu/vk/eden's (as earlier LS commits did); top-levelthird_party/isuntouched.
If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 [email protected]
WIP: fix(vk): a word after a shader program's end that reads as a stray branch no longer leaves the program outto WIP: fix(vk): shader programs over 1 MiB, or with a stray branch after their end, are no longer left outIndependent 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.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.