Repository navigation
Shader recompiler: non-invariant descriptor Phi causes GetImageResource validation abort #7459
Description
Observed failure
A relinked Windows program reaches Vulkan device/swapchain creation and shader preparation, then aborts with the following fragment-shader descriptor error:
[libc] unhandled exception 'St13runtime_error'
GetImageResource dword 0 is not a valid runtime value; chain: Phi(Immediate, LoadAddressU32@pc=0x00000214(GetAddressResource(Phi(GetUserData(Void), CompositeExtractU32x4(Ballot(LogicalAnd(IEqual32(GetBuiltin, Immediate), ULessThan32(Immediate, SelectU32))), Immediate)), Phi(GetUserData(Void), CompositeExtractU32x4(Ballot(LogicalAnd(IEqual32(GetBuiltin, Immediate), ULessThan32(Immediate, SelectU32))), Immediate))), Immediate, Immediate, Immediate))
FATAL: abort() (SIGABRT)
The excerpt omits the shader address, request filename and serialized request payload. Both available run logs show the same printed chain, at different shader addresses. The reported stage is 5 (ShaderStage::Fragment). I do not have a minimal redistributable reproducer attached, so this report alone is not sufficient to replay the shader.
Environment and checks
- Checkout:
1b0c7cfa02bec9eef9f55ec31c4cdd5ca087675e; no tracked source modifications in the current checkout. - Windows 11 Pro, build 26300; AMD Ryzen 9 3900X; NVIDIA GeForce RTX 3080 Ti. The currently installed NVIDIA driver reports 610.88; the captured log does not independently establish the driver version at capture time.
- WinLibs GCC 15.2.0, x86_64-ucrt-posix-seh.
cmake --build buildandcmake --build build --target libsboth returned 0 on incremental verification. This is not a clean-build or test-suite claim.- The captured request was accepted by the repository's
RequestSerializer::Deserialize, reporting stage 5 and 568 shader code words. This checks deserialization only; it is not a standalone recompiler replay.
Source reading, not a tested fix
At the cited commit, GetHandle throws this diagnostic when ValidateSource fails.
My reading is that the first refusal for this printed chain is the outer Phi, rather than Ballot or CompositeExtractU32x4:
- Descriptor validation uses the default
RuntimeValueType::Any. - The
Phibranch of RuntimeValidator callsResolveInvariantPhibefore validating an invariant result. ResolveInvariantPhirequires equivalent non-Phi leaves. Here the outer leaves are an immediate and aLoadAddressU32, which are not equivalent under that implementation.
Consequently, adding Ballot and CompositeExtractU32x4 to IsRuntimeUniformOp alone would not address this outer-Phi refusal. I have not changed the whitelist or tested a fix.
Related work
- PPSA07412: ShaderRecompiler abort "GetImageResource dword 0 is not a valid runtime value" (waterfall loop over per-lane T#) #7345 reports the same diagnostic with a different chain.
- fix(shader): plan image tables through invariant descriptor phis #6417 concerns invariant descriptor phis.
- feat(shader): resolve nested image tables through runtime pointers #7223 and feat(shader): read samplers through nested and pointer-keyed descriptor tables #7395 concern nested/pointer-keyed descriptor tables. In particular, feat(shader): read samplers through nested and pointer-keyed descriptor tables #7395 describes handling a Phi arm containing a word of a
Ballotlane mask.
Those PRs have not been tested against this capture. I am reporting the observed chain, not claiming a new root cause or that existing work cannot cover it. Is this shape already expected to be handled by #7395 or another existing change?
Report prepared with AI assistance; the source references and local command results were checked. No shader request payload is attached.
Activity
MarcZX10 commentedon Oct 18, 2026
I checked the current #7395 implementation. It handles a Phi in the low word of a pointer used to look up a captured record (including the lane-mask arm); it does not change
ResolveInvariantPhivalidation for the outerGetImageResourcedescriptor value shown here. So the reportedPhi(Immediate, LoadAddressU32)shape appears to be a separate follow-up, although the missing serialized request prevents me from confirming it with a replay/test. A minimal request or the smallest reproducible shader would make that distinction testable.embersas commentedon Oct 18, 2026
@MarcZX10 Thanks. I have now kept a real Ballot-to-pointer dependency in synthetic requests and tested the public #7395 branch, alongside its base and our local main.
1b0c7cfa0261d3d3080ced4f148a3craw-full-direct-no-outer-phi.reqraw-full-direct-outer-phi.reqraw-full-ballot-no-outer-phi.reqraw-full-ballot-outer-phi.reqrecord-ballot-no-outer-phi.reqrecord-ballot-outer-phi.reqThe raw-full cases use pointers from user data and load all eight descriptor words. The record cases instead load a pointer from a synthetic buffer record, matching the provenance #7395 recognises. On the PR head, the record/Ballot case prepares (9804 SPIR-V words) and fully recompiles (7940 words) without the outer phi. Adding only the outer descriptor branch makes it fail again. The direct raw-load control likewise prepares until that outer branch is added.
Each outer/no-outer pair has identical non-code request data. Removing its two comparison/branch words makes the code vectors identical. The loaded descriptor differs from the literal descriptor, so this is not an equal-arm phi.
These are independently constructed wave32 fragment shaders with invented descriptors, addresses and memory, not original shader bytes. Ballot is generated by a vector comparison under EXEC and its low word feeds a pointer phi. This retains more of the reported dependency, but differs from the original: only the low pointer word is merged and the comparison lacks the original SelectU32 operand. The record variant also deliberately changes pointer provenance. I am offering these as minimal examples of the analysis boundary, not as proof that #7395 fixes the title or that this is its only remaining problem.
The PR-head record/Ballot outer case reports:
Save each block below verbatim using its filename. This is already RequestSerializer's Base64 text format; do not decode it before replaying.
raw-full-direct-no-outer-phi.req
raw-full-direct-outer-phi.req
raw-full-ballot-no-outer-phi.req
raw-full-ballot-outer-phi.req
record-ballot-no-outer-phi.req
record-ballot-outer-phi.req
From a configured checkout, replay the same files on the selected revision. These commands are for PowerShell on Windows; adjust the MinGW and request paths:
The combined replay exits 1 because the second case is refused. Omit
--preparefor full recompilation; setAPS5_DUMP_IR=allto see the pointer and descriptor phis.The base/head comparison uses pinned public source archives and externally rebuilt shader libraries/replay tools, with the existing source-built glslang/SPIRV libraries and local libc runtime reused. Disk caching was disabled. #7395 is stacked on #7223, so this compares branches rather than isolating every change to #7395. These are CPU analysis/compilation results on Windows with GCC 15.2.0 and SPIRV-Tools disabled; no GPU draw, title run, Linux run or full-suite result is claimed.
Prepared with AI assistance; the generated requests, decoded instruction paths, pairwise input differences and replay results were checked locally.
MarcZX10 commentedon Oct 18, 2026
Thanks for the six request files and the controlled comparisons; they make the outer-Phi boundary distinct from #7395 much clearer. I checked current main: the refusal is consistent with both
RuntimeValidatorandBasicEvaluator::EvaluatePhirequiringResolveInvariantPhi, so changing validation alone would still leave the value unevaluable. A generic fix needs to establish whether this Phi is uniform across invocations and how the SRT evaluator should represent that choice; accepting divergent arms as a per-draw value could select the wrong descriptor. If you can share the original captured request (or establish the branch predicate’s uniformity from the title shader), that would help identify a safe implementation boundary. I have not opened a PR while that semantic question remains unresolved.embersas commentedon Oct 19, 2026
I traced the original captured shader locally to address the predicate-uniformity question.
The raw graph has 16 blocks, eight conditional branches and nine entry-to-exit paths. All eight conditional branches use SCC. A reaching-definition audit across every path shows that their predicates depend only on two scalar words loaded together in the entry block through a pointer read from user data. Four predicates depend on the first word and four on the second. The pointer chain and predicate expressions have no lane ID, fragment input, EXEC, VCC-derived vector comparison or Ballot dependency. This follows the reaching definitions; it is not an inference from the registers being scalar.
That establishes invocation-independent predicate expressions. They are uniform across the draw provided the user data and the shared memory supplying those words are stable for the draw. I cannot independently establish that memory-stability condition from this capture: its two user-data words are zero, and its captured memory contains only the shader code and header. It therefore does not recover the title's original non-null parameter values or demonstrate their lifetime.
There is also a control-flow distinction relevant to the reported Phi. Every original path reaching either of the two affected image-read blocks first executes the descriptor load. The path overwriting the pointer registers with a comparison mask goes to the exit and cannot reach that load or those reads. On paths that do reach the load, the pointer registers still have their incoming user-data definitions. The immediate/load descriptor Phi and the user-data/Ballot pointer Phis arise at the fallback structurizer's guard joins; they are not two reachable descriptor choices at the image reads in the original graph.
I checked evaluator support separately. All eight recovered predicates pass RuntimeValidator with RuntimeValueType::Integer. With invented non-null addresses and memory, the existing SRT evaluator successfully evaluated all eight over 25 parameter combinations, 200 evaluations total, with both Boolean outcomes exercised for every predicate. This checks the recovered expressions' evaluability under fixed shared inputs; it does not prove memory stability in the title.
On fd423f9, with disk caching disabled, the original capture prepares, recompiles and materializes. Forcing APS5_FORCE_STRUCTURIZER_FALLBACK=1 in the same build reproduces the reported refusal chain. This is consistent with #6405's structurizer fix. Because of the null inputs above, the successful materialization is not evidence of correct non-null resource capture, GPU output or title progress.
For this capture, the evidence supports preserving the original control-flow reachability rather than relaxing ResolveInvariantPhi to accept a genuinely divergent choice. The six earlier synthetic requests place a merge after a branch and should not be treated as proof that the original image reads have both alternatives reachable.
The original capture, shader bytes, disassembly, IR and captured memory remain private. This summary contains only analysis in my own words. Prepared with AI assistance; the graph audit, evaluator checks and replay outcomes were checked locally.
MarcZX10 commentedon Oct 19, 2026
Thanks for tracing the captured request and checking the normal path. I read this as narrowing the report: on current main the original capture prepares, recompiles, and materializes with the ordinary structurizer; the refusal is reproducible only when
APS5_FORCE_STRUCTURIZER_FALLBACK=1is set. That makes the fallback replay a useful diagnostic boundary, but it does not currently demonstrate a failure in the normal title path.Open PR #7694 independently covers the synthetic direct descriptor-Phi case, but its report also says the original title shader was unavailable, so I would not treat it as title validation. Given the missing non-null captured inputs and lack of evidence that the normal path reaches this refusal, I don’t see a separate source change justified by this capture yet.
embersas commentedon Oct 19, 2026
@MarcZX10 Thanks. I’m sharing the original captured request below for direct inspection, under
shader_206fd8a100.req.txt.It is the unchanged serialized request from the original report: a wave64 fragment shader with 568 code words. Save the contents of the text block as a file and pass it directly to
agc_shader_replay; it is already RequestSerializer’s Base64 format and should not be decoded first. The original file has no trailing newline.SHA-256 of the original text file:
65be6c98cdf96c78fe585f46c7aa0430fa4a1613b64cbb0dcc9ffd49d9a3f40a.One limitation matters: its two user-data words are zero, and the two captured memory regions contain the shader code and header. It therefore permits inspection of the original shader and control flow, but does not provide the non-null parameter memory needed to establish draw-time memory stability.
On
fd423f9b2, with caching disabled, it prepares, recompiles and materializes normally. SettingAPS5_FORCE_STRUCTURIZER_FALLBACK=1reproduces the reported refusal. I agree this does not demonstrate a current failure in the normal title path.I’m providing it to make the predicate and reachability analysis independently checkable. It contains title-derived shader code and captured memory, so it is for diagnostic review rather than inclusion as a repository fixture. Any committed regression should use independently authored synthetic inputs.
shader_206fd8a100.req.txt — original captured request
Prepared with AI assistance; the capture contents and replay results were checked locally.
embersas commentedon Oct 20, 2026
I have now tested the actual title, Street Fighter 6 (PPSA02632), on unmodified main
525e299fc085c400de4a703c6b1ab105eefb50c7.With a fresh Release build of the relinker and the full system-library set, the original
GetImageResourcedescriptor-Phi abort did not recur through startup. The game reached Press any button, then the offline World Tour / Battle Hub / Fighting Ground mode-selection menu. No forced structurizer fallback or local source changes were used.This adds actual title progress to the earlier replay result. It does not establish generic non-invariant descriptor-Phi support, correct rendering in every case, or interactive fighting gameplay. Gameplay was not tested, and I am not proposing a separate shader change from this result.
Severe slowdown remains. A restart reached 13,033 shader-cache hits with zero misses at the title screen, which was still around 2-3 FPS in the observed samples. Those are diagnostic observations, not a controlled performance comparison; I will keep the performance investigation separate from this issue.
Tested on Windows 11 Pro 10.0.26300, Ryzen 9 3900X, 64 GB RAM, RTX 3080 Ti 12 GB, NVIDIA 610.88, WinLibs GCC 15.2.0. Matching deployed libraries were checked against the build.
AI-assisted report; the build provenance, cache counters and on-screen progress were checked locally.