Skip to content

perf(recompiler): emit wave-uniform values once for both lanes of a two-lane invocation - #304

Merged
boykopovar merged 1 commit into
boykopovar:mainfrom
oneandonlydean:perf/recompiler-two-lane-uniform-values
Oct 3, 2026
Merged

boykopovar merged 1 commit into
boykopovar:mainfrom
oneandonlydean:perf/recompiler-two-lane-uniform-values

Conversation

@oneandonlydean

@oneandonlydean oneandonlydean commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

What

A wave64 compute or mesh program on a 32-wide host runs two lanes per invocation (laneCount == 2), and EmitStructuredBlock emits every IR value once per lane. That includes the values the whole wave shares: each SGPR value, scalar memory load and loop phi exists twice in the module, which raises register pressure for every such program.

Changes:

  • WaveUniformValues (in SpirvAnalysis) finds the values every lane of the wave computes alike. It is a fixed point over the program:
    • Uniform sources: user data, the shader base, resources, ReadConst/ReadConstBuffer, Ballot, ReadLane and ReadFirstLane.
    • Lane sources: LaneId, builtins other than the workgroup id, attributes and interpolation parameters, DPP/permlane/bpermute/swizzle, WriteLane, append/consume, and every buffer, shared, address or image access.
    • A scalar load (an address read of a ScalarAddress resource) counts as uniform when its operands are, since the wave performs it once.
    • Anything else is lane-varying exactly when one of its operands is.
  • For a value in that set that has a type and no side effects, the second lane reuses the first lane's definition instead of emitting the instruction again. Both lanes are one invocation, so they always take the same control flow, and the first lane's definition sits exactly where the second lane's would have been.

I checked that no emitter puts the first lane's definition inside a conditional of its own. The conditionals some emitters open (BDA reads, EmitValueOrZeroIfCondition, BDA fault returns, storage image mip switches) close at their merge block, and the result is an OpPhi there. The per-lane loop emits each instruction unconditionally for both lanes. A forward reference that the second lane allocated earlier through Result gets an OpCopyObject from Define, as before.

On my integration branch, four ray-traced lighting programs and one other wave64 compute program of PPSA21564 compile to about 12% fewer SPIR-V words with this change. On the RTX 4090, the two-lane modules of those programs had needed more than 255 registers and spilled to local memory.

Tested

  • Linux (CachyOS, RTX 4090, NVIDIA 610.57.04, subgroup size 32).
  • agc_shader_memory_tests:
    • verifyWaveUniformValues checks the analysis on a small program. User data, a sum of it, an address resource, a scalar load through a uniform address, a value computed from that load and a ballot are uniform. LaneId, a sum with it, a scalar load at a lane-dependent offset, a global load, a value computed from the global load and a per-lane compare are not.
    • verifyTwoLaneUniformValues recompiles a wave64 program (s_load_dwordx2, s_mul_i32, v_add_nc_u32, buffer_store_dword) for 32- and 64-wide hosts and requires the same number of OpIMul in both. Without the emitter change it fails (a two-lane invocation computes a scalar value once per lane); with the change it passes.
  • New agc_driver_wave_uniform execution test (tests/execution/WaveUniform.cpp, so ctest runs it like the other tests there): a 64-thread wave64 program with an s_buffer_load_dwordx2, a scalar loop whose phis feed a multiply, s_bcnt1_i32_b64 of a VCC ballot and a v_readfirstlane_b32 checks every lane's stored result. It passes on the RTX 4090. As a check of the test itself, wrongly treating builtins as uniform makes it fail (thread 1 stored 1022, expected 1002). Like the other execution tests it exits 77 (skipped) without a Vulkan device, and it also skips on a subgroup narrower than 32 (lavapipe's is 8), where a wave64 does not fit in two lanes.
  • agc_driver_bda_device_tests and agc_driver_mesh_tests pass. agc_driver_recompiler_tests fails with missing register at DWORD 0x207 on main as well.
  • ctest: 106/106 pass, including the execution tests.
  • PPSA21564: this change has been on my integration branch through repeated passes of my visual regression check (from the intro video through the title and save menu into gameplay; checkpoint frames, flicker, video tearing and FPS).

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 in TechnicalDebt
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@github-actions github-actions Bot added build shader core/shader agc libSceAgc* labels Oct 2, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 2, 2026
@oneandonlydean
oneandonlydean force-pushed the perf/recompiler-two-lane-uniform-values branch from 8e9e30b to 984a89c Compare October 2, 2026 08:12
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 2, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 3, 2026
…wo-lane invocation

A wave64 compute or mesh program on a 32-wide host runs two lanes per
invocation, and the emitter emitted every IR value once per lane, scalar ones
included: each SGPR value, scalar memory load and loop phi existed twice in
the module, raising register pressure for every such program.

WaveUniformValues (SpirvAnalysis) finds the values every lane of the wave
computes alike: user data, constants, resources, ballots, readlane and
readfirstlane results, scalar loads through a wave-uniform address, and what
follows from them alone. Lane ids, builtins other than the workgroup id, lane
exchanges and every other memory or image read differ by lane, as does
anything computed from them. For such a value without side effects, the
second lane reuses the first lane's definition. Both lanes are one
invocation, so they share control flow and the definition dominates every
use of the second lane.

agc_shader_memory checks the analysis and that a two-lane module computes a
scalar product as often as the one-lane module. The agc_driver_wave_uniform
execution test runs a wave64 program with a scalar loop, a ballot and a
readfirstlane and checks every lane's result. It skips on a subgroup
narrower than 32, such as lavapipe's.
@oneandonlydean
oneandonlydean force-pushed the perf/recompiler-two-lane-uniform-values branch from 984a89c to a791e7d Compare October 3, 2026 07:02
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 3, 2026
@boykopovar
boykopovar merged commit ff96a3b into boykopovar:main Oct 3, 2026
3 checks passed
@oneandonlydean
oneandonlydean deleted the perf/recompiler-two-lane-uniform-values branch October 3, 2026 07:35
oneandonlydean added a commit to oneandonlydean/AnyPS5 that referenced this pull request Oct 3, 2026
…oykopovar#304 boykopovar#305 boykopovar#306 boykopovar#322 boykopovar#409 boykopovar#416, boykopovar#276 boykopovar#277 boykopovar#275 boykopovar#307, shader ops boykopovar#226 boykopovar#255 boykopovar#260 boykopovar#280 boykopovar#308 boykopovar#309 boykopovar#311 boykopovar#318 boykopovar#320 boykopovar#324 boykopovar#326 boykopovar#379 boykopovar#384 boykopovar#399, boykopovar#261, kernel boykopovar#335 boykopovar#341 boykopovar#349 boykopovar#355, libs boykopovar#227 boykopovar#244 boykopovar#245 boykopovar#273 boykopovar#274 boykopovar#279 boykopovar#286 boykopovar#287 boykopovar#289 boykopovar#296 boykopovar#300 boykopovar#328 boykopovar#358 boykopovar#402) into astrobot

Where our PR merged in a revised form, main's version is taken; where
astrobot carries the same work plus later local commits, astrobot's code
stays and main's additions appear once.

Conflicts:
- AvPlayer (Export.cpp both libs, include/AvPlayer.hpp, src/Player.cpp,
  src/Source.cpp, AvPlayerLibrary.cmake, CMakeLists.txt, SceTypes.hpp,
  tests/GuestAvPlayer.cpp): main's boykopovar#307 (our reviewed squash, which carries
  9c8e7fd e9ea5c8 adcede3 fb4eeb9 953057c ac456ee). It links the
  shared ffmpeg-core target, so astrobot's second FFmpeg build goes:
  3rdparty/FFmpeg submodule, core/libs/FFmpeg.cmake and src/Exports.cpp.
- libScePngEnc: main's boykopovar#227 (supersedes OverkillLabs' encoder).
- libScePad Export.cpp: main's boykopovar#287 tilt correction (supersedes 49d45f5).
- libSceAudioPropagation Export.cpp: astrobot's object model, which
  defines every function boykopovar#289 declares as a stub.
- libSceSaveData.native Export.cpp: both (SaveDataHoldWrites and boykopovar#289's
  unknown exports).
- VideoOutDriver.cpp: astrobot's (2546bd1 counters around boykopovar#416's
  lastFlipVblank store, same value). tests/Flip.cpp: boykopovar#416's pacing test and
  boykopovar#417's after-GPU test both kept.
- boykopovar#278 files (DisplayFormat, VulkanDevice, GpuColorTransfer,
  ResidentPresent, agc CMakeLists): main's comment-free boykopovar#278 plus
  astrobot's P5 present-direct (b29728d), which removed DetileImage,
  ResidentPresent.hpp and the recorder-test hook; the resident present test
  stays its own executable.
- VulkanDevice.cpp/.hpp: astrobot's GDS, local memory probe, sample dumps
  and P5 code; boykopovar#303/boykopovar#409 comments as main has them; main's shader clock
  (boykopovar#324) and DepthSurface-free includes.
- RequestSerializer.cpp: version stays 8 (6 mesh passthrough, 7
  nonConstantImageOffsets, 8 astrobot's later field). boykopovar#303's auto-merged
  second write and read of nonConstantImageOffsets removed.
- SpirvEmitter.hpp, SpirvEmitterState.hpp, Recompiler.hpp/.cpp, StageIO.hpp,
  SpirvOptimizer.cpp, InstructionTranslator.cpp, ShaderReplay.cpp: local
  fields kept, boykopovar#303/boykopovar#305 comments as main has them.
- SpirvAluEmitterMath.cpp: v_readlane keeps the split-wave path and takes
  boykopovar#322's wrap at the wave size for the others.
- SpirvEmitter.cpp: astrobot's single-lane layout check.
- SpirvFlowEmitter.cpp: boykopovar#304 as merged (the second lane emits a shared
  value itself when the first lane defined nothing).
- SpirvImageEmitter.cpp: main's Offset operand placed before MinLod.
- boykopovar#304: main's WaveUniformValues (SpirvAnalysis.cpp) replaces astrobot's
  copy in SpirvWaveExchange.cpp.
- Recorder.hpp/.cpp, Draw.hpp, Graphics Draw.cpp, ShaderResources.cpp:
  astrobot's boykopovar#276/boykopovar#277 with its later snapshot work (per-use budgets,
  stale-snapshot refresh, profile counters, boykopovar#410, boykopovar#424, fan GS); boykopovar#409/boykopovar#306
  comments as main has them.
- PacketMemory.cpp: boykopovar#409 as merged plus astrobot's two opt-outs.
- tests: ShaderMemory.cpp takes boykopovar#303's and boykopovar#304's new tests; Recorder.cpp
  takes boykopovar#409's afterRecordedWorkTests and drops main's copies of the
  boykopovar#276/boykopovar#277 tests astrobot already has; Structurizer.cpp keeps boykopovar#413's case.

boykopovar#143 (NGG passthrough) stays as on astrobot: upstream did not touch its
files since the boykopovar#258 revert.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants