Skip to content

fix(shader): read a buffer V#'s presence at run time instead of specializing it - #9957

Open
mrfelek wants to merge 1 commit into
boykopovar:mainfrom
mrfelek:fix/shader-runtime-buffer-presence
Open

mrfelek wants to merge 1 commit into
boykopovar:mainfrom
mrfelek:fix/shader-runtime-buffer-presence

Conversation

@mrfelek

@mrfelek mrfelek commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What

A buffer V#'s presence (dwords[2] != 0 && (dwords[0] != 0 || (dwords[1] & 0xffff) != 0)) was supplied as specialization constant BufferBase + i*BufferWords + 2 from the dispatch's V# snapshot, and buildSpecializedModule turns supplied specializations into literal constants. The same guest program therefore compiled to two SPIR-V modules, one where the buffer was bound and one where it was still null, with different RecompileResult::Identity() and no VkPipelineCache hit between them; which one a dispatch got depended on timing.

PPSA26344 has eight compute programs of 1.2–1.45M SPIR-V words reached in both states; every new state cost a 54–320 s NVIDIA pipeline compile, and pipeline journal replay of the other state did not help.

descriptor() in SpirvRuntimeBuffer.cpp now computes presence from the V#'s words at run time, as the registration-module path already does; the formatted-buffer helper takes it from its existing valid parameter, and DescriptorBindingBuilder no longer supplies BufferBase + 2. The GuardedMemoryLoads fixture now encodes validity in the V# words instead of the specialization. ShaderDiskCache FormatVersion is bumped because generated SPIR-V changes.

Tested

Ubuntu 26.10, Ryzen 9 5950X, RTX 3080 Ti 12 GB, NVIDIA 615.71.09. PPSA26344 02.002.000 title-scene load (with a separate blend change applied locally to get past its earlier stop).

  • Before: captures of the 1,231,429-word program from two runs differed in exactly one specialization literal (BufferBase+2: 1 vs 0) and 20 OpLogicalAnd folding it. Warm run: pipelines over 100k words took 1,221 s in the first 15 min because 5 journal-replayed giants were rebuilt at 54–70 s each.
  • After, cold run: one capture per giant across 8 programs (+~2% SPIR-V each).
  • After, warm run: identical capture hashes, no giant rebuilt (journal replay 48 ms–1.26 s, live 18–74 ms); pipelines over 100k words took 21.8 s in the first 15 min.
  • Focused tests (tools/select_tests.py, 1143 selected): 1140 pass; the same 3 failures (agc_driver_sample_order, agc_driver_sample_order_grid_reference, agc_driver_sample_order_recipe_depth_locations) occur on main 895f47f on this machine.
  • Same tests with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation on main and on this change: the same 3 failures and identical VUID sets (four pre-existing kinds), no new validation errors.

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
  • Fixes: none
  • Unblocks: PPSA26344 Ghost of Yōtei (PPSA26344) #675
  • GPUs tested: NVIDIA RTX 3080 Ti, driver 615.71.09, Ubuntu 26.10
  • AI-assisted: yes

Related: #639 added registration-time modules, which already read presence from the V# at run time; this brings the dispatch-time specialization path in line.
Conflicts with #7484 only in the GuardedMemoryLoads test fixture (emitLoad/AccessTrace signatures); whichever lands second needs a small fixture rebase.

AI-assisted. Tested by me on my own machine.

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 23, 2026

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

I reviewed the exact head 5106dae. The change removes dispatch-time buffer-presence specialization from the cache identity and derives it from the descriptor words at runtime. The specialized GCN path masks the high-base word before testing its low 16 bits, and the formatted-buffer helper now receives validity from its existing runtime parameter. The GuardedMemoryLoads fixture encodes descriptor validity in the V# words, and the shader disk-cache format is bumped. I found no correctness issue in those paths.\n\nLocal checks run: and All conventions checks passed. both pass. I did not run the local GPU test suite because no usable build tree is available and the root filesystem is full. I inspected the reported same-machine NVIDIA baseline/change results: the author reports 1,140/1,143 selected tests passing on both, the same three existing sample-order failures and identical pre-existing VUID sets, plus matching warm capture hashes across eight large programs. Hosted relinker/product build jobs are policy-skipped for this PR, so I am relying on the author-provided runtime report for GPU validation.

@MarcZX10

Copy link
Copy Markdown
Contributor

Correction to the review text: the local commands were git diff --check upstream/main...refs/review/9957 and python3 tools/check_conventions.py --base upstream/main --head refs/review/9957; both passed. The shell removed the inline command text from my initial review body. The test and hosted-check attribution in that review is otherwise unchanged.

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

Reviewed the four-file change and the reported same-machine main/change Vulkan validation runs. The dispatch-time path now derives buffer presence from V# words at run time; the formatted-buffer path uses the existing runtime parameter, and the disk-cache version is bumped for the SPIR-V change. The fixture now encodes validity in descriptor words rather than specialization, matching the production behavior. The author reports the same three known test failures and VUID set on main and the change, with no new validation errors; required intake/conventions checks pass. Approved at head 5106dae.

@github-actions

github-actions Bot commented Oct 23, 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 5371 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 5609 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 6280 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 6412 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp
PR 6539 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 7484 core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp
core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp
PR 7642 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp
PR 7689 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp
PR 9897 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 10049 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
PR 10193 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp

@klosixgaming-arch

Copy link
Copy Markdown
Contributor

Hardware & Environment

  • OS: Windows 11 Version 24H2 (OS Build 26100.3194)
  • CPU: AMD Ryzen 9 9950X3D (16 Cores, 32 Threads)
  • GPU: NVIDIA GeForce RTX 5090 32 GB (Driver: 617.42)
  • Toolchain: MinGW-w64 GCC 15.2.0, Ninja 1.13.0, CMake 3.31.6

Test Methodology & Verification

  1. Security Audit:

    • Inspected commit 5106dae4ce37c9217addcf6d63bf744953d236d3 across all 4 touched files:
      • core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp
      • core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
      • core/shader/recompiler/ShaderDiskCache.hpp
      • core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp
    • Verified no untrusted network calls, credential exfiltration, obfuscated binaries, or external downloads.
  2. Negative Baseline Verification:

    • Reverting the runtime buffer changes in SpirvRuntimeBuffer.cpp while retaining the updated test fixture reproduces the pipeline specialization mismatch, with agc_guarded_memory_loads failing due to unexpected specialization constant behavior when binding dynamic vs null buffers.
  3. Positive Test Results:

    • Recompiled and executed targeted suite:
      • agc_guarded_memory_loads: PASSED (0.03s)
      • agc_driver_graphics: PASSED (7.21s)
      • shader_cache_native_paths: PASSED (0.99s)
      • shader_cache_runtime_sources: PASSED (55.34s)
    • Reading buffer V# presence at runtime directly from descriptor words instead of specializing via specialization constant BufferBase + 2 avoids generating redundant SPIR-V module permutations for bound vs null descriptors and prevents pipeline cache bloat.
  4. Conventions:

    • python tools/check_conventions.py --base origin/main passed cleanly with zero warnings or errors.

LGTM!

AI-assisted: yes. Written with AI assistance and reviewed by me.

@mrfelek

mrfelek commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Prior art I missed when opening this: #5353 (@Pipodi) moved both buffer stride and presence to run time for the same Yōtei compute programs. It was closed on Oct 17 because, with cold caches, the first frame came later (261 s) and total compile work didn't drop.

How this one differs:

  • It covers presence only. Stride, the format word and base misalignment stay specialized. The SPIR-V grows ~2% per giant (e.g. 1,231,429 → 1,255,837 words).
  • The gain is on later launches, not the cold one. Before this change, a giant first seen with a buffer still null and later with it bound (or the other way round) got a second identity. The journal-replayed pipeline then missed and was rebuilt live at 54–70 s each. Warm run, first 15 min: 1,221 s → 21.8 s for pipelines over 100k words, with no giant rebuilt.
  • Cold cost is about the same, as perf(recompiler): read the buffer stride and presence at run time in compute modules #5353 found. The cold run with this change compiled one module per giant (8 programs, 68–78 s each, 320 s for the 1.45M-word one). I haven't measured cold time-to-first-frame against main.
  • I haven't measured per-dispatch GPU time, which @CnRJay asked for on perf(recompiler): read the buffer stride and presence at run time in compute modules #5353 (Pipodi measured no per-dispatch GPU cost for the stride+presence variant there). I can add an A/B if wanted.

The format-word variant Pipodi found on Oct 22 (discussion #675) is a separate key; this PR doesn't touch it.

This post was prepared with AI assistance (Claude); I reviewed the results.

@BKlaasWerkman

Copy link
Copy Markdown

Intel run, since the generated SPIR-V changes and the runs above are NVIDIA only. Head 5106dae was merged into main 15e71db (clean merge) and compared with that main on the same machine.

Setup: Windows 11 Pro 10.0.26300, WinLibs GCC 15.2.0 r7, Release. Environment: VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (1.4.363), ANYPS5_REQUIRE_VULKAN=1 and ANYPS5_NO_SHADER_CACHE=1. The run covers the 1,250 tests that tools/select_tests.py selects for this diff.

GPU this PR main
Intel Arc Graphics (Meteor Lake, 101.9150) 1222 passed, 26 skipped, 2 failed the same
NVIDIA GeForce RTX 3060 Ti (617.42) 1243 passed, 4 skipped, 3 failed the same
  • No test changes result, and on both GPUs the validation messages are the same as main's.
  • agc_guarded_memory_loads, with the updated fixture, prints the same seven passing cases on both GPUs.
  • The failures also occur on main:
    • On Intel, agc_storage_write_back and agc_driver_multisample_retirement fail.
    • On NVIDIA, the agc_driver_sample_order trio fails.

AI-assisted: yes (Claude Code)

@oneandonlydean oneandonlydean self-assigned this Oct 23, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@mrfelek this now conflicts with main in ShaderDiskCache.hpp: the branch bumps FormatVersion 44 -> 45, and main is already at 46 (#8564's 4_4_4_4 target change and the one before it). Could you merge main and take the next free number? That's 47 today, but #9852 and #10049 are also moving the cache format, so whichever lands first keeps 47 and the others move up. The rest of the branch merges cleanly, so it's just the version line, then a CI rerun.

@mrfelek
mrfelek force-pushed the fix/shader-runtime-buffer-presence branch from 5106dae to 2e0c5c3 Compare October 24, 2026 10:34
@mrfelek

mrfelek commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean Rebased onto main 887a964 and took FormatVersion 47; full ctest with validation matches main (same 3 sample_order failures). AI-assisted. Tested by me on my own machine.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 24, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@mrfelek Thanks for the rebase. A correction to my earlier ask, sorry for the churn: this PR doesn't need a FormatVersion bump at all. It changes runtime SPIR-V generation, not the encoded cache layout (there's no ShaderDiskCache.cpp change), and each changed runtime source already changes SourceVersion, which keys the cache directory and is checked on read. Could you drop the ShaderDiskCache.hpp FormatVersion edit and keep main's value? Several other open PRs need the next numbers.

…alizing it

A buffer V#'s presence came from the dispatch's V# snapshot as the
specialization constant BufferBase + 2, which buildSpecializedModule
turns into a literal. One guest program then compiled to two SPIR-V
modules, one with the buffer bound and one with it still null, each a
separate pipeline. descriptor() now reads presence from the V# words at
run time, as registration modules already do, and the formatted-buffer
helper takes it from its valid parameter.

AI-assisted. Tested by me on my own machine.
@mrfelek
mrfelek force-pushed the fix/shader-runtime-buffer-presence branch from 2e0c5c3 to 2031190 Compare October 24, 2026 13:03
@mrfelek

mrfelek commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean done: dropped the FormatVersion change and kept main's value; the PR now only touches the runtime SPIR-V and its test. AI-assisted. Tested by me on my own machine.

Celegans12 added a commit to Celegans12/AnyPS5 that referenced this pull request Oct 24, 2026
…me) into local/merge23

Our tree already read presence at run time (runtimePresent) and took the
formatted helper's valid from its parameter; kept ours and added the PR's
GCN word1 mask to runtimePresent. The BufferBase + 2 specialization is
no longer recorded. FormatVersion 52 -> 53.

AI-assisted: yes
Celegans12 added a commit to Celegans12/AnyPS5 that referenced this pull request Oct 24, 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

unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants