From fdbd6938b83be295edb95a5eb65cab46a0536ec8 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:32:02 +0200 Subject: [PATCH 1/3] fix(agc): accept bindless image tables with entries wider than 32 bytes The bindless table planner only recognised a T# table whose entries are exactly 32 bytes apart (key << 5 or key * 32). A title (PPSA07411) loads its T# from a table with 48-byte entries, the T# in the last 32 bytes (key * 48, load offset 16), so the descriptor stayed a ReadConstBuffer and the recompiler stopped with "GetImageResource dword 0 is not a valid runtime value". IndirectImage now carries entryStride (default 32). MatchTableOffset accepts a multiplier that is a multiple of 4 and at least 32, the planner requires the eight T# dwords to lie inside one entry, and the materializer uses the stride for the entry count, the partial-descriptor check and the entry address. A 32-byte table behaves as before. agc_shader_bindless gets a 48-byte-stride case; it fails on main with the same message and passes with this change. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 27 +++++++++++++++++++ .../IrMetadata/ControlFlowInfo.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 6 ++--- .../Optimization/src/ResourceTracker.cpp | 13 ++++++--- core/shader/recompiler/ShaderDiskCache.cpp | 8 +++--- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 130508267e2e..e82d394a0082 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1355,7 +1355,7 @@ IrResourcePlan handBuiltPlan() { DescriptorSource table; table.dwordCount = 8u; table.dwords[0] = &readConst(3); - table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true}; + table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, 48u, true}; DescriptorSource heap; heap.dwordCount = 4u; heap.dwords[1] = &readConst(6); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 902164752015..cdcb37d1881f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1076,6 +1076,33 @@ void verifyBindlessTable() { #if ANYPS5_ENABLE_SPIRV_TOOLS static_cast(ValidateAndOptimizeSpirv(queried->spirv, query.target.vulkanVersion, query.target.spirvVersion)); #endif + static std::array strided{}; + for (std::uint32_t entry = 0; entry < 4u; entry++) { + std::fill_n(strided.begin() + entry * 12u, 4u, 0xdead0000u + entry); + std::copy(heap[entry].begin(), heap[entry].end(), strided.begin() + entry * 12u + 4u); + } + const std::vector stridedCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x9310ff10u, 0x00000030u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto stridedHeapV = bufferDescriptor(strided.data(), 48u, 4u); + std::copy(stridedHeapV.begin(), stridedHeapV.end(), srt.begin()); + auto stridedRequest = makeRequest(stridedCode); + const auto stridedPlan = GetResourcePlan(stridedRequest); + std::uint32_t stridedTables = 0; + for (const auto& source : stridedPlan->descriptorSources) { + if (!source.indirectImage.has_value()) continue; + stridedTables++; + require(!source.indirectImage->hasMaterial && source.indirectImage->entryStride == 48u && source.indirectImage->entryOffset == 16u, "bindless: the 48-byte table entries were not recorded"); + } + require(stridedTables == 1, "bindless: a 48-byte table was not planned"); + AgcDriver::ShaderMemory stridedMemory({}); + const auto stridedCapture = stridedMemory.Capture(stridedRequest); + const auto stridedDirect = static_cast(stridedPlan->info.images.size()); + const auto stridedRoot = tableRoot(*stridedCapture, stridedDirect); + const auto stridedMapping = mappingOf(stridedCapture->snapshot); + require(std::vector(stridedMapping.begin(), stridedMapping.begin() + 7) == std::vector{3u, 0u, 0u, 1u, 1u, 3u, 3u}, "bindless: the 48-byte table is not the identity mapping"); + require(stridedCapture->snapshot.images[stridedRoot].dwords == heap[0] && stridedCapture->snapshot.images[stridedDirect].dwords == heap[1] && stridedCapture->snapshot.images[stridedDirect + 1u].dwords == heap[2] && stridedCapture->snapshot.images[stridedDirect + 2u].dwords == heap[3], "bindless: the 48-byte table slots are wrong"); + stridedRequest.context.memory = stridedMemory.Regions(); + require(!Recompile(stridedRequest, *stridedCapture)->spirv.empty(), "bindless: the 48-byte table did not compile"); + fillSrt(4u); // A table wider than the slots without a material pattern is rejected. fillSrt(100u); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index f9df378d48d8..7817e319e701 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -31,6 +31,7 @@ struct DescriptorSource { std::uint32_t selectorOffset = 0; std::uint32_t keyArg = 0; std::uint32_t entryOffset = 0; + std::uint32_t entryStride = 32; bool hasMaterial = false; bool operator==(const IndirectImage& other) const = default; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 37da1a30cfe8..a354214d8f43 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -333,8 +333,8 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con const ShaderBufferResource heap = decodeBufferDescriptor(heapValue); const std::uint64_t heapSize = heap.GetSize(); if (heap.Type() != 0u || heap.Base48() > std::numeric_limits::max() - heapSize) throw std::runtime_error("bindless heap buffer is invalid"); - if (heapSize != 0u && (heapSize < table.entryOffset || (heapSize - table.entryOffset) % TableEntryBytes != 0u)) throw std::runtime_error("bindless heap contains a partial descriptor"); - const auto entries = heapSize > table.entryOffset ? static_cast(std::min((heapSize - table.entryOffset) / TableEntryBytes, std::numeric_limits::max())) : 0u; + if (heapSize != 0u && (heapSize < table.entryOffset + TableEntryBytes || (heapSize - table.entryOffset - TableEntryBytes) % table.entryStride != 0u)) throw std::runtime_error("bindless heap contains a partial descriptor"); + const auto entries = heapSize >= table.entryOffset + TableEntryBytes ? static_cast(std::min((heapSize - table.entryOffset - TableEntryBytes) / table.entryStride + 1u, std::numeric_limits::max())) : 0u; const auto readWord = [&](std::uint64_t address, std::uint32_t& word) { if (!runtime.readMemory(runtime.userContext, address, &word)) { throw std::runtime_error("failed to read a bindless image table from memory"); @@ -392,7 +392,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con for (std::size_t i = 0u; i < keys.size(); ++i) { auto& candidate = candidates[i]; candidate.dwordCount = 8u; - const std::uint64_t address = heapBase + table.entryOffset + static_cast(keys[i]) * TableEntryBytes; + const std::uint64_t address = heapBase + table.entryOffset + static_cast(keys[i]) * table.entryStride; for (std::uint32_t dword = 0u; dword < 8u; ++dword) readWord(address + dword * sizeof(std::uint32_t), candidate.dwords[dword]); if (nullImageDescriptor(candidate)) { ++paddedNull; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index ee4eaee5f81d..b0e86a2d25bc 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -460,7 +460,7 @@ class Tracker { } // The table entry offset `key * 32` (a shift or a multiply), optionally plus an immediate. - static bool MatchTableOffset(IrValue* value, IrValue*& key, std::uint32_t& entryOffset) { + static bool MatchTableOffset(IrValue* value, IrValue*& key, std::uint32_t& entryOffset, std::uint32_t& entryStride) { value = value->Resolve(); entryOffset = 0; if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { @@ -483,6 +483,7 @@ class Tracker { return false; } key = value->Argument(0)->Resolve(); + entryStride = 32u; return true; } if (value->Opcode() != IrOpcode::IMul32) { @@ -495,7 +496,8 @@ class Tracker { } else { return false; } - return scale == 32u; + entryStride = scale; + return scale >= 32u && scale % sizeof(std::uint32_t) == 0u; } // A T# whose eight dwords are scalar reads of one V# (the table) at `entryOffset + key * 32` @@ -538,10 +540,14 @@ class Tracker { IrValue* key = nullptr; std::uint32_t entryOffset = 0; - if (!MatchTableOffset(heapOffset, key, entryOffset) || key->Type() != IrType::U32) { + std::uint32_t entryStride = 0; + if (!MatchTableOffset(heapOffset, key, entryOffset, entryStride) || key->Type() != IrType::U32) { return false; } entryOffset += immediateOffset; + if (entryOffset + 8u * sizeof(std::uint32_t) > entryStride) { + return false; + } for (const auto* read : heapReads) { if (!usedOnlyAsImage(*read, heapReads)) { @@ -559,6 +565,7 @@ class Tracker { table.heapSource = heapSourceIndex; table.materialSource = heapSourceIndex; table.entryOffset = entryOffset; + table.entryStride = entryStride; DescriptorSource materialSource = heapSource; std::uint32_t materialMemoryIndex = 0; const MemoryInfo* materialMemory = ScalarReadMemory(*key, materialMemoryIndex); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 355e720e7670..e30e07ca31f4 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -108,7 +108,7 @@ static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value en static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 104, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource::IndirectImage) == 28, "IndirectImage changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource::IndirectImage) == 32, "IndirectImage changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); static_assert(sizeof(UniformFill) == 28, "UniformFill changed: update the plan encoder"); @@ -147,7 +147,7 @@ static_assert(fieldCount() == 21, "IrResourcePlan changed: updat static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 30, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 7, "IndirectImage changed: update the plan encoder"); +static_assert(fieldCount() == 8, "IndirectImage changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); static_assert(fieldCount() == 5, "UniformFill changed: update the plan encoder"); @@ -972,6 +972,7 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Value(table.selectorOffset); out.Value(table.keyArg); out.Value(table.entryOffset); + out.Value(table.entryStride); out.Value(table.hasMaterial); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { @@ -1103,7 +1104,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { } } reader.List(plan.memoryInfo, 78, decodeMemoryInfo); - reader.List(plan.descriptorSources, 62, [&](Reader& in, DescriptorSource& source) { + reader.List(plan.descriptorSources, 66, [&](Reader& in, DescriptorSource& source) { for (auto& dword : source.dwords) dword = valueAt(in.Get()); in.Value(source.dwordCount); const bool indirect = in.Get(); @@ -1114,6 +1115,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(table.selectorOffset); in.Value(table.keyArg); in.Value(table.entryOffset); + in.Value(table.entryStride); in.Value(table.hasMaterial); source.indirectImage = indirect ? std::optional(table) : std::nullopt; }); From 3093460c9e5949277e22659fc7815e30b1d7a4ff Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:22:49 +0200 Subject: [PATCH 2/3] fix(agc): treat a bindless heap as partial only when it ends inside a T# The partial-descriptor check required the heap to end exactly at the end of the last T#, which only holds when the T# is the last 32 bytes of each entry. A table with 64-byte entries and the T# first, backed by a V# that covers whole entries, was refused although every descriptor is complete. A descriptor is partial only when the heap ends inside a T#: the remainder of the heap after the entry offset, modulo the stride, is non-zero and below 32 bytes. 32-byte tables keep the old behaviour; the entry count is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 25 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index cdcb37d1881f..cb6d15a55cd4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1102,6 +1102,31 @@ void verifyBindlessTable() { require(stridedCapture->snapshot.images[stridedRoot].dwords == heap[0] && stridedCapture->snapshot.images[stridedDirect].dwords == heap[1] && stridedCapture->snapshot.images[stridedDirect + 1u].dwords == heap[2] && stridedCapture->snapshot.images[stridedDirect + 2u].dwords == heap[3], "bindless: the 48-byte table slots are wrong"); stridedRequest.context.memory = stridedMemory.Regions(); require(!Recompile(stridedRequest, *stridedCapture)->spirv.empty(), "bindless: the 48-byte table did not compile"); + + static std::array leading{}; + for (std::uint32_t entry = 0; entry < 4u; entry++) { + std::copy(heap[entry].begin(), heap[entry].end(), leading.begin() + entry * 16u); + std::fill_n(leading.begin() + entry * 16u + 8u, 8u, 0xdead0000u + entry); + } + const std::vector leadingCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x9310ff10u, 0x00000040u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto leadingHeapV = bufferDescriptor(leading.data(), 64u, 4u); + std::copy(leadingHeapV.begin(), leadingHeapV.end(), srt.begin()); + auto leadingRequest = makeRequest(leadingCode); + const auto leadingPlan = GetResourcePlan(leadingRequest); + std::uint32_t leadingTables = 0; + for (const auto& source : leadingPlan->descriptorSources) { + if (!source.indirectImage.has_value()) continue; + leadingTables++; + require(!source.indirectImage->hasMaterial && source.indirectImage->entryStride == 64u && source.indirectImage->entryOffset == 0u, "bindless: the 64-byte table entries were not recorded"); + } + require(leadingTables == 1, "bindless: a 64-byte table was not planned"); + AgcDriver::ShaderMemory leadingMemory({}); + const auto leadingCapture = leadingMemory.Capture(leadingRequest); + const auto leadingDirect = static_cast(leadingPlan->info.images.size()); + const auto leadingRoot = tableRoot(*leadingCapture, leadingDirect); + require(leadingCapture->snapshot.images[leadingRoot].dwords == heap[0] && leadingCapture->snapshot.images[leadingDirect].dwords == heap[1] && leadingCapture->snapshot.images[leadingDirect + 1u].dwords == heap[2] && leadingCapture->snapshot.images[leadingDirect + 2u].dwords == heap[3], "bindless: the 64-byte table with the T# first and data behind it has wrong slots"); + leadingRequest.context.memory = leadingMemory.Regions(); + require(!Recompile(leadingRequest, *leadingCapture)->spirv.empty(), "bindless: the 64-byte table did not compile"); fillSrt(4u); // A table wider than the slots without a material pattern is rejected. diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index a354214d8f43..e946e75c8d7b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -333,7 +333,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con const ShaderBufferResource heap = decodeBufferDescriptor(heapValue); const std::uint64_t heapSize = heap.GetSize(); if (heap.Type() != 0u || heap.Base48() > std::numeric_limits::max() - heapSize) throw std::runtime_error("bindless heap buffer is invalid"); - if (heapSize != 0u && (heapSize < table.entryOffset + TableEntryBytes || (heapSize - table.entryOffset - TableEntryBytes) % table.entryStride != 0u)) throw std::runtime_error("bindless heap contains a partial descriptor"); + if (heapSize != 0u && (heapSize < table.entryOffset + TableEntryBytes || ((heapSize - table.entryOffset) % table.entryStride != 0u && (heapSize - table.entryOffset) % table.entryStride < TableEntryBytes))) throw std::runtime_error("bindless heap contains a partial descriptor"); const auto entries = heapSize >= table.entryOffset + TableEntryBytes ? static_cast(std::min((heapSize - table.entryOffset - TableEntryBytes) / table.entryStride + 1u, std::numeric_limits::max())) : 0u; const auto readWord = [&](std::uint64_t address, std::uint32_t& word) { if (!runtime.readMemory(runtime.userContext, address, &word)) { From ee950660358c6d199a23131ad00de6f96db50fba Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:20:48 +0200 Subject: [PATCH 3/3] fix(agc): keep wider-stride records the runtime walk can evaluate as direct images A table entry stride other than 32 now only plans a bindless table when the T# is not a valid uniform runtime value; otherwise the existing direct-image path (uniform table image in tests/ShaderMemory.cpp) applies as before. Co-Authored-By: Claude Sonnet 5.5 --- .../recompiler/Optimization/src/ResourceTracker.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index b0e86a2d25bc..1d9109fbf33e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -548,6 +548,14 @@ class Tracker { if (entryOffset + 8u * sizeof(std::uint32_t) > entryStride) { return false; } + if (entryStride != 32u) { + DescriptorSource direct; + MakeSource(handle, 8u, false, false, direct); + std::uint32_t badDword = 0; + if (ValidateSource(direct, badDword)) { + return false; + } + } for (const auto* read : heapReads) { if (!usedOnlyAsImage(*read, heapReads)) {