Skip to content

fix(shader): drop module constants nothing references after specialization - #5315

Open
AlxcBTW wants to merge 1 commit into
boykopovar:mainfrom
AlxcBTW:fix/specialized-dead-constants
Open

AlxcBTW wants to merge 1 commit into
boykopovar:mainfrom
AlxcBTW:fix/specialized-dead-constants

Conversation

@AlxcBTW

@AlxcBTW AlxcBTW commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

Two pipeline variants of one program that differ only in a folded specialization value now produce the same SPIR-V. The second variant is then served by the pipeline cache instead of a cold compile.

buildSpecializedModule materializes a variant's specialization values as OpConstant, and SpecializeSpirv folds them into the code. The folding left the materialized constants in the module with no use.

Image channel selectors (RuntimeImageSwizzle) are such values. So two variants of a program whose only difference was an image's channel order, 4,5,6,7 vs 6,5,4,7, had identical code but different SPIR-V, and the second one was created and compiled again.

removeUnusedConstants now also removes scalar and null module constants (OpConstant, OpConstantTrue/False, OpConstantNull) that no instruction references any more. OpConstantComposite is not removed: VisitIdOperands counts a composite's own result id as a reference, so a composite and its members always stay. Run already drops the names and decorations of removed ids.

Evidence from Sword of the Sea (PPSA25344), loading on a cold cache, compute programs of 100k+ SPIR-V words. Every pipeline's SPIR-V was dumped (APS5_DUMP_COMPUTE_PIPELINES) and diffed with spirv-dis --raw-id:

  • Dead-constant pairs. In a 202 s window, 15 programs were created twice. In 14 of the 15 pairs, the two modules differ only in 2 to 7 OpConstant values that nothing in the module uses: each id occurs once, at its definition. The values are 4/5/6/7 in another order. The 15th pair is a real variant (10,738 differing lines).
  • Repeat creations. In a longer load window, 439 pipelines were created for 168 programs. 163 creations took over 1 s:
    • 7 first creations of a program: 85 s;
    • 156 repeat creations of an already compiled program: 611 s.

Test change: agc_formatted_buffer_once

@adriadam10, this touches the bound from 56d5a49. That bound checks that a formatted load from a descriptor with a known format does not carry every format's decoder in registration form, measured against the specialized module.

This change shrinks both modules, the specialized one more, so the ratio between them moves. Sizes in words, from this PR's fork CI and from main 0214069 on the same runners:

Linux, SPIRV-Tools on: main Linux: this PR Windows: main Windows: this PR
vertex, specialized 1348 1126 2130 1915
vertex, registration 1917 1739 2927 2756
registration / specialized 1.42 1.54 1.37 1.44
compute, 1 load (known descriptor) 1209 987 1947 1732
compute, GPU-selected descriptor (every decoder) 85973 85945 114447 114419

The bound moves from 1.5x to 1.6x of the specialized module. A registration module that kept every decoder would be about as large as the GPU-selected compute module, about 14,000 words on current main (13,897; it was 85,945 when this PR was opened, before #3656), so 1.6x still catches it.

I first tried bounding the registration module against a vertex module with a GPU-selected descriptor instead, which would have measured the property directly. The recompiler rejects that request in both forms ("buffer specialization has an unsupported format"), so the test keeps its original comparison.

Tested

  • agc_shader_disk_cache, new verifySpecializationDropsDeadConstants:
    • two modules that differ only in a constant the folding consumes specialize to the same words;
    • that constant and its OpName are gone;
    • an array length constant stays;
    • the store still reads the selected value;
    • the result is stable under a second pass.
  • Fork CI, full ctest on this branch (run 38432808073):
    • Linux, lavapipe, SPIRV-Tools on: 1077/1077.
    • Windows: 1095/1095.
  • The before/after of the cold load on one machine and scene follows as a comment (the Sword of the Sea dump is gone; it is measured on Minecraft Dungeons II PPSA16064).

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
  • Unblocks: none
  • GPUs tested: lavapipe (fork CI, Linux). The Sword of the Sea dumps and timings above are from an RTX 5070 Laptop GPU, NVIDIA 617.42, Windows 11
  • AI-assisted: yes

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 13, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 14, 2026
…time CPU (#482)

* perf(shader): rewrite high-fan-out IR uses in linear time

Ported from upstream PR boykopovar#4035

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(shader): index specialization tables by SPIR-V id

Ported from upstream PR boykopovar#4772

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(shader): honor disabled optimization during specialization

Ported from upstream PR boykopovar#4827

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(shader): discard helper functions unreachable after specialization

Ported from upstream PR boykopovar#5064

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(shader): drop module constants nothing references after specialization

Ported from upstream PR boykopovar#5315

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(shader): cap SPIR-V scalar replacement

Ported from upstream PR boykopovar#4288

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(recompiler): iterate structurizer fixpoints in post-order and bound shared merge splitting growth

Includes the stacked upstream commit that structurizes refused control flow
with reach flags, which this change builds on.

Ported from upstream PR boykopovar#5028

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(shader): emit image_bvh_intersect_ray as one shared function per variant

Ported from upstream PR boykopovar#5455

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(shader): avoid redundancy traversal map copies

Patches the bundled SPIRV-Tools redundancy elimination (only built with
ANYPS5_ENABLE_SPIRV_TOOLS=ON).

Ported from upstream PR boykopovar#5511

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(metal): accept a specialized BDA module that dropped one of its two BDA descriptors

Specialization removes descriptor variables the final module no longer
references. With helper functions now discarded after specialization, a
compute module can drop its page-table variable while keeping the fault
buffer, and the bridge then rejected the dispatch ("BDA ABI requires
page-table and fault-buffer descriptor metadata"). Require at least one of
the two roles instead of both; the module's own descriptors are still
checked against the prepared plan.

Not a port; needed by the port of upstream PR boykopovar#5064.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@oneandonlydean oneandonlydean self-assigned this Oct 14, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Conflicts with main in tests/ShaderDiskCache.cpp; please rebase.
The description says the cold-load before/after follows as a comment; it isn't here yet. Please add it (main against main plus this PR, same machine and scene), since the gain is the reason for the change.

moong15 added a commit to moong15/AnyPS5 that referenced this pull request Oct 14, 2026
# Conflicts:
#	core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Still needs the rebase for tests/ShaderDiskCache.cpp (10-14) and the cold-load before/after the description promises: main against main plus this PR, same machine and scene.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 14265 core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp L126-129
PR 14367 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp L2691

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 5609 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6280 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6858 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6924 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7085 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7223 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7642 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7684 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 8004 core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp
PR 9852 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 9889 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 9897 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 10049 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 10193 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 13928 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 13929 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 14032 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 14217 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 14218 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 14853 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 14944 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp

…ation

A specialized pipeline variant materializes its specialization values as
OpConstant, and SpecializeSpirv folds them into the code. The folding left
the materialized constants in the module with no use. Image channel
selectors are such values (RuntimeImageSwizzle). Two variants of one program
that differed only in an image's channel order therefore had identical code
but different SPIR-V, and the second one was created and compiled again
instead of coming from the pipeline cache.

removeUnusedConstants now also removes module constants (OpConstant,
OpConstantTrue/False, OpConstantComposite, OpConstantNull) that no
instruction references any more, repeating until nothing changes. Run
already drops the names and decorations of removed ids.

Test: agc_shader_disk_cache verifySpecializationDropsDeadConstants. Two
modules that differ only in a constant the folding consumes specialize to
the same words, that constant and its name are gone, a constant still in
use (an array length) stays, the store still reads the selected value, and
the result is stable.

agc_formatted_buffer_once bounds its vertex registration module by 1.5x
the specialized one. Both modules lose dead constants, the specialized one
more (Linux with SPIRV-Tools: 1348 -> 1126 words, registration 1917 -> 1739),
so their ratio moves from 1.42 to 1.54. The bound moves to 1.6x; a module
that kept the format table for every format would be about 14,000 words, like
the GPU-selected compute module (13,897).
@AlxcBTW
AlxcBTW force-pushed the fix/specialized-dead-constants branch from 76bb7da to 1ea471b Compare October 15, 2026 18:38
@AlxcBTW

AlxcBTW commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean rebased onto main 03c4cb5.

Conflict. tests/ShaderDiskCache.cpp: main added the resource plan fixtures at the place where this PR adds
verifySpecializationDropsDeadConstants. Both are kept, the new test stays right after verifySpecializationLiveness. The
#include <set> was not used by the file and is gone. SpirvSpecialization.cpp and FormattedBufferOnce.cpp applied
without changes. tools/check_conventions.py --base origin/main passes.

Tests (fork, Linux, lavapipe, SPIRV-Tools on, all 605 agc_ tests):

  • this PR (run 38781694714): 604 pass. agc_driver_mesh_index_cache aborts with LLVM ERROR: Cannot emit physreg copy instruction inside llvmpipe's JIT.
  • main 03c4cb5 (run 38781698319): the same test aborts the same way, 604 pass. It is not related to this change.
  • agc_shader_disk_cache (with the new test), agc_formatted_buffer_once, agc_spirv_module, agc_spirv_optimizer and
    the other recompiler tests pass in both runs.
  • Windows: the libs and relinker targets compile (fork run 38781966955, MinGW). The Windows ctest run was not repeated on this base.

Sizes in agc_formatted_buffer_once, words, Linux with SPIRV-Tools, measured on the rebased tree:

main 03c4cb5 this PR
vertex, specialized 1348 1126
vertex, registration 1917 1739
registration / specialized 1.42 1.54
compute, 1 load (known descriptor) 1209 987
compute, GPU-selected descriptor 13925 13897

The 1.6x bound is still needed: 1.5x of 1126 is 1689 words and the registration module has 1739; 1.6x gives 1801. The
GPU-selected compute module is 13,897 words now (it was 85,945 when this PR was opened; #3656 decodes it from a format
table), so "tens of thousands of words" in the description no longer fits. A registration module that kept the decoders of
every format would be about as large as that compute module, roughly 14,000 words, far above 1801.

Cold load. The Sword of the Sea dump the description was written against is gone, so the before/after runs on Minecraft Dungeons II (PPSA16064) at its title screen, one machine (RTX 5070 Laptop 8 GB, NVIDIA 617.42, Windows 11), main 03c4cb5 against main plus this PR, four alternating cold rows with the shader cache emptied before each. It is queued on the lab machine tonight; the numbers follow in the next comment.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@JJenkx JJenkx mentioned this pull request Oct 16, 2026
11 tasks done
@oneandonlydean

Copy link
Copy Markdown
Collaborator

The rebase is right, both tests kept. The agc_driver_mesh_index_cache llvmpipe abort also happens on main, so it isn't this PR. The size table is useful (specialized vertex 1348 to 1126 words, compute with a known descriptor 1209 to 987). What's left is the four alternating cold rows with the cache emptied before each.

@AlxcBTW

AlxcBTW commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Cold-load numbers from Minecraft Dungeons II (PPSA16064) on Windows 11, NVIDIA GeForce RTX 5070 Laptop GPU (617.42): two rows per arm, main and this PR alternating, each with an empty AnyPS5 shader cache, boot to the title.

main (2 rows) this PR (2 rows)
distinct SPIR-V modules after specialization 23 / 23 21 / 21
words of those modules 4.02 M 3.60 M (-10 %)
creations that needed no new module 0 2
groups that differed only in dead constants 2 0
large pipelines / programs / graphics pipelines 23 / 21 / 110 23 / 21 / 110
pipeline creation time 0.7 s / 0.7 s 15.7 s / 0.7 s
vk-pipelines cache file 26.7 MiB 25.7 MiB

So the change does what it says: the two specializations that differed only in constants nothing references now share one module, and the modules are 10 % smaller. The 15.7 s in the PR's first row is the NVIDIA driver compiling the new modules once: the driver keeps its own cache outside the row's LOCALAPPDATA, so the main rows started with modules it had already seen, and the PR's second row is back at 0.7 s. That row does not measure the change, and I'd not claim a load-time win from these numbers; the gain is fewer and smaller modules.

AI-assisted: yes (Claude Code)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@AlxcBTW Since your 15 Oct rebase, main gained 3e1f932 (#6176, "share folded SPIR-V constants and drop identity bitcasts"). It already removes unused folded constants, but it keeps the definitions that undecoded consumers (subgroup and extended instructions) still name.

Merged onto current main, this PR's sweep doesn't keep those, so two tests fail:

  • main's agc_spirv_optimizer: "specialization kept an unreferenced folded constant or lost a referenced one"
  • your own verifySpecializationDropsDeadConstants in agc_shader_disk_cache: "variants that differ only in a folded constant specialized to different SPIR-V"

With main's SpirvSpecialization.cpp, the optimizer test passes. The text merge is clean, so CI on the old base doesn't show this.

Could you rebase onto current main, keep the constants that main's removal keeps, and show both tests passing? Please also say what this PR still adds on top of 3e1f932, for example module word counts before and after on main. Thanks!

TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
…onstant of its value

boykopovar#6176 aliases every folded result to the first module constant of the same
type and value. A select that boykopovar#5315's dead-constant test folds to a copy of
one constant then aliased to the variant's own selector constant whenever the
two held the same value, so variants that differ only in that folded constant
kept it (and its name) and specialized to different SPIR-V
(agc_shader_disk_cache). A folded OpCopyObject now aliases to its source
constant (or that constant's alias); other folds keep boykopovar#6176's canonical choice.

AI-assisted: yes
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
…ts referenced constants

agc_spirv_optimizer (built with ANYPS5_ENABLE_SPIRV_TOOLS, as upstream's Linux
job does; build-local has it off) failed on this branch before today's local
fixes as well. boykopovar#6176's test counted five module constants: %one and %three
stayed because nothing removed unreferenced module constants on its head.
boykopovar#5315 now drops them, so the module keeps %two, 7 and 8, the three the stored
lane + 23 reads. The test expects 3.

AI-assisted: yes
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
…that cannot fault

Upstream 7e4ce1a makes an unaligned 32-bit buffer atomic act on its
containing dword, so the specialized atomic records no fault. Upstream
keeps the uncalled fault function and with it the fault buffer binding;
our boykopovar#5315 specialization removes dead functions, so the module no longer
uses the fault buffer, Select drops the binding and bdaAbiVersion is 0
("the program has no fault buffer for its unaligned atomic"). The test
now expects that, and still checks no address tables, the atomic element
flag, Int64 and no fault call.

AI-assisted: yes

@THEMANnt THEMANnt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed 1ea471b57089 against merge base 03c4cb5e78e0. Recommend approval; no actionable finding in the complete three-file patch.

The iterative cleanup removes unused specialized constants and associated metadata while retaining referenced array lengths and the composite operands preserved by the current visitor. Reviewed the specialization-to-cache/compilation flow and the adjusted formatted-buffer size bound.

Validation: Built agc_shader_disk_cache_tests on candidate and base with Windows GCC 15.3; both CTest runs pass. An additional review-only harness passes checks for dead scalar/bool/null constants, consumed-value/name removal, retained array/composite operands, unchanged input and idempotence. Its initially incorrect composite-removal expectation was corrected before the final passing run; no production source was changed.

Scope: ANYPS5_ENABLE_SPIRV_TOOLS=0, so no independent SPIR-V validator ran. No local Linux/lavapipe execution; hosted product jobs were skipped. The baseline's original-suite pass is not a failing-old-code regression control.

@jotitadev

Copy link
Copy Markdown
Contributor

Merge-skew check on Linux (i7-8700, Ubuntu 26.04, GCC 15.2, Release). Head 1ea471b57, merged with --no-ff onto main b7105f728 (together with other non-overlapping PRs; the failing test is this PR's own).

agc_shader_disk_cache fails 3/3 on lavapipe and 3/3 on an RTX 3070 (NVIDIA 595.99): "variants that differ only in a folded constant specialized to different SPIR-V". main alone passes.

Mechanism (adds to @oneandonlydean's note about #6176 / 3e1f932): main's SpecializeSpirv records a canonical id per (type, value) from the module constants in declaration order, and a folded result whose value already has a canonical id is aliased to it. In the test, OpSelect folds to a copy of %7 (value 4). With selector 4, %6 (the selector, also value 4) is declared first, so the store is rewritten to %6 and the selector stays alive; with selector 6 the store keeps %7. The two variants therefore differ again, which is also the real 4,5,6,7 vs 6,5,4,7 swizzle case from the description.

Verified minimal fix (main's file, 6 lines): when the folded instruction is an OpCopyObject of a known constant, alias to that source instead of the first constant with the same value. With it the test passes 3/3 on both devices.

Fix diff (against main)
diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp
index a3e385452..6caa64369 100644
--- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp
+++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp
@@ -803,8 +803,12 @@ private:
             const auto type = types.at(instruction[1]);
             const auto bits = type.width == 32u ? *result : *result & ((1u << type.width) - 1u);
             values[instruction[2]] = bits;
-            const auto [existing, created] = canonical.emplace(std::pair{instruction[1], bits}, instruction[2]);
-            if (!created) aliases.emplace(instruction[2], existing->second);
+            if (op == spv::OpCopyObject && values.contains(instruction[3])) {
+                const auto* shared = aliases.find(instruction[3]);
+                aliases.emplace(instruction[2], shared != nullptr ? *shared : instruction[3]);
+            } else if (const auto [existing, created] = canonical.emplace(std::pair{instruction[1], bits}, instruction[2]); !created) {
+                aliases.emplace(instruction[2], existing->second);
+            }
             constants.push_back(type.boolean ? Make(bits != 0u ? spv::OpConstantTrue : spv::OpConstantFalse, {instruction[1], instruction[2]}) : Make(spv::OpConstant, {instruction[1], instruction[2], bits}));
             instruction.clear();
             changed = true;

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@jotitadev jotitadev 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.

Follow-up to my merge-skew note above, now with ANYPS5_ENABLE_SPIRV_TOOLS=ON (that build did not have it). Linux, i7-8700, Ubuntu 26.04, GCC 15.2, Release. Head 1ea471b57 merged with --no-ff onto main 8df2c3ed0. lavapipe (Mesa 26.0.3).

The disk-cache fix from my earlier comment does not cover the optimizer test @oneandonlydean mentioned:

agc_shader_disk_cache agc_spirv_optimizer (+ _full, _none)
main pass pass
main + this PR fail 3/3 fail 3/3
main + this PR + the 6-line SpirvSpecialization.cpp fix pass 3/3 fail 3/3

Only one check fails in testSpecializedConstantsAreShared: "specialization kept an unreferenced folded constant or lost a referenced one" (constants.size() == 5u). The module itself is fine: ValidateAndOptimizeSpirv accepts it and every other check in that test passes (sharing, no bitcasts or copies, lane + 23, stable second pass).

The two missing constants are not folded ones. main leaves %one = 1 and %three = 3 from the source in place (it only drops unreferenced constants that the folding itself materialized), so the test expects 1, 2, 3, 7, 8. This PR's sweep also removes original module constants that nothing references, so the output has 2, 7, 8. That is the intended effect of the PR, so it comes down to which behaviour main wants:

  • keep the sweep and update the expectation to 3u, or
  • limit the sweep to what main's removeUnusedConstants already removes.

With the disk-cache fix plus 5u → 3u, the ^agc suite on lavapipe with SPIRV-Tools ON (1053 tests, display/flip/lds_atomics64 excluded) matches main: 1051/1053, failing only agc_driver_multisample_retirement and agc_driver_guest_memory_lifetime_converted, which fail on main too.

GPUs tested: lavapipe (Mesa 26.0.3) for everything above. On main, the 10 tests that only exist with SPIRV-Tools ON also pass on an RTX 3070 (NVIDIA 595.99.02).

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@jotitadev thanks for the merge-skew check and for tracing the mechanism: it confirms the conflict with #6176 that we flagged. @AlxcBTW the fix still needs to keep the folded-constant definitions that undecoded consumers name, so that both agc_spirv_optimizer and this PR's agc_shader_disk_cache case pass on top of current main; @jotitadev's note shows why the 6-line SpirvSpecialization.cpp change covers only the second.

@Astronaut10

Copy link
Copy Markdown
Contributor

Data from another title on whether #6176 already covers this: Astro Bot (PPSA21564), Linux, RADV, a local build on main 27ad7244d2 (#6176 included; our local patches don't touch specialization). I dumped every fragment stage module as built and compared them with unreferenced constants removed:

module keys distinct SPIR-V distinct without unreferenced constants
fragment modules in registration form 283 206 206
specialized fragment modules 100 100 45
specialized, with a local diagnostic that specializes all pixel modules 240 240 113

The five hottest fragment programs had 36 specialized modules but only 5 distinct codes. For example, two of one program's 21 variants differ only in three OpConstants (1 vs 7), each of which occurs once, at its definition. So with #6176 in, the duplicates remain (as of 27ad7244d2), and each variant's identical code is compiled again. I haven't run this PR on it.

@blawesom

Copy link
Copy Markdown
Contributor

Static review plus an independent Linux/AMD run on both trees per the repo AGENTS shader-recompiler protocol (CachyOS, kernel 7.2.10, GCC 16.2.1, Ninja 1.13.2, Release, BUILD_TESTING=ON, Ryzen 7 9800X3D). No findings in the change itself; one rebase note at the end.

What I checked

  • The new fixed-point loop in removeUnusedConstants() is sound: it re-computes referencedIds() after each sweep and drops constant-class definitions (OpConstant/OpConstantTrue/OpConstantFalse/OpConstantComposite/OpConstantNull) whose result id nothing references any more — so a constant used only by another dropped constant is removed in the second pass, and the loop terminates because each pass that changes something strictly shrinks the instruction list.
  • The test (in ShaderDiskCache.cpp::verifySpecializationDropsDeadConstants) is meaningful, not tautological: it builds two modules differing only in a folded-away constant value, asserts the specialized outputs are byte-identical (proving the constant is gone from the output), asserts the other constant (array-length, id 21) is retained (proving nothing over-prunes), asserts the store now reads the surviving value, and re-specializes to check the removal is a fixed point (stability under idempotence).
  • FormattedBufferOnce.cpp's ratio loosening (words/2 → words*3/5) matches the PR body's intent — the tighter bound no longer holds now that dead constants are dropped — and the other bound (gpuOne.words / 2) is untouched.

Focused paired run (AGENTS shader-recompiler protocol)

VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 \
ctest --test-dir build -R 'shader_disk_cache|formatted_buffer_once' -V
tree agc_shader_disk_cache agc_formatted_buffer_once extra tests matched on main
this head 1ea471b570 Passed (1.12s) Passed (0.02s) (none — see note)
main bb04a4f1ea Passed (0.92s) Passed (0.01s) agc_shader_disk_cache_pack_ownership, agc_shader_disk_cache_large_file — both passed

Both logs: 0 was not found but was requested by env var VK_INSTANCE_LAYERS, 0 VUID- validation errors. Comparing the logs, no new errors introduced by this change.

Rebase note

This branch is behind main and predates two related fixtures that landed since (agc_shader_disk_cache_pack_ownership, agc_shader_disk_cache_large_file). Both pass on main and weren't run here against this change because they don't exist at the PR's base. Please rebase and let CI confirm — if either newly added fixture is touched by this pruning loop's behavior, that's where a regression would show. The overlap tooling also flags ShaderDiskCache.hpp as contended by several other open PRs (e.g. 6340-style single-line edits in the same file) worth a glance before merge.

@TechLevelZero

TechLevelZero commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

❌ NVIDIA GPU test: failed

PR head 1ea471b merged onto main 1c8c0ef · tested 2026-11-04 06:34 UTC · 18 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ❌ 2,708 passed · 7 failed (4 new: 3 pass on main with the PR's tests, 1 fail on main with the PR's tests too, 3 also fail on main) · 50 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,765 of 3,842 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (944 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 112.5 s

Failing tests

This PR changes test files, so each failing test was also built and run on main with only the PR's test changes applied. ✅ there means the failure comes from the PR's other changes; ❌ means the test fails on main too, so this run does not show the PR caused it.

Test With this PR Same test on main 1c8c0ef (main + this PR's test changes only)
agc_shader_disk_cache ❌ failed ❌ also fails
agc_spirv_optimizer ❌ failed ✅ passed
agc_spirv_optimizer_full ❌ failed ✅ passed
agc_spirv_optimizer_none ❌ failed ✅ passed
agc_shader_disk_cache: Failed (also failed when re-run alone), assertion and diagnostics
ctest: Failed

APS5_NO_FAILURE_MEMO=1 emitted the failing program twice
plan round trip: user data buffer, 8 values, 1198 bytes per entry, 968 of 1000 runtimes materialized
plan round trip: guarded image, 116 values, 4454 bytes per entry, 548 of 1000 runtimes materialized
plan round trip: guarded image stores, 64 values, 3752 bytes per entry, 493 of 1000 runtimes materialized
variants that differ only in a folded constant specialized to different SPIR-V
agc_spirv_optimizer: Failed (also failed when re-run alone), assertion and diagnostics
ctest: Failed

1 optimizer check(s) failed
agc_spirv_optimizer_full: Failed (also failed when re-run alone), assertion and diagnostics
ctest: Failed

1 optimizer check(s) failed
agc_spirv_optimizer_none: Failed (also failed when re-run alone), assertion and diagnostics
ctest: Failed

1 optimizer check(s) failed
Failing on main too (3, not counted against this PR)
agc_driver_image_table
guest_mmap_executable
guest_socket_options_multicast
Validation IDs already present on main (8 IDs in 531 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (531 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 523 more
VUID-vkCmdCopyImage-srcImage-01548 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_srgb8_color_target
VUID-VkSamplerCreateInfo-addressModeU-01079 (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeU-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeV-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeW-parameter (1 test): agc_driver_image_gather_volume
Skipped (50)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_driver_vertex_elected_atomic_max: skipped, the device runs vertex programs in subgroups, so they keep their cross-lane reductions
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_dcc: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_deferred: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
vide
Reproduce
git fetch origin 1ea471b57089e902583e51e5cf8aa0a7b8a6a5c2 1c8c0efc8c41314402e6966da5bef6ea3d639d10
git checkout 1c8c0efc8c41314402e6966da5bef6ea3d639d10 && git merge --no-ff 1ea471b57089e902583e51e5cf8aa0a7b8a6a5c2
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1
# the failing tests, on this merge and on main:
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc_shader_disk_cache|agc_spirv_optimizer|agc_spirv_optimizer_full|agc_spirv_optimizer_none)$' --parallel 1 -V

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@fogydes

fogydes commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Verdict

should-fix

Findings

  1. [verified] Clean merge-tree against current origin/main (bb04a4f1ea): git merge-tree --write-tree origin/main 1ea471b57089e902583e51e5cf8aa0a7b8a6a5c2 exits cleanly with code 0 (clean tree bd9d979ed66c5697ddae42073b8f95cdb46e067e) despite being 12,458 commits behind main.
  2. [verified] Conventions check passed: python tools/check_conventions.py --base 03c4cb5e78e0679db09cbad743981068c40ed2c6 in a scratch worktree succeeds with 0 findings and 0 errors.
  3. [read-only] Missing PR checklist line: The PR checklist omits one required template line from .github/pull_request_template.md:
    • - [ ] Fixes: <!-- issue it closes as Fixes #N, or none -->
      Per AGENTS.md, keep every line and tick every box (Fixes: none if no issue is closed).
  4. [read-only] Shader recompiler validation layer comparison requirement: Per AGENTS.md, AGC and shader recompiler changes require running the same focused tests on main and the branch with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation and comparing logs to verify no new VUID- validation errors are logged. The PR description reports lavapipe / Windows CI results, but no Vulkan validation layer comparison.
  5. [author-reported] New test verifySpecializationDropsDeadConstants in agc_shader_disk_cache verifies dead constant removal; adjusted formatted buffer specialization ratio bound from 1.5x to 1.6x in agc_formatted_buffer_once.

Freshness envelope

  • Head: 1ea471b57089e902583e51e5cf8aa0a7b8a6a5c2
  • Base: 03c4cb5e78e0679db09cbad743981068c40ed2c6
  • Behind origin/main: 12,458 commits
  • Merge-tree: exit 0 (clean tree bd9d979ed66c5697ddae42073b8f95cdb46e067e)

Cross-PR ledger

  • No other open PR currently touches core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp, core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp, or core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp.

@github-actions github-actions Bot added review queue Next in the maintainer review queue: checks pass, no open change request, no conflict and removed review queue Next in the maintainer review queue: checks pass, no open change request, no conflict labels Nov 3, 2026

@BunnyFl BunnyFl 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.

@AlxcBTW the new verifySpecializationDropsDeadConstants fails once this PR is merged into current main (runs below). So on main, the two variants in the test still specialize to different SPIR-V.

What I ran: head 1ea471b570 merged onto main 9ab6a281ea (clean merge, e4a39e6cd3). Windows 11, WinLibs GCC 15.2.0, AMD RX 9070 XT, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation:

  • agc_formatted_buffer_once passes.
  • agc_shader_disk_cache fails with variants that differ only in a folded constant specialized to different SPIR-V.
  • With main's SpirvSpecialization.cpp restored, it fails the same way.
  • No VUID- errors and no missing-layer lines in either run.
  • The head alone, unmerged (base 03c4cb5e78): agc_shader_disk_cache passes. So the failure comes from what main has changed in SpirvSpecialization.cpp since then (13 commits, merges included).

Why, as far as I read it (main 9ab6a281ea):

  • At parse time, each scalar module constant registers as the canonical id for its type and value. The first one wins, because it's an emplace (SpirvSpecialization.cpp#L394).
  • When fold turns an instruction into a constant whose value is already canonical, it records an alias to that id (#L623). shareConstants (#L849) then rewrites the uses to the canonical id. Run calls it (#L428) before removeUnusedConstants (#L431).
  • In the test module, the selector is %6 and the selected constant is %7 = 4. With selector 4, %6 and %7 both hold 4, and %6 is emitted first. By the emplace at #L394 above, the folded OpSelect then aliases to %6, and the store reads %6. With selector 6, the store reads %7.
  • So after this PR removes the unused one, the two modules still differ: OpConstant %3 %6 4 plus OpStore %8 %6, against OpConstant %3 %7 4 plus OpStore %8 %7.
  • Probe: on the same merge, I changed only the test's first variant from module(4u) to module(8u), which gives selector 8, a value no other constant in the test module has. agc_shader_disk_cache then passes. That fits the explanation above.

The channel selectors from the description (4, 5, 6, 7) are the kind of values that may equal another constant in a real module, so the cache misses may persist on main for those programs. I haven't checked that on a real module. For the fix, one option is to make the canonical id independent of the selector's value. For example, re-number the surviving constants after removal, or prefer the constant that the folded instruction's own operands name. Either way, the test needs to pass on the merge with current main.

Also checked:

  • pr_overlap.py --branch on the head: no conflicting lines or shared exports. 20 PRs that conflict with main share files and couldn't be compared. The only one on SpirvSpecialization.cpp is #8004, which moves helpers near VisitInputs and doesn't touch removeUnusedConstants or the constant sharing.
  • Minor point for the description: it says OpConstantComposite is not removed, but the loop does include it in the opcodes it removes. Whether a composite is ever unreferenced depends on VisitIdOperands, which I haven't read through.

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed review queue Next in the maintainer review queue: checks pass, no open change request, no conflict labels Nov 4, 2026

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* needs rebase Conflicts with main, rebase needed shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants