Repository navigation
perf(recompiler): drop exec selects whose inactive lanes nobody reads - #611
Merged
oneandonlydean merged 2 commits intoOct 5, 2026
Merged
oneandonlydean merged 2 commits into
oneandonlydean merged 2 commits into
Conversation
9 tasks done
Every VGPR write under EXEC is translated to Select(exec, new, old), so in SSA each register's previous value stays live until the next write, even when the guest only used the register as a temporary. In NGG mesh shaders at 255 registers that keeps dead values alive and the driver spills them to local memory. MaskedSelectEliminator (after SSA, folding and the readlane pass) replaces r = Select(m, new, old) by new when no lane where m is false can observe r: - every value derived from r is either a lane-local pure op or a phi, and their uses are checked the same way; - or the derived value reaches the true operand of a select, or an operand of a LogicalAnd, whose condition implies m: m itself, a LogicalAnd containing m, or a thread bit rebuilt from a ballot of such a condition (also through ANDs of the mask words); - anything else (cross-lane ops, ballots, readfirstlane, DPP, swizzle, permutes, stores, exports, atomics, branch references, implicit-LOD pixel samples, the false operand of any select) keeps the select. The select and every value it reaches must be in blocks outside loops, so each of them runs at most once per invocation and every use sees the same instance of m. Selects whose condition is the full wave ((0xffffffff >> (lane & 31)) & 1) are replaced in any block. Exact: in lanes where m is true r equals new, so every derived value is unchanged there; in lanes where m is false only derived values change, and each path from them ends in a select or AND that discards those lanes. The selects are removed latest first and dead code is removed between rounds, so chains of writes to one register collapse.
…lect A register written under EXEC is usually consumed by an LDS or buffer access or an export under the same EXEC. MaskedSelectEliminator kept the select in that case, which leaves the NGG mesh shaders where they were. The emitter performs buffer and LDS loads and stores (8 to 128 bits, GPU-selected V# included) and SetAttribute only in lanes whose active operand is true: stores and exports sit in an if on it, loads return 0 outside it. Every other operand is read only in those lanes. So such an access whose active operand implies the select's condition reads the derived value only where it equals the original, and it now ends the search like a masked select does. The active operand itself, atomics, append/consume, scratch and address-space accesses and image accesses still keep the select.
oneandonlydean
force-pushed
the
perf/recompiler-masked-select-elim
branch
from
October 5, 2026 05:13
81c8543 to
60c6552
Compare
oneandonlydean
added a commit
to oneandonlydean/AnyPS5
that referenced
this pull request
Oct 5, 2026
Takes upstream's merged versions of our boykopovar#586 boykopovar#611 boykopovar#612 boykopovar#505 boykopovar#490 boykopovar#619 boykopovar#503 boykopovar#627 boykopovar#628 boykopovar#618 and drops astrobot's local copies, plus gate-batch-3 (boykopovar#469 boykopovar#500 boykopovar#504 boykopovar#509 boykopovar#510 boykopovar#514 boykopovar#525 boykopovar#526 boykopovar#528), round o/p (boykopovar#617 boykopovar#620 boykopovar#621 boykopovar#622 boykopovar#623 boykopovar#624 boykopovar#501 boykopovar#508 boykopovar#629 boykopovar#630 boykopovar#631). Conflicts: - SpirvAnalysis.hpp/.cpp, SpirvBufferAccess.cpp: upstream's boykopovar#586/boykopovar#612. The 64-bit LDS atomics keep the full array (f27d6e5 no longer needed). - SpirvMemoryInstructions.cpp: upstream's constant slot plus offset; astrobot's split-wave append/consume kept. - GuestTextureResource.cpp, TextureTiling.cpp and their tests: upstream's boykopovar#490/boykopovar#503 (astrobot had no other change there). - MaskedSelect.cpp test, ShaderMemory.cpp LDS test: upstream's. - State.cpp: upstream's boykopovar#505 export count and boykopovar#469 null pixel program; astrobot's depth export rejection and depth target model kept. - Pipeline.cpp: upstream's pipeline key; astrobot's depth attachment and blend check (a trailing unused attachment stays valid, as colorGapPipelineTests expects). - Draw.cpp, Recorder.cpp, ShaderResources.cpp: astrobot's (renderPassKey with depth, moved sample functions, residency maintenance), which already hold the merged boykopovar#505/boykopovar#628/boykopovar#490 changes. - ShaderInputState.cpp: both register sets. - State.hpp: the duplicate exportIndex member from the auto-merge removed. - CMakeLists.txt: astrobot's test list (a superset of upstream's). - tests/Graphics.cpp: upstream's CompactedExportTests replaces astrobot's inline copy; tests/Recorder.cpp: upstream's viewPastLastMipTests added.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every VGPR write under EXEC becomes
r = Select(exec, new, old). In SSA the register's previous value then stays live until the next write, even when the guest used the register as a temporary. NGG mesh shaders at 255 registers keep many dead values alive this way, and on NVIDIA the driver spills them to local memory (up to 784 bytes per thread in PPSA21564).MaskedSelectEliminator(new Optimization pass, after the readlane pass and before the SRT walk, then DCE) replacesrwithnewwhen no lane wheremis false can observer. It walks every value derived fromr:LogicalAnd, whose condition impliesm. That condition ismitself, aLogicalAndcontaining it, or a thread bit rebuilt from a ballot of such a condition;SetAttribute, whose active operand impliesm;Removal goes latest first, with DCE between rounds, so a chain of writes to one temporary collapses.
Why it is exact:
mis true,requalsnew, so every derived value is unchanged.mis false, only derived values change, and each path from them ends in a select, an AND or a guarded access that ignores that lane. The emitter wraps those stores and exports inif (active)and returns 0 for those loads outside it.m.NoContraction, so removing a select between them can't change rounding.Tested
BUILD_TESTING=ON;ctestpasses (240/240).agc_masked_select(22 cases):The rest of the mesh spill is real pressure: with every exec select removed (wrong code, measured only as a bound), 500149c00 still needs 448 B.
Pipelines created through the intro, from the traced local memory per thread:
The worst mesh shaders went from 720 B to 704 B and from 672 B to 528 B. About 2.3 KB of the FS drop is the LDS follow-up (3 pipelines at 864 B now at 96 B).
FPS is unchanged within noise. Two passing runs of each:
My visual regression check passes; 0 GPU faults (Xid 31/109) in the 11 runs of these builds.
Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt.mdfiles or images added to the repository (attach them to this PR)