Skip to content

fix(shader): capture image descriptors read from constant buffers at uniform offsets - #5188

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
EagleBearerYS:fix/shader-images-from-constant-buffers
Oct 14, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
EagleBearerYS:fix/shader-images-from-constant-buffers

Conversation

@EagleBearerYS

Copy link
Copy Markdown
Contributor

What

Resident Evil Village (PPSA01556, content 01.210.000) stops preparing one of its mesh programs while it creates its PSO cache:

shader 0x2086a1e500 stage 7 (shader_2086a1e500.req): ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x000002d0(...)

The program reads its T# with s_buffer_load_dwordx8 from 48-byte records, with the T# at +16. The record index comes through two more constant-buffer loads keyed by user data. Everything in the chain is uniform per draw, so the CPU walk can evaluate it.

ResourceTracker::GetHandle refused every T# whose dwords are scalar buffer loads (ReadConstBuffer) before ValidateSource looked at them. Tables with a 32-byte stride and a wave-uniform key are planned as bindless tables before GetHandle runs, so only the other cases hit the refusal: a T# read at an offset the walk can evaluate. ValidateSource (SrtRuntimeValidator) already accepts ReadConstBuffer with uniform operands and refuses the rest. The pre-check is removed, and the walk reads the T# at each capture.

Tested

  • Windows 11, WinLibs GCC 15.2.0 r7, Release, this branch merged into main a252fcd: agc_shader_memory verifyUniformTableImage.
    • The test is a compute program that reads its T# with s_buffer_load_dwordx8 at s2 * 48 + 16.
    • With main's ResourceTracker.cpp it fails with the title's message: GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x0000001c(...).
    • With the change, the plan has one ordinary (not bindless) image, and captures with s2 = 2 and s2 = 1 read the T#s of records 2 and 1.
    • agc_shader_memory, agc_shader_bindless, agc_shader_srt_read_patching, agc_shader_frontend_pair, agc_shader_frontend_pair_off and agc_shader_disk_cache pass, also with the other two Resident Evil Village fixes (listed in Resident Evil Village (PPSA01556) #5169) merged in.
  • Linux (WSL2 Ubuntu 24.04, GCC 13.3, CI configuration), with the three Resident Evil Village fixes (listed in Resident Evil Village (PPSA01556) #5169) merged into main a252fcd: the same six tests pass.
  • PPSA01556 on Windows 11 (RTX 3070 Laptop GPU), on a local branch past the title's earlier stops (on main it stops at the first frame, fix(agc): start the sample location registers at their clear-state value #4949): the mesh program prepares, and the PSO cache build continues past it.
  • python3 tools/check_conventions.py --base origin/main: passed.

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: PPSA01556 Resident Evil Village (PPSA01556) #5169
  • AI-assisted: yes

…uniform offsets

GetHandle refused every T# whose dwords are scalar buffer loads (ReadConstBuffer) before ValidateSource looked at them. Tables with a 32-byte stride and a wave-uniform key are planned as bindless tables before GetHandle runs, so the refusal only hit the other cases: a T# read at an offset the runtime walk can evaluate, like Resident Evil Village's mesh programs (48-byte records with the T# at +16, the record index read through two more constant buffers keyed by user data). ValidateSource already decides whether such a chain is a uniform runtime value, so leave the decision to it; the walk then reads the T# at each capture.
boykopovar#757 and boykopovar#758 add their image table checks where this check was, after
verifyBindlessTable, so the pull requests conflicted.
RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 14, 2026
* Metal semantics: run upstream image execution tests through the Metal driver

The existing semantics shim binds only guest buffers. A second shim
(tests/metal_semantics/driver_shim) runs the upstream execution tests that
bind images, samplers and color targets through the native Metal driver:
dispatches bind their descriptors with MetalShaderResources and draws go
through MetalDraw::DrawSynchronously, as in a title. The tests' host arrays
are the guest memory; the device borrows every range the shader's buffer
and image descriptors and the color targets name.

ImagePlaneAddressOnLine and ImageVolumeAddressOnPlane (already on main)
now run on Metal and pass.

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

* fix(shader): capture image descriptors read from constant buffers at uniform offsets

Removes the tracker check that refused a T# whose dwords come from a
ReadConstBuffer, so a T# loaded with s_buffer_load from a table record that
user data selects is captured like any other runtime descriptor.

Metal: tests/metal_semantics/ImageTableRecord.cpp runs the upstream
program through the Metal driver (MetalShaderResources image binding) and
checks that the record user data selects is the texture sampled. It fails
on main with the removed "is not a valid runtime value" error.

Ported from upstream PR boykopovar#5188

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

* fix(shader): accept TYPE=0 images during materialization

A T# with TYPE=0 is null on AMD hardware even when its base and other
words are nonzero; the materializer now accepts it as a null image
instead of refusing it as invalid (reserved TYPE=1 is still refused).
Upstream's frontend test is ported to tests/ShaderMemory.cpp.

Metal: the Metal image binding already treats TYPE=0 as null
(IsNullTextureResource), so such an image binds no texture. The Metal
ImageTableRecord test now also samples a TYPE=0 record with a nonzero
base and reads zero; on main it fails with "runtime image descriptor is
invalid". Debt note added to docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5473

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

* fix(shader): read 1D T#s through 3D image_sample and image_load accesses

A DIM 3D image_sample_lz, plain image_sample or mip-less image_load
whose T# is 1D gets a runtime image mode that binds the 1D texture: the
sample reads the texel u selects, and the load reads the x texel when z
is 0 and zero otherwise, as measured on RDNA1. Other 3D accesses of such
an image still throw.

ImageResource gains flatVolumeLineCompatible, so the shader disk cache
FormatVersion goes from 35 to 36. This is the only bump in this series:
the later image-mode fields reuse it.

Metal: the 1D mode binds through the existing Image1D shape (a 2D
texture of height 1) with no new view. Upstream's ImageVolumeAddressOnLine
execution test runs unchanged through the Metal driver
(anyps5_metal_driver_semantics_imagevolumeaddressonline). It fails
without the materializer hunk ("incompatible with the static runtime
image interface"). Debt note added to docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5375

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

* fix(shader): sample single-level volumes through biased 2D accesses

A pixel-stage, direct, plain 32-bit image_sample_b with DIM 2D whose T#
is a one-slice, single-level 3D image gets the 3D runtime image mode:
the sample supplies a zero third coordinate and keeps the bias, as
measured on RDNA2. The binding requires a normalized point sampler and
the descriptor one slice and mip zero; other cases throw.

ImageResource gains singleMipPlaneBiasCompatible; it is encoded by the
shader disk cache under the FormatVersion 36 bump of this series
(upstream's own bump to 31 is not taken).

Metal: the 3D mode binds the volume as the Image3D shape it already has,
with no new view. Upstream's ImagePlaneBiasOnSingleLevelVolume execution
test (a pixel draw) runs unchanged through MetalDraw
(anyps5_metal_driver_semantics_imageplanebiasonsinglelevelvolume). It
fails without the tracker hunk ("incompatible with the static runtime
image interface"). Debt note added to docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5461

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

* fix(shader): decode forced degamma on biased RGBA8_UINT samples

A direct RGBA8_UINT image sampled only by plain biased pixel accesses
(the one-slice volume case of boykopovar#5461) through samplers that all set
FORCE_DEGAMMA is materialized as RGBA8_SRGB, which returns the same
decoded floats as the hardware. Mixed sampler states, indirect images,
other accesses and formats still throw.

Metal: MetalSampler refused every FORCE_DEGAMMA S#, so this (and the
existing sRGB case of ImageSampleForceDegamma) could not bind. Metal now
does what the Vulkan driver does: MetalShaderResources::Bindings creates
the sampler with forceDegammaPaired and refuses a sampled image of a
non-sRGB format paired with a forcing sampler (the same message as
RequireDegammaFormat). The rewritten descriptor binds through the
existing sRGB pixel-format view. Upstream's extended
ImagePlaneBiasOnSingleLevelVolume and the existing
ImageSampleForceDegamma tests now pass on Metal; both failed with "forces
sRGB decoding which is not implemented". Debt note added to
docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5472

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

* fix(shader): read cube T#s through DIM 2D image_sample and image_load as face 0

A DIM 2D image_sample_lz, plain image_sample, image_sample_l or mip-less
image_load whose T# is a cube gets the cube runtime image mode and reads
the T#'s BASE_ARRAY face with face coordinate 0, as measured on RDNA1.
Other DIM 2D accesses through a cube T# still throw.

ImageResource gains cubeFaceCompatible; encoded under the series'
FormatVersion 36 bump.

Metal: a cube T# is already a 2D-array texture of faces and the cube mode
binds the Image2DArray shape, so no new view is needed. Upstream's
ImagePlaneAddressOnCube execution test runs unchanged through the Metal
driver (anyps5_metal_driver_semantics_imageplaneaddressoncube) and fails
without the materializer hunk. Debt note added to
docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5509

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

* fix(shader): compare through 2D image_sample_c on 1D T#s

The flat-line (2D access through a 1D T#) compatibility now ignores the
compare flag, so image_sample_c and image_sample_c_lz with DIM 2D on a
1D T# take the 1D mode like their non-compare forms.

Metal: the 1D mode binds through the existing Image1D shape (a 2D texture
of height 1), with no new view. Upstream's ImagePlaneCompareOnLine execution test runs unchanged
through the Metal driver
(anyps5_metal_driver_semantics_imageplanecompareonline) and fails without
the tracker hunk. The debt note in docs/dev/TechnicalDebt.md is updated
as upstream's.

Ported from upstream PR boykopovar#5456

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

* fix(shader): apply the V# DST_SEL to formatted buffer stores

buffer_store_format_* (D16 included) wrote memory component i from data
VGPR i whatever the V#'s DST_SEL. A formatted store now writes each
format component from the slot its selector names, only for the
DST_SELs measured on RDNA2 or covered by conformance tests: the identity,
Z,Y,X,W on 8_8_8_8_UNORM and 16_16_16_16_UINT, and 0,0,0,X on 8_UNORM
and 8_UINT. Every other DST_SEL on a formatted store is refused: a host V#
throws in DescriptorBindingBuilder::Prepare, and a GPU-selected V#
records an invalid-descriptor BDA fault and skips the store.

BufferResource gains formattedWritten; encoded under the series'
FormatVersion 36 bump.

Metal: the store is lowered in the shared SPIR-V (SpirvRuntimeBuffer), so
no Metal binding change. Upstream's BufferStoreFormatDstSel execution test
runs unchanged through the Metal driver
(anyps5_metal_driver_semantics_bufferstoreformatdstsel). Without the
SpirvRuntimeBuffer hunk it fails (Z,Y,X,W on 8_8_8_8_UNORM writes
0x4033bfff, expected 0x40ffbf33). Debt note added to
docs/dev/TechnicalDebt.md.

Ported from upstream PR boykopovar#5172

Co-Authored-By: Claude Opus 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 self-assigned this Oct 14, 2026
@oneandonlydean
oneandonlydean merged commit eecf909 into boykopovar:main Oct 14, 2026
8 checks passed
@mrfelek

mrfelek commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Data point from another title. Marvel's Wolverine (PPSA03671, 01.001.005), Linux, RTX 3080 Ti (NVIDIA 615.71.09): on main 43349d4 with a local patch that drops draws and dispatches whose shader fails, 36 shaders failed with GetImageResource dword 0 is not a valid runtime value (chain: ReadConstBuffer(GetBufferResource(ReadConst(GetSrtResource, ...)))). With this PR merged on top (together with #5620, #5550 and #5473), those 36 go to 0 in the same scene and shader cache fresh. The shaders then stop at the next limit instead: bindless storage image tables are unsupported (13) and static bindless image capacity exceeded (4). The picture does not change yet, because 105 other shaders fail on GetSamplerResource (a sampler read the same way). More in #5516.

AI-assisted; the runs are from my machine.

oneandonlydean pushed a commit that referenced this pull request Oct 18, 2026
… table cannot express its operation

An image table cannot store, update atomically or read PCK and BY2/BY4
through its records (#757): those operations fault and read zeros. #6243
turns a heap T# whose words and record offset the SRT walker evaluates
into a selected one-record table, which kept that limitation although
such a T# is one known descriptor, as #5188's direct images were.

PlanTableColumns now binds such a T# directly, for every operation of the
shader on it, when any of them is a store, an atomic, a packed or a
BY2/BY4 access. A T# keyed per lane, or by a value the walker cannot
evaluate, keeps its table and its unsupported-operation fault. The disk
format moves to 37 so that plans cached by #6243's builds are not reused.
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