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

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 3247 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 4650 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 5609 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6177 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 6486 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6539 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6924 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6941 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6946 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6947 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6959 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7223 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7395 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7550 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp
PR 7642 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
@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)

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* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants