Skip to content

perf(recompiler): keep relatively addressed vector registers in a window array - #3856

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
ErenYeager1836:carry/recompiler-vector-window
Oct 11, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
ErenYeager1836:carry/recompiler-vector-window

Conversation

@ErenYeager1836

@ErenYeager1836 ErenYeager1836 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

Carrying @Mouad-Scriptz-0's fix from #3842 (commit f8ed012).

v_movrels_b32, v_movreld_b32, v_movrelsd_b32, v_movrelsd_2_b32 and v_swaprel_b32 lowered to a compare-and-select chain over every register from the base to the program's last allocated register, so a program addressing 154 registers 104 times turned 3176 ISA words into 649,515 SPIR-V words that Intel's compiler did not finish. The registers from the lowest relative base up now live in a function-scope array per invocation (LoadVectorWindow/StoreVectorWindow), so an indexed access is one load or store; plain accesses to those registers go through the same array, DPP-modified relative writes keep their DppUpdateU32 merge, an index past the window reads 0 and drops its write, and programs without relative addressing are unchanged. The same program recompiles to 392,333 words with 4,992 selects instead of 32,383.

Fixes #3842.

Tested

  • Windows (MinGW GCC 15.2.0, x86_64-ucrt-posix-seh): python tools/check_conventions.py --base origin/main passes.
  • Clean merge against current main (a11c8f7f7).

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 (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
  • AI-assisted: yes (Claude Code)

…dow array

`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`
and `v_swaprel_b32` lowered to a compare-and-select chain over every
register from the base to the program's last allocated register, so a
program addressing 154 registers 104 times turned 3176 ISA words into
649,515 SPIR-V words that Intel's compiler did not finish. The registers
from the lowest relative base up now live in a function-scope array per
invocation (`LoadVectorWindow`/`StoreVectorWindow`), so an indexed access
is one load or store; plain accesses to those registers go through the
same array, DPP-modified relative writes keep their `DppUpdateU32` merge,
an index past the window reads 0 and drops its write, and programs
without relative addressing are unchanged. The same program recompiles
to 392,333 words with 4,992 selects instead of 32,383.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Mouad-Scriptz-0

Copy link
Copy Markdown

Linux results for this commit, from #3842: agc_vector_window, agc_driver_relative_indexing, agc_driver_scalar_relative_indexing and agc_driver_dpp16_operands pass on Fedora 44 / Intel Arc Meteor Lake (Mesa 26.2.3); ctest -j4 --timeout 180 on a tree carrying this with #3622, #3534 and two further local AGC fixes, without the seven agc_driver_* tests that fail the same way on a clean main build on this iGPU: 821 passed, 5 skipped, 1 failed (agc_driver_emulated_color_compare, an ANV sRGB sampling limit that fails identically on clean main). Killzone: Liberation (PPSA11142) goes from freezing on its first PSP frame to playing its logos and reaching its language menu at 30 fps; a new pipeline variant of that program compiles in about 20 s with APS5_NO_PIPELINE_LTO=1. Thanks for carrying it.

AI-assisted: yes (Claude Code).

@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 carrying this, and thanks @Mouad-Scriptz-0 for the Arc and Killzone runs. The window array itself looks good: a build and the scoped ctest pass here (405 tests), including agc_vector_window, agc_driver_relative_indexing, agc_driver_scalar_relative_indexing and agc_driver_dpp16_operands. The problem is the out-of-range behaviour the PR adds. We measured it on RDNA2 hardware (gfx1035, 40 VGPRs allocated, a different v0 in every lane), and it doesn't match what the PR does:

  • An out-of-range read returns v0, not 0. "Out of range" means base + M0 ≥ the VGPR allocation, and the write is dropped. For example, with v_movrels_b32 v10, v32, any M0 ≥ 8 reads v0. The PR reads 0 (ControlFlowInstructions.cpp:318, Select(inRange, loaded, 0)). So the TechnicalDebt line ("reads 0 ... as the RDNA ISA describes") is wrong.
  • M0 is unsigned. M0 = 0xffffffff, 0xfffffffe, 0x80000000 and so on are all out of range: v0 is read and nothing is written. The PR computes raw = (reg - windowBase) + M0 in 32 bits (:307). When the base register is above the window start, M0 = -1 wraps back into the window, so the PR reads or writes the register below the base.
  • v_swaprel_b32 with either side out of range writes nothing, to the destination or to the source. The PR still writes the value it read out of range into the in-range side. v_movrelsd_b32/_2 with only the source out of range reads v0 and does write an in-range destination.

To check this against the PR's own code, we appended these rows to tests/execution/RelativeIndexing.cpp (window v30..v37, lane value x = 0x1000·(lane+1)). The HW and PR columns are for lane 0, and the pattern is the same for every lane:

rows appended after the existing program HW (gfx1035) this PR
s_mov_b32 m0, -1 / v_movrels_b32 v21, v32 → v21 v0 (= lane id, 0x0) 0x1001 (v31)
v_movreld_b32 v33, v4 → then v32 0x1030 (unchanged) 0x1000
v_movrelsd_b32 v35, v34 → then v34 0x1002 (unchanged) 0x1020
→ v33 0x1020 0x1020
s_mov_b32 m0, 0x103ff / v_swaprel_b32 v31, v32 → v32 0x1030 (unchanged) 0x0

main doesn't get these right either: its select chain falls back to the base register. But this PR replaces that with another model and documents it as the ISA's. What would fix it:

  1. In relativeWindowAccess, check the range on M0 itself: offset < windowSize - (reg - windowBase), unsigned. The clamped index can stay as it is.
  2. Read v0 when the index is out of range (GetVectorReg(0), which goes through the window when v0 is in it). Only v_swaprel_b32 writes, and only when both sides are in range.
  3. Initialise the window array (an OpConstantNull initializer on the OpVariable). An unwritten register currently reads an undefined SPIR-V value; on main the SSA builder gives it 0.
  4. Add the rows above, with their HW values, to RelativeIndexing.cpp, plus a row where the source is out of range and the destination is in range (v_movrelsd_2_b32 with M0 = 0x000103ff: the destination gets v0). Add the same rows to a wave64 test as well, since the window is one array per lane half. VectorWindow.cpp only counts IR shapes, so it can stay as a regression guard for the select count, but the behaviour needs value rows.
  5. Update TechnicalDebt.md:580. Say that out-of-range reads return v0 and writes are dropped, measured on gfx1035. Say that the window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler doesn't know. So a relative write past the named registers but inside the allocation is dropped, while hardware keeps it and a later relative read would see it. Also give the actual size (649,515 → 392,333 words) instead of "noted in its pull request".
  6. Remove the comment above RelativeWindowAccess (ControlFlowInstructions.cpp:296-297). It describes the select chain this PR replaces, and CONVENTIONS asks for no comments in code.

Mouad-Scriptz-0 and others added 2 commits October 11, 2026 18:28
…red semantics

M0 is unsigned, so an index at or past the window reads v0 and drops its
write, v_swaprel_b32 writes neither side when either is out of range, and
the window array starts zeroed like unwritten SSA registers; the window
load counts as a lane source so a wave64 program's second half keeps its
own registers. agc_driver_relative_indexing runs in wave32 and wave64 and
pins the rows measured on gfx1035 in the review of boykopovar#3856, and the
TechnicalDebt entry states the measured behaviour, the window's end at the
last named register and the sizes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two more agc_driver_relative_indexing rows read a register of the window
the program never wrote, directly and through v_movrels_b32, and expect 0,
which the zeroed window array gives like an unwritten SSA register did;
the TechnicalDebt entry says the console's value is undefined.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Mouad-Scriptz-0

Copy link
Copy Markdown

Thanks for the measurements. Fixed on feat/recompiler-vector-window in f9fd0f12 (already in this PR) and 876c4eca, for @ErenYeager1836 to add:

  1. relativeWindowAccess checks offset < windowSize - (reg - windowBase) on M0 itself, unsigned; the clamped index is unchanged.
  2. An out-of-range read returns v0 (GetVectorReg(0), through the window when v0 is in it); writes are dropped, and v_swaprel_b32 writes neither side unless both are in range (writeRelativeVector takes an enable; the swap passes the conjunction of both ranges).
  3. The window OpVariable has an OpConstantNull initializer; two new rows read a never-written window register directly and through v_movrels_b32 and expect 0.
  4. RelativeIndexing.cpp runs the program in wave32 with 32 lanes and wave64 with 64 lanes, computes the expectations per lane, and pins your rows (movrels with M0 = -1 reads v0; movreld/movrelsd with M0 = -1 leave v32, v34 and v33; swaprel with M0 = 0x103ff leaves v32; movrelsd_2 with M0 = 0x103ff gives the destination v0). The wave64 run found one more defect: LoadVectorWindow was not a LaneSource in SpirvAnalysis, so a wave64 program's second half reused the first half's load; it is one now. VectorWindow.cpp stays as the IR-shape guard.
  5. TechnicalDebt.md states the measured semantics (gfx1035, agc_driver_relative_indexing), that an unwritten window register reads 0 where the console's value is undefined, the window ending at the last named register with the dropped-write consequence, and the sizes.
  6. The comment above the access helper is gone.

Tested on Fedora 44 / Intel Arc Meteor Lake (Mesa 26.2.3), all three commits: agc_driver_relative_indexing (wave32 and wave64, 19 rows), agc_driver_scalar_relative_indexing, agc_driver_dpp16_operands and agc_vector_window pass; full ctest -j4 --timeout 180 without the seven agc_driver_* tests that fail on a clean main build on this iGPU: 827 tests, 1 failed (agc_driver_emulated_color_compare, an ANV sRGB sampling limit that fails identically on clean main); tools/check_conventions.py --base origin/main and git diff --check pass. I cannot run the hardware oracle here (no AMD GPU), so your rows are the measurements.

AI-assisted: yes (Claude Code).

@ErenYeager1836

Copy link
Copy Markdown
Contributor Author

Updated with @Mouad-Scriptz-0's commit addressing the review: M0 is unsigned, out-of-range reads return v0, v_swaprel drops writes when either side is out of range, the window array is zero-initialized, and the gfx1035 test rows and TechnicalDebt updates are added. Conventions check passes.

@github-actions github-actions Bot added build shader core/shader docs agc libSceAgc* labels Oct 11, 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.

Checked the measured out-of-range semantics (unsigned M0, out-of-range read returns v0, writes dropped, v_swaprel_b32 writes neither side unless both are in range, zero-initialised window), agc_vector_window and agc_driver_relative_indexing (wave32 and wave64) on the PR merged onto main, and that both fail without the change. Thanks @Mouad-Scriptz-0 for the Arc/Killzone runs. Merging once CI is green.

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

Labels

agc libSceAgc* build docs shader core/shader

Projects

None yet

3 participants