Repository navigation
fix(shader): accept a nested S# whose V# gives the key of a sibling V#'s T# array - #15555
hugodearaujo wants to merge 133 commits into
Conversation
An S# read from a V# that a scalar record selects through a pointer was refused, so recompiling failed with "GetSamplerResource dword 0 is not a valid runtime value". The nested branch now takes sampler columns: they get child tables per distinct V# like nested T#s, share equal children, count against the nested key budget and the S# entry budget, and fault on an unmapped, non-buffer or uncaptured parent. The plan decoder accepts a nested sampler column; the plan encoding is unchanged.
An S# or T# loaded through a pointer that a scalar record holds was refused when a Phi merged the pointer's low word with constants, so recompiling failed with "GetSamplerResource dword 0 is not a valid runtime value". Such a column is now pointer-keyed: the snapshot captures every record's pointer and the descriptor behind it, the map holds boykopovar#7223's sorted pointer dictionary with descriptor records as its codes, and the GPU looks up the actual 64-bit pointer. A pointer that was not captured faults as outside the snapshot, and a null one as unmapped. The plan gains TableColumn::pointerKeyed and the snapshot gains ImageTableColumnSnapshot::pointerKeyed, so the disk-cache format moves to 43 and the image-table ABI to 4.
A T# loaded through a record pointer whose low word passes nested Phis was refused when one Phi arm held a word of a Ballot lane mask, as in a waterfall loop that reuses the pointer's SGPR for an s_and_saveexec or v_cmp mask, so recompiling failed with "GetImageResource dword 0 is not a valid runtime value". A pointer-keyed column now accepts such a word as an arm that is not a record pointer, as it does for constants, and still refuses any other foreign arm. At run time a pointer built from a lane mask is not a captured pointer and faults as outside the snapshot. A pointer-keyed column also accepts low-word reads, and a high-word read, of the same record word under different keys: same table, record offset, stride and addend. The GPU looks the pointer up by its value, so the key does not matter there. The column takes its table and record offset from the high-word read, which dominates the descriptor load. A shader that planned before plans the same way, so the disk-cache format stays at 43.
…ed record pointer A T# loaded through a record pointer was still refused with "GetImageResource dword 0 is not a valid runtime value" when the pointer's low word passed ANDs with lane masks, as a waterfall loop that copies EXEC into the pointer's SGPR builds them, or when its high word was a Phi of the record's high word read under several keys. For a pointer-keyed column, the walk of the low word now takes an AND with a lane-mask operand (a Ballot word, or an AND or Phi of only Ballot words and immediates) and keeps walking its other operand, so its record reads must still match the column and anything else still refuses. The high word is walked the same way: its record reads must be the record word at the low word's offset plus 4. A high word that is one read still gives the column its table and record offset, as before. Otherwise the table comes from a record read whose V# words are defined before the descriptor load, and the record offset from the high word, which the GPU does not read for a pointer-keyed column. A high-word arm read from another buffer, such as one whose V# is loaded through a pointer, stays refused. A shader that planned before plans the same way, so the disk-cache format does not change.
… a pointer-keyed record pointer A T# loaded through a record pointer was still refused with "GetImageResource dword 0 is not a valid runtime value" when a Phi of the pointer's low word merged the record read, a zero, and the waterfall's lane mask with the handled lane's bit cleared, BitwiseAnd32(CompositeExtractU32x4(Ballot(...), 0), BitwiseNot32(ShiftLeftLogical32(1, index))), as s_ff1, s_lshl and s_not with s_and, or s_andn2, build it in the SGPR the pointer reuses. The walk took only Ballot words and ANDs of them as lane masks, so it walked into the NOT and refused. A lane-mask operand is now a Ballot word or the loop's lane bit, a shift of 1 by an index from FindILsb32, FindUMsb32 or ReadFirstLane (through the AND, add, subtract and select that s_ff1_i32_b64, s_bitset0 and the shift's own mask put around it), or an AND, OR, NOT or Phi of only those and immediates. An arm of the pointer that is entirely such an operand is skipped. An AND with a record read in it still has the read matched against the column, and anything else still refuses. agc_driver_pointer_phi_lane_not_table builds that shape in a wave64 waterfall: the low word's Phis merge the record read at +0, a zero and the lane mask without the handled lane's bit, cleared by s_not and s_and and by s_andn2, and the high word is one read at +4. Each lane samples the T# of its record, and every masked word faults outside the snapshot. agc_driver_pointer_vsharp_select_table reads a record keyed by ReadFirstLane(SelectU32(LogicalAnd, IAdd32, Phi)), a pointer from it, a V# through the pointer and the T# through the V#; it passes without this change and keeps that chain checked, including a record pointer moved to the other V# without a recompile. Shaders that planned before plan the same way, and a refusal is never cached, so the disk-cache format does not change.
…keyed records A pointer-keyed record pointer now also takes lane-mask ANDs, the waterfall loop's lane bit and its OR and NOT forms, and a high word merged from the record's next word. A high word read from a second buffer stays refused.
The low and high words of a pointer-keyed record pointer were walked separately, so a high-word arm on a path whose low word is a lane mask refused the plan even though that pair is no pointer. Ratchet & Clank: Rift Apart (PPSA01474) stops this way at LoadAddressU32@pc=0x9a30: one high-word arm reads a second buffer, and on that edge the low word is a lane mask, the waterfall loop reusing the SGPR pair. Walk the two words together. Where both are Phis of one block, each low-word edge takes the high word of the same predecessor. An edge whose low word is a constant or a lane mask is skipped with its high word. A record low word still needs every high word it can meet to be a constant, a lane mask or the same record column at offset + 4, so a second-buffer high word paired with the record word stays refused. agc_driver_pointer_phi_high_second_lane_table adds the lane-mask low word to the second-buffer arm: it plans, each group samples the T# its record selects, and that arm faults outside the snapshot.
…nter The two words of the pointer are now walked edge by edge, and an edge whose low word is a constant or a lane mask is skipped with its high word. A high word read from a second buffer stays refused on an edge whose low word is the record word.
Brings boykopovar#8991 (match image-table T# halves offset before and after readfirstlane). ResourceTracker.cpp: main's FirstLaneOffset, ParseFirstLaneOffset and SameFirstLaneOffset go before TryMakeTableColumn, which keeps this branch's signature with the buffer parameter. Main's first-lane base check in the column loop merged on its own.
…ane-masks Brings boykopovar#7395's merge of main bd4d801, with boykopovar#8991. It merged without conflicts.
TryMakeIndirectColumn refused every pointer hop once the record's pointer words were merged, so a T# read through a pointer loaded from a pointer-keyed record pointer stayed unplanned (Gran Turismo 7's World Map compute shaders, issue boykopovar#6191). Only buffer handles still refuse hops: the materializer keys the records by the innermost pointer before it follows the hops, and the plan's parent pointer is that innermost one.
…one-key child From hugodearaujo's change on boykopovar#8939 (Marvel's Spider-Man 2, PPSA03016).
Spider-Man 2's shape: a record keyed by a first-lane read, its dwords 2-3 a pointer, a V# loaded at that pointer plus 0xa0, and the T# read at offset 0 or 0x40 of that V#'s buffer with no second key. Two records point at V#s of different T#s; a moved record pointer and a moved V# must change the result without recompiling.
…andles as nested children Marvel's Spider-Man 2 (PPSA03016) stops at 131 s on boykopovar#9457 in a compute shader whose record pointer, at dwords 2-3 of a waterfall-keyed record with its low word merged by a Phi with a constant, leads to a V# that two GetBufferResource handles read: a T# at offset 0 and a T# at 0x4c0, each sampled. The nested branch of TryMakeTableColumn refused the first T# because the V#'s words also fed the second handle, whose reads belong to the other T#. A parent V# whose word reads feed several GetBufferResource handles is now accepted when every other handle has the same words and its uses are only reads whose users are T# or S# descriptor handles. Each descriptor plans its child column from its own handle; both walks reach the same reads, so the parent interns to one source, the materializer reads its records once (NestedColumn takes them from Column()), and each child keeps its own offset. A handle with any other use still refuses, and a sibling descriptor that cannot be planned still refuses the shader, as its words are not runtime values. T# or S# words that have other users stay refused. Planning keeps those reads in the program: PlanTableColumnPass marks a read planning-only only when every user is a planned handle (ResourceTracker.cpp:1770-1781). But they read through the V# handle, whose arguments Run() replaces with the column offset and the parent pointer words (:154-160), and whose own word loads become planning-only once every handle over them is planned, so LoadAddress emits nothing for them (SpirvMemoryInstructions.cpp:599-601). The kept read would address a V# that only the snapshot holds. Allowing those uses in this shape, where the second T#'s dwords 0 and 1 reach the pointer registers on one edge before a later s_load, crashes lavapipe in the dispatch. Taking it needs that V# kept alive for those reads: an unrewritten copy of the handle and real loads of its words through the record pointer. Shape from hugodearaujo's IR on boykopovar#8939 (issuecomment-6340144242).
…d other instructions Marvel's Spider-Man 2 (PPSA03016) still stops at pc 0x8fa0 with the shared-V# planning alone: its shader moves dwords 0 and 1 of the second T# read through the pointer-loaded V# into the record pointer's registers, so those reads have Phi users. The nested branch of TryMakeTableColumn refused that T#, and the first T# with it, because the sibling handle's reads were not only descriptor words. d132754 added the refusal after such a read crashed lavapipe: the read stayed in the program but went through the V# handle that Run() rewrites to the column offset and parent pointer words, while the V#'s own word loads became planning-only and were not emitted, so it read a garbage address. A planned nested child's T# or S# read with other users now goes through an unrewritten copy of the V# GetBufferResource. PlanTableColumnPass keeps a read real while any planned handle using it has a use that stays real, so the V#'s four loads through the record pointer are emitted for the copy, and the descriptor handles still take the snapshot. No kept read goes through a rewritten handle, so the other users read the guest's V# and descriptor words at dispatch. The copy needs the V#'s words to be loads through the record pointer that the program emits. Any other V#, and a data read through a handle on the V#'s words, remain refused. The shared-V# reuse test now checks both T#s per group and the word the later s_load reads behind T#2's dwords 0 and 1, which the test backs with a snapshot at that address. The T# and S# consumer cases that d132754 refused now check the real descriptor word. A data read through a third handle and a T# consumer whose V# comes from a keyed table record stay refused. Shape from hugodearaujo's IR on boykopovar#8939 (issuecomment-6340144242).
… the same buffer Marvel's Spider-Man 2 (PPSA03016) on boykopovar#9790 stops at 135 s in compute 0x11e2b40f00 at ReadConstBuffer@pc=0x9a50, a T# read through a pointer-loaded V# at (word >> 16) * 32, where the word is a data read through the same V#, next to an S# read through that V#. hugodearaujo's run: boykopovar#9790 (comment) On boykopovar#9790 the T# plans as a nested child with an unkeyed domain, but the S# is refused: the key read's handle shares the V#'s words with the S#'s handle, and DescriptorWordReads takes only descriptor words, so the S# falls to GetHandle and its dword 0 is not a valid runtime value. A sibling read is now taken as a child key word when it is a scalar read at an immediate offset through a rereadable V# and its uses only reach the offsets of T# or S# reads through the same V#, as scale * ((word >> s) & m) + c. The key read keeps its handle and the V#'s loads, so the dispatch reads the guest's word. The T# or S# column gets a child key domain (offset, shift, mask): the snapshot reads that word from each child buffer and maps only the entry it selects. possibleU32Bits follows a right shift by a constant, so the child table spans at most min(num_records / stride, the key's range), 65,536 entries for a 16-bit field. A key the dispatch reads differently from the snapshot faults outside the key domain, and a key past num_records reads a null T#, as s_buffer_load_dwordx8 does past num_records. A store through a handle on the V#'s words, keys from two words or from a word at a runtime offset, and other data reads through the V# stay refused. KeyDomain gains child, shift and mask, so the shader disk cache format moves to 50. The test reads a key word at 0x14 and an S# at 0x20 of each child buffer and checks keys 2, 5, 17, 30 and 47 (the last of the V#'s 48 T#s), both S#s, a moved record pointer, null T#s for keys 48 and 65535, a fault for a key the dispatch reads differently from the snapshot, and the refusals above. This does not make the title pass: the same IR also merges the 0x4c0 T#'s dwords 0 and 1 into the record pointer, which stays refused here.
…ed V#'s record pointer In hugodearaujo's IR for Marvel's Spider-Man 2 (PPSA03016), compute 0x11e2b40f00 reloads the V# at pc 0x9a3c through a record pointer whose two words are Phis: record dwords 2 and 3 on most edges, and dwords 0 and 1 of the T# read at 0x4c0 through the same V# on the edge from $353. boykopovar#9790 (comment) The T# read at ReadConstBuffer@pc=0x9a50 through that V# is refused with "GetImageResource dword 0 is not a valid runtime value", whatever its offset: TryMakeIndirectColumn walked a merged V# pointer with PointerRecordReads, which takes only the record read and constants on the low word, and it needed the high word to be the record read itself. A merged V# pointer now goes through PointerRecordPairs, the edge-paired walk that a pointer-keyed T# already uses, and a word read as part of a T# or S# is a pointer-free arm there, like a constant or a lane mask. The child is looked up by the actual 64-bit pointer, so that arm, which the snapshot never captures, faults outside the snapshot when a lane takes it instead of reading a T#. The record reads on every edge must still be one read per word for a V#. The test samples the T# at 0x4c0, moves its dwords 0 and 1 into the record pointer under a condition, reloads the V# behind that Phi, and reads the key word, the T# at the key and the S# through it, as the IR does. It checks the three reads per lane with the arm not taken, and a group that takes the arm faulting outside the snapshot. That last run replaces the key read with a constant: a data read through a V# loaded from an unmapped pointer crashes lavapipe on boykopovar#9790 as well, as its shared-V# reuse shader does with an unmapped record pointer. On boykopovar#9790, and with the previous commit alone, the test is refused with the text above. This does not claim the title passes.
Keeps this branch's FormatVersion above main's 45 (boykopovar#7831's barycentric bump).
A scalar memory load ignores the two low bits of its address. The emitter cleared them in the offset and the immediate of an s_load through an SGPR pair, but kept them in the 64-bit base, so a base whose low word ends in 01, 10 or 11 read past the dword the hardware reads. A record pointer taken from a T#'s first word therefore read up to three bytes too far and faulted when those bytes were not mapped. The base pair's low word is now cleared like the offset's. A read through a user-data anchor translates from the cleared base, and ImageTableAbi::ScalarAddressDword, which the materializer uses to read the records behind such pointers and to plan their read ranges, clears the base the same way. The SRT walker already did. agc_driver_bda_anchor_reads runs two scalar loads through a user-data base with low bits 01, 10 and 11, and the scalar address rule in agc_shader_memory checks the same bases on the CPU. The pointer image table case that expected a table base off a dword boundary to leave every record unmapped now expects the records at its dword, the same as the aligned base.
… every platform The reuse case builds a record pointer from T#2's first word, the texel address shifted right by 8, so its low two bits are bits 8 and 9 of wherever the linker put the texel array: set on macOS, clear on Linux. The texels now sit 0x100 and 0x300 bytes into a 1024-aligned array, so the two T#s' first words end in 01 and 11 everywhere and the scalar load through the pointer always has to ignore them.
✅ NVIDIA GPU test: passedPR head
Unstable on main (3, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (3, not counted against this PR)Validated but not compared (2, not in the main baseline's validation pass)Validation IDs already present on main (4 IDs in 536 tests)Skipped (48)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
|
A note on the red |
✅ AMD GPU test: passedPR head
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (9, not counted against this PR)Validated but not compared (2, not in the main baseline's validation pass)Validation IDs already present on main (4 IDs in 536 tests)Skipped (59)ReproduceAutomated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
XairRrR
left a comment
There was a problem hiding this comment.
Reviewed — approved
The PR's own change, three commits on top of #9969's head 9e7ac69470, lets ChildKeyWord accept a key whose T# reads go through a sibling V# loaded through the same record pointer. It is narrowly scoped, it is covered by a test that fails without it, and its gaps are recorded in the debt file.
What I ran
Head e358b7af22d8023d10fa14f34710bcda0a30e89e, main 086c0686c8adff3ee648fdbe34e0b0b647e1d511, merged tree 814f9af6ef17ae7e33d8ac275e0069ffa2cc4d51. Machine: Debian 13 VM (32 vCPU Xeon Gold 6136), GCC 14.2.0, lavapipe (Mesa 26.1.6, LLVM 19).
git diff --check 086c0686c8adff3ee648fdbe34e0b0b647e1d511...e358b7af22d8023d10fa14f34710bcda0a30e89e
python3 tools/check_conventions.py --base 086c0686c8adff3ee648fdbe34e0b0b647e1d511
git diff 9e7ac694706c e358b7af22d8 # the PR's own three commits
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 VK_LOADER_DEBUG=error,warn,layer \
ctest --test-dir build -j8 --test-load 32 --output-on-failure -R "agc_driver_(nested_image_table.*|nested_sampler_table|pointer_.*_table|image_table)$"
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 VK_LOADER_DEBUG=error,warn,layer \
ctest --test-dir build -V -R "agc_driver_pointer_vsharp_(sibling|child)_key_table$"
# negative control (see below)
git diff 9e7ac694706c e358b7af22d8 -- core/shader/recompiler/Optimization/src/ResourceTracker.cpp | git apply -R
ninja -C build -j32 -l32 agc_driver_image_table_tests
ctest --test-dir build --output-on-failure -R "agc_driver_pointer_vsharp_(sibling|child)_key_table$"
# VUID origin check: every production file of the PR restored from main
ninja -C build -j32 -l32 agc_driver_image_gather_volume_tests
env VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 VK_LOADER_DEBUG=error,warn,layer \
ctest --test-dir build -V -R "^agc_driver_image_gather_volume$"
# files restored from 814f9af6ef17, rebuilt, git status --porcelain empty
Results
| Check | Result |
|---|---|
git diff --check |
clean |
check_conventions.py |
All conventions checks passed |
| Configure / build / libs (script) | passed |
| Script's selected test run (2261 tests) | 0 new failures, 0 timeouts; 7 known failures also fail on main |
| 21 focused image-table tests with the validation layer | 21/21 passed, 0 VUID- lines, 0 "was not found but was requested" |
agc_driver_pointer_vsharp_sibling_key_table |
passed in 0.66 s with the layer loaded |
| Validation comparison (script) | one test with VUIDs not in the baseline: agc_driver_image_gather_volume (VUID-VkSamplerCreateInfo-addressModeU-01079 and -parameter). The test was not run on the baseline. It reports the same VUID with every production file of the PR restored from main, so it comes from main, not this PR. The diff doesn't touch sampler creation. |
New test output (merged tree)
1852: pointer V# table with a T# array in one V# indexed by a word read from a sibling V# of the same record pointer: T#s read from the sibling V# at the key read through the first one, with the first one's S#, checked for several keys, the largest inside the sibling V#, keys past it and a moved record pointer
100% tests passed, 0 tests failed out of 2
Negative control
I reverse-applied only the PR's own ResourceTracker.cpp change, the diff from 9e7ac694706c to the head, to the merged tree and rebuilt agc_driver_image_table_tests. The new test then fails with the refusal the description quotes, and #9969's agc_driver_pointer_vsharp_child_key_table still passes:
Output without the change
1/2 Test #1850: agc_driver_pointer_vsharp_child_key_table ..... Passed 0.71 sec
2/2 Test #1852: agc_driver_pointer_vsharp_sibling_key_table ...***Failed 0.12 sec
ShaderRecompiler::Recompile: GetSamplerResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x000000c0(GetBufferResource(LoadAddressU32@pc=0x00000090(GetAddressResource(Phi(ReadConstBuffer@pc=0x00000074(...
After I restored the merged file and rebuilt, the test passes again and git status --porcelain is empty.
Findings
Blocking
None.
Non-blocking
Unblocks:line has no Game bring-up discussion. The line names PPSA03016 but not its Game bring-up discussion. CONTRIBUTING.md asks for both (Unblocks: PPSA01325 #3232). Suggested fix: add the PPSA03016 discussion number.
Notes on the code, which need no change:
SiblingVsharp(ResourceTracker.cpp#L2408-L2422) needs all eight words of the two V#s to beLoadAddressU32from one, or an equivalent,GetAddressResource.- Only
DescriptorWordReadspassessiblings = true(line 864), and only on itsrereadablepath. So the key stays the guest's value at dispatch, as the description says. - The sibling's T# column is still planned on its own through
MatchKeyDomain, becauseMatchChildKeystill requires the same handle. If the sibling can't be planned, the shader is still refused. - The debt entry in
docs/dev/debt/shader.mdstates the gap first: the column is not narrowed, and the other forms stay refused. - The test's decoy key and swapped S# in the sibling's buffer would catch A's words being read from B.
Not verified
- Hardware GPUs: I ran only on lavapipe. The author reports RADV RX 9070 XT, and the NVIDIA and AMD CI comments on this head report 0 new failures and no new validation IDs.
- The title (PPSA03016): I did not launch it.
- The rest of #9969's chain, which this diff also shows against
main. I reviewed only this PR's three commits on top of9e7ac69470. The chain's tests ran in the script's selection and in the 21 focused tests above. - The memory cost of an unnarrowed sibling column. It can bind up to 65,536 entries per distinct child V# within the existing inner key-slot budget. No test covers a large sibling
num_records.
|
@XairRrR thanks for the review and the negative control. I added the bring-up discussion to the checklist: |
boykopovar#7223 has merged. The conflicts are this branch's additions on top of it: its tests in the image-table test lists, pointerKeyed in the image-table column and its equality check, the plan encoder's field count and list version, and FormatVersion 63. agc_shader_memory_tests.cmake merged without a conflict but registered agc_shader_nested_snapshot_retry twice: boykopovar#7223's block after agc_shader_descriptor_phis and this branch's before it. The merge keeps this branch's block, which adds agc_shader_nested_sampler_snapshot_retry, at boykopovar#7223's place.
e94fb3f is boykopovar#7395 with its merge of main after boykopovar#7223. The two conflicts are this branch's own changes over boykopovar#7395: FormatVersion 66, and its wording of the pointer-keyed image-table debt entry.
4769d4a is boykopovar#8939 with boykopovar#7395's merge of main after boykopovar#7223. The one conflict is this branch's FormatVersion 67.
…sharp-reuse 6e403b4 is boykopovar#9457 with the stack's merges of boykopovar#7395's main merge after boykopovar#7223. FormatVersion stays this branch's 71. In the debt file, boykopovar#7395 adds a sentence on nested children's registered views to "Nested image tables", which this branch rewrites. The entry keeps this branch's wording, and the sentence moves into its own entry after it, since both together are over the debt files' 1000 characters.
b41beed is boykopovar#9790 with the stack's merges of boykopovar#7395's main merge after boykopovar#7223. FormatVersion stays this branch's 73, and the debt file keeps this branch's three entries, with boykopovar#7395's "Nested image table children" entry added after "Nested image tables".
e358b7a to
34213d6
Compare
|
Rebased onto #9969's new head |
✅ NVIDIA GPU test: passedPR head
Unstable on main (4, these also failed in recent main runs on this machine; not counted against this PR)Failing on main too (3, not counted against this PR)Validated but not compared (1, not in the main baseline's validation pass)Validation IDs already present on main (13 IDs in 558 tests)Skipped (49)ReproduceAutomated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs. |
What
Marvel's Spider-Man 2 (PPSA03016 01.004.003), with #9969 applied, stops on a compute shader whose S# the recompiler refuses:
The same block also appears 0xbb8 higher, at pc 0x9a78 / 0x99fc. The shape, from the IR posted on #9790 on 2026-11-02:
#9969 handles a key word whose T# reads go through the same V#. Here the key read through A indexes B.
ChildKeyWordrequired the T# reads to use the key's own handle, soDescriptorWordReadsrefused A's key read, and with it the S# column read through A.ChildKeyWordnow also accepts T# reads through a sibling V# whenDescriptorWordReadsasks on behalf of a V# that is read again at dispatch. A sibling is aGetBufferResourcewhose four words areLoadAddressU32s from the same, or an equivalent,GetAddressResourceas the key's V#. Nothing else changes:MatchChildKeystill requires the same handle, so the column falls back toMatchKeyDomain: B's table binds every key the key's known bits allow, up to B's num_records. Narrowing it would need the materializer to read the key from the other V#'s buffer.The debt entry for nested image tables records this form and its limits.
Tested
Arch Linux (kernel 7.2.11), AMD Radeon RX 9070 XT (RADV GFX1201, Mesa 26.2.6), Release. The branch is #9969's head
c147aa8647plus the three commits of this PR.New test
agc_driver_pointer_vsharp_sibling_key_table(agc_driver_image_table_tests --pointer-vsharp-sibling-key). It uses #9969's child-key shader with the T# array in a second V# loaded through the same record pointer. A's buffer holds the key and the S#. B's buffer holds the T# array, plus a decoy key and the opposite S# at the same offsets, so reading A's words from B would show up. It checks:None of these recompile the shader.
Paired validation of the 21
agc_driver_(nested_image_table.*|nested_sampler_table|pointer_.*_table|image_table)tests, withVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 VK_LOADER_DEBUG=layer:VUID-linesc147aa8647+ the new test,ResourceTracker.cppfromc147aa8647On the base, the new test fails the way the title does:
The validation layer was inserted in every test of both runs. The 57
^shader|^agc_shadertests pass on this branch. These results are from the rebase ontoc147aa8647(2026-11-04); the same pair on the earlier base9e7ac69470gave the same counts.Title: Spider-Man 2, relinked with
--module-dir . --registry, fresh shader cache, Cross every 30 s, until the first stop. Base:main4f41f4c + #99699e7ac69470+ #893958ebe4b440+ #1364617770dd69f+ #153997a9ecdc365. #13646 keeps the bindless table's textures inside VRAM, and #15399 stops an intermittentruntime resource snapshot did not stabilize.RenderFrameIndexat 5683. The refusing runs stopped at frame 1460 (this base) and at frames 4856 and 5015 (the earlier head).device-local heap 1 uses 16314 of its 16304 MiB budget), and my memory guard stopped it at 161 s. That fill comes from the bindless table (Marvel's Spider-Man 2 (PPSA03016) #10195), not from this change.Not verified: the picture. On this machine the bench's screenshots of this title are black after about 60 s in every run, with or without this change, so I can't judge the rendering. The new test checks the selected T# and S# against the guest's own.
Checklist
main; no other open PR implements the same functions (fix(shader): index a pointer-loaded V#'s T# array by a word read from the same buffer #9969 and fix(shader): plan several T#s read through one pointer-loaded V#, keeping the V# real for their other uses #9790 handle a key read through the T#'s own V#; this is the sibling-V# form neither covers)NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)mainalso shows fix(shader): index a pointer-loaded V#'s T# array by a word read from the same buffer #9969's chain)