Skip to content

perf(shader): validate optimizer input once - #6659

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
JJenkx:perf/spirv-single-input-validation
Oct 19, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
JJenkx:perf/spirv-single-input-validation

Conversation

@JJenkx

@JJenkx JJenkx commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

ValidateAndOptimizeSpirv validates its input, then asks SPIRV-Tools to optimize it. Optimizer::Run validates the same input again by default. Set OptimizerOptions::set_run_validator(false) after the wrapper's successful input check to remove that duplicate work. The explicit input check, its diagnostic, and final output validation remain unchanged; no optimizer pass or shader semantics change.

The pinned implementation performs the duplicate check in Optimizer::Run. The branch is one commit on main 2083bc700f12; change 64a7f53b9e9d. The title measurements below retain their original main 5d9dcc42d2ec and change 8d3415bd1619 pins. No merge commits or experimental title fixes are included.

Tested

Arch Linux 7.2.9, GCC 16.2.1 Release, Ryzen 9 7950X, 128 GiB RAM, RX 6650 XT 8 GiB, RADV Mesa 26.2.4-arch1.1.

  • Built all runtime libraries and the four tests below.
  • agc_spirv_optimizer, agc_shader_disk_cache, agc_driver_graphics, agc_driver_mesh: 4/4 pass, no test skips. Optimizer coverage includes invalid inputs, storage-only narrow types and specialized selection/loop control flow.
  • Negative control: temporarily remove only the remaining explicit input check while keeping this change. The optimizer test fails 23 checks, including accepting an illegal unused storage-only narrow constant. Restore the check: 4/4 tests pass again. That negative control is not committed.
  • Windows, other GPUs and the full test suite were not checked locally; independent coverage is attributed below.

Rebase validation on main 2083bc700f12 plus this change 64a7f53b9e9d: rebuilt all runtime libraries and the four test binaries; the same four tests pass again on RX 6650 XT with Khronos core validation enabled, zero validation errors/VUIDs and zero skips. Conventions and diff checks pass. No additional title speed measurement is claimed for this rebase.

Title: Afterdream PPSA18391, restart to the language-selection screen, no pad input, 1920×1080 rendering and the same visible 2972×1672 window. All six measured runs close normally; screenshots after the recording confirm the same screen. No gameplay FPS gain is claimed.

Cache condition / run order Main: maximum startup frame gap Change: maximum startup frame gap
Cold shader cache, A then B 1163.759 ms 989.139 ms
Cold shader cache, B then A 1170.693 ms 1029.918 ms
Warm shader cache, A then B 318.797 ms 280.166 ms

The cold-run mean maximum gap is 1167.226 → 1009.529 ms (about 13.5% lower). The single warm pair is about 12.1% lower. Median frame time remains 16.683 ms in every capture. These numbers describe the largest startup presentation gap in this scene, not total load time, total shader compile time, or a gameplay speedup. Only one Shader.PrepareArtifact scope is recorded per trace; cold results are 22.31–22.71 ms on main and 19.42–22.80 ms with the change. Those partial scope timings do not establish a total shader-compilation saving.

Capture protocol: APS5_TRACE_SECONDS=0:30 only, no other trace/profile switch, game log inherited the terminal without redirection. A complete preliminary run warmed the driver cache. Four cold captures use separate initially empty ANYPS5_SHADER_CACHE_DIR directories in A B B A order. The last two follow the performance form's warm-cache protocol: restart using each arm's populated shader cache. The cold-cache captures are an additional startup measurement, not presented as warm-cache runs. No builds, tests or debugger sampling ran during these six captures. Source revision, clean build and loaded driver mapping were checked before each launch.

Six official traces, summary lines, trace digests, per-run metadata and checksums. The release assets contain no title files, shader code or raw game logs. Each trace opens in Perfetto; digests.txt is the output of python3 tools/trace_digest.py for each trace.

Summary lines, in the table's chronological order (A, B, B, A, warm A, warm B):

{"commit":"5d9dcc42d2ec","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30015.076},"presents":1730,"frame_ms":{"p50":16.683,"p90":16.691,"p99":16.739,"max":1163.759},"gpu_busy_ms_per_present":8.975,"worker_busy_pct":{"queue worker 0x0":95.313},"top_phases_ms_per_frame":{"VideoOut.Flip":16.624,"Driver.Packet":16.509,"VideoOut.Flip.vblank_wait":15.742,"Driver.Packet.rendering_wait":14.027,"Driver.Packet.draw":0.942,"Driver.Draw":0.93,"Driver.Present":0.854,"VideoOut.Flip.present":0.845},"trace_dropped":false,"trace":"aps5-trace-5d9dcc42d2ec-4172201.json.gz"}
{"commit":"8d3415bd1619","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30006.93},"presents":1733,"frame_ms":{"p50":16.683,"p90":16.69,"p99":16.739,"max":989.139},"gpu_busy_ms_per_present":8.928,"worker_busy_pct":{"queue worker 0x0":95.734},"top_phases_ms_per_frame":{"VideoOut.Flip":16.68,"Driver.Packet":16.549,"VideoOut.Flip.vblank_wait":15.745,"Driver.Packet.rendering_wait":14.07,"Driver.Present":0.923,"Driver.Packet.draw":0.914,"Driver.Draw":0.902,"VideoOut.Flip.present":0.899},"trace_dropped":false,"trace":"aps5-trace-8d3415bd1619-27095.json.gz"}
{"commit":"8d3415bd1619","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30004.623},"presents":1723,"frame_ms":{"p50":16.683,"p90":16.69,"p99":16.739,"max":1029.918},"gpu_busy_ms_per_present":8.824,"worker_busy_pct":{"queue worker 0x0":95.596},"top_phases_ms_per_frame":{"VideoOut.Flip":16.765,"Driver.Packet":16.619,"VideoOut.Flip.vblank_wait":15.666,"Driver.Packet.rendering_wait":14.047,"Driver.Present":1.065,"VideoOut.Flip.present":1.063,"Driver.Packet.draw":0.933,"Driver.Draw":0.92},"trace_dropped":false,"trace":"aps5-trace-8d3415bd1619-62765.json.gz"}
{"commit":"5d9dcc42d2ec","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30005.647},"presents":1724,"frame_ms":{"p50":16.683,"p90":16.691,"p99":16.736,"max":1170.693},"gpu_busy_ms_per_present":8.999,"worker_busy_pct":{"queue worker 0x0":95.355},"top_phases_ms_per_frame":{"VideoOut.Flip":16.678,"Driver.Packet":16.567,"VideoOut.Flip.vblank_wait":15.704,"Driver.Packet.rendering_wait":14.025,"Driver.Packet.draw":0.943,"VideoOut.Flip.present":0.935,"Driver.Present":0.933,"Driver.Draw":0.93},"trace_dropped":false,"trace":"aps5-trace-5d9dcc42d2ec-92221.json.gz"}
{"commit":"5d9dcc42d2ec","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30007.219},"presents":1753,"frame_ms":{"p50":16.683,"p90":16.692,"p99":16.755,"max":318.797},"gpu_busy_ms_per_present":8.061,"worker_busy_pct":{"queue worker 0x0":97.839},"top_phases_ms_per_frame":{"VideoOut.Flip":16.863,"Driver.Packet":16.725,"VideoOut.Flip.vblank_wait":15.653,"Driver.Packet.rendering_wait":14.037,"Driver.Present":1.199,"VideoOut.Flip.present":1.171,"Driver.Packet.draw":0.952,"Driver.Draw":0.939},"trace_dropped":false,"trace":"aps5-trace-5d9dcc42d2ec-121856.json.gz"}
{"commit":"8d3415bd1619","dirty":false,"env":{"APS5_DRAW_CACHE_ENTRIES":"4096","APS5_HOST_IMPORT_MIB":"16384","APS5_PAD_INPUT_FILE":"<path>","APS5_PIPELINED_DRAWS":"1","APS5_TRACE_SECONDS":"0:30"},"gpu":"AMD Radeon RX 6650 XT (RADV NAVI23)","driver":"radv Mesa 26.2.4-arch1.1","os":"Linux 7.2.9-arch1-1","resolution":"1920x1080","window":{"spec":"seconds 0:30","first_present":0,"ms":30014.614},"presents":1775,"frame_ms":{"p50":16.683,"p90":16.692,"p99":16.741,"max":280.166},"gpu_busy_ms_per_present":7.896,"worker_busy_pct":{"queue worker 0x0":98.108},"top_phases_ms_per_frame":{"VideoOut.Flip":16.692,"Driver.Packet":16.566,"VideoOut.Flip.vblank_wait":15.698,"Driver.Packet.rendering_wait":13.914,"Driver.Packet.draw":0.968,"Driver.Present":0.968,"Driver.Draw":0.954,"VideoOut.Flip.present":0.953},"trace_dropped":false,"trace":"aps5-trace-8d3415bd1619-156154.json.gz"}

#4288 changes scalar-replacement bounds in the same optimizer function; it does not remove input validation duplication. #5315 changes module cleanup after specialization. Neither is a dependency of this one-line change.

Independent Windows/NVIDIA cold-boot check: m335671 reports Sword of the Sea (PPSA25344, retail 01.510.000), cold boot to the title screen without input, on Windows 11 / Ryzen 9 7900 / RTX 4070 (616.92). Main d7a455a90 versus the same plus 64a7f53b9, separate empty AnyPS5 shader caches, 1,324 misses each, driver cache warmed first, ABBA order: last shader write averages 24.10 s on main and 23.88 s with the change (0.9%, within the main-to-main spread); process CPU time also shows no measurable gain. This is contributor-provided evidence, not a run on our machine. It does not cover the larger New Game load. The Afterdream maximum-frame-gap measurements above do not establish a general total-compilation or load-time improvement.

Checklist

  • Based on current main; no other open PR implements the same change
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths still throw; no new exports or silent stubs
  • No notes, investigation files or images added to the repository
  • No new third-party code
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: AMD Radeon RX 6650 XT, RADV Mesa 26.2.4-arch1.1, Arch Linux 7.2.9
  • AI-assisted: yes

@JJenkx

JJenkx commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Additional title-path evidence from Demon's Souls PPSA01341 v01.007.000 on the same RX 6650 XT/RADV Linux machine.

On main 5d9dcc42d plus #5872 (f4d42bb0c4f2, opt-in lazy preparation), the debug-start route reached the tutorial load. A GDB sample of the active compute worker during the gray loading screen captured this call chain:

spvtools::val::Construct::blocks
spvtools::val::StructuredControlFlowChecks
spvtools::val::PerformCfgChecks
spvValidateWithOptions
spvtools::SpirvTools::Validate
spvtools::Optimizer::Run
ShaderRecompiler::ValidateAndOptimizeSpirv
ShaderRecompiler::buildSpecializedModule
ShaderRecompiler::specializeModule
ShaderRecompiler::PreparedShaderInvocation::Materialize
Driver::dispatch -> Driver::dispatchIndirect

That Optimizer::Run validation is the duplicate input-validation path this PR disables; the wrapper's explicit input and output validation remain. This is evidence that the path is reached during this title's loading, not a measured title speedup or proof that it accounts for the entire stall. The diagnostic run closed normally (exit 0); gameplay was not reached.

The combined branch (main + #5872 + this PR) now builds libs and passes 15/15 selected tests, no skips: optimizer, shader disk cache, graphics, mesh, and all 11 prepared-shader cases including lazy/lazy_sync. This combined candidate has now completed the native follow-up below. Source: testing/demons-lazy-validator-5d9dcc42d, head afc3bb34ea774f7753d4f40e602fb6325db3ed1b.

The existing Afterdream matched-scene evidence in the description remains the performance evidence for this PR. AI-assisted: yes.

Native follow-up on combined head afc3bb34ea77: the supervised diagnostic rerun rendered the debug start/menu/map screens, accepted the verified Tutorial (Fort Entrance) selection, and closed normally at the planned endpoint (exit 0, 453.14 seconds, 1,620 presentations). The tutorial remained on its gray loading screen; no level frame or gameplay was observed. A sample near the end was in RADV via CreateComputePipeline -> VulkanDevice::computePipeline -> PrepareDispatch -> Driver::dispatch -> dispatchIndirect. Removing duplicate validator work does not resolve all loading costs.

The compiler edit changed the automatic shader-cache source version, so the first candidate attempt rebuilt entries instead of reusing the earlier baseline's cache. That attempt was interrupted by SIGTERM 15 at about 428 seconds and is not counted as an ordinary-close pass. The supervised rerun reused 168 startup entries but still reported 120 misses/writes during the tutorial load. A debugger sample adds overhead. These runs establish neither a title speedup nor a regression; they are not a matched A/B comparison. Runtime arguments and the temporary kernel setting were restored. Native run accounting and limitations.

@github-actions github-actions Bot added the shader core/shader label Oct 16, 2026
@github-actions

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 6644 core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp

@m335671

m335671 commented Oct 18, 2026

Copy link
Copy Markdown

Windows / NVIDIA numbers for this change, on total cold shader-compile time rather than the largest frame gap. Sword of the Sea (PPSA25344, retail 01.510.000, UE5), cold boot to the title screen with no input. Windows 11 Pro 26200, Ryzen 9 7900, RTX 4070 12 GB (NVIDIA 616.92), WinLibs GCC 15.2.0.

  • A: main d7a455a90. B: the same plus this PR's commit 64a7f53b9, cherry-picked (one line). Only libs/ differs; it's the same relinked executable.
  • Each run has its own empty ANYPS5_SHADER_CACHE_DIR (0 hits, 1,324 misses in every run). One uncounted warm-up run filled the NVIDIA driver cache first, then the runs went A B B A, each 90 s from launch. No APS5_TRACE_* / APS5_PROFILE_* switch was set.
  • "Last shader write" is the newest file's mtime in that run's shader cache, measured from launch. With 1,324 misses, that's when the boot's last shader compile was stored. CPU is the process's total CPU time at 90 s.
run libs last shader write process CPU at 90 s
1 A 24.15 s 252.1 s
2 B 23.80 s 253.2 s
3 B 23.95 s 252.3 s
4 A 24.04 s 257.6 s

Means: A 24.10 s, B 23.88 s (−0.9 %), which is within the A-to-A spread. I see no measurable change in total compile time or CPU time on this boot. This doesn't cover the New Game load, which compiles far more and is where the 120 s RenderThread watchdog fires on a cold cache. Two runs per arm, no further repeats.

AI-assisted: yes (Claude Code). The builds, the runs and this comment were done and written by Claude Code on my machine.

@oneandonlydean oneandonlydean self-assigned this Oct 19, 2026
@oneandonlydean
oneandonlydean merged commit 574762f into boykopovar:main Oct 19, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf Performance change shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants