Skip to content

perf(recompiler): keep the front-end resource plan in the shader disk cache - #4883

Merged
oneandonlydean merged 9 commits into
boykopovar:mainfrom
nikowolf123:perf/shader-cache-resource-plan
Oct 14, 2026
Merged

oneandonlydean merged 9 commits into
boykopovar:mainfrom
nikowolf123:perf/shader-cache-resource-plan

Conversation

@nikowolf123

@nikowolf123 nikowolf123 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

With a warm shader cache, every launch still runs the whole recompiler front end once for every shader. getSource builds ResourceProgram (decode, CFG, structurizer, translation, SSA, the fold passes, SrtWalker, ResourceTracker, then ExtractPlan) before findOrCompileVariant looks in the disk cache, because the disk cache keeps compiled variants but not the resource plan. Only SPIR-V emission is skipped. #2292 measured 0.3 to 0.8 s per registration with 0 cache misses and lists this fix as its option 4. #2774 (about 1,950 hits, no misses) found the plan "is built before ShaderDiskCache::Load is consulted". #3504 hit its slow pass "with a warm shader cache", and #4404 reports hundreds of queued preparation jobs on a warm run with zero misses.

This PR stores the plan in the disk cache and loads it before the front end:

  • Since perf(shader): build a shader's front end once on first use #471 the plan owns its values (ownPlanValues), so it can be written out. EncodePlan writes the value graph in storage order as LEB128 integers (opcode, type, id, flags, register, immediate, each argument as the zigzag distance to the value's own index, phi predecessor blocks), the roots as value indices, and the plain data. ShaderInfo uses the variant's encoder, now split out of encodeInfo. DecodePlan rebuilds the graph the way ownPlanValues does. It rejects an unknown opcode, an overlong integer, and a value, block, descriptor source or image index past the end, including an image table's heap and material sources and the targets of control-flow edges.
  • A plan entry uses the variant entry's header, key and payload hashes, directory and background writer. Its key is the in-memory source key (RecompileCacheKey::Build and the host subgroup size), every code word and the switch key, under a plan tag. The binding layout is not in it, so all layouts of a shader share one plan. FormatVersion 30. Size asserts and field counts cover the plan structures (below).
  • getSource tries LoadPlan first. On a hit the program stays null: findOrCompileVariant already rebuilds it when no stored variant fits a layout. On a miss it builds the plan as before and stores it. A plan the front end refuses is not stored. The disk is not used when the cache is off, under the debug probe, with APS5_DUMP_IR, or with APS5_NO_PERF_DISK_PLANS=1, a cache-neutral switch for comparisons.
  • The 10 s cache report counts plan hits, misses, writes and load failures. FrontEndBuilds() counts PrepareResourceProgram calls, for the tests.

Field counts: the plan and ShaderInfo encoders write their structures field by field, and until now the size asserts were the only reminder to update them. A field that fits in existing padding does not change the size. main's #5097 added ShaderInfo::layerExported and sizeof(ShaderInfo) stayed 232. The three bools #5089 adds to DescriptorSource::IndirectImage keep it at 28. A size assert catches neither, so a missed encoder update would go unnoticed and a warm cache would hand back entries without the new field. Next to the size asserts, a static_assert now pins the field count of every aggregate the plan codec reaches: IrResourcePlan, GuestRegister, MemoryInfo, DescriptorSource, IndirectImage, ResourceBlock, SrtRead, UniformFill, UniformFillPlan, and ShaderInfo with its buffer, image, sampler, sampled-pair and stage input and output records. The count is found by brace-initializing the aggregate with more and more values of a type that converts to anything, until that stops compiling. This is plain C++20 and does not depend on the ABI, so unlike the size asserts it is checked on MinGW too. IrValue is a class with private state, so it keeps only its size assert.

This adds no new correctness assumption. A warm run on main already pairs a variant compiled by an earlier process with a plan rebuilt by the current one, and both come from the same front end under the same SourceVersion. Specialized modules are still built in each process. Storing them would be a separate change.

Bytes per plan entry: 1.4 to 7.1 KB for the 15 in-tree fixtures in the test (4.3 KB on average). Control-flow conditions can pull most of a program into the plan, so the worst case is large. A synthetic compute shader with a branch every 20 dwords has plans of 518 KB, 2.07 MB and 8.25 MB at 2,038, 8,038 and 32,038 dwords. Its variant entries are 935 KB, 3.70 MB and 14.7 MB. Fixed-width fields would have made those plans 2.5 MB at 2,038 dwords, which is why values are LEB128.

Overlap, all textual: #4806 changes Words() to Read() next to these hunks in Recompiler.cpp and ShaderDiskCache.cpp. #4732 generates the cache version, and #4475 and other open PRs also bump FormatVersion. #4271 and #4757 edit Recompiler.cpp near getSource. #1341 adds an execution plan to IrResourcePlan, which would be rebuilt after DecodePlan. #5089 adds three fields to IndirectImage. Whichever of #5089 and this PR lands second must add them to the plan encoder, and the field count turns a missed update into a build error. No open or closed PR stores the plan.

Tested

WSL Ubuntu, i9-14900KF, GCC 15, the CI configuration (Release, SPIRV-Tools on, lavapipe), with the series on main d1fd937. Also native Windows 11 (WinLibs GCC 15.2.0, Release with tests) on an RTX 5080 (NVIDIA 616.92), same head: full ctest with ANYPS5_REQUIRE_VULKAN=1, 1021 tests, 0 failed, 7 skipped as tools/check_skips.py --job windows expects; agc_shader_disk_cache, including its two-process runs, passes there. Not tested with a title.

  • Full ctest: 1004 tests, 0 failures. 41 tests skipped themselves because lavapipe lacks a feature or there is no display. The WSL distro restarted several times during the session, so the final run went in chunks of 100 tests.
  • agc_shader_disk_cache (1.2 s) gains these tests:
    • Round trips of 15 in-tree shader fixtures and a hand-built plan. The fixtures come from agc_shader_memory, except "descriptor invariant phi", which is its "descriptor phi image" with both branches loading the same descriptor. They cover SRT reads, guarded and chained pointers, control-flow conditions, an indirect image table, a loop phi, an invariant descriptor phi and a fragment shader. The hand-built plan gives every plan-level field, every MemoryInfo and image table field, the uniform fill and a 64-bit immediate a value other than the default.
    • Every decoded plan must equal its original field by field, compared without the codec: the value graph by storage index (opcode, type, id, flags, register, immediate, arguments, phi blocks), the block ids, the roots and the plain data.
    • Each fixture's original and decoded plan is also materialized over 1000 random user-data and memory runtimes, and the snapshot, poison, null-root count and read trace (or the error) must match. The hand-built plan is not a walkable SRT, so it is covered by the field comparison only.
    • Truncated, bit-flipped, previous-format and other-source-version entries are rejected, and malformed plans do not decode.
    • The plan key follows the code, the target and the host subgroup size, but not the layout, the addresses or the user data.
    • Two processes share one cache directory and prepare, capture and materialize all 15 fixtures. The second process must run the front end 0 times, hit 15 plans and 15 variants, and produce byte-identical SPIR-V, bindings, push constants, snapshots, read traces and plans. It then builds each fixture's plan with its own front end, and that plan must equal the stored one field by field.
    • With APS5_NO_PERF_DISK_PLANS=1, a process on the same cache runs the front end for all 15 fixtures, loads and stores no plan, still hits the 15 stored variants, and produces the same output.
    • A damaged plan entry is rejected, rebuilt alone with the same output, and stored again. A changed code word and another switch value miss, and a refused plan is not stored.
  • Fails before, passes after (run before the invariant phi fixture was added, so with 14 fixtures): with only the getSource hunk reverted, the second process reports front end builds 14, plan hits 0, plan misses 0, plan load failures 0, variant hits 14, variant misses 0 and the test fails. With the change it passes.
  • Field coverage: for each of 51 plan fields and value parts in turn, I made EncodePlan write the default value instead, which is the same as leaving the field out of both the encoder and the decoder. The test fails for every one of them.
  • Whole suite on the earlier base 43e3a70, with local instrumentation that is not part of the PR: every ResourceMaterializer::Materialize call also encoded, decoded and re-encoded its plan, materialized the decoded copy with the same runtime, and aborted on any difference. All 1002 tests passed. Across 266 test executables besides the disk-cache test, more than 3,700 materializations matched (snapshots, read traces and error messages), with no mismatch.

Performance: there is no title or Performance report summary line here, only a synthetic harness. It is a compute shader with 16 V#s loaded from an SRT and a buffer load, an ALU chain and a divergent branch every 20 dwords. It calls PrepareShader in one process with an empty cache (cold) and then in a new process with the same cache (warm), with APS5_SPIRV_OPT=none. main 43e3a70 and this series on the same base were built from one tree and run interleaved on the same machine. Since then main has gained one front-end pass and one ShaderInfo field (#4031's ancillary word), which these runs do not include. Load average was 7 to 20 from other jobs. Medians of 7 runs (5 at 32k):

  • 2,038 dwords: warm PrepareShader 395 ms on main, 25 ms with this PR (15.7x).
  • 8,038 dwords: 711 ms on main, 39 ms with this PR (18.1x).
  • 32,038 dwords: 6.7 s on main, 0.27 s with this PR (25x). This size is the noisiest: main's warm runs ranged from 3.2 to 12.1 s and this PR's from 0.19 to 0.37 s.
  • With APS5_NO_PERF_DISK_PLANS=1, the branch's warm runs match main: 420 ms, 698 ms and 5.2 s.
  • Cold runs did not change measurably at 2k and 8k (702 and 1,337 ms on main, 672 and 1,281 ms with this PR). At 32k both builds ranged from 7 to 19 s under that load. The plan is encoded on the writer thread, and that encode took 6, 41 and 83 ms at the three sizes. Decoding took 10, 68 and 135 ms.

I can attach the JSON lines of every run, the harness source and the instrumentation patch. A warm-start comparison on a title would show the real gain: start-up time with and without APS5_NO_PERF_DISK_PLANS=1 against the same cache, from the reporters of #2292, #2774 or #4404.

  • Rebased onto main 0c3dc9c for this pull request: it builds in the CI configuration and the 32 shader, pipeline-library, structurizer and control-flow tests (ctest -R 'agc_shader|agc_driver_prepared|agc_pipeline_library|agc_structurizer|control_flow') pass on lavapipe.

Rebased onto main 60fb618. main had added a reclaim argument to getSource and ResourceProgram (57b2dc8), which conflicted with the disk-plan path in getSource. The plan loaded from the disk cache is used as before; when there is none, ResourceProgram is built with main's reclaim argument. PrepareShader still passes reclaim = false, and on a disk-plan hit its program is rebuilt by the existing fallback in findOrCompileVariant when a variant must be compiled. The per-file diff is unchanged. WSL2 Ubuntu 26.04, lavapipe (Mesa 26.0.8), CI configuration: the disk cache, structurizer and control-flow tests 16 of 16, full ctest 1067 of 1067.

Phi coverage with #5181: the new fixture "descriptor invariant phi" loads one image descriptor from the same SRT offset on both sides of a branch. Its plan keeps eight invariant phis as descriptor dwords, and the CPU walk evaluates them, from the built and from the stored plan, to the image descriptor in the SRT. The only other planned phi is the loop counter of "bindless sample in a loop latch", in a latch condition the walk cannot evaluate. With #5181 16bd650 merged locally (the plan codec moved to its value arena), the phi check passes with or without the new fixture, because #5181 counts CompositeExtractU32x2 as evaluable on its own and keeps that latch condition. With extracts checked through their source instead, it fails with no fixture plans a phi without the fixture and passes with it. On the branch, ctest -R 'disk_cache|shader_cache|agc_shader|recompiler|structurizer': 16 of 16.

Rebased onto main 0214069. main's #5097 added layerExported to the ShaderInfo encoder, which this PR splits into encodeShaderInfo and decodeShaderInfo. The flag is now written last in both, right after rasterPrimitive, as on main. A variant entry's payload therefore has the same bytes as on main, and plans store the flag too. The entry round trip now sets layerExported, as it does the other ShaderInfo flags, so an encoder and decoder that both skip it fail with entry round trip: shader info differs. #5097 also took FormatVersion 29, so this PR moves to 30. Against main, the series changes the same lines as before the rebase except the FormatVersion line, plus the field-count asserts. Nothing else main changed after 60fb618 touches a structure the codecs write, apart from the new StageInputKind::LayerExported, which the stage-input encoder writes as its integer value. WSL2 Ubuntu 26.04, lavapipe (Mesa 26.0.8), CI configuration: ctest -R 'disk_cache|shader_cache|agc_shader|recompiler|structurizer' 16 of 16, full ctest 1077 of 1077 (43 skipped themselves: lavapipe features, and the WSLg display has no 120 Hz mode). The field-count asserts compile in that build (GCC 15.2), with Clang 21, and in a native Windows compile of ShaderDiskCache.cpp with MinGW GCC 15.2.0 (WinLibs r7). With #5089's three bools added to IndirectImage, every size assert still holds and the build stops at static assertion failed: IndirectImage changed: update the plan encoder (the comparison reduces to '(10 == 7)') with each of those compilers. A bool added in the padding of ShaderInfo, MemoryInfo, IrResourcePlan, StageInput or SamplerResource also keeps its size and stops the build at that structure's field count.

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 (Mesa 26.0.8, llvmpipe) on WSL2 Ubuntu 26.04; NVIDIA GeForce RTX 5080, driver 616.92, Windows 11
  • AI-assisted: yes. Written with Claude Code; I reviewed every line and ran the tests above.

🤖 Generated with Claude Code

@EagleBearerYS

Copy link
Copy Markdown
Contributor

A title measurement: Resident Evil Village (PPSA01556, 01.210.000) on Windows 11, RTX 3070 Laptop, WinLibs GCC 15.2.0 r7, Release. It registers its whole PSO cache at boot behind a black screen, about 6,000 shaders, so the boot measures shader preparation directly.

The build is main b478fbb plus #4949's register change (so it gets past its first frame) and this PR. Merging this PR needed one conflict resolved in Recompiler.cpp: #4271 added a reclaim parameter to getSource and ResourceProgram, which I kept. I filled the cache with one cold run, then ran warm runs on the same cache, alternating APS5_NO_PERF_DISK_PLANS=1 and the default. Shaders back are the [shader-disk-cache] totals:

Run Plans from the cache Shaders back at 32 s at 64 s at 80 s
1 off 1,380 1,994 2,182
2 on 2,851 (all cached)
3 off 1,463 2,007 2,233
4 on 2,961 (all cached)

The cold run took about 240 s to reach 2,800 shaders. The front end is the bottleneck of this boot, and with this PR a warm boot skips most of it.

A heads-up on the tests: #5181 leaves branch conditions the CPU walk can never evaluate out of resource plans. I had the same idea locally. With it, agc_shader_disk_cache's coverage check failed on top of this PR with "no fixture plans a phi": the only fixture phis are the loop counter of the "descriptor phi loop" fixture (not invariant, so never evaluable) and the branch on it. Each PR passed on its own. #5181 is likely to hit the same check. A fixture whose plan keeps an evaluable phi would cover both, for example an invariant phi that feeds a descriptor dword.

@nikowolf123
nikowolf123 force-pushed the perf/shader-cache-resource-plan branch from d6cd9f2 to 3d86335 Compare October 13, 2026 15:32
@nikowolf123

Copy link
Copy Markdown
Contributor Author

Thank you, that's the title measurement this PR needed. On a warm cache, Resident Evil Village gets all of its roughly 2,900 shaders back by 32 s with plans from the disk cache, against about 1,400 without them.

The reclaim conflict you resolved is the one I resolved in today's rebase (3d86335), the same way: the disk-plan path stays, and ResourceProgram gets main's reclaim argument when there is no stored plan.

Thanks also for the #5181 heads-up. I'm adding a fixture whose plan keeps an evaluable phi, so the coverage check holds in whichever order the two PRs land. I'll post here when it's pushed.

nikowolf123 and others added 9 commits October 13, 2026 18:23
…lope

encodeInfo and decodeInfo wrote the ShaderInfo fields inline between the
compiled-shader header and the binding layout, and EncodeEntry and
DecodeEntry built and checked the file header (magic, format, source
version, key and payload hashes) inline. Move both into helpers,
encodeShaderInfo/decodeShaderInfo and wrapEntry/unwrapEntry, so that a
second kind of entry can reuse them.

The bytes of a compiled variant entry do not change.

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

The disk cache keeps compiled variants (SPIR-V, shader info, binding
layout) but not the IrResourcePlan the front end extracts for the
resource walk, so a process can only get a plan by running the whole
front end. Since boykopovar#471 the plan owns its values (ownPlanValues clones the
graph behind every root into valueStorage and blockStorage), so it can
be written out and rebuilt.

- EncodePlan writes the value graph in storage order, each value as
  LEB128 integers: opcode, type, id, flags, register, operand count,
  the immediate if it has one, each argument as the zigzag distance to
  the value's own index (0 for null) and, for phis, the index of each
  predecessor block. The roots (descriptor source dwords, SRT reads,
  control-flow conditions, uniform-fill values) are value indices; the
  plain data (memory info, indirect image tables, control-flow edges,
  materialization sources, guarded, clean and pure flat slots, the
  flags, the ShaderInfo through the variant's encoder) follows. A plan
  whose roots or arguments point outside its storage is not encoded.
  Control-flow conditions can pull most of a program into the plan:
  a synthetic 2,038-dword compute shader with a branch every 20 dwords
  has 42,775 plan values, which take 518 KB this way and 2.5 MB with
  fixed-width fields; its compiled variant entry is 935 KB.
- DecodePlan rebuilds the graph the way ownPlanValues does and rejects
  an unknown opcode, an overlong integer, an index past the values or
  blocks, a descriptor source, image, buffer or sampler past the
  sources, a branch without two successors, an edge past the blocks,
  and a uniform fill of more than four words.
- BuildPlanKey is the in-memory source key (RecompileCacheKey::Build and
  the host subgroup size), every code word and the switch key, after a
  plan entry tag. It leaves out the binding layout, so all layouts of a
  shader share one plan.
- EncodePlanEntry/DecodePlanEntry use the variant's header and hashes;
  LoadPlan and StorePlan use the same directory and background writer
  and count plan hits, misses, writes and load failures on their own.
- Size asserts cover the plan and the structures it is written from.
- FormatVersion 30.

agc_shader_disk_cache now round-trips the plans of 14 in-tree shader
fixtures (SRT reads, guarded and chained pointers, control-flow
conditions, an indirect image table, a loop phi, a fragment shader) and
a hand-built plan with a uniform fill. For each it materializes the
original and the decoded plan over 1000 random user-data and memory
runtimes and requires the same snapshot, poison, null-root count and
read trace, or the same error. Truncated, bit-flipped and
other-format entries are rejected, malformed plans do not decode, and
the key follows the code, the target and the host subgroup size but not
the layout, addresses or user data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he front end

getSource built ResourceProgram (PrepareResourceProgram and ExtractPlan)
for the first request of every shader a process saw, before
findOrCompileVariant looked in the disk cache. With a warm cache every
launch still decoded, built the CFG, structurized, translated and ran
SSA, the fold passes, SrtWalker and ResourceTracker for every shader;
only SPIR-V emission was skipped.

getSource now builds the plan key and tries LoadPlan first. On a hit it
sets the plan and leaves the program null: findOrCompileVariant already
rebuilds the program when no stored variant fits the layout. The loaded
plan takes the request's code address as its shader hash, as a plan
built in this process would. On a miss it builds the plan as before and
stores it. A plan the front end refuses is not stored. The disk is not
used when the cache is off, under the debug probe, with APS5_DUMP_IR
(which prints the front-end passes), or with APS5_NO_PERF_DISK_PLANS=1,
a cache-neutral switch for comparisons.

FrontEndBuilds() counts PrepareResourceProgram calls. The new
cross-process test prepares, captures and materializes 14 shader
fixtures in one process and repeats it in a second process sharing the
cache directory: the second process must run the front end 0 times, hit
14 plans and 14 variants, and produce byte-identical SPIR-V, bindings,
push constants, snapshots, read traces and plans. A damaged plan entry
is rejected and rebuilt alone with the same output and stored again; a
changed code word and another switch value miss.

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

DecodePlan checked every descriptor source index a plan holds except the
two inside an indirect image table: the heap source and the material
source. A damaged entry could therefore decode with a table that points
past the plan's descriptor sources, and only the materializer's own
bounds checks stood between it and an out-of-range read. The front end
interns both sources before the image source that names them, so a plan
it builds always passes the new check.

agc_shader_disk_cache now requires a plan whose table heap or material
source lies past the sources not to decode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan tests compared a decoded plan with the original only through
the codec itself (re-encoding must give the same bytes) and through
materialization. Neither catches a field that EncodePlan and DecodePlan
both leave out: the bytes still agree, and the walk does not read every
field. Leaving userDataBase, the plan's sRGB formats, the value ids or
several MemoryInfo fields (secondaryOffset, dataDwords, dataFormat,
planningOnly, idxen) out of both sides passed every test. The hand-built
plan also never materializes (its uniform values cannot be evaluated),
so its 1000 runtimes all failed with the same error, and no plan held an
immediate wider than 32 bits.

samePlan compares every field of two plans without the codec: the value
graph by storage index (opcode, type, id, flags, register, immediate,
arguments, phi blocks), the block ids, the roots, and the plain data
through the structures' own equality. The round trip now requires it
for the 14 fixtures and the hand-built plan, which sets every MemoryInfo
field, all indirect table fields, a second buffer, the whole uniform
fill and a 64-bit immediate to values other than their defaults. The
second process also builds each fixture's plan with its own front end
and requires it to equal the plan it used, so a stored plan must match
what the front end builds in another process, not only its own
re-encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
APS5_NO_PERF_DISK_PLANS=1 is listed as a neutral switch, so it stays out
of the variant key, and it exists to compare warm starts with and
without stored plans. Nothing tested either property: the cross-process
test ran with the switch unset only.

A fourth child process now sets the switch against the cache the first
process filled. It must run the front end for all 14 fixtures, neither
load nor store a plan, still hit all 14 stored variants, and produce the
same SPIR-V, bindings, push constants, snapshots, read traces and plans
as the first process.

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

The round trip requires some fixture to plan a phi, and only one did:
the loop counter of "bindless sample in a loop latch", which reaches the
plan through the latch condition. The counter is not invariant, so the
CPU walk can never evaluate that condition. A plan that keeps only the
conditions the walk can evaluate, as boykopovar#5181 aims to, would hold no phi
and fail the check with "no fixture plans a phi".

"descriptor invariant phi" loads the image descriptor from the same SRT
offset on both sides of a branch on an SRT dword. Its eight dwords are
phis of equivalent reads: the tracker keeps them, since it splits only
descriptor phis that are not invariant, and the walk evaluates them
through the invariant read. The plan stores them as descriptor dwords,
whichever conditions it keeps, so the phi encoding stays covered. The
fixture goes after the other compute fixtures, so they keep their seeds
and their damaged-entry checks.

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

The plan and ShaderInfo encoders write these structures field by field,
and the sizeof asserts are their only reminder to update the encoders. A
field that fits in existing padding keeps the size: main's layerExported
landed in ShaderInfo with sizeof still 232, and the three bools boykopovar#5089
adds to DescriptorSource::IndirectImage keep it at 28. A warm cache
would then hand back entries without the new field and nothing would
fail.

Count the fields of every aggregate the plan codec reaches (IrResourcePlan,
GuestRegister, MemoryInfo, DescriptorSource and IndirectImage,
ResourceBlock, SrtRead, UniformFill, UniformFillPlan, and ShaderInfo with
its resource and stage I/O records) by brace-initializing each with more
and more convertible-to-anything values until the initializer no longer
compiles, and assert the counts. Unlike the size asserts this does not
depend on the ABI, so it is checked on every platform. IrValue is a
class with private state and keeps only its size assert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boykopovar#5097 added ShaderInfo::layerExported to the entry encoder, and this
series moves that line into encodeShaderInfo, which plan entries use
too. The entry round trip left the flag false, so an encoder and a
decoder that both skipped it still passed. Set it like the other
ShaderInfo flags: with layerExported dropped from both, the round trip
now fails with "entry round trip: shader info differs".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nikowolf123 added a commit to nikowolf123/AnyPS5 that referenced this pull request Oct 13, 2026
buffer_store_format_* and its D16 forms wrote memory component i from
data VGPR i and ignored the V#'s DST_SEL, while loads through the same
V# apply it. A store through a swizzled V# therefore did not read back
what it stored: with 8_8_8_8_UNORM and DST_SEL Z,Y,X,W, storing
(1.0, 0.75, 0.2, 0.25) wrote 0x4033bfff instead of 0x40ffbf33.

The RDNA 2 ISA guide (section 8.1.4, table 31) takes DST_SEL from the
resource for BUFFER_STORE_FORMAT_*. RADV builds texel-buffer V#s with
the format's swizzle (Z,Y,X,W for B8G8R8A8, 0,0,0,X for A8) and ACO
stores through them unchanged, so each format component takes the data
slot whose selector names it, and slots that select 0 or 1 write
nothing. StoreTexel applies the same rule to image stores. TBUFFER
stores keep the identity selectors.

These sources settle only a DST_SEL that names each format component
once. A V# that names one twice or not at all, or holds a reserved
selector, now throws when the binding plan is prepared, and a
GPU-selected one records an invalid-descriptor fault and skips the
store. ShaderInfo records which buffers have formatted stores (disk
cache format 31, as the
open boykopovar#4883 takes 30). Technical debt records that none of this is measured
on hardware.

agc_driver_buffer_store_format_dst_sel stores through Z,Y,X,W
8_8_8_8_UNORM (also D16 and store_format_x), Y,Z,X,W 16_16_16_16_UINT,
0,0,0,X 8_UINT and X,0,0,1 32_UINT V#s in wave32 and wave64, reads the
first three back with buffer_load_format_xyzw, checks that three
unsettled selector sets are refused, and compiles a store through a
GPU-selected V#. On main it fails with red and blue swapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nikowolf123
nikowolf123 force-pushed the perf/shader-cache-resource-plan branch from 3d86335 to 5d3ea5f Compare October 13, 2026 17:33
@nikowolf123

Copy link
Copy Markdown
Contributor Author

@EagleBearerYS the fixture is pushed in ce8944f. "descriptor invariant phi" loads one image descriptor from the same SRT offset on both sides of a branch. Its plan keeps eight invariant phis as descriptor dwords, and the CPU walk evaluates them from both the built plan and the stored one.

As it turns out, with #5181's current head (16bd650) the phi check would pass even without it. #5181's MayEvaluate counts CompositeExtractU32x2 as evaluable without looking at its source. So the loop-latch condition of "bindless sample in a loop latch" stays in the plan, and so does its counter phi, even though the walk can never evaluate that condition. If extracts were checked through their source, that condition and its phi would be dropped, and the check would fail with no fixture plans a phi. The new fixture keeps phis covered in either case, whichever PR lands first.

The same push rebases onto main (format 30, with main's layerExported carried into the split ShaderInfo codec) and adds 664ad77, which pins the field count of every structure the plan codec writes, so a field added inside padding (as in #5089) breaks the build until the codec writes it.

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

github-actions Bot commented Oct 13, 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
#5315 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp L20-21, L1084
#5500 core/shader/recompiler/ShaderDiskCache.cpp L560-561, L671

@oneandonlydean oneandonlydean self-assigned this Oct 13, 2026
RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 14, 2026
…474)

* fix(shader): verify materialized snapshot identity before memo reuse

Ported from upstream PR boykopovar#4757.

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

* perf(shader): emit both registration layouts of a pixel shader from one program

Ported from upstream PR boykopovar#4884. The libSceAgcDriver test hunks are not
ported: those tests are not built by any macOS configuration.

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

* perf(recompiler): keep the front-end resource plan in the shader disk cache

Ported from upstream PR boykopovar#4883. A warm launch loads the stored resource
plan and skips the shader front end. ShaderDiskCache FormatVersion
32 -> 33. The libSceAgcDriver test hunks are not ported: those tests
are not built by any macOS configuration.

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

* perf(shader): compile reusable SRT expression plans

Ported from upstream PR boykopovar#1341. The disk-cache plan decoder from boykopovar#4883
rebuilds the execution plan after a stored plan is decoded, since it is
derived from the value graph and not stored. The libSceAgcDriver test
hunks are not ported: those tests are not built by any macOS configuration.

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

* perf(shader): keep only evaluable branch conditions in resource plans

Ported from upstream PR boykopovar#5181. The plan values now live in an
IrValueArena, so the disk-cache plan codec from boykopovar#4883 walks and fills
the arena instead of a vector of owned values. The libSceAgcDriver test
hunks and the docs hunk are not ported.

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

* fix(shader): ignore unused descriptor fields for scalar buffer loads

Ported from upstream PR boykopovar#5049. The test and docs hunks are not ported.

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

* perf(agc): cheaper shader-memory captures per draw (ShaderMemory half)

Ported from upstream PR boykopovar#1193, ShaderMemory.hpp/.cpp of its last commit
only: word-mask page marks, pooled pages with a last-page shortcut, and
lazily copied 64-byte blocks for pages no recorded GPU work writes.
Adapted for native guest memory: a lazy block is read through
GuestMemory::Read instead of the guest address as a host pointer, and
NativeGuestMemory gains a no-op FlushGpuWrites (it keeps no deferred
GPU writes). The SRT evaluator half is superseded by boykopovar#1341; the draw
cache commits are stacked on upstream #494/boykopovar#1191 and not ported. The
capture profile line keeps its current fields.

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

* perf(graphics): retain vertex descriptor decode plans (ShaderInputState half)

Ported from upstream PR boykopovar#1313, Graphics/ShaderInputState only. The
Metal draw path calls DecodeVertexStageInfo per draw, which now builds
a VertexStagePlan and reads only the referenced, coalesced table words.
The Vulkan DrawDecode/DrawCache hooks that keep the plan across draws
are not ported (that driver is not built on macOS).

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

* perf(graphics): retain vertex input layouts (Metal draw)

Ported from upstream PR boykopovar#1316 and adapted to Metal. VertexInputCache
(LRU per shader variant, shape-checked on every hit, a variant seen with
two shapes is bypassed) now takes a layout builder, so MetalDraw.mm
keeps the layout it built with the Metal format predicate instead of
calling BuildVertexInputLayout on every draw. Vertex buffer addresses
are still validated per draw. The Vulkan Context/VulkanDevice/Draw.cpp
hooks and the libSceAgcDriver test are not ported.

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

* test(shader): build and run the shader disk cache codec tests on macOS

Registers anyps5_metal_shader_disk_cache with ctest in the Metal guest-replay
configuration and adds a plan round trip: value arena, rebuilt SRT execution
plan (evaluated against the original), phi blocks, indirect image tables, and
the 9-field SamplerResource / 5-field SampledResourcePair encodings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@oneandonlydean
oneandonlydean merged commit ce5c839 into boykopovar:main Oct 14, 2026
8 of 9 checks passed
ThatsLeo added a commit to ThatsLeo/AnyPS5 that referenced this pull request Oct 14, 2026
EncodePlan and DecodePlan from boykopovar#4883 walked valueStorage as a vector of
owned values; they now use IrValueArena, and decoded operands are detached.
MichalSy pushed a commit to MichalSy/AnyPS5 that referenced this pull request Oct 14, 2026
EncodePlan and DecodePlan from boykopovar#4883 walked valueStorage as a vector of
owned values; they now use IrValueArena, and decoded operands are detached.

(cherry picked from commit dc0ca9d)
oneandonlydean pushed a commit to ThatsLeo/AnyPS5 that referenced this pull request Oct 14, 2026
EncodePlan and DecodePlan from boykopovar#4883 walked valueStorage as a vector of
owned values; they now use IrValueArena, and decoded operands are detached.
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
…2bdcdb) with the boykopovar#757/boykopovar#758 integration (b26df6a, a9a751c)

From SleglJan (Jan Šlégl), fix/757-key-domain-v2 @ 92bdcdb: a table
column's identity is its heap access shape alone, so reads of one table
through different keys share one column, one table image and one set of
decode modes. The column holds a list of key domains and an unkeyed
flag; each access merges its domain into the shared column and keeps its
own domain source alive. An access without a domain freezes the column
unkeyed (whole snapshot); a store to any key buffer clears every domain;
narrowing takes the union of all domains' records.

Integrator adaptations as in b26df6a / a9a751c: a masked
(key << n) & mask read marks the shared column unkeyed; a shared column
takes the widest maxKey of its reads; `address` is part of the column's
identity. On the game branch `recordPointer` is part of it as well.

Skipped: the boykopovar#4883 plan encoder/decoder fields and plan tests (the game
branch has no front-end resource plan in the disk cache), and SRT value
key domains (KeyDomain::value), which the game branch's boykopovar#757 predates.
SleglJan's verifyImageTableKeyUnion/LateKey/UnkeyedAccess are ported
with keys read from the material V# at a lane-uniform selector instead
of SRT constants.
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.

3 participants