Skip to content

perf(shader): run wave64 compute at one lane where no lane can tell - #12324

Draft
meh7an wants to merge 2 commits into
boykopovar:mainfrom
meh7an:perf/shader-single-lane-compute
Draft

meh7an wants to merge 2 commits into
boykopovar:mainfrom
meh7an:perf/shader-single-lane-compute

Conversation

@meh7an

@meh7an meh7an commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

Draft. With the sound proof below, no program on the measured S3K page that takes more than 1 ms qualifies, so the change shows no gain there. It stays a draft until a heavy compute program in some title qualifies.

On a 32-lane host, main runs a wave64 compute program at two guest lanes per invocation (laneCount 2), so each invocation does every ALU operation, load and sample twice. This PR proves, on the IR, which wave64 compute programs give every lane the same results at one guest lane per invocation, where each host subgroup holds half a wave, and emits those programs that way. The emitter already supports this layout: APS5_SINGLE_LANE=<addresses> forces it by hand. Two commits:

  1. perf(shader): fold bitwise self and complement identities (ConstantFolder). It folds And32/Or32/Xor32 of a value with itself, with its complement in either order, and with an absorbing constant (x & 0, x | ~0). It also folds bit 0 of all ones shifted right by an amount masked to 0-31 to one, and 0 >> y to zero. s_orn2_saveexec_b64 of exec gives exec | ~exec, so its lane bits and the exec selects they guard fold away. The half-wave scan matcher (2489437) proved "every lane" only from the exec | ~exec structure on hosts as wide as the wave, which no longer survives the fold. A constant-true lane bit now counts as every lane on every host, the same proof, so EveryLaneBit and its narrow-only flag go. On its own this gave no measurable gain; it simplifies the lane masks the second commit reads.

  2. perf(shader): run wave64 compute at one lane where no lane can tell. SingleLaneAnalysis decides before emission. A program qualifies when:

    • no lane reads another lane's value by index: readlane and readfirstlane read only uniform values, and there are no permutes, DPP, swizzles or GPU-selected buffer descriptors;
    • lane masks are combined bit by bit and read only as each lane's own bit, with the same expression over both 32-bit halves;
    • LDS is lane-private: every access is one affine function of the local invocation id and no two lanes' footprints share a byte, and a one-wave workgroup does not write it;
    • each term of a VCC branch's predicate is uniform or implied by the exec mask at the branch;
    • nothing leaves a region that one half of the wave can skip unless it is the same whether that half ran the region or not;
    • no barrier shares the program with a branch one half can decide alone.

    Skipped regions. At one lane, EmitWaveMaskBranchCondition decides execz, execnz, vccz and vccnz per host subgroup, so a half with no active lane skips what the whole wave runs. For each exec branch, and each VCC branch on lane-varying terms, the region is what the taken side reaches before the branch's immediate post-dominator and the other side does not. A value leaving it qualifies when it is a VGPR written under the branch's exec, a lane-mask word that keeps its bits outside that exec, or a vector-window store that keeps the old value outside it. Scalars, VCC and goto flags written in the region keep two lanes. A value no instruction reads, such as a phi cycle that only feeds itself, leaves nothing to compare. A memory write in the region needs a lane predicate inside the branch's exec. A loop left through such a branch must carry values that stay the same per lane and an exec that only narrows. Workgroups of at most 32 lanes get every check, since whole-wave mode still decides branches per half. The check runs in the first simplify pass, before VectorWindowStoreEliminator and MaskedSelectEliminator drop the exec selects whose inactive lanes nothing reads, and IrProgramMetadata keeps its verdict for the layout.

    Relatively addressed VGPRs live in a function-scope window per lane (LoadVectorWindow/StoreVectorWindow, f8ed012), so window accesses are lane-local; a window load counts as lane-varying, so readfirstlane of one still keeps two lanes. Only hosts whose wave64 compute target is 32 lanes wide are candidates. A qualifying program is emitted with a 64-lane hostSubgroupSize, as APS5_SINGLE_LANE does, and the artifact records it; computeSubgroupSize then still requires 32-lane subgroups where the device can.

    • BlockInfo::exec records the exec mask at each VCC and exec branch. ReclaimUnusedValues, ResolveControlFlowIdentities and RedundantValueEliminator keep it current, as they do condition.
    • The disk cache key marks the requests the rule lays out, so a run with APS5_NO_SINGLE_LANE_RULE=1, which keeps every program at two lanes, does not load their results. APS5_TRACE_SINGLE_LANE=1 prints each program's verdict and the reason a program keeps two lanes. Both are in docs/dev/ENVIRONMENT.md.
    • agc_shader_memory checks the two-lane SPIR-V of 64-thread programs, so it sets APS5_NO_SINGLE_LANE_RULE.

Correction to the first version. The first push admitted S3K's heaviest compute program and measured it at 4.87 to 2.82 ms per present. That admission was wrong. The program's loop at guest 0x2bfc is a per-lane state machine in v32. Its exit test runs on alive & state == 0, but the body then restores exec to every live lane and runs the state-2 handler (buffer loads, lane kills) for lanes still in state 2. A half with no state-0 lane would leave the loop at one lane while the whole wave keeps running that half's state-2 lanes. The program depends on the other half and keeps two lanes now.

Relation to #5405, #7922 and #8004. Spanned waves (APS5_SPANNED_WAVE) cover exactly-64-thread workgroups whose lanes exchange values, through workgroup barriers. This proof needs no exchange and covers any workgroup size, including multi-wave groups, which never span. A proven 64-thread program takes one lane without spanning; a program the proof rejects still spans when spanning is on, with or without #7922's default. Neither approach covers the other.

Tested

Windows 11 (10.0.26300), NVIDIA GeForce RTX 2080 Ti, driver 617.14, i9-9900K, GCC 15.2 (WinLibs), Release. Base: main f8f5b24; this PR's head is 7a8da11, with no other PR applied.

  • agc_single_lane (35 cases), run twice by ctest: agc_single_lane and agc_single_lane_off, the second with APS5_NO_SINGLE_LANE_RULE=1, where its end-to-end Recompile case expects a 32-lane layout instead of 64.
    • accepted: lane-private LDS in one and two dimensions, a lane's own ballot bit, readfirstlane of user data, a vector window indexed by user data, a VCC branch on a uniform compare under the branch's exec, an exec-masked VGPR and a kept-value window store under s_cbranch_execz, an execz loop carrying an exec-masked VGPR out, an exec-masked VGPR carried through a loop inside a skippable region, and a scalar written in a skippable region that nothing reads;
    • rejected: LDS shared between lanes or read past a lane's footprint, LDS at a loaded address, LDS writes in a one-wave group, a counted ballot, a mask whose halves differ, the lane id read as data (also in a group of at most 32 lanes, which now gets every check), readfirstlane of a local id or of a window load, a VCC branch on a lane compare or on bits from a narrower exec, a VCC branch without its exec, DPP, lane 63 read in whole-wave mode, a barrier beside exec branches (also in a 32-thread group and beside a VCC branch on exec-implied bits), a scalar, a VCC word, a whole-wave VGPR write and an unmasked window store leaving a skippable region, a scalar loop counter used after an execz loop, and a loop whose exit test reads fewer lanes than its body runs;
    • plus one fold case: a select on a lane's bit of ~exec | exec folds to its true arm;
    • with the two last proof fixes reverted (values no instruction reads, and phi cycles in the skipped-value walk), exactly the two cases written for them fail.
  • agc_bitwise_identities (8 cases) covers the first commit.
  • agc_shader_memory: a wave64 compute UMin scan under s_or_saveexec_b64 -1 on a 32-lane target stays at two lanes and emits no OpGroupNonUniformUMin, since the scan matchers run only for non-compute stages at one lane (SpirvAluEmitterMath.cpp:1151). Next to it, the pixel version on 64 lanes still reads lanes 31 and 63 as two half-wave reductions.
  • Full ctest on this PR: 2840 of 2840 pass. guest_kernel_sys_threads_main, guest_kernel_sys_threads_main_patched and net_ipv6only_patched were left out: on main they stop with Windows' "Entry Point Not Found" dialog, which test(libs): give the prx loader tests the MinGW runtime on their PATH #12426 fixes.
  • Validation layer. 110 focused tests ran with ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1 and VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation through ctest --test-dir build -R <pattern> -V: the lane, exec and scalar-mask tests, the folder tests, the three new targets, and the 85 driver tests that took one-lane programs under the first version. All 110 pass on the change, and the 107 of them that exist on main f8f5b24 pass there. Neither log has a VUID- error or a was not found but was requested by env var VK_INSTANCE_LAYERS line.
  • Sonic Origins (PPSA05324), the S3K main-menu page, 3840x2160. The page's 9 compute programs were dumped with APS5_DUMP_SHADERS=1 and replayed through agc_shader_replay with APS5_TRACE_SINGLE_LANE=1. Five run at one lane; of these only 0x20092a600 runs on every page frame, and it is small. Four keep two lanes: the 16-thread writer (a lane mask whose halves differ), a 512-thread program with a VCC branch on lane-varying bits, one with FPInterpolateF16, and the heaviest one (16x16 threads), whose state-machine loop the correction above describes.
  • APS5_PROFILE_GPU=1: one boot per arm, warm caches, 12 page readings, [gputime] blocks 4 onward (9 on main, 8 on this PR):
per present main this PR
heaviest compute (two lanes in both) 5.37 ms 5.43 ms
16-thread writer (two lanes in both) 4.23 ms 4.10 ms
0x20092a600, 5 dispatches (one lane in this PR) 0.91 ms 0.89 ms
all compute 10.51 ms 10.42 ms
all GPU ranges 68.22 ms 66.71 ms
draw class 31.29 ms 30.69 ms
FPS (median, profiled) 13.59 13.25

No change beyond block-to-block noise; main's first block (74.0 ms) is its own outlier. With no speed claim, I took one pair and no traces. The traces attached to the first version measured the wrong admission and no longer apply.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 2080 Ti, driver 617.14, Windows 11
  • AI-assisted: yes

@github-actions github-actions Bot added the perf Performance change label Oct 28, 2026

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a safety-check bypass in analyze(): the workgroup size <= 32 fast path returns eligible before scanning instructions or applying the later guards. In particular, it skips the explicit barrier && execBranches rejection (and the tests only cover that rejection with the default 16x16 workgroup). A wave64 compute program with 32 or fewer work items, a barrier, and an ExecZero/ExecNonZero branch is therefore emitted with laneCount = 1 despite the analysis explicitly rejecting barriers in programs with exec branches. That can put a workgroup barrier under non-uniform control flow and hang or invalidate the dispatch. Please retain the simple one-to-one lane mapping optimization for small groups, but still run the structural checks that apply to them; add a <=32-work-item regression containing an exec branch and barrier and require the analysis to reject it.

I have not approved this shader change. The current report has extensive tests and a same-scene performance pair, but that does not cover this early-return path.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1896 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 4472 docs/dev/ENVIRONMENT.md
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/Optimization/src/ConstantFolder.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5609 core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp
core/shader/recompiler/Recompiler.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/CMakeLists.txt
PR 6146 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6290 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 6416 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6858 core/shader/recompiler/Recompiler.cpp
PR 6924 core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 7085 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7395 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 7410 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 7449 docs/dev/ENVIRONMENT.md
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 7508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 7522 core/shader/recompiler/Recompiler.cpp
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 7622 core/shader/recompiler/CMakeLists.txt
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 7643 docs/dev/ENVIRONMENT.md
PR 7684 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
PR 7689 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8004 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 8089 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8246 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8939 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 9012 docs/dev/ENVIRONMENT.md
PR 9020 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 9248 docs/dev/ENVIRONMENT.md
PR 9289 docs/dev/ENVIRONMENT.md
PR 9335 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 9345 docs/dev/ENVIRONMENT.md
PR 9356 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 9387 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 9457 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 9508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 9633 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 9676 docs/dev/ENVIRONMENT.md
PR 9712 docs/dev/ENVIRONMENT.md
PR 9748 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 9749 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 9790 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 9852 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 9875 docs/dev/ENVIRONMENT.md
PR 9889 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 9897 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 9969 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 10043 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 10193 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 10233 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 10351 core/shader/recompiler/Recompiler.cpp
PR 10359 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 10442 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 10443 docs/dev/ENVIRONMENT.md
PR 10489 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 10652 core/shader/recompiler/Recompiler.cpp
PR 10678 docs/dev/ENVIRONMENT.md
PR 10867 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 10894 docs/dev/ENVIRONMENT.md
PR 10920 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 11435 docs/dev/ENVIRONMENT.md
PR 11436 docs/dev/ENVIRONMENT.md
PR 11483 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 11551 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 12458 core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp
PR 12544 docs/dev/ENVIRONMENT.md
PR 12745 docs/dev/ENVIRONMENT.md
PR 12746 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 12790 core/shader/recompiler/Recompiler.cpp
PR 12828 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 12840 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 12848 docs/dev/ENVIRONMENT.md
PR 12849 docs/dev/ENVIRONMENT.md
PR 12855 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 12863 docs/dev/ENVIRONMENT.md
PR 12895 docs/dev/ENVIRONMENT.md
PR 12930 docs/dev/ENVIRONMENT.md
PR 12944 core/shader/recompiler/Recompiler.cpp
PR 12970 docs/dev/ENVIRONMENT.md
PR 12994 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 13148 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 13263 docs/dev/ENVIRONMENT.md
PR 13276 core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 13486 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Optimization/src/ConstantFolder.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
docs/dev/ENVIRONMENT.md
PR 13707 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 13793 docs/dev/ENVIRONMENT.md
PR 13809 docs/dev/ENVIRONMENT.md
PR 13863 docs/dev/ENVIRONMENT.md
PR 13920 docs/dev/ENVIRONMENT.md
PR 13928 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 13929 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp
core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 14032 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 14051 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 14068 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 14072 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 14217 core/shader/recompiler/Recompiler.cpp
PR 14218 core/shader/recompiler/Recompiler.cpp
PR 14260 docs/dev/ENVIRONMENT.md
PR 14262 docs/dev/ENVIRONMENT.md
PR 14341 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 14344 docs/dev/ENVIRONMENT.md
PR 14367 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 14561 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 14636 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 14794 docs/dev/ENVIRONMENT.md
PR 14830 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 14853 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 14871 core/shader/recompiler/Recompiler.cpp
PR 14904 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 14944 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 15015 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 15059 docs/dev/ENVIRONMENT.md
PR 15136 docs/dev/ENVIRONMENT.md
PR 15169 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 15181 docs/dev/ENVIRONMENT.md
PR 15334 core/shader/recompiler/Recompiler.cpp
PR 15338 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15343 core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 15398 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 15416 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15417 docs/dev/ENVIRONMENT.md
PR 15423 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 15456 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 15458 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15488 core/shader/recompiler/CMakeLists.txt
PR 15512 core/shader/recompiler/CMakeLists.txt
PR 15520 docs/dev/ENVIRONMENT.md
PR 15529 docs/dev/ENVIRONMENT.md
PR 15548 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 15555 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp
PR 15569 docs/dev/ENVIRONMENT.md
PR 15570 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 15572 docs/dev/ENVIRONMENT.md
PR 15573 core/shader/recompiler/CMakeLists.txt
PR 15608 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15611 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15671 docs/dev/ENVIRONMENT.md
PR 15674 docs/dev/ENVIRONMENT.md
PR 15680 core/shader/recompiler/CMakeLists.txt
PR 15719 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 15738 docs/dev/ENVIRONMENT.md
PR 15792 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 15917 core/shader/recompiler/Recompiler.cpp
docs/dev/ENVIRONMENT.md
PR 15918 docs/dev/ENVIRONMENT.md
PR 15919 docs/dev/ENVIRONMENT.md
PR 15948 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 15974 docs/dev/ENVIRONMENT.md
PR 16088 core/shader/recompiler/CMakeLists.txt
PR 16115 docs/dev/ENVIRONMENT.md
PR 16116 docs/dev/ENVIRONMENT.md
PR 16147 core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp
PR 16217 docs/dev/ENVIRONMENT.md
PR 16234 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 16260 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 16286 docs/dev/ENVIRONMENT.md
PR 16294 docs/dev/ENVIRONMENT.md
PR 16350 core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp
PR 16371 docs/dev/ENVIRONMENT.md
PR 16372 docs/dev/ENVIRONMENT.md
PR 16391 core/shader/recompiler/CMakeLists.txt
core/shader/recompiler/Recompiler.cpp
PR 16413 core/shader/recompiler/Recompiler.cpp
PR 16420 core/shader/recompiler/CMakeLists.txt
PR 16427 docs/dev/ENVIRONMENT.md
PR 16472 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp
core/shader/recompiler/Recompiler.cpp

@meh7an

meh7an commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 The early return is deliberate. A workgroup of at most 32 work items fits in one 32-lane host subgroup at either layout, the same subgroup that holds it at two lanes per invocation on main (with the second lane idle). Exec branches are decided by a subgroup ballot (EmitWaveMaskBranchCondition), so every invocation of such a workgroup takes the same branch, and a barrier stays in uniform control flow. The rule against barriers beside exec branches exists for larger groups, where one wave spans two subgroups and each half decides its exec branches on its own. At 32 or fewer it has nothing to catch, so no change here.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@meh7an thanks, the proof is carefully built and the negatives are good. There is one case I don't think it covers yet: scalar values that leave a region one half of the wave can skip.

At one lane per invocation, each 32-lane host subgroup decides s_cbranch_execz/execnz (and an exec-implied VCC branch) for its own half. Vector writes inside the region are Select(exec, new, old), so a half that skips keeps the same values it would have had. Scalar writes are not exec-guarded, though, so a scalar value assigned in the region reaches the merge phi as the old value in the skipping half and as the new value in the other half. On hardware the whole wave runs the region whenever any lane is active:

v_cmp_gt_u32 vcc, 32, v0          ; 64x1 workgroup, lanes 0-31
s_and_saveexec_b64 s[0:1], vcc
s_cbranch_execz skip
s_mov_b32 s2, 1
skip:
s_or_b64 exec, exec, s[0:1]
v_mov_b32 v1, s2                  ; stored per lane

Hardware: every lane gets 1. Single-lane: lanes 32-63 get the old s2. As far as I can tell, every rule passes here: there is no barrier, the saved exec is a symmetric ballot pair combined bit by bit, and Uniformity treats the phi as uniform because it doesn't model control dependence. A loop that leaves through s_cbranch_execnz with a scalar counter used after the loop has the same problem: each half iterates to its own maximum, not the wave's.

Compiler output normally sends such live-outs through VGPRs, but the analysis claims a proof. Could it reject this shape? One option: at the merge of an exec/VCC branch, or at the exit of a loop whose back edge is an exec/VCC branch, reject any phi whose incoming value from the region isn't exec-selected (or isn't identical on both paths). A negative case for the snippet above in agc_single_lane would pin it down.

A smaller question: the first commit's AllLanesBit change makes a constant-true lane bit count as every lane in MatchHalfWaveScan/MatchRowScan for all programs, not just single-lane ones (on main that was narrow-only). Is there a test that covers a two-lane (laneCount 2) program matching a scan through a constant-true bit?

One more question: SingleLaneAnalysis returns early for workgroups of 32 threads or fewer and skips every check. Whole-wave mode (s_or_saveexec_b64 with -1, as used for wave-wide scans) can activate lanes 32-63, and main emulates those in the second slot, which the one-lane layout doesn't have. Does the shortcut still hold for programs that enter whole-wave mode, or should it require that none do?

@meh7an

meh7an commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, all three hold up.

  1. Confirmed. writeRawU32 writes SGPRs unmasked while VGPR writes are Select(exec, new, old), and at one lane EmitWaveMaskBranchCondition decides execz/execnz/vccz/vccnz per host subgroup, so a half with no active lane can skip what the wave runs. The same gap covers VCC words a v_cmp writes in that region (inactive lanes get 0 on hardware), structurizer goto flags set there, and vector-window stores. I'll add a live-out rule: for each exec or VCC branch, every value defined in the blocks the wave can run while a half without active lanes skips them, and used after them (merge phis, loop exits), must leave that half's value unchanged: an exec-masked select chain whose mask implies the exec at the branch, or a lane bit or mask word that is 0 for those lanes either way. For a loop left through such a branch, the masks are checked against the loop header's exec. Scalars, flags, unmasked and whole-wave writes keep two lanes. Negative cases: your snippet, an execz-exited loop with a scalar counter used after it, a VCC written in the region and read after the merge, and a whole-wave write in the region; positive: an exec-masked VGPR live-out of each.
  2. No, nothing covers it at two lanes: verifyHalfWaveReduction runs pixel programs on 32- and 64-lane hosts. The change keeps main's matches: main accepted the exec | ~exec structure on every host, the fold turns it into a constant-true lane bit, and that bit is the same proof. I'll add a wave64 compute case of the GFX10 reduction under s_or_saveexec_b64 -1 on a 32-lane target, which stays at two lanes, and check that it still reads as subgroup reductions.
  3. You're right. A group of 32 or fewer starts with LocalInvocationIndex < size as its exec, two lanes keep lanes 32-63 in the second slot where whole-wave mode can wake them, and the shortcut accepts the cross-lane reads that carry their values back. I'll drop the early return so every size runs the full checks, with a test of a 32-thread group reading lane 63 in whole-wave mode.

These can change which programs qualify, so I'll re-measure the S3K pair and update the numbers with the push.

@adriadam10 adriadam10 self-assigned this Oct 28, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful proof and the negative cases. The head is still 5d770a5ff0, and the three gaps raised in this thread (which you confirmed) are open, so this can't merge yet:

  • SingleLaneAnalysis.cpp:925: the <= 32 early return skips every check, including barrier && execBranches (line 984). Please run the structural checks for small groups too, and add a <=32-thread regression with an exec branch and a barrier that must be rejected (@MarcZX10's request).
  • Same early return: whole-wave mode (s_or_saveexec_b64 -1) wakes lanes 32-63, which exist only in the second slot at two lanes. Drop the shortcut and add the 32-thread group reading lane 63 in whole-wave mode, as you proposed.
  • Scalar/VCC/goto-flag/window-store values defined in a region that one half can skip via execz/execnz/exec-implied VCC reach the merge phi (or loop exit) with different values per half. Uniformity treats those phis as uniform. Please add the live-out rule you described, with the negatives (@oneandonlydean's snippet, an execz-exited loop with a scalar counter, a VCC written in the region and read after it, a whole-wave write in the region) and the exec-masked VGPR positives.
  • The first commit makes a constant-true lane bit count as every lane in MatchHalfWaveScan/MatchRowScan for two-lane programs too. Please add the wave64 compute GFX10 reduction under s_or_saveexec_b64 -1 on a 32-lane target that you described.

Please also refresh the S3K numbers after the push, since the live-out rule may change which programs qualify.

Fold And32, Or32 and Xor32 of a value with itself, with its complement
and with an absorbing constant, bit 0 of all ones shifted right by an
amount masked to 0-31, and 0 shifted right by any amount. The lane masks
the translator builds reduce this way: s_orn2_saveexec_b64 of exec gives
exec | ~exec, all ones, so its lane bits and the exec selects they guard
fold away.

The half-wave scan matcher (2489437) took a constant-true lane bit
only on hosts narrower than the wave and otherwise required the
exec | ~exec structure, which no longer survives the fold. A constant-true
bit now counts as every lane on every host, the same proof as x | ~x, so
EveryLaneBit and its narrow-only flag go.
On a 32-lane host a wave64 compute program runs two guest lanes per
invocation, so each invocation does every ALU operation, load and sample
twice. SingleLaneAnalysis proves on the IR that the program gives every
lane the same results at one guest lane per invocation, where each host
subgroup holds half a wave, and the emitter then lays it out that way.
A program qualifies when:
- no lane reads another lane's value by index: readlane and readfirstlane
  read only uniform values, and there are no permutes, DPP, swizzles or
  GPU-selected buffer descriptors;
- lane masks are combined bit by bit and read only as each lane's own
  bit, with the same expression over both 32-bit halves;
- LDS is lane-private: every access is one affine function of the local
  invocation id and no two lanes' footprints share a byte, and a one-wave
  workgroup does not write it;
- each term of a VCC branch's predicate is uniform or implied by the exec
  mask at the branch;
- nothing leaves a region that one half of the wave can skip unless it is
  the same whether that half ran the region or not;
- no barrier shares the program with a branch one half can decide alone.

A half decides an exec branch, and a VCC branch on lane-varying terms,
by itself. For each such branch the skipped region is what the taken side
reaches before the immediate post-dominator and the other side does not.
A value leaving it qualifies when it is a VGPR written under the branch's
exec, a lane mask word that keeps its bits outside that exec, or a vector
window store that keeps the old value outside it; scalar, VCC and goto
flag values written in the region reject. A value no instruction reads,
such as a phi cycle that only feeds itself, leaves nothing to compare. A
memory write in the region needs a lane predicate inside the branch's
exec. A loop left through such a branch must carry values that stay the
same per lane and an exec that only narrows. Workgroups of at most 32
lanes get the same checks, since whole-wave mode still decides branches
per half.

The region check runs in the first simplify pass, before the vector
window store and masked select eliminators drop exec selects whose
inactive lanes nothing reads, and the program metadata keeps its verdict
for the layout. Relatively addressed VGPRs live in a function-scope
window per lane (LoadVectorWindow, StoreVectorWindow), so window accesses
are lane-local; a window load counts as lane-varying, so readfirstlane of
one keeps two lanes.

BlockInfo::exec records the exec mask at each VCC and exec branch; the IR
reclaim, identity resolution and redundant value elimination keep it
current. A proven program needs no spanned layout, so APS5_SPANNED_WAVE
applies only to programs the analysis rejects.

The disk cache key marks the requests the rule lays out, so a run with
APS5_NO_SINGLE_LANE_RULE=1 does not load their results.
APS5_TRACE_SINGLE_LANE=1 prints each program's verdict and the reason a
program keeps two lanes.
@github-actions github-actions Bot added build shader core/shader docs agc libSceAgc* labels Oct 28, 2026
@meh7an
meh7an force-pushed the perf/shader-single-lane-compute branch from 5d770a5 to 7a8da11 Compare October 28, 2026 18:06
@meh7an
meh7an marked this pull request as draft October 28, 2026 18:06
@github-actions github-actions Bot added the perf numbers missing perf pull request without a before and after summary line label Oct 28, 2026
@github-actions

Copy link
Copy Markdown

This pull request is labeled perf, so it needs the summary line of a capture on main and one with the change, from the same title, scene and machine, as the Performance report form asks. Please paste both aps5-summary.jsonl lines, attach the two traces and add the output of tools/trace_digest.py.

@meh7an

meh7an commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean @adriadam10 updated in 7a8da11, on main f8f5b24. I've made the PR a draft; the numbers are at the end.

  1. Live-out rule. For each branch one half can decide alone (exec branches, and VCC branches on lane-varying terms), the region is what the taken side reaches before the branch's immediate post-dominator and the other side does not. A value leaving it qualifies only as a VGPR written under the branch's exec, a lane-mask word that keeps its bits outside that exec, or a vector-window store that keeps the old value. Scalars, VCC and goto flags written there keep two lanes. Memory writes in the region need a lane predicate inside the branch's exec. A loop left through such a branch must carry per-lane-stable values and an exec that only narrows. The check runs in the first simplify pass, before VectorWindowStoreEliminator and MaskedSelectEliminator drop exec selects, since on the final IR a dead-lane write looks like a whole-wave one. Values no instruction reads, such as phi cycles that only feed themselves, are ignored. Your snippet and the other shapes are tests now: a scalar, a VCC word, a whole-wave VGPR write and an unmasked window store leaving the region, a scalar loop counter, and a loop whose exit test reads fewer lanes than its body runs.
  2. A correction to my earlier answer. I said the compute scan would still read as subgroup reductions. In fact the scan matchers run only for non-compute stages at one lane (SpirvAluEmitterMath.cpp:1151), so no two-lane compute program reaches them. The ShaderMemory case I added checks that: a wave64 compute UMin scan under s_or_saveexec_b64 -1 on a 32-lane target emits no UMin reductions. A pixel case on 64 lanes next to it still reads lanes 31 and 63 as two half-wave reductions.
  3. The 32-or-fewer early return is gone. Tests cover a small group under the full checks, a 32-thread group reading lane 63 in whole-wave mode, and a barrier beside s_cbranch_execz in a 32-thread group.

Re-measure. With the sound rule, S3K's heaviest compute (16x16 threads, about 5.4 ms per present) keeps two lanes, and rightly. Its loop at guest 0x2bfc is a per-lane state machine: the exit test runs on alive & state == 0, but the body then restores exec to every live lane and runs the state-2 handler for lanes still in state 2. A half with no state-0 lane would leave at one lane while the whole wave keeps running its state-2 lanes. The first version admitted this program, and its 4.87 to 2.82 ms came from that. On main f8f5b24 against this PR, page GPU time is flat (all compute 10.51 against 10.42 ms per present; table in the body). That's why it's a draft until a heavy program somewhere qualifies. This draft makes no speed claim; the captures the perf label asks for (summary lines, traces and digests) will come with the head that takes it out of draft.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked — prior finding resolved

I inspected the current head 7a8da113db96ef882b362d52af10e10ee08a9773. AnalyzeSingleLane no longer has the small-workgroup early return; it runs the branch/uniformity checks and rejects barrier && execBranches before returning eligibility. The added 32-thread barrier/exec-branch case directly covers the scenario from my earlier review, so I withdraw that blocker.

The author also reports adding live-out checks and regressions for the scalar/VCC/goto/window/loop cases raised by the other reviewer. I have not independently audited that new analysis or run its shader tests in this pass, so this comment is not an approval of the whole compiler change. The author has kept the PR in draft because the revised S3K comparison is flat and the large workload remains on two lanes; the description makes no current speedup claim. That seems appropriate while there is no measured benefit to support a performance PR.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs needs rebase Conflicts with main, rebase needed perf numbers missing perf pull request without a before and after summary line perf Performance change shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants