From 41220941d8b8b363a596ebe8d83f4d315ab96669 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:25:35 +0000 Subject: [PATCH 01/34] feat(shader): resolve S# reads through nested descriptor tables 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 19 +++ .../tests/execution/ImageTable.cpp | 150 ++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 2 +- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 6 files changed, 175 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6ece0cbe7718..029fac316431 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2177,9 +2177,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_nested_image_table COMMAND agc_driver_image_table_tests --nested) add_test(NAME agc_driver_nested_image_table_large COMMAND agc_driver_image_table_tests --nested-large) add_test(NAME agc_driver_nested_image_table_budget COMMAND agc_driver_image_table_tests --nested-budget) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_nested_sampler_table COMMAND agc_driver_image_table_tests --nested-sampler) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 89df3219affc..7fdacb313d45 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1717,6 +1717,25 @@ std::vector planFixtures() { nestedCode.insert(nestedCode.begin() + 15u, 0x87148114u); add("nested pointer image table", 64u, std::move(nestedCode), srtPointer, {{PlanSrtAddress, nestedSrt}, {PlanTableAddress, parents}, {PlanPointerAddress, nestedBuffers}, {PlanPayloadAddress, nestedImages}, {PlanMaterialAddress, {0u, 0u, 0u, 1u, 1u, 0u, 1u, 1u}}}); fixtures.back().groupId = true; + std::vector samplerSrt(20u, 0u); + std::copy(nestedSrt.begin(), nestedSrt.end(), samplerSrt.begin()); + place(samplerSrt, 12u, imageWords(PlanTextureAddress)); + const std::vector pointClamp{0x92u, 0x400000u, 0u, 0u}; + const std::vector pointWrap{0u, 0x400000u, 1u << 24u, 0u}; + std::vector nestedSamplers(32u, 0u); + place(nestedSamplers, 0u, pointWrap); + place(nestedSamplers, 4u, pointClamp); + place(nestedSamplers, 16u, pointClamp); + place(nestedSamplers, 20u, pointWrap); + std::vector samplerCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf40c0900u, 0xfa000030u, + 0x8f108302u, 0xf4200444u, 0x20000000u, 0xf4200504u, 0x20000004u, 0x8f128411u, 0x8f148414u, + 0xf4240b02u, 0x24000008u, 0xf4080616u, 0xfa0000a0u, 0xf428080cu, 0x28000000u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,}; + samplerCode.insert(samplerCode.begin() + 13u, 0x87118311u); + samplerCode.insert(samplerCode.begin() + 15u, 0x87148114u); + add("nested pointer sampler table", 64u, std::move(samplerCode), srtPointer, {{PlanSrtAddress, samplerSrt}, {PlanTableAddress, parents}, {PlanPointerAddress, nestedBuffers}, {PlanPayloadAddress, nestedSamplers}, {PlanMaterialAddress, {0u, 0u, 0u, 1u, 1u, 0u, 1u, 1u}}}); + fixtures.back().groupId = true; PlanFixture fragment; fragment.name = "fragment parameters"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 87039d58471f..ed3259bdc8f8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -297,6 +297,14 @@ alignas(256) constexpr std::array NestedRecordCode{ 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array NestedSamplerCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf40c0900u, 0xfa000030u, + 0x8f108302u, 0xf4200444u, 0x20000000u, 0xf4200504u, 0x20000004u, 0x8f128411u, 0x8f148414u, + 0xf4240b02u, 0x24000008u, 0xf4080616u, 0xfa0000a0u, 0xf428080cu, 0x28000000u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -1545,6 +1553,144 @@ void RunPointerChainRecordTests(AgcDriver::VulkanDevice& device) { RequireRed(relinked, 1, Lanes, texels[0], "float texels after record 1's second pointer moved to another T#"); } +void RunNestedSamplerTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(512) std::array, 2> records{}; + alignas(256) std::array, 2> palettes{}; + std::vector extra; + const std::array, 2>, 2> samplers{{{PointWrap, PointClamp}, {PointClamp, PointWrap}}}; + Table = {}; + Keys = {}; + for (std::uint32_t outer = 0; outer < 2u; ++outer) { + auto descriptor = Buffer(palettes[outer].data(), 1u); + descriptor[1] |= 64u << 16u; + std::copy(descriptor.begin(), descriptor.end(), records[outer].begin() + 40u); + for (std::uint32_t inner = 0; inner < 2u; ++inner) std::copy(samplers[outer][inner].begin(), samplers[outer][inner].end(), palettes[outer].begin() + inner * 4u); + } + for (std::uint32_t record = 0; record < 3u; ++record) { + const auto pointer = reinterpret_cast(records[record % 2u].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + } + const auto image = Image(FloatTexels.data(), Format32Float); + const auto run = [&](std::span code = NestedSamplerCode, std::uint32_t waveSize = 64u) { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(image.begin(), image.end(), Srt.begin() + 12u); + std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + {reinterpret_cast(palettes.data()), std::as_bytes(std::span(palettes))}, + }; + if (!extra.empty()) memory.push_back({reinterpret_cast(extra.data()), std::as_bytes(std::span(extra))}); + const auto inputTable = Table; + const auto inputKeys = Keys; + const auto inputRecords = records; + const auto inputPalettes = palettes; + const auto inputExtra = extra; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, waveSize); + Require(Table == inputTable && Keys == inputKeys && records == inputRecords && palettes == inputPalettes && extra == inputExtra, "nested sampler table: dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), "nested sampler table: dispatch wrote past its output"); + return result; + }; + const auto sampled = [&](std::uint32_t record, std::uint32_t child) { + const bool wrap = samplers[record % 2u][child] == PointWrap; + return std::function([wrap](std::uint32_t x) { return std::bit_cast(FloatTexels[wrap ? x : Width - 1u]); }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const std::array keys{0u, 0u, 0u, 1u, 1u, 0u, 1u, 1u}; + std::copy(keys.begin(), keys.end(), Keys.begin()); + const auto original = run(); + Require(!Faulted(original), "nested sampler table: valid independent keys faulted:\n" + original.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(original, group, Lanes, sampled(keys[group * 2u], keys[group * 2u + 1u]), "nested sampler table: the sampler selected through record, pointer and V#"); + const auto wave32 = run(NestedSamplerCode, 32u); + Require(!Faulted(wave32) && wave32.words == original.words, "nested sampler table: wave32 and wave64 disagree"); + std::copy(PointWrap.begin(), PointWrap.end(), palettes[1].begin()); + const auto edited = run(); + Require(!Faulted(edited) && Group(edited, 2) == Group(original, 0) && Group(edited, 3) == Group(original, 3), "nested sampler table: changing only a child S# retained a stale sampler"); + Require(edited.artifactId == original.artifactId, "nested sampler table: editing a child S# recompiled the shader"); + std::copy(PointClamp.begin(), PointClamp.end(), palettes[1].begin()); + Keys[0] = 3u; + const auto unmapped = run(); + Require(Faulted(unmapped) && unmapped.log.find("unmapped") != std::string::npos, "nested sampler table: an unreadable parent did not fault:\n" + unmapped.log); + Require(untouched(unmapped), "nested sampler table: a faulting dispatch published copied output"); + Keys[0] = 0u; + records[0][43] |= 1u << 30u; + const auto notBuffer = run(); + Require(Faulted(notBuffer) && notBuffer.log.find("not a buffer") != std::string::npos, "nested sampler table: a non-buffer parent was accepted:\n" + notBuffer.log); + Require(untouched(notBuffer), "nested sampler table: a non-buffer parent published copied output"); + records[0][43] &= ~(1u << 30u); + const auto savedRecords = records; + std::copy_n(records[0].begin() + 40u, 4u, records[1].begin() + 40u); + const auto moved = run(); + Require(!Faulted(moved) && Group(moved, 2) == Group(original, 0) && Group(moved, 3) == Group(original, 1), "nested sampler table: replacing the nested V# retained a stale sampler"); + Require(moved.artifactId == original.artifactId && moved.tableMap.size() < original.tableMap.size(), "nested sampler table: equal V#s did not share one child table"); + records = savedRecords; + const auto refused = [&](const std::vector& code, const char* what) { + bool rejected = false; + try { static_cast(run(code)); } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find("not a valid runtime value") != std::string::npos; } + Require(rejected, std::string("nested sampler table: ") + what + " was not refused by resource planning"); + }; + auto shared = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); + shared.insert(shared.begin() + 31u, 0x7e0e0232u); + shared.insert(shared.begin() + 21u, {0xf4200c8cu, 0xfa000000u}); + refused(shared, "a V# with another buffer consumer"); + auto consumer = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); + consumer.insert(consumer.begin() + 31u, 0x7e0e0221u); + refused(consumer, "an S# with another consumer"); + auto phi = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); + phi.insert(phi.begin() + 17u, {0xbf068011u, 0xbf840002u, 0xbeac03ffu, 0x000000eau}); + for (std::uint32_t group = 0; group < 4u; ++group) Keys[group * 2u] = 1u; + const auto phiValid = run(phi); + Require(!Faulted(phiValid), "nested sampler table: a captured pointer selected through Phi faulted:\n" + phiValid.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(phiValid, group, Lanes, sampled(1u, Keys[group * 2u + 1u]), "nested sampler table: Phi-selected pointer"); + Keys[0] = 0u; + const auto phiFault = run(phi); + Require(Faulted(phiFault) && phiFault.log.find("outside the snapshot") != std::string::npos, "nested sampler table: an uncaptured pointer did not fault:\n" + phiFault.log); + Require(untouched(phiFault), "nested sampler table: an uncaptured pointer published copied output"); + Require(phiFault.artifactId == phiValid.artifactId, "nested sampler table: the pointer choice was specialized on the CPU"); + std::copy(keys.begin(), keys.end(), Keys.begin()); + extra.assign(320u * 4u, 0u); + for (std::uint32_t entry = 0; entry < 320u; ++entry) { + std::copy(PointWrap.begin(), PointWrap.end(), extra.begin() + entry * 4u); + extra[entry * 4u + 2u] |= entry; + } + const auto many = Buffer(extra.data(), static_cast(extra.size() * sizeof(std::uint32_t))); + std::copy(many.begin(), many.end(), records[0].begin() + 40u); + Keys[1] = 319u; + auto unkeyed = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); + unkeyed.insert(unkeyed.begin() + 14u, {0x8714ff14u, 0x000001ffu}); + const auto overBudget = run(unkeyed); + Require(Faulted(overBudget) && overBudget.log.find("over budget") != std::string::npos, "nested sampler table: an S# past the sampler budget did not fault:\n" + overBudget.log); + Require(untouched(overBudget), "nested sampler table: an S# past the sampler budget published copied output"); + extra.assign((NestedKeyLimit + 1u) * 4u, 0u); + std::copy(PointClamp.begin(), PointClamp.end(), extra.begin()); + const auto large = Buffer(extra.data(), static_cast(extra.size() * sizeof(std::uint32_t))); + std::copy(large.begin(), large.end(), records[0].begin() + 40u); + Keys[1] = 0u; + const auto bounded = run(); + Require(Faulted(bounded) && bounded.log.find("outside the snapshot") != std::string::npos, "nested sampler table: an uncaptured child did not fault:\n" + bounded.log); + Require(bounded.tableMap.size() < NestedKeyLimit + 1024u, "nested sampler table: the two levels produced an unbounded map"); + Require(untouched(bounded), "nested sampler table: a dispatch outside the snapshot published copied output"); + std::copy(large.begin(), large.end(), records[1].begin() + 40u); + const auto sharedBudget = run(); + Require(!Faulted(sharedBudget) && sharedBudget.tableMap.size() < NestedKeyLimit + 1024u, "nested sampler table: equal V#s consumed the snapshot budget twice"); + for (std::uint32_t group = 0; group < 4u; ++group) { + const bool clamp = keys[group * 2u + 1u] == 0u; + RequireRed(sharedBudget, group, Lanes, [clamp](std::uint32_t x) { return std::bit_cast(FloatTexels[clamp ? Width - 1u : x]); }, "nested sampler table: aliased parent at the snapshot budget"); + } + std::puts("nested sampler table: S# through record, pointer and V# checked on wave64 and wave32, with sharing, budgets and refusals"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -1770,6 +1916,10 @@ int main(int argc, char** argv) { RunNestedRecordTests(*device, NestedCase::Budget); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--nested-sampler") == 0) { + RunNestedSamplerTests(*device); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index d5ae83da142f..3b2b476ecfb4 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -655,7 +655,7 @@ class Tracker { DescriptorSource heapSource; std::uint32_t heapIndex = 0; if (!MakeRuntimeSource(*heapHandle, address ? IrOpcode::GetAddressResource : IrOpcode::GetBufferResource, address ? 2u : 4u, heapIndex, heapSource)) { - if (sampler || address || buffer || heapHandle->Opcode() != IrOpcode::GetBufferResource || !std::ranges::all_of(heapHandle->Uses(), [&](const IrValue* use) { return std::ranges::find(plan.reads, use) != plan.reads.end(); })) return false; + if (address || buffer || heapHandle->Opcode() != IrOpcode::GetBufferResource || !std::ranges::all_of(heapHandle->Uses(), [&](const IrValue* use) { return std::ranges::find(plan.reads, use) != plan.reads.end(); })) return false; const auto sameHandle = [&](const IrValue* user) { if (user->Opcode() != handle.Opcode() || user->ArgumentCount() != handle.ArgumentCount()) return false; for (std::uint32_t argument = 0; argument < handle.ArgumentCount(); argument++) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 859ea5fa4bd6..c45704d3298c 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1293,7 +1293,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { const auto& column = *source.tableColumn; if (column.buffer && (column.sampler || column.dwordCount != 4u)) return false; const auto& parent = plan.descriptorSources[column.heapSource].tableColumn; - if (parent.has_value() && (column.buffer || column.sampler || column.address || column.indirect || !parent->buffer || !parent->indirect || plan.descriptorSources[parent->heapSource].tableColumn.has_value())) return false; + if (parent.has_value() && (column.buffer || column.address || column.indirect || !parent->buffer || !parent->indirect || plan.descriptorSources[parent->heapSource].tableColumn.has_value())) return false; } if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 51e8823d300e..7a078b48a528 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -37,7 +37,7 @@ - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (T#s and S#s that `s_buffer_load` reads at `key * stride + addend` from a V#) are snapshotted when the dispatch or draw is recorded; a later CPU write to a record is not seen. - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. -- [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# words read by anything but their image handle, nested sampler columns and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. +- [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. From 9200c3a2d5d75ffe593629fc9ec4f94b0162868d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:36:13 +0000 Subject: [PATCH 02/34] feat(shader): look up a Phi-merged record pointer to an S# or T# 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 #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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 17 +++ .../tests/execution/ImageTable.cpp | 133 ++++++++++++++++++ core/shader/recompiler/ImageTableAbi.hpp | 2 +- .../IrMetadata/ControlFlowInfo.hpp | 3 +- .../IrMetadata/ResourcePlan.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 21 +-- .../Optimization/src/ResourceTracker.cpp | 7 +- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 11 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 4 + docs/dev/debt/shader.md | 1 + 13 files changed, 187 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 029fac316431..0904c16df94f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2178,9 +2178,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_nested_image_table_large COMMAND agc_driver_image_table_tests --nested-large) add_test(NAME agc_driver_nested_image_table_budget COMMAND agc_driver_image_table_tests --nested-budget) add_test(NAME agc_driver_nested_sampler_table COMMAND agc_driver_image_table_tests --nested-sampler) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_phi_sampler_table COMMAND agc_driver_image_table_tests --pointer-phi-sampler) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 7fdacb313d45..21e4ac2d0f94 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1736,6 +1736,16 @@ std::vector planFixtures() { samplerCode.insert(samplerCode.begin() + 15u, 0x87148114u); add("nested pointer sampler table", 64u, std::move(samplerCode), srtPointer, {{PlanSrtAddress, samplerSrt}, {PlanTableAddress, parents}, {PlanPointerAddress, nestedBuffers}, {PlanPayloadAddress, nestedSamplers}, {PlanMaterialAddress, {0u, 0u, 0u, 1u, 1u, 0u, 1u, 1u}}}); fixtures.back().groupId = true; + std::vector pointees(128u, 0u); + place(pointees, 8u, pointWrap); + place(pointees, 72u, pointClamp); + std::vector pointerCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf40c0900u, 0xfa000030u, + 0x8f108302u, 0xf4200444u, 0x20000000u, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf4080816u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,}; + pointerCode.insert(pointerCode.begin() + 14u, {0xbf068011u, 0xbf840002u, 0xbeac03ffu, 0x000000eau}); + add("pointer phi sampler table", 64u, std::move(pointerCode), srtPointer, {{PlanSrtAddress, samplerSrt}, {PlanTableAddress, parents}, {PlanPointerAddress, pointees}, {PlanMaterialAddress, {1u, 0u, 0u, 0u, 1u, 0u, 0u, 0u}}}); + fixtures.back().groupId = true; PlanFixture fragment; fragment.name = "fragment parameters"; @@ -2128,6 +2138,7 @@ void verifyMalformedPlans() { rejects("an image table cycle", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->heapSource = 1u; }); rejects("an image table parent that is not a buffer", [](IrResourcePlan& plan) { plan.descriptorSources[2].tableColumn = *plan.descriptorSources[1].tableColumn; }); rejects("an image table selection past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->selection = 4u; }); + rejects("a pointer-keyed table that is not indirect", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->pointerKeyed = true; }); auto foreign = handBuiltPlan(); IrValue stray(IrOpcode::Void, IrType::U32, 99u); foreign.srtReads[0].value = &stray; @@ -2157,6 +2168,12 @@ void verifyPlanRoundTrip() { const auto bufferBytes = encodedPlan(bufferTable); IrResourcePlan bufferDecoded; require(bufferBytes != handBytes && ShaderDiskCache::DecodePlan(bufferBytes, bufferDecoded) && samePlan(bufferTable, bufferDecoded), "the nested buffer column changed in a disk-cache round trip"); + auto pointerTable = handBuiltPlan(); + pointerTable.descriptorSources[1].tableColumn->indirect = true; + pointerTable.descriptorSources[1].tableColumn->pointerKeyed = true; + const auto pointerBytes = encodedPlan(pointerTable); + IrResourcePlan pointerDecoded; + require(pointerBytes != handBytes && ShaderDiskCache::DecodePlan(pointerBytes, pointerDecoded) && samePlan(pointerTable, pointerDecoded) && pointerDecoded.descriptorSources[1].tableColumn->pointerKeyed, "the pointer-keyed column changed in a disk-cache round trip"); PlanFixture handFixture; handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; std::vector handSrt(16, 0u); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index ed3259bdc8f8..f7aeb2a0362a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -305,6 +305,20 @@ alignas(256) constexpr std::array NestedSamplerCode{ 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerSamplerCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf40c0900u, 0xfa000030u, + 0x8f108302u, 0xf4200444u, 0x20000000u, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf4080816u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerImageCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108302u, 0xf4200444u, 0x20000000u, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000040u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -1691,6 +1705,121 @@ void RunNestedSamplerTests(AgcDriver::VulkanDevice& device) { std::puts("nested sampler table: S# through record, pointer and V# checked on wave64 and wave32, with sharing, budgets and refusals"); } +void RunPointerSamplerTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 2> records{}; + const std::array, 2> samplers{PointWrap, PointClamp}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + for (std::uint32_t record = 0; record < 2u; ++record) { + std::copy(samplers[record].begin(), samplers[record].end(), records[record].begin() + 8u); + std::copy(images[record].begin(), images[record].end(), records[record].begin() + 16u); + } + for (std::uint32_t record = 0; record < 3u; ++record) { + const auto pointer = reinterpret_cast(records[record % 2u].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + } + auto root = images[0]; + std::uint32_t tableType = 0u; + const auto run = [&](std::span code, std::uint32_t waveSize = 64u) { + auto table = Buffer(Table.data(), 64u); + table[3] |= tableType; + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(root.begin(), root.end(), Srt.begin() + 12u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + }; + const auto inputTable = Table; + const auto inputKeys = Keys; + const auto inputRecords = records; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, waveSize); + Require(Table == inputTable && Keys == inputKeys && records == inputRecords, "pointer sampler table: dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), "pointer sampler table: dispatch wrote past its output"); + return result; + }; + const auto sampled = [](bool wrap) { + return std::function([wrap](std::uint32_t x) { return std::bit_cast(FloatTexels[wrap ? x : Width - 1u]); }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const std::array keys{1u, 2u, 1u, 2u}; + for (std::uint32_t group = 0; group < 4u; ++group) Keys[group * 2u] = keys[group]; + const auto direct = run(PointerSamplerCode); + Require(!Faulted(direct), "pointer sampler table: an S# read through a record's pointer faulted:\n" + direct.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(direct, group, Lanes, sampled(keys[group] % 2u == 0u), "pointer sampler table: the S# read through the record's pointer"); + auto phi = std::vector(PointerSamplerCode.begin(), PointerSamplerCode.end()); + phi.insert(phi.begin() + 14u, {0xbf068011u, 0xbf840002u, 0xbeac03ffu, 0x000000eau}); + const auto original = run(phi); + Require(!Faulted(original) && original.words == direct.words, "pointer sampler table: a captured pointer selected through Phi changed the result:\n" + original.log); + const auto wave32 = run(phi, 32u); + Require(!Faulted(wave32) && wave32.words == original.words, "pointer sampler table: wave32 and wave64 disagree"); + std::copy(PointWrap.begin(), PointWrap.end(), records[1].begin() + 8u); + const auto edited = run(phi); + Require(!Faulted(edited) && Group(edited, 0) == Group(original, 1) && Group(edited, 2) == Group(original, 3), "pointer sampler table: changing an S# retained a stale sampler"); + Require(edited.artifactId == original.artifactId, "pointer sampler table: editing an S# recompiled the shader"); + std::copy(PointClamp.begin(), PointClamp.end(), records[1].begin() + 8u); + Keys[0] = 0u; + const auto uncaptured = run(phi); + Require(Faulted(uncaptured) && uncaptured.log.find("outside the snapshot") != std::string::npos, "pointer sampler table: an uncaptured pointer did not fault:\n" + uncaptured.log); + Require(untouched(uncaptured), "pointer sampler table: an uncaptured pointer published copied output"); + Require(uncaptured.artifactId == original.artifactId, "pointer sampler table: the pointer choice was specialized on the CPU"); + auto alias = phi; + alias[17] = static_cast(reinterpret_cast(records[1].data())); + const auto aliased = run(alias); + Require(!Faulted(aliased) && Group(aliased, 0) == Group(original, 0) && Group(aliased, 1) == Group(original, 1), "pointer sampler table: a Phi constant naming another captured pointer did not read that pointer's S#:\n" + aliased.log); + Keys[0] = 3u; + const auto unmapped = run(phi); + Require(Faulted(unmapped) && unmapped.log.find("unmapped") != std::string::npos, "pointer sampler table: a null pointer did not fault:\n" + unmapped.log); + Require(untouched(unmapped), "pointer sampler table: a null pointer published copied output"); + Keys[0] = 1u; + tableType = 1u << 30u; + const auto notBuffer = run(phi); + Require(Faulted(notBuffer) && notBuffer.log.find("not a buffer") != std::string::npos, "pointer sampler table: a non-buffer record table was accepted:\n" + notBuffer.log); + Require(untouched(notBuffer), "pointer sampler table: a non-buffer record table published copied output"); + tableType = 0u; + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + }; + { + const Registration registered(records.data(), sizeof(records)); + auto consumer = phi; + consumer.insert(consumer.begin() + 30u, 0x7e0e0221u); + const auto shared = run(consumer); + Require(!Faulted(shared), "pointer sampler table: an S# with another consumer faulted:\n" + shared.log); + for (std::uint32_t group = 0; group < 4u; ++group) { + auto expected = Group(original, group); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) expected[lane * 4u + 3u] = samplers[keys[group] % 2u][1]; + Require(Group(shared, group) == expected, "pointer sampler table: an S# with another consumer read the wrong words"); + } + } + root = PointWrap; + auto imagePhi = std::vector(PointerImageCode.begin(), PointerImageCode.end()); + imagePhi.insert(imagePhi.begin() + 14u, {0xbf068011u, 0xbf840002u, 0xbeac03ffu, 0x000000eau}); + const auto imageDirect = run(PointerImageCode); + const auto imageMerged = run(imagePhi); + Require(!Faulted(imageDirect) && !Faulted(imageMerged) && imageMerged.words == imageDirect.words, "pointer image table: a captured pointer selected through Phi changed the result:\n" + imageMerged.log); + for (std::uint32_t group = 0; group < 4u; ++group) { + const bool other = keys[group] % 2u != 0u; + RequireRed(imageMerged, group, Lanes, [other](std::uint32_t x) { return std::bit_cast(other ? OtherFloatTexels[x] : FloatTexels[x]); }, "pointer image table: the T# read through a Phi-selected pointer"); + } + Keys[0] = 0u; + const auto imageFault = run(imagePhi); + Require(Faulted(imageFault) && imageFault.log.find("outside the snapshot") != std::string::npos && untouched(imageFault), "pointer image table: an uncaptured pointer did not fault:\n" + imageFault.log); + std::puts("pointer sampler table: S# and T# through a record pointer whose low word passes a Phi checked on wave64 and wave32"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -1920,6 +2049,10 @@ int main(int argc, char** argv) { RunNestedSamplerTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-sampler") == 0) { + RunPointerSamplerTests(*device); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/ImageTableAbi.hpp b/core/shader/recompiler/ImageTableAbi.hpp index b9c9773b5f4b..27d982e2e04f 100644 --- a/core/shader/recompiler/ImageTableAbi.hpp +++ b/core/shader/recompiler/ImageTableAbi.hpp @@ -7,7 +7,7 @@ namespace ShaderRecompiler::ImageTableAbi { -inline constexpr std::uint32_t Version = 3; +inline constexpr std::uint32_t Version = 4; inline constexpr std::uint32_t HeaderVersion = 0; inline constexpr std::uint32_t HeaderElementBase = 1; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index e5f53a91bce5..343ee03aece4 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -55,9 +55,10 @@ struct TableColumn { std::uint32_t foldedCarry = 0; bool selected = false; std::uint32_t selection = 0; + bool pointerKeyed = false; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && buffer == other.buffer && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && pointerHops == other.pointerHops && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && buffer == other.buffer && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && pointerHops == other.pointerHops && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection && pointerKeyed == other.pointerKeyed; } }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 48a3c24aa89c..acaa1ed2aa95 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -110,6 +110,7 @@ struct ImageTableColumnSnapshot { std::uint32_t fault = 0; bool outside = false; bool nested = false; + bool pointerKeyed = false; SharedValues pointers; SharedValues codes; SharedValues words; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 926ca7afcae0..69b44cb401c5 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -960,7 +960,7 @@ class TableSnapshotter { const auto end = records.base + records.size; if (column.indirect) { unread(); - if (column.buffer) records.pointers.assign(records.keys, ImageTableAbi::UncapturedPointer); + if (column.buffer || column.pointerKeyed) records.pointers.assign(records.keys, ImageTableAbi::UncapturedPointer); for (std::uint32_t record = 0; record < records.keys; record++) { if (records.states[record] != RecordState::Read) continue; const auto offset = column.addend + record * column.stride; @@ -971,6 +971,7 @@ class TableSnapshotter { continue; } std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + if (column.pointerKeyed) records.pointers[record] = pointer; for (const auto& hop : column.pointerHops) { const auto low = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate); const auto high = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate + 4u); @@ -1300,6 +1301,7 @@ class TableSnapshotter { const auto& records = Column(source); auto& out = tables.tables.at(table); out = CopyRecords(records); + out.pointerKeyed = column.pointerKeyed; tableCounters().keys.fetch_add(records.keys, std::memory_order_relaxed); if (TableTraced()) { const auto poison = static_cast(std::count_if(out.codes.begin(), out.codes.end(), [](auto code) { return (code & ImageTableAbi::PoisonFlag) != 0u; })); @@ -1681,7 +1683,7 @@ bool sameColumns(const std::vector& left, const std::v for (std::size_t index = 0; index < left.size(); index++) { const auto& a = left[index]; const auto& b = right[index]; - if (a.base != b.base || a.size != b.size || a.records != b.records || a.keys != b.keys || a.fault != b.fault || a.outside != b.outside || a.nested != b.nested || !a.pointers.Shares(b.pointers) || !a.codes.Shares(b.codes) || !a.words.Shares(b.words)) return false; + if (a.base != b.base || a.size != b.size || a.records != b.records || a.keys != b.keys || a.fault != b.fault || a.outside != b.outside || a.nested != b.nested || a.pointerKeyed != b.pointerKeyed || !a.pointers.Shares(b.pointers) || !a.codes.Shares(b.codes) || !a.words.Shares(b.words)) return false; } return true; } @@ -1837,20 +1839,21 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i out.map[header + Abi::TableFault] = poison(resource, PoisonReason::UnsupportedOperation, nullptr); } outside[table] = outside[table] || column.outside; + const bool byPointer = column.nested || column.pointerKeyed; const auto mapStart = static_cast(out.map.size()); - if (!column.nested) { + if (!byPointer) { growMap(column.keys); out.map[header + Abi::TableMapStart] = mapStart; out.map.resize(out.map.size() + column.keys, 0u); } else if (column.pointers.size() != column.keys) { - throw std::runtime_error("nested image table snapshot has no pointer for each record"); + throw std::runtime_error("pointer-keyed image table snapshot has no pointer for each record"); } std::map pointers; if (column.nested) nestedRecords.Clear(); else records.Clear(); for (std::uint32_t key = 0; key < column.keys; key++) { const auto code = column.codes[key]; - if (column.nested && column.pointers[key] == Abi::UncapturedPointer) continue; + if (byPointer && column.pointers[key] == Abi::UncapturedPointer) continue; std::uint32_t mapped = Abi::NullCode; if ((code & Abi::PoisonFlag) != 0u) { const auto reason = static_cast(code & ~Abi::PoisonFlag); @@ -1876,15 +1879,15 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i records.Set(code, mapped); } } - if (column.nested) { + if (byPointer) { const auto [found, inserted] = pointers.try_emplace(column.pointers[key], mapped); - if (!inserted && found->second != mapped) throw std::runtime_error("nested image table captured conflicting values at one pointer"); + if (!inserted && found->second != mapped) throw std::runtime_error("pointer-keyed image table captured conflicting values at one pointer"); } else { out.map[mapStart + key] = mapped; } } - if (column.nested) { - if (pointers.size() > Abi::MaxParentKeys) throw std::runtime_error("nested image table has too many pointers"); + if (byPointer) { + if (pointers.size() > Abi::MaxParentKeys) throw std::runtime_error("pointer-keyed image table has too many pointers"); growMap(pointers.size() * Abi::PointerRecordWords); out.map[header + Abi::TableKeyCount] = static_cast(pointers.size()); out.map[header + Abi::TableMapStart] = static_cast(out.map.size()); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 3b2b476ecfb4..b8869704ed3f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -776,8 +776,8 @@ class Tracker { IrValue* bufferHandle = nullptr; IrValue* offset = nullptr; const bool buffer = handle.Opcode() == IrOpcode::GetBufferResource; - if (buffer && !hops.empty()) return false; - const bool merged = buffer && pointer->Argument(0)->Resolve()->IsPhi(); + const bool merged = pointer->Argument(0)->Resolve()->IsPhi(); + if (!hops.empty() && (buffer || merged)) return false; auto* low = merged ? PointerRecordOrigin(pointer->Argument(0)) : pointer->Argument(0)->Resolve(); if (low == nullptr) return false; std::uint32_t first = 0; @@ -819,6 +819,7 @@ class Tracker { column.buffer = buffer; column.indirectOffset = descriptorOffset; column.indirectImmediate = descriptorImmediate; + column.pointerKeyed = merged && !buffer; column.pointerHops = std::move(hops); const auto domain = merged ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; @@ -835,7 +836,7 @@ class Tracker { shared.keyDomains.push_back(*domain); } plan.offset = offset; - if (buffer) plan.parentPointer = pointer; + if (buffer || merged) plan.parentPointer = pointer; return true; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 777cb6b42fe9..8744be7ddbb6 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1339,6 +1339,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(table.fault); mix(table.outside ? 1u : 0u); mix(table.nested ? 1u : 0u); + mix(table.pointerKeyed ? 1u : 0u); mix(table.pointers.Hash()); mix(table.codes.Hash()); mix(table.words.Hash()); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index c45704d3298c..64e7bf71ffcd 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -108,8 +108,8 @@ static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); 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) == 192, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 112, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 200, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 120, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain 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"); @@ -149,7 +149,7 @@ static_assert(fieldCount() == 24, "IrResourcePlan changed: updat static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 19, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 20, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain 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"); @@ -1093,6 +1093,7 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Value(column.foldedCarry); out.Value(column.selected); out.Value(column.selection); + out.Value(column.pointerKeyed); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1223,7 +1224,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { value.SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } reader.List(plan.memoryInfo, 79, decodeMemoryInfo); - reader.List(plan.descriptorSources, 73, [&](Reader& in, DescriptorSource& source) { + reader.List(plan.descriptorSources, 74, [&](Reader& in, DescriptorSource& source) { for (auto& dword : source.dwords) dword = valueAt(in.Get()); in.Value(source.dwordCount); const bool table = in.Get(); @@ -1255,6 +1256,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(column.foldedCarry); in.Value(column.selected); in.Value(column.selection); + in.Value(column.pointerKeyed); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { @@ -1294,6 +1296,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (column.buffer && (column.sampler || column.dwordCount != 4u)) return false; const auto& parent = plan.descriptorSources[column.heapSource].tableColumn; if (parent.has_value() && (column.buffer || column.address || column.indirect || !parent->buffer || !parent->indirect || plan.descriptorSources[parent->heapSource].tableColumn.has_value())) return false; + if (column.pointerKeyed && (!column.indirect || column.buffer || parent.has_value())) return false; } if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5d721bb3004a..9afbea85638f 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 42; +inline constexpr std::uint32_t FormatVersion = 43; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 1c14a26e5491..ce623d74ed0c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1739,6 +1739,10 @@ TableWord EmitTableWord(SpirvValueEmitContext& ctx, const IrValue& inst, std::ui const auto offset = ctx.Def(handle->Argument(0)); const auto header = ImageTableAbi::TableHeader(table); const auto& parent = sources.at(column.heapSource).tableColumn; + if (!parent.has_value() && column.pointerKeyed) { + if (!column.indirect || handle->ArgumentCount() < 3u) ctx.Fail(inst, "has a pointer-keyed image table without its pointer"); + return LookupTablePointer(state, ctx.Def(handle->Argument(1)), ctx.Def(handle->Argument(2)), header); + } if (!parent.has_value()) return LookupTableWord(state, column, offset, header); if (!parent->buffer || !parent->indirect || handle->ArgumentCount() < 3u) ctx.Fail(inst, "has a nested image table without its buffer pointer"); const auto root = LookupTablePointer(state, ctx.Def(handle->Argument(1)), ctx.Def(handle->Argument(2)), header); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7a078b48a528..92fe7c99147b 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -39,6 +39,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged with constants by a Phi. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured faults when selected. Other origins of the low word remain refused. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From f6fb8b64a954e19b2e8ea129223396e6965f4e24 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:55:49 +0000 Subject: [PATCH 03/34] fix(shader): accept a lane-mask word in a pointer-keyed record pointer 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/execution/ImageTable.cpp | 111 ++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 36 +++++- docs/dev/debt/shader.md | 2 +- 4 files changed, 146 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0904c16df94f..cb7faaa727e1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2179,9 +2179,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_nested_image_table_budget COMMAND agc_driver_image_table_tests --nested-budget) add_test(NAME agc_driver_nested_sampler_table COMMAND agc_driver_image_table_tests --nested-sampler) add_test(NAME agc_driver_pointer_phi_sampler_table COMMAND agc_driver_image_table_tests --pointer-phi-sampler) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_phi_ballot_table COMMAND agc_driver_image_table_tests --pointer-phi-ballot) + add_test(NAME agc_driver_pointer_phi_copies_table COMMAND agc_driver_image_table_tests --pointer-phi-copies) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index f7aeb2a0362a..8bba6d98888a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -319,6 +319,29 @@ alignas(256) constexpr std::array PointerImageCode{ 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerBallotCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xd4c50034u, + 0x00020680u, 0xbf8cc07fu, 0xbf068112u, 0xbf840003u, 0xbeac03ffu, 0x000000eau, 0xbf820009u, 0xbf068212u, + 0xbf840002u, 0xbeac0334u, 0xbf820004u, 0xbf068312u, 0xbf840001u, 0xbeac0390u, 0xbf800000u, 0xbf800000u, + 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe1u, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerCopiesCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xbf068112u, 0xbf840003u, 0xbeac03ffu, 0x000000eau, 0xbf82000eu, 0xbf068212u, 0xbf840007u, 0x3a160613u, + 0x7e2c050bu, 0x8f178416u, 0xf4200b02u, 0x2e000008u, 0xbf8cc07fu, 0xbf820004u, 0xbf068312u, 0xbf840001u, + 0xbeac0390u, 0xbf800000u, 0xbf800000u, 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, + 0x8afe3032u, 0xbf89ffdeu, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -1820,6 +1843,86 @@ void RunPointerSamplerTests(AgcDriver::VulkanDevice& device) { std::puts("pointer sampler table: S# and T# through a record pointer whose low word passes a Phi checked on wave64 and wave32"); } +void RunPointerArmTests(AgcDriver::VulkanDevice& device, bool copies) { + Fill(); + alignas(256) std::array, 2> records{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + for (std::uint32_t record = 0; record < 2u; ++record) std::copy(images[record].begin(), images[record].end(), records[record].begin() + 16u); + for (std::uint32_t record = 0; record < 3u; ++record) { + const auto pointer = reinterpret_cast(records[record % 2u].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + } + const std::span code = copies ? std::span(PointerCopiesCode) : std::span(PointerBallotCode); + const std::string name = copies ? "pointer table with a second record read" : "pointer table with a lane-mask arm"; + const auto run = [&] { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + }; + const auto inputTable = Table; + const auto inputRecords = records; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && records == inputRecords, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + } + const auto sampled = [&keys](std::uint32_t group, std::uint32_t flip) { + return std::function([&keys, group, flip](std::uint32_t x) { + const auto key = keys[group][x < 32u ? 0u : 1u] ^ flip; + return std::bit_cast(key % 2u != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const auto taken = run(); + Require(!Faulted(taken), name + ": the record's pointer faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group, 0u), name + ": the T# read through the record's pointer in a waterfall loop"); + const auto outside = [&](std::uint32_t arm, const std::string& what) { + Keys[2] = arm; + const auto faulted = run(); + Keys[2] = 0u; + Require(Faulted(faulted) && faulted.log.find("outside the snapshot") != std::string::npos, name + ": " + what + " did not fault as outside the snapshot:\n" + faulted.log); + Require(untouched(faulted), name + ": " + what + " published copied output"); + Require(faulted.artifactId == taken.artifactId, name + ": " + what + " recompiled the shader"); + }; + outside(1u, "a constant pointer word"); + outside(3u, "a second constant pointer word"); + if (!copies) { + outside(2u, "a lane-mask pointer word"); + std::puts("pointer table with a lane-mask arm: a T# through a record pointer whose low word passes nested Phis with constants and a Ballot word checked, and the Ballot word faults outside the snapshot"); + return; + } + Keys[2] = 2u; + const auto again = run(); + Require(!Faulted(again) && again.words == taken.words, name + ": a second read of the same record changed the result:\n" + again.log); + Keys[3] = 3u; + const auto flipped = run(); + Keys[2] = 0u; + Keys[3] = 0u; + Require(!Faulted(flipped) && flipped.artifactId == taken.artifactId, name + ": a second read of another record faulted or recompiled:\n" + flipped.log); + RequireRed(flipped, 0u, Lanes, sampled(0u, 3u), name + ": the T# behind the other record's pointer word"); + for (std::uint32_t group = 1; group < 4u; ++group) Require(Group(flipped, group) == Group(taken, group), name + ": a group that kept its record changed its result"); + std::puts("pointer table with a second record read: a T# through a record pointer whose low word passes a second read keyed by another lane checked, by the pointer's value"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -2053,6 +2156,14 @@ int main(int argc, char** argv) { RunPointerSamplerTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-ballot") == 0) { + RunPointerArmTests(*device, false); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-copies") == 0) { + RunPointerArmTests(*device, true); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index b8869704ed3f..9a36b1244c15 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -719,10 +719,25 @@ class Tracker { return true; } - IrValue* PointerRecordOrigin(IrValue* value) const { + static bool LaneMaskWord(const IrValue& value) { + return value.Opcode() == IrOpcode::CompositeExtractU32x4 && value.ArgumentCount() == 2u && value.Argument(0)->Resolve()->Opcode() == IrOpcode::Ballot && value.Argument(1)->Resolve()->HasImmediate(); + } + + bool SameRecordColumn(IrValue& first, IrValue& second) const { + IrValue* firstKey = nullptr; + IrValue* secondKey = nullptr; + std::uint32_t firstStride = 0; + std::uint32_t firstAddend = 0; + std::uint32_t secondStride = 0; + std::uint32_t secondAddend = 0; + return EquivalentValue(m_program.Resources(), first.Argument(0), second.Argument(0)) && MatchAffineOffset(first.Argument(1), firstKey, firstStride, firstAddend) && MatchAffineOffset(second.Argument(1), secondKey, secondStride, secondAddend) && firstStride == secondStride && firstAddend == secondAddend; + } + + IrValue* PointerRecordOrigin(IrValue* value, bool pointerKeyed) const { std::vector pending{value}; std::unordered_set seen; IrValue* origin = nullptr; + std::uint32_t originOffset = 0; while (!pending.empty()) { auto* current = pending.back()->Resolve(); pending.pop_back(); @@ -732,10 +747,13 @@ class Tracker { continue; } if (current->HasImmediate() && current->Type() == IrType::U32) continue; + if (pointerKeyed && LaneMaskWord(*current)) continue; std::uint32_t memory = 0; - if (ScalarReadMemory(*current, memory) == nullptr || !MemoryIndexBelongsTo(memory, *current)) return nullptr; - if (origin != nullptr && !EquivalentValue(m_program.Resources(), origin, current)) return nullptr; + const MemoryInfo* info = ScalarReadMemory(*current, memory); + if (info == nullptr || !MemoryIndexBelongsTo(memory, *current)) return nullptr; + if (origin != nullptr && (pointerKeyed ? info->offset != originOffset || !SameRecordColumn(*origin, *current) : !EquivalentValue(m_program.Resources(), origin, current))) return nullptr; origin = current; + originOffset = info->offset; } return origin; } @@ -777,8 +795,9 @@ class Tracker { IrValue* offset = nullptr; const bool buffer = handle.Opcode() == IrOpcode::GetBufferResource; const bool merged = pointer->Argument(0)->Resolve()->IsPhi(); + const bool pointerKeyed = merged && !buffer; if (!hops.empty() && (buffer || merged)) return false; - auto* low = merged ? PointerRecordOrigin(pointer->Argument(0)) : pointer->Argument(0)->Resolve(); + auto* low = merged ? PointerRecordOrigin(pointer->Argument(0), pointerKeyed) : pointer->Argument(0)->Resolve(); if (low == nullptr) return false; std::uint32_t first = 0; for (std::uint32_t dword = 0; dword < 2u; dword++) { @@ -792,10 +811,15 @@ class Tracker { first = memory->offset; bufferHandle = read->Argument(0)->Resolve(); offset = read->Argument(1)->Resolve(); - } else if (memory->offset != first + static_cast(sizeof(std::uint32_t)) || read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + } else if (memory->offset != first + static_cast(sizeof(std::uint32_t)) || (pointerKeyed ? !SameRecordColumn(*low, *read) : read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1)))) { return false; } } + if (pointerKeyed) { + auto* high = pointer->Argument(1)->Resolve(); + bufferHandle = high->Argument(0)->Resolve(); + offset = high->Argument(1)->Resolve(); + } IrValue* key = nullptr; std::uint32_t stride = 0; std::uint32_t addend = 0; @@ -819,7 +843,7 @@ class Tracker { column.buffer = buffer; column.indirectOffset = descriptorOffset; column.indirectImmediate = descriptorImmediate; - column.pointerKeyed = merged && !buffer; + column.pointerKeyed = pointerKeyed; column.pointerHops = std::move(hops); const auto domain = merged ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 92fe7c99147b..3fdc5f2ca321 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -39,7 +39,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged with constants by a Phi. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured faults when selected. Other origins of the low word remain refused. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with words of a `Ballot` lane mask, and with reads of the same record word under another key. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the low word remain refused. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From 64b16cda88c5095147486bfee94b1468e333db76 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:26:41 +0000 Subject: [PATCH 04/34] fix(shader): take lane-mask ANDs and a high-word Phi in 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 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../tests/execution/ImageTable.cpp | 151 ++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 98 ++++++++++-- 3 files changed, 241 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index cb7faaa727e1..6d06c5059fd8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2181,9 +2181,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_sampler_table COMMAND agc_driver_image_table_tests --pointer-phi-sampler) add_test(NAME agc_driver_pointer_phi_ballot_table COMMAND agc_driver_image_table_tests --pointer-phi-ballot) add_test(NAME agc_driver_pointer_phi_copies_table COMMAND agc_driver_image_table_tests --pointer-phi-copies) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_phi_lane_and_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-and) + add_test(NAME agc_driver_pointer_phi_high_table COMMAND agc_driver_image_table_tests --pointer-phi-high) + add_test(NAME agc_driver_pointer_phi_high_second_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 8bba6d98888a..fda333ef1302 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -342,6 +342,44 @@ alignas(256) constexpr std::array PointerCopiesCode{ 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerLaneAndCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0xf4040300u, 0xfa000040u, 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, + 0x02061411u, 0xbeae047eu, 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, 0x8f198418u, 0xf4240b02u, + 0x32000008u, 0xd4c10034u, 0x00002900u, 0xd4c40036u, 0x00020014u, 0xbf8cc07fu, 0xbf068112u, 0xbf840003u, + 0xbeac03ffu, 0x000000eau, 0xbf820010u, 0xbf068212u, 0xbf840006u, 0xbebc0334u, 0xbf068115u, 0xbf840001u, + 0xbebc0336u, 0x872c343cu, 0xbf820007u, 0xbf068312u, 0xbf840004u, 0xbf068215u, 0xbf840001u, 0xbeac03a0u, + 0x872c342cu, 0xbf800000u, 0xbf800000u, 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, + 0x8afe3032u, 0xbf89ffd8u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerHighCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0xf4040300u, 0xfa000040u, 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, + 0x02061411u, 0xbeae047eu, 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, 0x8f198418u, 0xf4240b02u, + 0x32000008u, 0xd4c10034u, 0x00002900u, 0xd4c40036u, 0x00020014u, 0xbf8cc07fu, 0xbf068112u, 0xbf840002u, + 0xbeac03ffu, 0x000000eau, 0xbf068113u, 0xbf840003u, 0xbead03ffu, 0x00001234u, 0xbf820009u, 0xbf068213u, + 0xbf840006u, 0x3a160615u, 0x7e34050bu, 0x8f1b841au, 0xf4200b42u, 0x3600000cu, 0xbf8cc07fu, 0xbf800000u, + 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffdbu, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerHighSecondCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0xf4040300u, 0xfa000040u, 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, + 0x02061411u, 0xbeae047eu, 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, 0x8f198418u, 0xf4240b02u, + 0x32000008u, 0xd4c10034u, 0x00002900u, 0xd4c40036u, 0x00020014u, 0xbf8cc07fu, 0xbf068112u, 0xbf840002u, + 0xbeac03ffu, 0x000000eau, 0xbf068113u, 0xbf840003u, 0xbead03ffu, 0x00001234u, 0xbf820012u, 0xbf068313u, + 0xbf840007u, 0xf4080e06u, 0xfa000000u, 0xbf8cc07fu, 0xf4200b5cu, 0xfa000000u, 0xbf8cc07fu, 0xbf820009u, + 0xbf068213u, 0xbf840006u, 0x3a160615u, 0x7e34050bu, 0x8f1b841au, 0xf4200b42u, 0x3600000cu, 0xbf8cc07fu, + 0xbf800000u, 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffd2u, + 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -1923,6 +1961,107 @@ void RunPointerArmTests(AgcDriver::VulkanDevice& device, bool copies) { std::puts("pointer table with a second record read: a T# through a record pointer whose low word passes a second read keyed by another lane checked, by the pointer's value"); } +enum class PointerMask { LaneAnd, High, HighSecond }; + +void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { + Fill(); + alignas(256) std::array, 2> records{}; + alignas(256) std::array second{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + for (std::uint32_t record = 0; record < 2u; ++record) std::copy(images[record].begin(), images[record].end(), records[record].begin() + 16u); + for (std::uint32_t record = 0; record < 3u; ++record) { + const auto pointer = reinterpret_cast(records[record % 2u].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + } + const auto secondBuffer = Buffer(second.data() + 8u, 32u); + std::copy(secondBuffer.begin(), secondBuffer.end(), second.begin()); + const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : std::span(PointerHighSecondCode); + const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : "pointer table with a high word from a second buffer"; + const auto run = [&] { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + const auto secondAddress = reinterpret_cast(second.data()); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + Srt[16] = static_cast(secondAddress); + Srt[17] = static_cast(secondAddress >> 32u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + {secondAddress, std::as_bytes(std::span(second))}, + }; + const auto inputTable = Table; + const auto inputRecords = records; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && records == inputRecords, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 8u] = keys[group][0]; + Keys[group * 8u + 1u] = keys[group][1]; + } + if (shape == PointerMask::HighSecond) { + try { + static_cast(run()); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("GetImageResource dword 0 is not a valid runtime value") != std::string::npos, name + ": refused for another reason: " + error.what()); + std::puts("pointer table with a high word from a second buffer: a high-word arm read from a buffer whose V# is loaded through a pointer stays refused"); + return; + } + Require(false, name + ": a high-word arm read from a buffer whose V# is loaded through a pointer was planned"); + } + const auto sampled = [&keys](std::uint32_t group) { + return std::function([&keys, group](std::uint32_t x) { + const auto key = keys[group][x < 32u ? 0u : 1u]; + return std::bit_cast(key % 2u != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const auto taken = run(); + Require(!Faulted(taken), name + ": the record's pointer faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# read through the record's pointer in a waterfall loop"); + const auto outside = [&](std::uint32_t selector, std::uint32_t arm, std::uint32_t variant, const std::string& what) { + Keys[selector] = arm; + Keys[5] = variant; + const auto faulted = run(); + Keys[selector] = 0u; + Keys[5] = 0u; + Require(Faulted(faulted) && faulted.log.find("outside the snapshot") != std::string::npos, name + ": " + what + " did not fault as outside the snapshot:\n" + faulted.log); + Require(untouched(faulted), name + ": " + what + " published copied output"); + Require(faulted.artifactId == taken.artifactId, name + ": " + what + " recompiled the shader"); + }; + outside(2u, 1u, 0u, "a constant low word"); + if (shape == PointerMask::LaneAnd) { + outside(2u, 2u, 0u, "a lane-mask Phi ANDed with a lane mask"); + outside(2u, 2u, 1u, "the other lane-mask word ANDed with a lane mask"); + outside(2u, 3u, 0u, "a record word ANDed with a lane mask"); + std::puts("pointer table with lane-mask ANDs: a T# through a record pointer whose low word passes Phis of lane-mask ANDs checked, and every masked word faults outside the snapshot"); + return; + } + outside(3u, 1u, 0u, "a constant high word"); + Keys[3] = 2u; + const auto again = run(); + Keys[5] = 3u; + const auto flipped = run(); + Keys[3] = 0u; + Keys[5] = 0u; + Require(!Faulted(again) && again.words == taken.words && again.artifactId == taken.artifactId, name + ": a second read of the same record's high word changed the result:\n" + again.log); + Require(!Faulted(flipped) && flipped.words == taken.words && flipped.artifactId == taken.artifactId, name + ": the high word of the neighbouring record changed the result:\n" + flipped.log); + std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -2164,6 +2303,18 @@ int main(int argc, char** argv) { RunPointerArmTests(*device, true); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-lane-and") == 0) { + RunPointerMaskTests(*device, PointerMask::LaneAnd); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-high") == 0) { + RunPointerMaskTests(*device, PointerMask::High); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-high-second") == 0) { + RunPointerMaskTests(*device, PointerMask::HighSecond); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 9a36b1244c15..f5574955234e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -733,10 +733,29 @@ class Tracker { return EquivalentValue(m_program.Resources(), first.Argument(0), second.Argument(0)) && MatchAffineOffset(first.Argument(1), firstKey, firstStride, firstAddend) && MatchAffineOffset(second.Argument(1), secondKey, secondStride, secondAddend) && firstStride == secondStride && firstAddend == secondAddend; } - IrValue* PointerRecordOrigin(IrValue* value, bool pointerKeyed) const { + static bool LaneMaskOperand(const IrValue* value) { + const IrValue* root = value->Resolve(); + std::vector pending{root}; + std::unordered_set seen; + bool word = false; + while (!pending.empty()) { + const IrValue* current = pending.back()->Resolve(); + pending.pop_back(); + if (!seen.insert(current).second) continue; + if (LaneMaskWord(*current)) { + word = true; + continue; + } + if (current != root && current->HasImmediate() && current->Type() == IrType::U32) continue; + if (!current->IsPhi() && (current->Opcode() != IrOpcode::BitwiseAnd32 || current->ArgumentCount() != 2u)) return false; + for (std::size_t index = 0; index < current->ArgumentCount(); ++index) pending.push_back(current->Argument(index)); + } + return word; + } + + bool PointerRecordReads(IrValue* value, bool pointerKeyed, std::vector& reads) const { std::vector pending{value}; std::unordered_set seen; - IrValue* origin = nullptr; std::uint32_t originOffset = 0; while (!pending.empty()) { auto* current = pending.back()->Resolve(); @@ -748,14 +767,49 @@ class Tracker { } if (current->HasImmediate() && current->Type() == IrType::U32) continue; if (pointerKeyed && LaneMaskWord(*current)) continue; + if (pointerKeyed && current->Opcode() == IrOpcode::BitwiseAnd32 && current->ArgumentCount() == 2u) { + const bool left = LaneMaskOperand(current->Argument(0)); + const bool right = LaneMaskOperand(current->Argument(1)); + if (!left && !right) return false; + if (!left) pending.push_back(current->Argument(0)); + if (!right) pending.push_back(current->Argument(1)); + continue; + } std::uint32_t memory = 0; const MemoryInfo* info = ScalarReadMemory(*current, memory); - if (info == nullptr || !MemoryIndexBelongsTo(memory, *current)) return nullptr; - if (origin != nullptr && (pointerKeyed ? info->offset != originOffset || !SameRecordColumn(*origin, *current) : !EquivalentValue(m_program.Resources(), origin, current))) return nullptr; - origin = current; + if (info == nullptr || !MemoryIndexBelongsTo(memory, *current)) return false; + if (!reads.empty() && (pointerKeyed ? info->offset != originOffset || !SameRecordColumn(*reads.back(), *current) : !EquivalentValue(m_program.Resources(), reads.back(), current))) return false; + reads.push_back(current); originOffset = info->offset; } - return origin; + return !reads.empty(); + } + + bool Precedes(const IrValue* value, const IrValue& use) const { + value = value->Resolve(); + const IrBlock* definition = value->Parent(); + const IrBlock* target = use.Parent(); + if (definition == nullptr) return true; + if (target == nullptr) return false; + if (definition == target) { + for (const IrValue* inst : target->Instructions()) { + if (inst == value) return true; + if (inst == &use) return false; + } + return false; + } + std::vector pending{m_program.BlockOrder().front()}; + std::unordered_set seen{pending.front()}; + while (!pending.empty()) { + const IrBlock* block = pending.back(); + pending.pop_back(); + if (block == definition) continue; + if (block == target) return false; + for (const IrBlock* successor : block->Successors()) { + if (seen.insert(successor).second) pending.push_back(successor); + } + } + return true; } bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { @@ -797,11 +851,15 @@ class Tracker { const bool merged = pointer->Argument(0)->Resolve()->IsPhi(); const bool pointerKeyed = merged && !buffer; if (!hops.empty() && (buffer || merged)) return false; - auto* low = merged ? PointerRecordOrigin(pointer->Argument(0), pointerKeyed) : pointer->Argument(0)->Resolve(); - if (low == nullptr) return false; + std::vector lowReads; + if (merged && !PointerRecordReads(pointer->Argument(0), pointerKeyed, lowReads)) return false; + auto* low = merged ? lowReads.back() : pointer->Argument(0)->Resolve(); + auto* high = pointer->Argument(1)->Resolve(); + std::vector highReads; + if (pointerKeyed && !PointerRecordReads(high, true, highReads)) return false; std::uint32_t first = 0; for (std::uint32_t dword = 0; dword < 2u; dword++) { - IrValue* read = dword == 0u ? low : pointer->Argument(dword)->Resolve(); + IrValue* read = dword == 0u ? low : pointerKeyed ? highReads.back() : high; std::uint32_t memoryIndex = 0; const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { @@ -815,15 +873,29 @@ class Tracker { return false; } } + IrValue* recordOffset = offset; if (pointerKeyed) { - auto* high = pointer->Argument(1)->Resolve(); - bufferHandle = high->Argument(0)->Resolve(); - offset = high->Argument(1)->Resolve(); + IrValue* source = high; + if (highReads.size() != 1u || highReads.front() != high) { + lowReads.insert(lowReads.begin(), highReads.begin(), highReads.end()); + const auto found = std::ranges::find_if(lowReads, [&](const IrValue* read) { + const IrValue* table = read->Argument(0)->Resolve(); + for (std::size_t index = 0; index < table->ArgumentCount(); ++index) { + if (!Precedes(table->Argument(index), handle)) return false; + } + return true; + }); + if (found == lowReads.end()) return false; + source = *found; + } + bufferHandle = source->Argument(0)->Resolve(); + recordOffset = source->Argument(1)->Resolve(); + offset = source == high ? recordOffset : high; } IrValue* key = nullptr; std::uint32_t stride = 0; std::uint32_t addend = 0; - if (!MatchAffineOffset(offset, key, stride, addend)) { + if (!MatchAffineOffset(recordOffset, key, stride, addend)) { return false; } DescriptorSource heapSource; From 2788b8f98633b86225e13fa78a3021e7730acd08 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 05:40:22 +0000 Subject: [PATCH 05/34] fix(shader): skip lane-mask arms built from a waterfall's lane bit in 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/execution/ImageTable.cpp | 122 +++++++++++++++++- .../Optimization/src/ResourceTracker.cpp | 50 ++++++- 3 files changed, 165 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6d06c5059fd8..7c969b72d9ea 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2184,9 +2184,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_lane_and_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-and) add_test(NAME agc_driver_pointer_phi_high_table COMMAND agc_driver_image_table_tests --pointer-phi-high) add_test(NAME agc_driver_pointer_phi_high_second_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) + add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index fda333ef1302..f65082de4897 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -380,6 +380,28 @@ alignas(256) constexpr std::array PointerHighSecondCode{ 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerLaneNotCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, 0x8f198418u, 0xf4240b02u, 0x32000000u, 0xd4c10034u, + 0x00002900u, 0xbf8cc07fu, 0xbf068112u, 0xbf840002u, 0xbeac0380u, 0xbf82000au, 0xbf068212u, 0xbf840008u, + 0xbebc1334u, 0x8f3d3c81u, 0xbf068115u, 0xbf840002u, 0x8a2c3d34u, 0xbf820002u, 0xbebd073du, 0x872c3d34u, + 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe1u, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array PointerVsharpSelectCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0xd4c40038u, 0x000200c0u, 0x87ea386au, + 0x7e120215u, 0x4a141210u, 0x7e080211u, 0xbf068116u, 0xbf840001u, 0x7e080217u, 0x02061504u, 0xbeae047eu, + 0xd4c5003eu, 0x000200c0u, 0x87bc3e38u, 0xbeba243cu, 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, + 0x8f198418u, 0xf4240b02u, 0x32000008u, 0xbf8cc07fu, 0xf4080d16u, 0xfa0000a0u, 0x8f1a8513u, 0xbf8cc07fu, + 0xf42c091au, 0x34000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffedu, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -1961,7 +1983,7 @@ void RunPointerArmTests(AgcDriver::VulkanDevice& device, bool copies) { std::puts("pointer table with a second record read: a T# through a record pointer whose low word passes a second read keyed by another lane checked, by the pointer's value"); } -enum class PointerMask { LaneAnd, High, HighSecond }; +enum class PointerMask { LaneAnd, High, HighSecond, LaneNot }; void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { Fill(); @@ -1971,15 +1993,16 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { Keys = {}; const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; for (std::uint32_t record = 0; record < 2u; ++record) std::copy(images[record].begin(), images[record].end(), records[record].begin() + 16u); + const std::uint32_t pointerWord = shape == PointerMask::LaneNot ? 0u : 2u; for (std::uint32_t record = 0; record < 3u; ++record) { const auto pointer = reinterpret_cast(records[record % 2u].data()); - Table[record * 4u + 2u] = static_cast(pointer); - Table[record * 4u + 3u] = static_cast(pointer >> 32u); + Table[record * 4u + pointerWord] = static_cast(pointer); + Table[record * 4u + pointerWord + 1u] = static_cast(pointer >> 32u); } const auto secondBuffer = Buffer(second.data() + 8u, 32u); std::copy(secondBuffer.begin(), secondBuffer.end(), second.begin()); - const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : std::span(PointerHighSecondCode); - const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : "pointer table with a high word from a second buffer"; + const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : shape == PointerMask::LaneNot ? std::span(PointerLaneNotCode) : std::span(PointerHighSecondCode); + const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : shape == PointerMask::LaneNot ? "pointer table with a lane bit cleared from a lane mask" : "pointer table with a high word from a second buffer"; const auto run = [&] { const auto table = Buffer(Table.data(), 64u); const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); @@ -2050,6 +2073,12 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { std::puts("pointer table with lane-mask ANDs: a T# through a record pointer whose low word passes Phis of lane-mask ANDs checked, and every masked word faults outside the snapshot"); return; } + if (shape == PointerMask::LaneNot) { + outside(2u, 2u, 0u, "a lane mask whose handled lane's bit s_not and s_and cleared"); + outside(2u, 2u, 1u, "a lane mask whose handled lane's bit s_andn2 cleared"); + std::puts("pointer table with a lane bit cleared from a lane mask: a T# through a record pointer whose low word passes Phis of a zero and of a lane mask without the handled lane's bit checked, and every masked word faults outside the snapshot"); + return; + } outside(3u, 1u, 0u, "a constant high word"); Keys[3] = 2u; const auto again = run(); @@ -2062,6 +2091,81 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value"); } +void RunPointerVsharpTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 2> records{}; + alignas(256) std::array, 2> palettes{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float), Image(UintTexels.data(), Format32UInt)}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return std::bit_cast(OtherFloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + }; + for (std::uint32_t outer = 0; outer < 2u; ++outer) { + auto descriptor = Buffer(palettes[outer].data(), 1u); + descriptor[1] |= 64u << 16u; + std::copy(descriptor.begin(), descriptor.end(), records[outer].begin() + 40u); + for (std::uint32_t inner = 0; inner < 2u; ++inner) std::copy(images[outer + inner].begin(), images[outer + inner].end(), palettes[outer].begin() + inner * 8u); + } + const auto link = [&](std::uint32_t record, std::uint32_t target) { + const auto pointer = reinterpret_cast(records[target].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + }; + for (std::uint32_t record = 0; record < 3u; ++record) link(record, record % 2u); + const auto run = [&] { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::span code(PointerVsharpSelectCode); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + {reinterpret_cast(palettes.data()), std::as_bytes(std::span(palettes))}, + }; + const auto inputTable = Table; + const auto inputRecords = records; + const auto inputPalettes = palettes; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && records == inputRecords && palettes == inputPalettes, "pointer V# table: dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), "pointer V# table: dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u, 0u}, {2u, 1u, 1u}, {1u, 1u, 1u}, {2u, 2u, 0u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 8u] = keys[group][0]; + Keys[group * 8u + 1u] = keys[group][1]; + Keys[group * 8u + 3u] = keys[group][2]; + } + std::array targets{0u, 1u, 0u}; + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + const auto record = keys[group][x < 32u ? 0u : 1u]; + return texels[targets[record] + keys[group][2]](x); + }); + }; + const auto taken = run(); + Require(!Faulted(taken), "pointer V# table: the T# read through the V# behind the record's pointer faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), "pointer V# table: the T# read through the V# behind the record's pointer in a waterfall loop"); + link(1u, 0u); + targets[1] = 0u; + const auto moved = run(); + Require(!Faulted(moved), "pointer V# table: the T# behind a moved record pointer faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, "pointer V# table: moving a record's pointer recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), "pointer V# table: the T# after record 1's pointer moved to the other V#"); + std::puts("pointer V# table: a T# read through a V# loaded through a record pointer keyed by a first-lane read of a select checked, by the records' values"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -2315,6 +2419,14 @@ int main(int argc, char** argv) { RunPointerMaskTests(*device, PointerMask::HighSecond); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-lane-not") == 0) { + RunPointerMaskTests(*device, PointerMask::LaneNot); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-select") == 0) { + RunPointerVsharpTests(*device); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index f5574955234e..3cc8f3ef163f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -733,24 +733,62 @@ class Tracker { return EquivalentValue(m_program.Resources(), first.Argument(0), second.Argument(0)) && MatchAffineOffset(first.Argument(1), firstKey, firstStride, firstAddend) && MatchAffineOffset(second.Argument(1), secondKey, secondStride, secondAddend) && firstStride == secondStride && firstAddend == secondAddend; } + static bool LaneIndex(const IrValue* value) { + std::vector pending{value}; + std::unordered_set seen; + while (!pending.empty()) { + const IrValue* current = pending.back()->Resolve(); + pending.pop_back(); + if (!seen.insert(current).second || (current->HasImmediate() && current->Type() == IrType::U32)) continue; + switch (current->Opcode()) { + case IrOpcode::FindILsb32: + case IrOpcode::FindUMsb32: + case IrOpcode::ReadFirstLane: continue; + case IrOpcode::BitwiseAnd32: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + if (current->ArgumentCount() != 2u) return false; + pending.push_back(current->Argument(0)); + pending.push_back(current->Argument(1)); + continue; + case IrOpcode::SelectU32: + if (current->ArgumentCount() != 3u) return false; + pending.push_back(current->Argument(1)); + pending.push_back(current->Argument(2)); + continue; + default: return false; + } + } + return true; + } + + static bool LaneBit(const IrValue& value) { + if (value.Opcode() != IrOpcode::ShiftLeftLogical32 || value.ArgumentCount() != 2u) return false; + const IrValue* one = value.Argument(0)->Resolve(); + return one->HasImmediate() && one->Type() == IrType::U32 && one->ImmediateU32() == 1u && LaneIndex(value.Argument(1)); + } + static bool LaneMaskOperand(const IrValue* value) { const IrValue* root = value->Resolve(); std::vector pending{root}; std::unordered_set seen; - bool word = false; + bool lane = false; while (!pending.empty()) { const IrValue* current = pending.back()->Resolve(); pending.pop_back(); if (!seen.insert(current).second) continue; - if (LaneMaskWord(*current)) { - word = true; + if (LaneMaskWord(*current) || LaneBit(*current)) { + lane = true; continue; } if (current != root && current->HasImmediate() && current->Type() == IrType::U32) continue; - if (!current->IsPhi() && (current->Opcode() != IrOpcode::BitwiseAnd32 || current->ArgumentCount() != 2u)) return false; + const auto opcode = current->Opcode(); + const bool combines = (opcode == IrOpcode::BitwiseAnd32 || opcode == IrOpcode::BitwiseOr32) && current->ArgumentCount() == 2u; + const bool inverts = opcode == IrOpcode::BitwiseNot32 && current->ArgumentCount() == 1u; + if (!current->IsPhi() && !combines && !inverts) return false; for (std::size_t index = 0; index < current->ArgumentCount(); ++index) pending.push_back(current->Argument(index)); } - return word; + return lane; } bool PointerRecordReads(IrValue* value, bool pointerKeyed, std::vector& reads) const { @@ -766,7 +804,7 @@ class Tracker { continue; } if (current->HasImmediate() && current->Type() == IrType::U32) continue; - if (pointerKeyed && LaneMaskWord(*current)) continue; + if (pointerKeyed && LaneMaskOperand(current)) continue; if (pointerKeyed && current->Opcode() == IrOpcode::BitwiseAnd32 && current->ArgumentCount() == 2u) { const bool left = LaneMaskOperand(current->Argument(0)); const bool right = LaneMaskOperand(current->Argument(1)); From f877d5e5dc2449ab35193648f259a0ca9627ef6b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:29:41 -0700 Subject: [PATCH 06/34] docs(shader): list lane-mask arms and a merged high word for pointer-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. --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 3fdc5f2ca321..1659ff6d543f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -39,7 +39,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with words of a `Ballot` lane mask, and with reads of the same record word under another key. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the low word remain refused. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with lane masks (words of a `Ballot`, the waterfall loop's lane bit, and ANDs, ORs and NOTs of them), with the record word ANDed with a lane mask, and with reads of the same record word under another key, and its high word merged the same way from the record's next word. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the two words remain refused, among them a high word read from a second buffer. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From 92cede35a05257369a32a58c95846d19bd74e308 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:59:38 -0700 Subject: [PATCH 07/34] fix(shader): pair a pointer-keyed record pointer's two words by Phi edge 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../tests/execution/ImageTable.cpp | 29 ++++++- .../Optimization/src/ResourceTracker.cpp | 75 +++++++++++++++---- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0f019694926e..aa2a552668c6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2368,11 +2368,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_lane_and_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-and) add_test(NAME agc_driver_pointer_phi_high_table COMMAND agc_driver_image_table_tests --pointer-phi-high) add_test(NAME agc_driver_pointer_phi_high_second_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second) + add_test(NAME agc_driver_pointer_phi_high_second_lane_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second-lane) add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index f65082de4897..f1b32368e681 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -380,6 +380,20 @@ alignas(256) constexpr std::array PointerHighSecondCode{ 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerHighSecondLaneCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0xf4040300u, 0xfa000040u, 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, + 0x02061411u, 0xbeae047eu, 0x7e300503u, 0xd4c20030u, 0x00020618u, 0xbeb22430u, 0x8f198418u, 0xf4240b02u, + 0x32000008u, 0xd4c10034u, 0x00002900u, 0xd4c40036u, 0x00020014u, 0xbf8cc07fu, 0xbf068112u, 0xbf840002u, + 0xbeac03ffu, 0x000000eau, 0xbf068113u, 0xbf840003u, 0xbead03ffu, 0x00001234u, 0xbf820013u, 0xbf068313u, + 0xbf840008u, 0xf4080e06u, 0xfa000000u, 0xbf8cc07fu, 0xf4200b5cu, 0xfa000000u, 0xbf8cc07fu, 0xbeac037eu, + 0xbf820009u, 0xbf068213u, 0xbf840006u, 0x3a160615u, 0x7e34050bu, 0x8f1b841au, 0xf4200b42u, 0x3600000cu, + 0xbf8cc07fu, 0xbf800000u, 0xf40c0916u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, + 0xbf89ffd1u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, + 0xbf810000u, +}; + alignas(256) constexpr std::array PointerLaneNotCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108502u, 0xf42c0404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, @@ -1983,7 +1997,7 @@ void RunPointerArmTests(AgcDriver::VulkanDevice& device, bool copies) { std::puts("pointer table with a second record read: a T# through a record pointer whose low word passes a second read keyed by another lane checked, by the pointer's value"); } -enum class PointerMask { LaneAnd, High, HighSecond, LaneNot }; +enum class PointerMask { LaneAnd, High, HighSecond, HighSecondLane, LaneNot }; void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { Fill(); @@ -2001,8 +2015,8 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { } const auto secondBuffer = Buffer(second.data() + 8u, 32u); std::copy(secondBuffer.begin(), secondBuffer.end(), second.begin()); - const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : shape == PointerMask::LaneNot ? std::span(PointerLaneNotCode) : std::span(PointerHighSecondCode); - const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : shape == PointerMask::LaneNot ? "pointer table with a lane bit cleared from a lane mask" : "pointer table with a high word from a second buffer"; + const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : shape == PointerMask::LaneNot ? std::span(PointerLaneNotCode) : shape == PointerMask::HighSecondLane ? std::span(PointerHighSecondLaneCode) : std::span(PointerHighSecondCode); + const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : shape == PointerMask::LaneNot ? "pointer table with a lane bit cleared from a lane mask" : shape == PointerMask::HighSecondLane ? "pointer table with a second-buffer high word under a lane-mask low word" : "pointer table with a high word from a second buffer"; const auto run = [&] { const auto table = Buffer(Table.data(), 64u); const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); @@ -2080,6 +2094,7 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { return; } outside(3u, 1u, 0u, "a constant high word"); + if (shape == PointerMask::HighSecondLane) outside(3u, 3u, 0u, "a lane-mask low word with a high word from the second buffer"); Keys[3] = 2u; const auto again = run(); Keys[5] = 3u; @@ -2088,6 +2103,10 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { Keys[5] = 0u; Require(!Faulted(again) && again.words == taken.words && again.artifactId == taken.artifactId, name + ": a second read of the same record's high word changed the result:\n" + again.log); Require(!Faulted(flipped) && flipped.words == taken.words && flipped.artifactId == taken.artifactId, name + ": the high word of the neighbouring record changed the result:\n" + flipped.log); + if (shape == PointerMask::HighSecondLane) { + std::puts("pointer table with a second-buffer high word under a lane-mask low word: a T# through a record pointer whose high word reads a second buffer only on the edge whose low word is a lane mask checked, and that pair faults outside the snapshot"); + return; + } std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value"); } @@ -2419,6 +2438,10 @@ int main(int argc, char** argv) { RunPointerMaskTests(*device, PointerMask::HighSecond); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-high-second-lane") == 0) { + RunPointerMaskTests(*device, PointerMask::HighSecondLane); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-lane-not") == 0) { RunPointerMaskTests(*device, PointerMask::LaneNot); return 0; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index bdc3dd299b01..03cbace5bdbd 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -804,10 +805,9 @@ class Tracker { return lane; } - bool PointerRecordReads(IrValue* value, bool pointerKeyed, std::vector& reads) const { + bool PointerRecordReads(IrValue* value, std::vector& reads) const { std::vector pending{value}; std::unordered_set seen; - std::uint32_t originOffset = 0; while (!pending.empty()) { auto* current = pending.back()->Resolve(); pending.pop_back(); @@ -817,25 +817,69 @@ class Tracker { continue; } if (current->HasImmediate() && current->Type() == IrType::U32) continue; - if (pointerKeyed && LaneMaskOperand(current)) continue; - if (pointerKeyed && current->Opcode() == IrOpcode::BitwiseAnd32 && current->ArgumentCount() == 2u) { - const bool left = LaneMaskOperand(current->Argument(0)); - const bool right = LaneMaskOperand(current->Argument(1)); - if (!left && !right) return false; - if (!left) pending.push_back(current->Argument(0)); - if (!right) pending.push_back(current->Argument(1)); - continue; - } std::uint32_t memory = 0; const MemoryInfo* info = ScalarReadMemory(*current, memory); if (info == nullptr || !MemoryIndexBelongsTo(memory, *current)) return false; - if (!reads.empty() && (pointerKeyed ? info->offset != originOffset || !SameRecordColumn(*reads.back(), *current) : !EquivalentValue(m_program.Resources(), reads.back(), current))) return false; + if (!reads.empty() && !EquivalentValue(m_program.Resources(), reads.back(), current)) return false; reads.push_back(current); - originOffset = info->offset; } return !reads.empty(); } + static bool PointerFreeWord(const IrValue& word) { + return (word.HasImmediate() && word.Type() == IrType::U32) || LaneMaskOperand(&word); + } + + static IrValue* MaskedRecordWord(const IrValue& word) { + if (word.Opcode() != IrOpcode::BitwiseAnd32 || word.ArgumentCount() != 2u) return nullptr; + const bool left = LaneMaskOperand(word.Argument(0)); + if (left == LaneMaskOperand(word.Argument(1))) return nullptr; + return word.Argument(left ? 1u : 0u); + } + + bool PointerRecordRead(IrValue& read, std::vector& reads) const { + std::uint32_t memory = 0; + const MemoryInfo* info = ScalarReadMemory(read, memory); + if (info == nullptr || !MemoryIndexBelongsTo(memory, read)) return false; + if (std::ranges::find(reads, &read) != reads.end()) return true; + if (!reads.empty() && (ScalarReadMemory(*reads.back(), memory)->offset != info->offset || !SameRecordColumn(*reads.back(), read))) return false; + reads.push_back(&read); + return true; + } + + bool PointerRecordPairs(IrValue* low, IrValue* high, std::vector& lowReads, std::vector& highReads) const { + std::vector> pending{{low, high}}; + std::set> seen; + while (!pending.empty()) { + auto* lowWord = pending.back().first->Resolve(); + auto* highWord = pending.back().second->Resolve(); + pending.pop_back(); + if (!seen.emplace(lowWord, highWord).second || PointerFreeWord(*lowWord)) continue; + if (lowWord->IsPhi()) { + const bool paired = highWord->IsPhi() && highWord->Parent() == lowWord->Parent(); + for (std::size_t index = 0; index < lowWord->ArgumentCount(); ++index) { + IrValue* edge = paired ? PhiIncoming(*highWord, lowWord->PhiBlock(index)) : highWord; + if (edge == nullptr) return false; + pending.emplace_back(lowWord->Argument(index), edge); + } + continue; + } + if (IrValue* word = MaskedRecordWord(*lowWord)) { + pending.emplace_back(word, highWord); + continue; + } + if (!PointerRecordRead(*lowWord, lowReads)) return false; + if (highWord->IsPhi()) { + for (std::size_t index = 0; index < highWord->ArgumentCount(); ++index) pending.emplace_back(lowWord, highWord->Argument(index)); + } else if (IrValue* word = MaskedRecordWord(*highWord)) { + pending.emplace_back(lowWord, word); + } else if (!PointerFreeWord(*highWord) && !PointerRecordRead(*highWord, highReads)) { + return false; + } + } + return !lowReads.empty() && !highReads.empty(); + } + bool Precedes(const IrValue* value, const IrValue& use) const { value = value->Resolve(); const IrBlock* definition = value->Parent(); @@ -903,11 +947,10 @@ class Tracker { const bool pointerKeyed = merged && !buffer; if (!hops.empty() && (buffer || merged)) return false; std::vector lowReads; - if (merged && !PointerRecordReads(pointer->Argument(0), pointerKeyed, lowReads)) return false; + std::vector highReads; + if (pointerKeyed ? !PointerRecordPairs(pointer->Argument(0), pointer->Argument(1), lowReads, highReads) : merged && !PointerRecordReads(pointer->Argument(0), lowReads)) return false; auto* low = merged ? lowReads.back() : pointer->Argument(0)->Resolve(); auto* high = pointer->Argument(1)->Resolve(); - std::vector highReads; - if (pointerKeyed && !PointerRecordReads(high, true, highReads)) return false; std::uint32_t first = 0; for (std::uint32_t dword = 0; dword < 2u; dword++) { IrValue* read = dword == 0u ? low : pointerKeyed ? highReads.back() : high; From a7d1e62b534c203eaf0b3e5969545e52e52861c0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:59:38 -0700 Subject: [PATCH 08/34] docs(shader): note the edge-paired walk of a pointer-keyed record pointer 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. --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 89e6412439b4..5c25530a6686 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -41,7 +41,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with lane masks (words of a `Ballot`, the waterfall loop's lane bit, and ANDs, ORs and NOTs of them), with the record word ANDed with a lane mask, and with reads of the same record word under another key, and its high word merged the same way from the record's next word. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the two words remain refused, among them a high word read from a second buffer. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with lane masks (words of a `Ballot`, the waterfall loop's lane bit, and ANDs, ORs and NOTs of them), with the record word ANDed with a lane mask, and with reads of the same record word under another key, and its high word merged the same way from the record's next word. The two words are walked together, edge by edge, and an edge whose low word is a constant or a lane mask is skipped along with its high word. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the two words remain refused, among them a high word read from a second buffer on an edge whose low word is the record word. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From 6681187712a3d31b833358f6f006bc1eab34e8f6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:36:16 +0000 Subject: [PATCH 09/34] fix(shader): follow pointer hops after a pointer-keyed record pointer 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 #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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../tests/execution/ImageTable.cpp | 97 +++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 2 +- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 07e4d72eb89f..5a6df642dad0 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2450,9 +2450,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_high_second_lane_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second-lane) add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_hop_table COMMAND agc_driver_image_table_tests --pointer-hop) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index f1b32368e681..e35cd06654e8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -415,6 +415,16 @@ alignas(256) constexpr std::array PointerVsharpSelectCode{ 0xf42c091au, 0x34000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffedu, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerHopCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xd4c50034u, + 0x00020680u, 0xbf8cc07fu, 0xbf068112u, 0xbf840003u, 0xbeac03ffu, 0x000000eau, 0xbf820003u, 0xbf068212u, + 0xbf840001u, 0xbeac0334u, 0xf4040e16u, 0xfa000000u, 0xbf8cc07fu, 0xf40c091cu, 0xfa000340u, 0xbf8cc07fu, + 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe4u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, + 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; @@ -2185,6 +2195,89 @@ void RunPointerVsharpTests(AgcDriver::VulkanDevice& device) { std::puts("pointer V# table: a T# read through a V# loaded through a record pointer keyed by a first-lane read of a select checked, by the records' values"); } +void RunPointerHopTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 2> links{}; + alignas(256) std::array, 2> targets{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + const auto link = [&](std::uint32_t from, std::uint32_t to) { + const auto pointer = reinterpret_cast(targets[to].data()); + links[from][0] = static_cast(pointer); + links[from][1] = static_cast(pointer >> 32u); + }; + for (std::uint32_t target = 0; target < 2u; ++target) { + std::copy(images[target].begin(), images[target].end(), targets[target].begin() + 0x340u / 4u); + link(target, target); + } + for (std::uint32_t record = 0; record < 3u; ++record) { + const auto pointer = reinterpret_cast(links[record % 2u].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + } + const std::string name = "pointer table with a pointer hop"; + const auto run = [&] { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::span code(PointerHopCode); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(links.data()), std::as_bytes(std::span(links))}, + {reinterpret_cast(targets.data()), std::as_bytes(std::span(targets))}, + }; + const auto inputTable = Table; + const auto inputLinks = links; + const auto inputTargets = targets; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && links == inputLinks && targets == inputTargets, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + } + std::array targetOf{0u, 1u}; + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + const auto record = keys[group][x < 32u ? 0u : 1u]; + return std::bit_cast(targetOf[record % 2u] != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const auto taken = run(); + Require(!Faulted(taken), name + ": the T# behind the record pointer's hop faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# read through the pointer loaded from the record's pointer in a waterfall loop"); + const auto outside = [&](std::uint32_t arm, const std::string& what) { + Keys[2] = arm; + const auto faulted = run(); + Keys[2] = 0u; + Require(Faulted(faulted) && faulted.log.find("outside the snapshot") != std::string::npos, name + ": " + what + " did not fault as outside the snapshot:\n" + faulted.log); + Require(untouched(faulted), name + ": " + what + " published copied output"); + Require(faulted.artifactId == taken.artifactId, name + ": " + what + " recompiled the shader"); + }; + outside(1u, "a constant pointer word"); + outside(2u, "a lane-mask pointer word"); + link(1u, 0u); + targetOf[1] = 0u; + const auto moved = run(); + Require(!Faulted(moved), name + ": the T# behind a moved hop faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, name + ": moving the pointer behind a record's pointer recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), name + ": the T# after the pointer behind record 1's pointer moved to the other T#"); + std::puts("pointer table with a pointer hop: a T# read through a pointer loaded from a record pointer whose low word passes Phis with a constant and a Ballot word checked, by the records' values"); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -2450,6 +2543,10 @@ int main(int argc, char** argv) { RunPointerVsharpTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-hop") == 0) { + RunPointerHopTests(*device); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 19cbd9b97fde..dcc56de21c11 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -988,7 +988,7 @@ class Tracker { const bool buffer = handle.Opcode() == IrOpcode::GetBufferResource; const bool merged = pointer->Argument(0)->Resolve()->IsPhi(); const bool pointerKeyed = merged && !buffer; - if (!hops.empty() && (buffer || merged)) return false; + if (!hops.empty() && buffer) return false; std::vector lowReads; std::vector highReads; if (pointerKeyed ? !PointerRecordPairs(pointer->Argument(0), pointer->Argument(1), lowReads, highReads) : merged && !PointerRecordReads(pointer->Argument(0), lowReads)) return false; From 0843cca39d4fdc5ac5855d3497c0befb71fcd761 Mon Sep 17 00:00:00 2001 From: hugodearaujo <175634979+hugodearaujo@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:37:28 +0000 Subject: [PATCH 10/34] fix(shader): read a T# at a fixed offset of a pointer-loaded V# as a one-key child From hugodearaujo's change on #8939 (Marvel's Spider-Man 2, PPSA03016). --- .../recompiler/Optimization/src/ResourceTracker.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index dcc56de21c11..fba2fc696798 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -703,7 +703,13 @@ class Tracker { if (address && immediateU32(offset, descriptorOffset)) { return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); } - if (!MatchAffineOffset(offset, key, stride, addend)) { + std::uint32_t fixedOffset = 0; + const bool fixed = !address && !buffer && immediateU32(offset, fixedOffset); + if (fixed) { + key = &m_builder.Constant(0u); + stride = width * static_cast(sizeof(std::uint32_t)); + addend = fixedOffset; + } else if (!MatchAffineOffset(offset, key, stride, addend)) { if (!MatchMaskedOffset(offset, key, stride)) { return false; } @@ -711,7 +717,9 @@ class Tracker { } DescriptorSource heapSource; std::uint32_t heapIndex = 0; - if (!MakeRuntimeSource(*heapHandle, address ? IrOpcode::GetAddressResource : IrOpcode::GetBufferResource, address ? 2u : 4u, heapIndex, heapSource)) { + if (MakeRuntimeSource(*heapHandle, address ? IrOpcode::GetAddressResource : IrOpcode::GetBufferResource, address ? 2u : 4u, heapIndex, heapSource)) { + if (fixed) return false; + } else { if (address || buffer || heapHandle->Opcode() != IrOpcode::GetBufferResource || !std::ranges::all_of(heapHandle->Uses(), [&](const IrValue* use) { return std::ranges::find(plan.reads, use) != plan.reads.end(); })) return false; const auto sameHandle = [&](const IrValue* user) { if (user->Opcode() != handle.Opcode() || user->ArgumentCount() != handle.ArgumentCount()) return false; From c00cb0f875201a771403eddfd864ad2e5ac21b07 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:39:16 +0000 Subject: [PATCH 11/34] test(shader): read a T# at a fixed offset of a pointer-loaded V# 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. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/execution/ImageTable.cpp | 104 ++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5a6df642dad0..e72d2cd94a9e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2451,9 +2451,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) add_test(NAME agc_driver_pointer_hop_table COMMAND agc_driver_image_table_tests --pointer-hop) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pointer_vsharp_fixed_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed) + add_test(NAME agc_driver_pointer_vsharp_fixed_offset_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed-offset) + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index e35cd06654e8..0b9d83f672eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -425,6 +425,24 @@ alignas(256) constexpr std::array PointerHopCode{ 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe4u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerFixedVsharpCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xf4080d16u, 0xfa0000a0u, 0xbf8cc07fu, 0xf42c091au, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, + 0x8afe3032u, 0xbf89ffeeu, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; +alignas(256) constexpr std::array PointerFixedVsharpOffsetCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xf4080d16u, 0xfa0000a0u, 0xbf8cc07fu, 0xf42c091au, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, + 0x8afe3032u, 0xbf89ffeeu, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; @@ -2278,6 +2296,84 @@ void RunPointerHopTests(AgcDriver::VulkanDevice& device) { std::puts("pointer table with a pointer hop: a T# read through a pointer loaded from a record pointer whose low word passes Phis with a constant and a Ballot word checked, by the records' values"); } +void RunPointerFixedVsharpTests(AgcDriver::VulkanDevice& device, std::uint32_t descriptorOffset) { + Fill(); + alignas(256) std::array, 2> records{}; + alignas(256) std::array, 2> palettes{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + const auto point = [&](std::uint32_t record, std::uint32_t palette) { + const auto descriptor = Buffer(palettes[palette].data(), static_cast(sizeof(palettes[palette]))); + std::copy(descriptor.begin(), descriptor.end(), records[record].begin() + 40u); + }; + for (std::uint32_t palette = 0; palette < 2u; ++palette) { + std::copy(images[palette].begin(), images[palette].end(), palettes[palette].begin() + descriptorOffset / 4u); + point(palette, palette); + } + const auto link = [&](std::uint32_t record, std::uint32_t target) { + const auto pointer = reinterpret_cast(records[target].data()); + Table[record * 4u + 2u] = static_cast(pointer); + Table[record * 4u + 3u] = static_cast(pointer >> 32u); + }; + for (std::uint32_t record = 0; record < 3u; ++record) link(record, record % 2u); + const std::string name = std::string("pointer V# table with a T# at fixed offset ") + Hex(descriptorOffset); + const auto run = [&] { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::span code = descriptorOffset == 0u ? std::span(PointerFixedVsharpCode) : std::span(PointerFixedVsharpOffsetCode); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(records.data()), std::as_bytes(std::span(records))}, + {reinterpret_cast(palettes.data()), std::as_bytes(std::span(palettes))}, + }; + const auto inputTable = Table; + const auto inputRecords = records; + const auto inputPalettes = palettes; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && records == inputRecords && palettes == inputPalettes, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + } + std::array paletteOf{0u, 1u, 0u}; + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + const auto record = keys[group][x < 32u ? 0u : 1u]; + return std::bit_cast(paletteOf[record] != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto taken = run(); + Require(!Faulted(taken), name + ": the T# read through the V# behind the record's pointer faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# read at a fixed offset of the V# behind the record's pointer in a waterfall loop"); + link(1u, 0u); + paletteOf[1] = 0u; + const auto moved = run(); + Require(!Faulted(moved), name + ": the T# behind a moved record pointer faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, name + ": moving a record's pointer recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), name + ": the T# after record 1's pointer moved to the other V#"); + point(0u, 1u); + paletteOf = {1u, 1u, 1u}; + const auto repointed = run(); + Require(!Faulted(repointed), name + ": the T# behind a moved V# faulted:\n" + repointed.log); + Require(repointed.artifactId == taken.artifactId, name + ": moving the V# behind a record's pointer recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(repointed, group, Lanes, sampled(group), name + ": the T# after the V# behind records 0 and 1 moved to the other palette"); + std::puts((name + ": a T# read at a fixed offset of a V# loaded through a record pointer checked, by the records' values").c_str()); +} + void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { Fill(); const auto floatImage = Image(FloatTexels.data(), Format32Float); @@ -2547,6 +2643,14 @@ int main(int argc, char** argv) { RunPointerHopTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-fixed") == 0) { + RunPointerFixedVsharpTests(*device, 0u); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-fixed-offset") == 0) { + RunPointerFixedVsharpTests(*device, 0x40u); + return 0; + } RunTests(*device); RunBorderSwizzleTests(*device); RunAnisoOverrideTests(*device); From 6a72841083bf4e8da213cabf74619dd3f4102a14 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:30:10 +0000 Subject: [PATCH 12/34] fix(shader): follow a conditional pointer chase through a V# behind a pointer-keyed record pointer A T# read at a pointer that a shader chases conditionally from a pointer-keyed record pointer, loading a V# at a fixed offset of the pointer and reading the next pointer from that buffer, stayed unplanned: the merged pointer's chase arms read another buffer at another offset than the record word, so PointerRecordRead refused them (Ratchet & Clank: Rift Apart stops at compute LoadAddressU32 with this shape). The walk now takes such an arm when its V# is read from a pointer that is the record pointer or another chase arm, records each distinct chase as a hop of the column, and refuses a chase that reaches itself or a depth over 4. The snapshot then also captures every record pointer's chase closure up to that depth as pointer-keyed entries, each with the T# at its own offset, so the GPU's lookup finds the pointer the shader reached. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../tests/execution/ImageTable.cpp | 142 +++++++++++++++ .../IrMetadata/ControlFlowInfo.hpp | 15 +- .../Optimization/src/ResourceMaterializer.cpp | 90 +++++++-- .../Optimization/src/ResourceTracker.cpp | 171 +++++++++++++++--- core/shader/recompiler/ShaderDiskCache.cpp | 21 ++- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/debt/shader.md | 1 + 8 files changed, 393 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e72d2cd94a9e..2af4527c9624 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2451,11 +2451,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) add_test(NAME agc_driver_pointer_hop_table COMMAND agc_driver_image_table_tests --pointer-hop) + add_test(NAME agc_driver_pointer_chase_table COMMAND agc_driver_image_table_tests --pointer-chase) add_test(NAME agc_driver_pointer_vsharp_fixed_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed) add_test(NAME agc_driver_pointer_vsharp_fixed_offset_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed-offset) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 0b9d83f672eb..cb0ac2e8629e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -425,6 +425,39 @@ alignas(256) constexpr std::array PointerHopCode{ 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe4u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerChaseCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xbf0d8012u, 0xbf840006u, 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b1eu, 0xfa000000u, 0xbf8cc07fu, + 0xbf0d8112u, 0xbf840006u, 0xf4081016u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b20u, 0xfa000000u, 0xbf8cc07fu, + 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe1u, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; +alignas(256) constexpr std::array PointerChaseLoopCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xbe960312u, 0xbf068016u, 0xbf850008u, 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b1eu, 0xfa000000u, + 0xbf8cc07fu, 0x80968116u, 0xbf82fff6u, 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, + 0x8afe3032u, 0xbf89ffe6u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, + 0x80070408u, 0xbf810000u, +}; +alignas(256) constexpr std::array PointerChaseDeepCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xbf0d8012u, 0xbf840006u, 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b1eu, 0xfa000000u, 0xbf8cc07fu, + 0xbf0d8112u, 0xbf840006u, 0xf4081016u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b20u, 0xfa000000u, 0xbf8cc07fu, + 0xbf0d8212u, 0xbf840006u, 0xf4081116u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b22u, 0xfa000000u, 0xbf8cc07fu, + 0xbf0d8312u, 0xbf840006u, 0xf4081216u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b24u, 0xfa000000u, 0xbf8cc07fu, + 0xbf0d8412u, 0xbf840006u, 0xf4081316u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b26u, 0xfa000000u, 0xbf8cc07fu, + 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffc9u, 0xbefe042eu, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; alignas(256) constexpr std::array PointerFixedVsharpCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, @@ -2296,6 +2329,111 @@ void RunPointerHopTests(AgcDriver::VulkanDevice& device) { std::puts("pointer table with a pointer hop: a T# read through a pointer loaded from a record pointer whose low word passes Phis with a constant and a Ballot word checked, by the records' values"); } +void RunPointerChaseTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 4> nodes{}; + alignas(256) std::array, 4> links{}; + alignas(256) std::array unsnapshotted{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + const std::array imageOf{0u, 1u, 0u, 1u}; + std::array next{}; + const auto address = [](const void* data) { return static_cast(reinterpret_cast(data)); }; + const auto chase = [&](std::uint32_t from, std::uint32_t to, std::uint32_t* link) { + link[0] = static_cast(address(nodes[to].data())); + link[1] = static_cast(address(nodes[to].data()) >> 32u); + const auto descriptor = Buffer(link, 8u); + std::copy(descriptor.begin(), descriptor.end(), nodes[from].begin() + 0x60u / 4u); + next[from] = to; + }; + for (std::uint32_t node = 0; node < 4u; ++node) { + std::copy(images[imageOf[node]].begin(), images[imageOf[node]].end(), nodes[node].begin()); + chase(node, node == 3u ? 3u : (node + 1u) % 3u, links[node].data()); + } + const std::array recordNode{2u, 0u, 1u, 2u}; + for (std::uint32_t record = 0; record < 4u; ++record) { + Table[record * 4u + 2u] = static_cast(address(nodes[recordNode[record]].data())); + Table[record * 4u + 3u] = static_cast(address(nodes[recordNode[record]].data()) >> 32u); + } + const std::string name = "pointer table with a conditional V# chase"; + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + }; + const Registration registeredNodes(nodes.data(), sizeof(nodes)); + const Registration registeredLinks(links.data(), sizeof(links)); + const Registration registeredUnsnapshotted(unsnapshotted.data(), sizeof(unsnapshotted)); + const auto run = [&](std::span code) { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(nodes.data()), std::as_bytes(std::span(nodes))}, + {reinterpret_cast(links.data()), std::as_bytes(std::span(links))}, + }; + const auto inputTable = Table; + const auto inputNodes = nodes; + const auto inputLinks = links; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && nodes == inputNodes && links == inputLinks, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + Keys[group * 4u + 2u] = group; + } + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + auto node = recordNode[keys[group][x < 32u ? 0u : 1u]]; + for (std::uint32_t step = 0; step < 2u; ++step) { + if ((group >> step) & 1u) node = next[node]; + } + return std::bit_cast(imageOf[node] != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto untouched = [](const Outcome& outcome) { return std::all_of(outcome.words.begin(), outcome.words.end(), [](auto word) { return word == 0xdeadbeefu; }); }; + const auto taken = run(PointerChaseCode); + Require(!Faulted(taken), name + ": the T# at the end of the chase faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# at the pointer reached by the chase steps taken in group " + std::to_string(group)); + chase(0u, 3u, links[0].data()); + const auto moved = run(PointerChaseCode); + Require(!Faulted(moved), name + ": the T# behind a moved chase link faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, name + ": moving a chased link recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), name + ": the T# after node 0's link moved to node 3 in group " + std::to_string(group)); + chase(0u, 1u, links[0].data()); + chase(1u, 3u, unsnapshotted.data()); + const auto outside = run(PointerChaseCode); + Require(Faulted(outside) && outside.log.find("outside the snapshot") != std::string::npos, name + ": a link outside the snapshot did not fault as outside the snapshot:\n" + outside.log); + Require(untouched(outside), name + ": a link outside the snapshot published copied output"); + Require(outside.artifactId == taken.artifactId, name + ": a link outside the snapshot recompiled the shader"); + const auto refused = [&](std::span code, const std::string& what) { + std::string refusal; + try { + static_cast(run(code)); + } catch (const std::runtime_error& error) { + refusal = error.what(); + } + Require(refusal.find("not a valid runtime value") != std::string::npos, name + ": " + what + " was not refused by resource planning: " + refusal); + }; + refused(PointerChaseLoopCode, "a chase repeated in a loop"); + refused(PointerChaseDeepCode, "five conditional chase steps"); + std::puts("pointer table with a conditional V# chase: a T# at a record pointer chased up to twice through a V# at the pointer plus 0x60 checked for every taken and skipped step, a moved link and a link outside the snapshot, and a chase in a loop and five chase steps refused"); +} + void RunPointerFixedVsharpTests(AgcDriver::VulkanDevice& device, std::uint32_t descriptorOffset) { Fill(); alignas(256) std::array, 2> records{}; @@ -2643,6 +2781,10 @@ int main(int argc, char** argv) { RunPointerHopTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-chase") == 0) { + RunPointerChaseTests(*device); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-fixed") == 0) { RunPointerFixedVsharpTests(*device, 0u); return 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index d114f59fc3e1..7e869d531b7a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -35,6 +35,17 @@ struct PointerHop { bool operator==(const PointerHop& other) const = default; }; +inline constexpr std::uint32_t MaxPointerChaseDepth = 4; + +struct PointerChase { + std::uint32_t offset = 0; + std::uint32_t immediate = 0; + std::uint32_t readOffset = 0; + std::uint32_t readImmediate = 0; + + bool operator==(const PointerChase& other) const = default; +}; + struct TableColumn { std::uint32_t heapSource = 0; std::uint32_t stride = 0; @@ -51,6 +62,8 @@ struct TableColumn { std::uint32_t indirectOffset = 0; std::uint32_t indirectImmediate = 0; std::vector pointerHops; + std::vector pointerChases; + std::uint32_t pointerChaseDepth = 0; std::uint32_t foldedAddend = 0; std::uint32_t foldedCarry = 0; bool selected = false; @@ -58,7 +71,7 @@ struct TableColumn { bool pointerKeyed = false; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && buffer == other.buffer && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && pointerHops == other.pointerHops && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection && pointerKeyed == other.pointerKeyed; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && buffer == other.buffer && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && pointerHops == other.pointerHops && pointerChases == other.pointerChases && pointerChaseDepth == other.pointerChaseDepth && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection && pointerKeyed == other.pointerKeyed; } }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ec5f4d77ca45..f481f175c7ed 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -949,6 +950,55 @@ class TableSnapshotter { if (column.buffer) parentKeysRemaining -= records.keys; } + std::optional Chase(std::uint64_t pointer, const PointerChase& chase) { + DescriptorValue descriptor; + descriptor.dwordCount = 4u; + for (std::uint32_t dword = 0; dword < 4u; dword++) { + const auto address = ImageTableAbi::ScalarAddressDword(pointer, chase.offset, chase.immediate + dword * 4u); + if (!address.has_value() || (*address & 3u) != 0u || !AddressAccessible(*address)) return std::nullopt; + descriptor.dwords[dword] = Read(*address); + descriptor.availableDwordMask |= static_cast(1u << dword); + tables.ranges.emplace_back(*address, sizeof(std::uint32_t)); + } + const auto buffer = decodeBufferDescriptor(descriptor); + if (buffer.Type() != 0u) return std::nullopt; + const auto low = ImageTableAbi::ScalarBufferDword(chase.readOffset, chase.readImmediate, buffer.GetSize()); + const auto high = ImageTableAbi::ScalarBufferDword(chase.readOffset, chase.readImmediate + 4u, buffer.GetSize()); + if (!low.has_value() || !high.has_value() || !AddressAccessible(buffer.Base48() + *low) || !AddressAccessible(buffer.Base48() + *high)) return std::nullopt; + tables.ranges.emplace_back(buffer.Base48() + *low, sizeof(std::uint32_t)); + tables.ranges.emplace_back(buffer.Base48() + *high, sizeof(std::uint32_t)); + return (static_cast(Read(buffer.Base48() + *high)) << 32u) | Read(buffer.Base48() + *low); + } + + template + void ChaseClosure(const TableColumn& column, ColumnRecords& records, std::uint32_t limit, const ReadEntry& readEntry) { + std::unordered_set known; + std::vector frontier; + for (std::uint32_t record = 0; record < records.keys; record++) { + const auto pointer = records.pointers[record]; + if (pointer != ImageTableAbi::UncapturedPointer && known.insert(pointer).second) frontier.push_back(pointer); + } + for (std::uint32_t depth = 0; depth < column.pointerChaseDepth && !frontier.empty(); depth++) { + std::vector reached; + for (const auto pointer : frontier) { + for (const auto& chase : column.pointerChases) { + const auto next = Chase(pointer, chase); + if (!next.has_value() || *next == 0u || !known.insert(*next).second) continue; + if (records.keys >= limit) { + records.outside = true; + continue; + } + records.states.push_back(RecordState::Read); + records.words.push_back(0u); + records.pointers.push_back(*next); + readEntry(records.keys++, *next); + reached.push_back(*next); + } + } + frontier = std::move(reached); + } + } + void ReadColumn(const TableColumn& column, ColumnRecords& records, std::uint32_t limit) { stamp = {}; comparedRuns.clear(); @@ -968,29 +1018,18 @@ class TableSnapshotter { if (column.indirect) { unread(); if (column.buffer || column.pointerKeyed) records.pointers.assign(records.keys, ImageTableAbi::UncapturedPointer); - for (std::uint32_t record = 0; record < records.keys; record++) { - if (records.states[record] != RecordState::Read) continue; - const auto offset = column.addend + record * column.stride; - const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); - const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); - if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { - records.states[record] = RecordState::Unmapped; - continue; - } - std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); - if (column.pointerKeyed) records.pointers[record] = pointer; + const auto readEntry = [&](std::uint32_t entry, std::uint64_t pointer) { for (const auto& hop : column.pointerHops) { const auto low = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate); const auto high = ImageTableAbi::ScalarAddressDword(pointer, hop.offset, hop.immediate + 4u); if (!low.has_value() || !high.has_value() || (*low & 3u) != 0u || *high != *low + sizeof(std::uint32_t) || !AddressAccessible(*low) || !AddressAccessible(*high)) { - records.states[record] = RecordState::Unmapped; - break; + records.states[entry] = RecordState::Unmapped; + return; } pointer = (static_cast(Read(*high)) << 32u) | Read(*low); tables.ranges.emplace_back(*low, 2u * sizeof(std::uint32_t)); } - if (records.states[record] != RecordState::Read) continue; - if (column.buffer) records.pointers[record] = pointer; + if (column.buffer) records.pointers[entry] = pointer; DescriptorValue value; value.dwordCount = column.sampler || column.buffer ? 4u : 8u; std::uint64_t first = std::numeric_limits::max(); @@ -999,19 +1038,32 @@ class TableSnapshotter { auto address = ImageTableAbi::ScalarAddressDword(pointer, column.indirectOffset, column.indirectImmediate + dword * 4u); if (address.has_value() && (*address & 3u) != 0u) address.reset(); if (!address.has_value() || !AddressAccessible(*address)) { - records.states[record] = RecordState::Unmapped; - break; + records.states[entry] = RecordState::Unmapped; + return; } value.dwords[dword] = Read(*address); value.availableDwordMask |= static_cast(1u << dword); first = std::min(first, *address); past = std::max(past, *address + sizeof(std::uint32_t)); } - if (records.states[record] != RecordState::Read) continue; - records.words[record] = WordsIndex(records, value); + records.words[entry] = WordsIndex(records, value); tables.ranges.emplace_back(first, past - first); if (column.buffer) records.readRanges.emplace_back(first, past - first); + }; + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); + const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); + if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { + records.states[record] = RecordState::Unmapped; + continue; + } + const std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + if (column.pointerKeyed) records.pointers[record] = pointer; + readEntry(record, pointer); } + if (column.pointerKeyed && column.pointerChaseDepth != 0u) ChaseClosure(column, records, limit, readEntry); return; } const bool memoized = TableSnapshotReuse() && !records.narrowed && records.keys != 0u; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index fba2fc696798..877753529263 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -898,36 +898,147 @@ class Tracker { return true; } - bool PointerRecordPairs(IrValue* low, IrValue* high, std::vector& lowReads, std::vector& highReads) const { - std::vector> pending{{low, high}}; - std::set> seen; - while (!pending.empty()) { - auto* lowWord = pending.back().first->Resolve(); - auto* highWord = pending.back().second->Resolve(); - pending.pop_back(); - if (!seen.emplace(lowWord, highWord).second || PointerFreeWord(*lowWord)) continue; - if (lowWord->IsPhi()) { - const bool paired = highWord->IsPhi() && highWord->Parent() == lowWord->Parent(); - for (std::size_t index = 0; index < lowWord->ArgumentCount(); ++index) { - IrValue* edge = paired ? PhiIncoming(*highWord, lowWord->PhiBlock(index)) : highWord; - if (edge == nullptr) return false; - pending.emplace_back(lowWord->Argument(index), edge); - } - continue; - } - if (IrValue* word = MaskedRecordWord(*lowWord)) { - pending.emplace_back(word, highWord); - continue; - } - if (!PointerRecordRead(*lowWord, lowReads)) return false; - if (highWord->IsPhi()) { - for (std::size_t index = 0; index < highWord->ArgumentCount(); ++index) pending.emplace_back(lowWord, highWord->Argument(index)); - } else if (IrValue* word = MaskedRecordWord(*highWord)) { - pending.emplace_back(lowWord, word); - } else if (!PointerFreeWord(*highWord) && !PointerRecordRead(*highWord, highReads)) { + struct ChaseSite { + IrValue* handle = nullptr; + IrValue* pointer = nullptr; + PointerChase chase; + std::vector inner; + }; + + bool ChaseRead(IrValue& read, IrValue*& handle, std::uint32_t& readOffset, std::uint32_t& readImmediate) const { + std::uint32_t memory = 0; + const MemoryInfo* info = ScalarReadMemory(read, memory); + if (info == nullptr || !MemoryIndexBelongsTo(memory, read) || !immediateU32(read.Argument(1), readOffset)) return false; + handle = read.Argument(0)->Resolve(); + readImmediate = info->offset; + return handle->Opcode() == IrOpcode::GetBufferResource && handle->ArgumentCount() == 4u; + } + + bool ChaseVsharp(IrValue& handle, IrValue*& pointer, PointerChase& chase) const { + for (std::uint32_t dword = 0; dword < 4u; dword++) { + IrValue* word = handle.Argument(dword)->Resolve(); + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarAddressMemory(*word, memoryIndex); + std::uint32_t offset = 0; + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *word) || !immediateU32(word->Argument(1), offset)) return false; + IrValue* address = word->Argument(0)->Resolve(); + if (address->Opcode() != IrOpcode::GetAddressResource || address->ArgumentCount() != 2u) return false; + if (dword == 0u) { + pointer = address; + chase.offset = offset; + chase.immediate = memory->offset; + } else if (offset != chase.offset || memory->offset != chase.immediate + dword * static_cast(sizeof(std::uint32_t)) || (address != pointer && !EquivalentValue(m_program.Resources(), pointer, address))) { return false; } } + return true; + } + + bool ChaseHighRead(IrValue& read, const IrValue& handle, std::uint32_t readOffset, std::uint32_t readImmediate) const { + IrValue* highHandle = nullptr; + std::uint32_t highOffset = 0; + std::uint32_t highImmediate = 0; + return ChaseRead(read, highHandle, highOffset, highImmediate) && (highHandle == &handle || EquivalentValue(m_program.Resources(), &handle, highHandle)) && highOffset == readOffset && highImmediate == readImmediate + static_cast(sizeof(std::uint32_t)); + } + + bool PointerRecordPairs(IrValue* low, IrValue* high, std::vector& lowReads, std::vector& highReads, std::vector& chases, std::uint32_t& chaseDepth) const { + std::vector sites; + std::vector looseHandles; + const auto site = [&](IrValue& handle, IrValue& pointer, const PointerChase& chase) { + const auto found = std::ranges::find_if(sites, [&](const ChaseSite& known) { return known.handle == &handle || EquivalentValue(m_program.Resources(), known.handle, &handle); }); + if (found != sites.end()) return static_cast(found - sites.begin()); + sites.push_back(ChaseSite{&handle, &pointer, chase, {}}); + return sites.size() - 1u; + }; + const auto walk = [&](IrValue* rootLow, IrValue* rootHigh, std::vector& reached) { + std::vector> pending{{rootLow, rootHigh}}; + std::set> seen; + while (!pending.empty()) { + auto* lowWord = pending.back().first->Resolve(); + auto* highWord = pending.back().second->Resolve(); + pending.pop_back(); + if (!seen.emplace(lowWord, highWord).second || PointerFreeWord(*lowWord)) continue; + if (lowWord->IsPhi()) { + const bool paired = highWord->IsPhi() && highWord->Parent() == lowWord->Parent(); + for (std::size_t index = 0; index < lowWord->ArgumentCount(); ++index) { + IrValue* edge = paired ? PhiIncoming(*highWord, lowWord->PhiBlock(index)) : highWord; + if (edge == nullptr) return false; + pending.emplace_back(lowWord->Argument(index), edge); + } + continue; + } + if (IrValue* word = MaskedRecordWord(*lowWord)) { + pending.emplace_back(word, highWord); + continue; + } + IrValue* handle = nullptr; + IrValue* pointer = nullptr; + PointerChase chase; + std::uint32_t looseOffset = 0; + std::uint32_t looseImmediate = 0; + if (ChaseRead(*lowWord, handle, chase.readOffset, chase.readImmediate) && ChaseVsharp(*handle, pointer, chase)) { + if (highWord->IsPhi()) { + for (std::size_t index = 0; index < highWord->ArgumentCount(); ++index) pending.emplace_back(lowWord, highWord->Argument(index)); + continue; + } + if (!ChaseHighRead(*highWord, *handle, chase.readOffset, chase.readImmediate)) return false; + const auto index = site(*handle, *pointer, chase); + if (std::ranges::find(reached, index) == reached.end()) reached.push_back(index); + continue; + } + if (!PointerRecordRead(*lowWord, lowReads)) return false; + if (highWord->IsPhi()) { + for (std::size_t index = 0; index < highWord->ArgumentCount(); ++index) pending.emplace_back(lowWord, highWord->Argument(index)); + } else if (IrValue* word = MaskedRecordWord(*highWord)) { + pending.emplace_back(lowWord, word); + } else if (PointerFreeWord(*highWord) || PointerRecordRead(*highWord, highReads)) { + continue; + } else if (IrValue* highHandle = nullptr; ChaseRead(*highWord, highHandle, looseOffset, looseImmediate)) { + looseHandles.push_back(highHandle); + } else { + return false; + } + } + return true; + }; + std::vector roots; + if (!walk(low, high, roots)) return false; + for (std::size_t index = 0; index < sites.size(); ++index) { + std::vector inner; + if (!walk(sites[index].pointer->Argument(0), sites[index].pointer->Argument(1), inner)) return false; + sites[index].inner = std::move(inner); + } + if (!std::ranges::all_of(looseHandles, [&](const IrValue* handle) { + return std::ranges::any_of(sites, [&](const ChaseSite& known) { return known.handle == handle || EquivalentValue(m_program.Resources(), known.handle, handle); }); + })) return false; + std::vector depths(sites.size(), 0u); + std::vector states(sites.size(), 0u); + const auto depthOf = [&](const auto& self, std::size_t index) -> std::optional { + if (states[index] == 2u) return depths[index]; + if (states[index] == 1u) return std::nullopt; + states[index] = 1u; + std::uint32_t deepest = 0; + for (const auto inner : sites[index].inner) { + const auto depth = self(self, inner); + if (!depth.has_value()) return std::nullopt; + deepest = std::max(deepest, *depth); + } + states[index] = 2u; + depths[index] = deepest + 1u; + return depths[index]; + }; + chaseDepth = 0; + for (const auto root : roots) { + const auto depth = depthOf(depthOf, root); + if (!depth.has_value()) return false; + chaseDepth = std::max(chaseDepth, *depth); + } + if (chaseDepth > MaxPointerChaseDepth) return false; + chases.clear(); + for (const auto& known : sites) { + if (std::ranges::find(chases, known.chase) == chases.end()) chases.push_back(known.chase); + } + if (chaseDepth == 0u) chases.clear(); return !lowReads.empty() && !highReads.empty(); } @@ -999,7 +1110,9 @@ class Tracker { if (!hops.empty() && buffer) return false; std::vector lowReads; std::vector highReads; - if (pointerKeyed ? !PointerRecordPairs(pointer->Argument(0), pointer->Argument(1), lowReads, highReads) : merged && !PointerRecordReads(pointer->Argument(0), lowReads)) return false; + std::vector chases; + std::uint32_t chaseDepth = 0; + if (pointerKeyed ? !PointerRecordPairs(pointer->Argument(0), pointer->Argument(1), lowReads, highReads, chases, chaseDepth) : merged && !PointerRecordReads(pointer->Argument(0), lowReads)) return false; auto* low = merged ? lowReads.back() : pointer->Argument(0)->Resolve(); auto* high = pointer->Argument(1)->Resolve(); std::uint32_t first = 0; @@ -1062,6 +1175,8 @@ class Tracker { column.indirectImmediate = descriptorImmediate; column.pointerKeyed = pointerKeyed; column.pointerHops = std::move(hops); + column.pointerChases = std::move(chases); + column.pointerChaseDepth = chaseDepth; const auto domain = merged ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 4797fa2e2417..4ee4f3e111a1 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -108,8 +108,8 @@ static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 128, "IrValue changed: update the plan value encoder"); 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) == 200, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 120, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 224, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 144, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 104, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -149,7 +149,7 @@ static_assert(fieldCount() == 24, "IrResourcePlan changed: updat static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 20, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 22, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 5, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); @@ -1093,6 +1093,13 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { hops.Value(hop.offset); hops.Value(hop.immediate); }); + out.List(column.pointerChases, [](Writer& chases, const PointerChase& chase) { + chases.Value(chase.offset); + chases.Value(chase.immediate); + chases.Value(chase.readOffset); + chases.Value(chase.readImmediate); + }); + out.Value(column.pointerChaseDepth); out.Value(column.foldedAddend); out.Value(column.foldedCarry); out.Value(column.selected); @@ -1257,6 +1264,13 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { hops.Value(hop.offset); hops.Value(hop.immediate); }); + in.List(column.pointerChases, 16, [](Reader& chases, PointerChase& chase) { + chases.Value(chase.offset); + chases.Value(chase.immediate); + chases.Value(chase.readOffset); + chases.Value(chase.readImmediate); + }); + in.Value(column.pointerChaseDepth); in.Value(column.foldedAddend); in.Value(column.foldedCarry); in.Value(column.selected); @@ -1303,6 +1317,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { const auto& parent = plan.descriptorSources[column.heapSource].tableColumn; if (parent.has_value() && (column.buffer || column.address || column.indirect || !parent->buffer || !parent->indirect || plan.descriptorSources[parent->heapSource].tableColumn.has_value())) return false; if (column.pointerKeyed && (!column.indirect || column.buffer || parent.has_value())) return false; + if (column.pointerChases.empty() != (column.pointerChaseDepth == 0u) || column.pointerChaseDepth > MaxPointerChaseDepth || (column.pointerChaseDepth != 0u && !column.pointerKeyed)) return false; } if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index d007fc126a7a..d61a2b9e2002 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 46; +inline constexpr std::uint32_t FormatVersion = 47; enum class LoadStatus { Loaded, diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index da23615fbaa8..37b7d1555711 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -42,6 +42,7 @@ - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with lane masks (words of a `Ballot`, the waterfall loop's lane bit, and ANDs, ORs and NOTs of them), with the record word ANDed with a lane mask, and with reads of the same record word under another key, and its high word merged the same way from the record's next word. The two words are walked together, edge by edge, and an edge whose low word is a constant or a lane mask is skipped along with its high word. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the two words remain refused, among them a high word read from a second buffer on an edge whose low word is the record word. +- A pointer-keyed [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) chase that reaches itself through a Phi (a loop), or passes more than 4 chase sites, stays refused. A chase arm is two words a scalar buffer load reads at a fixed offset through a V# that `s_load` reads at a fixed offset of the record pointer or of another chase arm; on an edge whose low word is a chase arm, the high word must be that arm's own read. The [snapshot](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) captures, for every record pointer, the pointers the chases reach up to the shader's depth, each with its own T# or S#. A V# that is not a buffer, or a read past the V# or outside the snapshot, ends that chase, and the GPU faults when it selects the pointer reached there. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From 2c1a65045c55be38c1d96fe99d4a679adb2e35d4 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:48:20 +0000 Subject: [PATCH 13/34] fix(shader): keep one chase site per V# word pair PointerRecordPairs found an existing chase site by its V# alone. A second chase arm that reads its next pointer through the same V# at other words therefore kept the first arm's PointerChase, and the snapshot never followed the second arm: a T# behind a node whose V# holds two links (words 0 and 1, words 2 and 3), each taken in its own lanes, faulted as outside the snapshot when the second link was taken. Sites are now keyed by the V# and the whole PointerChase, so each word pair of one V# is its own chase in the column and in the snapshot's closure. This follows the chase work for Ratchet & Clank: Rift Apart (PPSA01474); its trace has not shown this shape yet. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../tests/execution/ImageTable.cpp | 101 ++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 2 +- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2af4527c9624..9ee845bbcb89 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2452,11 +2452,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) add_test(NAME agc_driver_pointer_hop_table COMMAND agc_driver_image_table_tests --pointer-hop) add_test(NAME agc_driver_pointer_chase_table COMMAND agc_driver_image_table_tests --pointer-chase) + add_test(NAME agc_driver_pointer_chase_words_table COMMAND agc_driver_image_table_tests --pointer-chase-words) add_test(NAME agc_driver_pointer_vsharp_fixed_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed) add_test(NAME agc_driver_pointer_vsharp_fixed_offset_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed-offset) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index cb0ac2e8629e..6bf0187ef6df 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -458,6 +458,16 @@ alignas(256) constexpr std::array PointerChaseDeepCode{ 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffc9u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerChaseWordsCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xbf0d8012u, 0xbf840004u, 0xf4240b1eu, 0xfa000000u, 0xbf8cc07fu, + 0xbf820005u, 0xbf0d8112u, 0xbf840003u, 0xf4240b1eu, 0xfa000008u, 0xbf8cc07fu, 0xf40c0916u, 0xfa000000u, + 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe3u, 0xbefe042eu, 0x7e120202u, 0x34101286u, + 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; alignas(256) constexpr std::array PointerFixedVsharpCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, @@ -2434,6 +2444,93 @@ void RunPointerChaseTests(AgcDriver::VulkanDevice& device) { std::puts("pointer table with a conditional V# chase: a T# at a record pointer chased up to twice through a V# at the pointer plus 0x60 checked for every taken and skipped step, a moved link and a link outside the snapshot, and a chase in a loop and five chase steps refused"); } +void RunPointerChaseWordsTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 6> nodes{}; + alignas(256) std::array, 2> links{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + const std::array imageOf{0u, 1u, 0u, 1u, 1u, 0u}; + std::array first{}; + std::array second{}; + const auto address = [](const void* data) { return static_cast(reinterpret_cast(data)); }; + const auto link = [&](std::uint32_t from, std::uint32_t word, std::uint32_t to) { + links[from][word] = static_cast(address(nodes[to].data())); + links[from][word + 1u] = static_cast(address(nodes[to].data()) >> 32u); + (word == 0u ? first : second)[from] = to; + }; + for (std::uint32_t node = 0; node < 6u; ++node) std::copy(images[imageOf[node]].begin(), images[imageOf[node]].end(), nodes[node].begin()); + for (std::uint32_t node = 0; node < 2u; ++node) { + const auto descriptor = Buffer(links[node].data(), 16u); + std::copy(descriptor.begin(), descriptor.end(), nodes[node].begin() + 0x60u / 4u); + link(node, 0u, node + 2u); + link(node, 2u, node + 4u); + } + const std::array recordNode{0u, 0u, 1u, 0u}; + for (std::uint32_t record = 0; record < 4u; ++record) { + Table[record * 4u + 2u] = static_cast(address(nodes[recordNode[record]].data())); + Table[record * 4u + 3u] = static_cast(address(nodes[recordNode[record]].data()) >> 32u); + } + const std::string name = "pointer table with two chase links in one V#"; + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + }; + const Registration registeredNodes(nodes.data(), sizeof(nodes)); + const Registration registeredLinks(links.data(), sizeof(links)); + const auto run = [&](std::span code) { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(nodes.data()), std::as_bytes(std::span(nodes))}, + {reinterpret_cast(links.data()), std::as_bytes(std::span(links))}, + }; + const auto inputTable = Table; + const auto inputNodes = nodes; + const auto inputLinks = links; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && nodes == inputNodes && links == inputLinks, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + const std::array steps{1u, 2u, 0u, 2u}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + Keys[group * 4u + 2u] = steps[group]; + } + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + auto node = recordNode[keys[group][x < 32u ? 0u : 1u]]; + if ((steps[group] & 1u) != 0u) node = first[node]; + else if ((steps[group] & 2u) != 0u) node = second[node]; + return std::bit_cast(imageOf[node] != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto taken = run(PointerChaseWordsCode); + Require(!Faulted(taken), name + ": the T# behind one of the two links faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# at the pointer read from the link taken in group " + std::to_string(group)); + link(0u, 2u, 2u); + const auto moved = run(PointerChaseWordsCode); + Require(!Faulted(moved), name + ": the T# behind a moved second link faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, name + ": moving the second link recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), name + ": the T# after node 0's second link moved to node 2 in group " + std::to_string(group)); + std::puts("pointer table with two chase links in one V#: a T# at a record pointer chased through words 0 and 1 or words 2 and 3 of one V# at the pointer plus 0x60, each taken in its own groups, checked with a moved second link"); +} + void RunPointerFixedVsharpTests(AgcDriver::VulkanDevice& device, std::uint32_t descriptorOffset) { Fill(); alignas(256) std::array, 2> records{}; @@ -2785,6 +2882,10 @@ int main(int argc, char** argv) { RunPointerChaseTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-chase-words") == 0) { + RunPointerChaseWordsTests(*device); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-fixed") == 0) { RunPointerFixedVsharpTests(*device, 0u); return 0; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 877753529263..e0533136152d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -945,7 +945,7 @@ class Tracker { std::vector sites; std::vector looseHandles; const auto site = [&](IrValue& handle, IrValue& pointer, const PointerChase& chase) { - const auto found = std::ranges::find_if(sites, [&](const ChaseSite& known) { return known.handle == &handle || EquivalentValue(m_program.Resources(), known.handle, &handle); }); + const auto found = std::ranges::find_if(sites, [&](const ChaseSite& known) { return known.chase == chase && (known.handle == &handle || EquivalentValue(m_program.Resources(), known.handle, &handle)); }); if (found != sites.end()) return static_cast(found - sites.begin()); sites.push_back(ChaseSite{&handle, &pointer, chase, {}}); return sites.size() - 1u; From 9e50a74929f5509c056bb3e4768463aa3c97d336 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:09:26 -0700 Subject: [PATCH 14/34] fix(shader): refuse a pointer-keyed record arm whose two words are both immediates The pointer walk skips an edge whose low word is an immediate, as a waterfall loop's Phi gives one beside the record column read. Where the pointer's two words are Phis of one block, an edge on which both are immediates is a constant pointer that no record holds, such as a null pointer, so the walk now refuses the column there instead of planning it. --- .../tests/execution/ImageTable.cpp | 18 ++++++++++++++++-- .../Optimization/src/ResourceTracker.cpp | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index f1b32368e681..e850cb3ffcc1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -2015,7 +2015,7 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { } const auto secondBuffer = Buffer(second.data() + 8u, 32u); std::copy(secondBuffer.begin(), secondBuffer.end(), second.begin()); - const std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : shape == PointerMask::LaneNot ? std::span(PointerLaneNotCode) : shape == PointerMask::HighSecondLane ? std::span(PointerHighSecondLaneCode) : std::span(PointerHighSecondCode); + std::span code = shape == PointerMask::LaneAnd ? std::span(PointerLaneAndCode) : shape == PointerMask::High ? std::span(PointerHighCode) : shape == PointerMask::LaneNot ? std::span(PointerLaneNotCode) : shape == PointerMask::HighSecondLane ? std::span(PointerHighSecondLaneCode) : std::span(PointerHighSecondCode); const std::string name = shape == PointerMask::LaneAnd ? "pointer table with lane-mask ANDs" : shape == PointerMask::High ? "pointer table with a high-word Phi" : shape == PointerMask::LaneNot ? "pointer table with a lane bit cleared from a lane mask" : shape == PointerMask::HighSecondLane ? "pointer table with a second-buffer high word under a lane-mask low word" : "pointer table with a high word from a second buffer"; const auto run = [&] { const auto table = Buffer(Table.data(), 64u); @@ -2107,7 +2107,21 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { std::puts("pointer table with a second-buffer high word under a lane-mask low word: a T# through a record pointer whose high word reads a second buffer only on the edge whose low word is a lane mask checked, and that pair faults outside the snapshot"); return; } - std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value"); + alignas(256) static std::array nullArm{}; + nullArm = PointerHighCode; + nullArm[40] = 0xbeac0380u; + nullArm[41] = 0xbead0380u; + for (const std::size_t index : std::array{44u, 45u, 52u, 53u}) nullArm[index] = 0xbf800000u; + code = nullArm; + bool refused = false; + try { + static_cast(run()); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("GetImageResource dword 0 is not a valid runtime value") != std::string::npos, name + ": a record pointer arm whose two words are both immediates was refused for another reason: " + error.what()); + refused = true; + } + Require(refused, name + ": a record pointer arm whose two words are both immediates was planned"); + std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value, and an arm whose two words are both immediates is refused"); } void RunPointerVsharpTests(AgcDriver::VulkanDevice& device) { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 19cbd9b97fde..b0619dee4e37 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -903,6 +903,7 @@ class Tracker { for (std::size_t index = 0; index < lowWord->ArgumentCount(); ++index) { IrValue* edge = paired ? PhiIncoming(*highWord, lowWord->PhiBlock(index)) : highWord; if (edge == nullptr) return false; + if (paired && lowWord->Argument(index)->Resolve()->HasImmediate() && edge->Resolve()->HasImmediate()) return false; pending.emplace_back(lowWord->Argument(index), edge); } continue; From 9343a179b341396b0869ced149133f33f352ade7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:51:39 -0700 Subject: [PATCH 15/34] test(agc): stop asserting that a shared nested-sampler V# is refused The nested sampler table test expected resource planning to refuse a V# with another buffer consumer with "not a valid runtime value". Such a check fails once the shape is supported, so the case goes; the sampled results, sharing and budget checks stay. --- core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d805a4377078..efc31b4e9995 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1775,10 +1775,6 @@ void RunNestedSamplerTests(AgcDriver::VulkanDevice& device) { catch (const std::runtime_error& error) { rejected = std::string(error.what()).find("not a valid runtime value") != std::string::npos; } Require(rejected, std::string("nested sampler table: ") + what + " was not refused by resource planning"); }; - auto shared = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); - shared.insert(shared.begin() + 31u, 0x7e0e0232u); - shared.insert(shared.begin() + 21u, {0xf4200c8cu, 0xfa000000u}); - refused(shared, "a V# with another buffer consumer"); auto consumer = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); consumer.insert(consumer.begin() + 31u, 0x7e0e0221u); refused(consumer, "an S# with another consumer"); From cfb72cb70e0288c9fabe364a6bc2f3fbc7ff3a9f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:52:46 -0700 Subject: [PATCH 16/34] fix(shader): give this PR's recompiler change its own disk cache format, 51 The format ladder along the stack is #7395 50 and this PR 51, so a cache written by #7395's recompiler is not read as this one's. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5f25660eb725..dad5115a4d9c 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 50; +inline constexpr std::uint32_t FormatVersion = 51; enum class LoadStatus { Loaded, From 4750c85fcd8546e1701effa935447da7f3cc46a0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:52:48 -0700 Subject: [PATCH 17/34] test(agc): stop asserting that two pointer-table shapes are refused Two checks expected resource planning to refuse a shape with "not a valid runtime value": a high-word arm read from a buffer whose V# is loaded through a pointer, and a record pointer arm whose two words are both immediates. Such a check fails once the shape is supported, so both go. The first was the whole of agc_driver_pointer_phi_high_second_table's mode, so that mode and its registration go with it; the lane-mask, high-word and second-lane cases stay. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++-- .../tests/execution/ImageTable.cpp | 30 +------------------ 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5dc4e6d7e78a..8369b4dc439c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2878,13 +2878,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_phi_copies_table COMMAND agc_driver_image_table_tests --pointer-phi-copies) add_test(NAME agc_driver_pointer_phi_lane_and_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-and) add_test(NAME agc_driver_pointer_phi_high_table COMMAND agc_driver_image_table_tests --pointer-phi-high) - add_test(NAME agc_driver_pointer_phi_high_second_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second) add_test(NAME agc_driver_pointer_phi_high_second_lane_table COMMAND agc_driver_image_table_tests --pointer-phi-high-second-lane) add_test(NAME agc_driver_pointer_phi_lane_not_table COMMAND agc_driver_image_table_tests --pointer-phi-lane-not) add_test(NAME agc_driver_pointer_vsharp_select_table COMMAND agc_driver_image_table_tests --pointer-vsharp-select) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index fff8d830ad4d..dc46cef050a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -2143,16 +2143,6 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { Keys[group * 8u] = keys[group][0]; Keys[group * 8u + 1u] = keys[group][1]; } - if (shape == PointerMask::HighSecond) { - try { - static_cast(run()); - } catch (const std::exception& error) { - Require(std::string(error.what()).find("GetImageResource dword 0 is not a valid runtime value") != std::string::npos, name + ": refused for another reason: " + error.what()); - std::puts("pointer table with a high word from a second buffer: a high-word arm read from a buffer whose V# is loaded through a pointer stays refused"); - return; - } - Require(false, name + ": a high-word arm read from a buffer whose V# is loaded through a pointer was planned"); - } const auto sampled = [&keys](std::uint32_t group) { return std::function([&keys, group](std::uint32_t x) { const auto key = keys[group][x < 32u ? 0u : 1u]; @@ -2201,21 +2191,7 @@ void RunPointerMaskTests(AgcDriver::VulkanDevice& device, PointerMask shape) { std::puts("pointer table with a second-buffer high word under a lane-mask low word: a T# through a record pointer whose high word reads a second buffer only on the edge whose low word is a lane mask checked, and that pair faults outside the snapshot"); return; } - alignas(256) static std::array nullArm{}; - nullArm = PointerHighCode; - nullArm[40] = 0xbeac0380u; - nullArm[41] = 0xbead0380u; - for (const std::size_t index : std::array{44u, 45u, 52u, 53u}) nullArm[index] = 0xbf800000u; - code = nullArm; - bool refused = false; - try { - static_cast(run()); - } catch (const std::exception& error) { - Require(std::string(error.what()).find("GetImageResource dword 0 is not a valid runtime value") != std::string::npos, name + ": a record pointer arm whose two words are both immediates was refused for another reason: " + error.what()); - refused = true; - } - Require(refused, name + ": a record pointer arm whose two words are both immediates was planned"); - std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value, and an arm whose two words are both immediates is refused"); + std::puts("pointer table with a high-word Phi: a T# through a record pointer whose high word passes a Phi of reads under other keys and a constant checked, by the pointer's value"); } void RunPointerVsharpTests(AgcDriver::VulkanDevice& device) { @@ -2542,10 +2518,6 @@ int main(int argc, char** argv) { RunPointerMaskTests(*device, PointerMask::High); return 0; } - if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-high-second") == 0) { - RunPointerMaskTests(*device, PointerMask::HighSecond); - return 0; - } if (argc == 2 && std::strcmp(argv[1], "--pointer-phi-high-second-lane") == 0) { RunPointerMaskTests(*device, PointerMask::HighSecondLane); return 0; From 62992d7437c404aed453580f01be17bc9dbcdcc1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:54:51 -0700 Subject: [PATCH 18/34] fix(shader): give this PR's recompiler change its own disk cache format, 53 The format ladder along the stack is #8939 51 and this PR 53, so a cache written by #8939's recompiler is not read as this one's. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index dad5115a4d9c..fc2b6bea030d 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 51; +inline constexpr std::uint32_t FormatVersion = 53; enum class LoadStatus { Loaded, From 68d04647f3a2a35e915117a3e2729fc8ae5f7c0c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:55:22 -0700 Subject: [PATCH 19/34] fix(shader): give this PR's recompiler change its own disk cache format, 55 The format ladder along the stack is #9457 53 and this PR 55, so a cache written by #9457's recompiler is not read as this one's. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index dad5115a4d9c..41fad42aa6c6 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 51; +inline constexpr std::uint32_t FormatVersion = 55; enum class LoadStatus { Loaded, From ca14648806098a04f25d521ded3b098b681d85bf Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:18:28 -0700 Subject: [PATCH 20/34] test(agc): stop asserting that an S# with another consumer is refused The nested sampler table test expected resource planning to refuse an S# whose words have another consumer, with "not a valid runtime value". Such a check fails once the shape is supported, so the case goes, with the helper it was the last user of. The sampled results, sharing and budget checks stay. --- .../prx/libSceAgcDriver/tests/execution/ImageTable.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index efc31b4e9995..33b5c5c18081 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1769,15 +1769,6 @@ void RunNestedSamplerTests(AgcDriver::VulkanDevice& device) { Require(!Faulted(moved) && Group(moved, 2) == Group(original, 0) && Group(moved, 3) == Group(original, 1), "nested sampler table: replacing the nested V# retained a stale sampler"); Require(moved.artifactId == original.artifactId && moved.tableMap.size() < original.tableMap.size(), "nested sampler table: equal V#s did not share one child table"); records = savedRecords; - const auto refused = [&](const std::vector& code, const char* what) { - bool rejected = false; - try { static_cast(run(code)); } - catch (const std::runtime_error& error) { rejected = std::string(error.what()).find("not a valid runtime value") != std::string::npos; } - Require(rejected, std::string("nested sampler table: ") + what + " was not refused by resource planning"); - }; - auto consumer = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); - consumer.insert(consumer.begin() + 31u, 0x7e0e0221u); - refused(consumer, "an S# with another consumer"); auto phi = std::vector(NestedSamplerCode.begin(), NestedSamplerCode.end()); phi.insert(phi.begin() + 17u, {0xbf068011u, 0xbf840002u, 0xbeac03ffu, 0x000000eau}); for (std::uint32_t group = 0; group < 4u; ++group) Keys[group * 2u] = 1u; From 6c139245aa04aaba0e57c4009fd091312c4ef7ba Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:30:14 -0700 Subject: [PATCH 21/34] fix(shader): follow a pointer chase through a V# whose words the shader edits Ghostrunner (PPSA03685) compute shader 0x200c520000 reads its T# pointer as Phi(R, H(R)). R is a pointer-keyed record pointer. H(R) is read at 0x30 of a buffer whose V# comes from R+0x30, with word 1 ANDed with 0xffff, word 2 shifted left by 4 and word 3 ORed with 0x30000000 before use (TheSovietWaffle's IR after srt on #8939). The chase took V# words only as read, so that arm fell through to the record-column check and the request stopped at LoadAddressU32@pc=0x0002ef78 with "GetImageResource dword 0 is not a valid runtime value". A chased V# word may now be the read word shifted left, ANDed or ORed by one constant. The chase keeps that edit per word, the snapshot applies it to the words it reads before it decodes the V#, and the shader disk cache stores it. The cache format stays 55: the source version already keeps this recompiler's caches apart. agc_driver_pointer_chase_edited_table models the arm with a V# at R+0x30 that covers the read at 0x30 only after the shader's edits, checked for taken and skipped chase steps and a moved link. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../tests/execution/ImageTable.cpp | 99 +++++++++++++++++++ .../IrMetadata/ControlFlowInfo.hpp | 10 ++ .../Optimization/src/ResourceMaterializer.cpp | 2 +- .../Optimization/src/ResourceTracker.cpp | 18 +++- core/shader/recompiler/ShaderDiskCache.cpp | 13 ++- docs/dev/debt/shader.md | 2 +- 7 files changed, 143 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 984565bf99ca..4910156ad98f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2931,11 +2931,12 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pointer_hop_table COMMAND agc_driver_image_table_tests --pointer-hop) add_test(NAME agc_driver_pointer_chase_table COMMAND agc_driver_image_table_tests --pointer-chase) add_test(NAME agc_driver_pointer_chase_words_table COMMAND agc_driver_image_table_tests --pointer-chase-words) + add_test(NAME agc_driver_pointer_chase_edited_table COMMAND agc_driver_image_table_tests --pointer-chase-edited) add_test(NAME agc_driver_pointer_vsharp_fixed_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed) add_test(NAME agc_driver_pointer_vsharp_fixed_offset_table COMMAND agc_driver_image_table_tests --pointer-vsharp-fixed-offset) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_chase_edited_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PIPELINE_JOURNAL=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_nested_image_table agc_driver_nested_image_table_large agc_driver_nested_image_table_budget agc_driver_nested_sampler_table agc_driver_pointer_phi_sampler_table agc_driver_pointer_phi_ballot_table agc_driver_pointer_phi_copies_table agc_driver_pointer_phi_lane_and_table agc_driver_pointer_phi_high_table agc_driver_pointer_phi_high_second_lane_table agc_driver_pointer_phi_lane_not_table agc_driver_pointer_vsharp_select_table agc_driver_pointer_hop_table agc_driver_pointer_chase_table agc_driver_pointer_chase_words_table agc_driver_pointer_chase_edited_table agc_driver_pointer_vsharp_fixed_table agc_driver_pointer_vsharp_fixed_offset_table PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 2e17723cb512..a94720d5eb72 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -492,6 +492,16 @@ alignas(256) constexpr std::array PointerChaseWordsCode{ 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe3u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array PointerChaseEditedCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, + 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, + 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, + 0xbf0d8012u, 0xbf84000bu, 0xf4080f16u, 0xfa000030u, 0xbf8cc07fu, 0x873dff3du, 0x0000ffffu, 0x8f3e843eu, + 0x883fff3fu, 0x30000000u, 0xf4240b1eu, 0xfa000030u, 0xbf8cc07fu, 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, + 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe4u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, + 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; alignas(256) constexpr std::array PointerFixedVsharpCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, @@ -2606,6 +2616,91 @@ void RunPointerChaseWordsTests(AgcDriver::VulkanDevice& device) { std::puts("pointer table with two chase links in one V#: a T# at a record pointer chased through words 0 and 1 or words 2 and 3 of one V# at the pointer plus 0x60, each taken in its own groups, checked with a moved second link"); } +void RunPointerChaseEditedTests(AgcDriver::VulkanDevice& device) { + Fill(); + alignas(256) std::array, 4> nodes{}; + alignas(256) std::array, 4> links{}; + Table = {}; + Keys = {}; + const std::array images{Image(FloatTexels.data(), Format32Float), Image(OtherFloatTexels.data(), Format32Float)}; + const std::array imageOf{0u, 1u, 0u, 1u}; + std::array next{}; + const auto address = [](const void* data) { return static_cast(reinterpret_cast(data)); }; + const auto chase = [&](std::uint32_t from, std::uint32_t to) { + links[from][0x30u / 4u] = static_cast(address(nodes[to].data())); + links[from][0x34u / 4u] = static_cast(address(nodes[to].data()) >> 32u); + auto descriptor = Buffer(links[from].data(), 4u); + descriptor[1] |= 4u << 16u; + descriptor[3] &= ~0x30000000u; + std::copy(descriptor.begin(), descriptor.end(), nodes[from].begin() + 0x30u / 4u); + next[from] = to; + }; + for (std::uint32_t node = 0; node < 4u; ++node) { + std::copy(images[imageOf[node]].begin(), images[imageOf[node]].end(), nodes[node].begin()); + chase(node, node == 3u ? 3u : (node + 1u) % 3u); + } + const std::array recordNode{2u, 0u, 1u, 2u}; + for (std::uint32_t record = 0; record < 4u; ++record) { + Table[record * 4u + 2u] = static_cast(address(nodes[recordNode[record]].data())); + Table[record * 4u + 3u] = static_cast(address(nodes[recordNode[record]].data()) >> 32u); + } + const std::string name = "pointer table with an edited chase V#"; + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + }; + const Registration registeredNodes(nodes.data(), sizeof(nodes)); + const Registration registeredLinks(links.data(), sizeof(links)); + const auto run = [&](std::span code) { + const auto table = Buffer(Table.data(), 64u); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + Srt = {}; + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4u); + std::copy(output.begin(), output.end(), Srt.begin() + 8u); + std::copy(PointWrap.begin(), PointWrap.end(), Srt.begin() + 12u); + const std::vector memory{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(nodes.data()), std::as_bytes(std::span(nodes))}, + {reinterpret_cast(links.data()), std::as_bytes(std::span(links))}, + }; + const auto inputTable = Table; + const auto inputNodes = nodes; + const auto inputLinks = links; + auto result = Execute(device, code, Srt.data(), memory, 4u, Lanes, 64u); + Require(Table == inputTable && nodes == inputNodes && links == inputLinks, name + ": dispatch modified its input memory"); + Require(std::all_of(Output.begin() + 4u * Lanes * 4u, Output.end(), [](auto word) { return word == 0xdeadbeefu; }), name + ": dispatch wrote past its output"); + return result; + }; + const std::array, 4> keys{{{1u, 2u}, {2u, 1u}, {1u, 1u}, {2u, 2u}}}; + for (std::uint32_t group = 0; group < 4u; ++group) { + Keys[group * 4u] = keys[group][0]; + Keys[group * 4u + 1u] = keys[group][1]; + Keys[group * 4u + 2u] = group; + } + const auto sampled = [&](std::uint32_t group) { + return std::function([&, group](std::uint32_t x) { + auto node = recordNode[keys[group][x < 32u ? 0u : 1u]]; + if ((group & 1u) != 0u) node = next[node]; + return std::bit_cast(imageOf[node] != 0u ? OtherFloatTexels[x] : FloatTexels[x]); + }); + }; + const auto taken = run(PointerChaseEditedCode); + Require(!Faulted(taken), name + ": the T# behind the edited V# faulted:\n" + taken.log); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(taken, group, Lanes, sampled(group), name + ": the T# at the record pointer or at the pointer read through the edited V# in group " + std::to_string(group)); + chase(0u, 3u); + const auto moved = run(PointerChaseEditedCode); + Require(!Faulted(moved), name + ": the T# behind a moved link faulted:\n" + moved.log); + Require(moved.artifactId == taken.artifactId, name + ": moving a link behind the edited V# recompiled the shader"); + for (std::uint32_t group = 0; group < 4u; ++group) RequireRed(moved, group, Lanes, sampled(group), name + ": the T# after node 0's link moved to node 3 in group " + std::to_string(group)); + std::puts("pointer table with an edited chase V#: a T# at a record pointer or at the pointer read at 0x30 through a V# from the record pointer plus 0x30 whose words 1, 2 and 3 the shader ANDs with 0xffff, shifts left by 4 and ORs with 0x30000000, a V# that covers the read only after those edits, checked for taken and skipped steps and a moved link"); +} + void RunPointerFixedVsharpTests(AgcDriver::VulkanDevice& device, std::uint32_t descriptorOffset) { Fill(); alignas(256) std::array, 2> records{}; @@ -3009,6 +3104,10 @@ int main(int argc, char** argv) { RunPointerChaseWordsTests(*device); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--pointer-chase-edited") == 0) { + RunPointerChaseEditedTests(*device); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--pointer-vsharp-fixed") == 0) { RunPointerFixedVsharpTests(*device, 0u); return 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index 7e869d531b7a..edc4fd46e293 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -37,11 +37,21 @@ struct PointerHop { inline constexpr std::uint32_t MaxPointerChaseDepth = 4; +struct DescriptorWordEdit { + std::uint32_t shift = 0; + std::uint32_t mask = 0xffffffffu; + std::uint32_t bits = 0; + + [[nodiscard]] std::uint32_t Apply(std::uint32_t word) const { return ((word << shift) & mask) | bits; } + bool operator==(const DescriptorWordEdit& other) const = default; +}; + struct PointerChase { std::uint32_t offset = 0; std::uint32_t immediate = 0; std::uint32_t readOffset = 0; std::uint32_t readImmediate = 0; + std::array edits {}; bool operator==(const PointerChase& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index efe83b718c2f..b9dd80318568 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1075,7 +1075,7 @@ class TableSnapshotter { for (std::uint32_t dword = 0; dword < 4u; dword++) { const auto address = ImageTableAbi::ScalarAddressDword(pointer, chase.offset, chase.immediate + dword * 4u); if (!address.has_value() || (*address & 3u) != 0u || !AddressAccessible(*address)) return std::nullopt; - descriptor.dwords[dword] = Read(*address); + descriptor.dwords[dword] = chase.edits[dword].Apply(Read(*address)); descriptor.availableDwordMask |= static_cast(1u << dword); tables.ranges.emplace_back(*address, sizeof(std::uint32_t)); } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 500e95f63ceb..6ea7907ad534 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1023,9 +1023,25 @@ class Tracker { return handle->Opcode() == IrOpcode::GetBufferResource && handle->ArgumentCount() == 4u; } + static IrValue* EditedWord(IrValue* word, DescriptorWordEdit& edit) { + word = word->Resolve(); + std::uint32_t operand = 0; + if (word->ArgumentCount() != 2u) return word; + if (word->Opcode() == IrOpcode::ShiftLeftLogical32) { + if (!immediateU32(word->Argument(1), operand) || operand > 31u) return word; + edit.shift = operand; + return word->Argument(0)->Resolve(); + } + if (word->Opcode() != IrOpcode::BitwiseAnd32 && word->Opcode() != IrOpcode::BitwiseOr32) return word; + const bool left = immediateU32(word->Argument(0), operand); + if (!left && !immediateU32(word->Argument(1), operand)) return word; + (word->Opcode() == IrOpcode::BitwiseAnd32 ? edit.mask : edit.bits) = operand; + return word->Argument(left ? 1u : 0u)->Resolve(); + } + bool ChaseVsharp(IrValue& handle, IrValue*& pointer, PointerChase& chase) const { for (std::uint32_t dword = 0; dword < 4u; dword++) { - IrValue* word = handle.Argument(dword)->Resolve(); + IrValue* word = EditedWord(handle.Argument(dword), chase.edits[dword]); std::uint32_t memoryIndex = 0; const MemoryInfo* memory = ScalarAddressMemory(*word, memoryIndex); std::uint32_t offset = 0; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ed6dc611d2d8..5e5f06bb3445 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1166,6 +1166,11 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { chases.Value(chase.immediate); chases.Value(chase.readOffset); chases.Value(chase.readImmediate); + for (const auto& edit : chase.edits) { + chases.Value(edit.shift); + chases.Value(edit.mask); + chases.Value(edit.bits); + } }); out.Value(column.pointerChaseDepth); out.Value(column.foldedAddend); @@ -1332,11 +1337,16 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { hops.Value(hop.offset); hops.Value(hop.immediate); }); - in.List(column.pointerChases, 16, [](Reader& chases, PointerChase& chase) { + in.List(column.pointerChases, 64, [](Reader& chases, PointerChase& chase) { chases.Value(chase.offset); chases.Value(chase.immediate); chases.Value(chase.readOffset); chases.Value(chase.readImmediate); + for (auto& edit : chase.edits) { + chases.Value(edit.shift); + chases.Value(edit.mask); + chases.Value(edit.bits); + } }); in.Value(column.pointerChaseDepth); in.Value(column.foldedAddend); @@ -1386,6 +1396,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (parent.has_value() && (column.buffer || column.address || column.indirect || !parent->buffer || !parent->indirect || plan.descriptorSources[parent->heapSource].tableColumn.has_value())) return false; if (column.pointerKeyed && (!column.indirect || column.buffer || parent.has_value())) return false; if (column.pointerChases.empty() != (column.pointerChaseDepth == 0u) || column.pointerChaseDepth > MaxPointerChaseDepth || (column.pointerChaseDepth != 0u && !column.pointerKeyed)) return false; + if (std::ranges::any_of(column.pointerChases, [](const PointerChase& chase) { return std::ranges::any_of(chase.edits, [](const DescriptorWordEdit& edit) { return edit.shift > 31u; }); })) return false; } if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 55531d232fce..ddfd28ea78ed 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -53,7 +53,7 @@ - [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with lane masks (words of a `Ballot`, the waterfall loop's lane bit, and ANDs, ORs and NOTs of them), with the record word ANDed with a lane mask, and with reads of the same record word under another key, and its high word merged the same way from the record's next word. The two words are walked together, edge by edge, and an edge whose low word is a constant or a lane mask is skipped along with its high word. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the two words remain refused, among them a high word read from a second buffer on an edge whose low word is the record word. -- A pointer-keyed [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) chase that reaches itself through a Phi (a loop), or passes more than 4 chase sites, stays refused. A chase arm is two words a scalar buffer load reads at a fixed offset through a V# that `s_load` reads at a fixed offset of the record pointer or of another chase arm; on an edge whose low word is a chase arm, the high word must be that arm's own read. The [snapshot](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) captures, for every record pointer, the pointers the chases reach up to the shader's depth, each with its own T# or S#. A V# that is not a buffer, or a read past the V# or outside the snapshot, ends that chase, and the GPU faults when it selects the pointer reached there. +- A pointer-keyed [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) chase that reaches itself through a Phi (a loop), or passes more than 4 chase sites, stays refused. A chase arm is two words a scalar buffer load reads at a fixed offset through a V# that `s_load` reads at a fixed offset of the record pointer or of another chase arm, each V# word either as read or after one shift left, AND or OR by a constant, which the snapshot applies to the words it reads; other edits of a V# word stay refused. On an edge whose low word is a chase arm, the high word must be that arm's own read. The [snapshot](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) captures, for every record pointer, the pointers the chases reach up to the shader's depth, each with its own T# or S#. A V# that is not a buffer, or a read past the V# or outside the snapshot, ends that chase, and the GPU faults when it selects the pointer reached there. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. From d908758281f6f43123df0f2051333b4d94ddd290 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:01:41 -0700 Subject: [PATCH 22/34] test(shader): stop requiring nested tables with shared words to be refused The nested image table test required two valid shaders to throw "not a valid runtime value": one whose parent V# also feeds another buffer consumer, and one whose T# has another consumer. Neither shape is implemented, and the test policy keeps tests from pinning valid but unimplemented shapes to a throw, so both checks and their helper go. The sampled-result, wrap, bounds, non-buffer and empty-dictionary checks stay. --- .../libSceAgcDriver/tests/execution/ImageTable.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index deb7cd908527..be5f693cd094 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1594,20 +1594,6 @@ void RunNestedRecordTests(AgcDriver::VulkanDevice& device, NestedCase selected) const auto notBuffer = run(); Require(Faulted(notBuffer) && notBuffer.log.find("not a buffer") != std::string::npos, "nested image table: a non-buffer parent was accepted:\n" + notBuffer.log); records[0][43] &= ~(1u << 30u); - const auto refused = [&](const std::vector& code, const char* what) { - bool rejected = false; - try { static_cast(run(code)); } - catch (const std::runtime_error& error) { rejected = std::string(error.what()).find("not a valid runtime value") != std::string::npos; } - Require(rejected, std::string("nested image table: ") + what + " was not refused by resource planning"); - }; - auto shared = std::vector(NestedRecordCode.begin(), NestedRecordCode.end()); - shared.insert(shared.begin() + 30u, 0x7e0e0232u); - shared.insert(shared.begin() + 21u, {0xf4200c8cu, 0xfa000000u}); - std::fprintf(stderr, "nested image table: checking shared V# refusal\n"); - refused(shared, "a V# with another buffer consumer"); - auto consumer = std::vector(NestedRecordCode.begin(), NestedRecordCode.end()); - consumer.insert(consumer.begin() + 30u, 0x7e0e0225u); - refused(consumer, "a T# with another consumer"); rootBytes = 0u; const auto empty = run(); Require(Faulted(empty) && empty.log.find("unmapped") != std::string::npos, "nested image table: an empty pointer dictionary did not fault"); From e04a7057655626a8815d8c9293ae58651f9e180b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:45:42 -0700 Subject: [PATCH 23/34] fix(shader): give this PR's recompiler change its own disk cache format, 51 yabanana's #7223 now takes 50 above main's 49, so this PR takes 51 and a cache written by #7223's recompiler is not read as this one's. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5f25660eb725..dad5115a4d9c 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 50; +inline constexpr std::uint32_t FormatVersion = 51; enum class LoadStatus { Loaded, From 3e2ebbbf355fbeeec9c4076af1f73582a2204d80 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:45:54 -0700 Subject: [PATCH 24/34] fix(shader): give this PR's recompiler change its own disk cache format, 53 #7395 now takes 51 above #7223's 50, and 52 is held by #10000, so this PR takes 53. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index dad5115a4d9c..fc2b6bea030d 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 51; +inline constexpr std::uint32_t FormatVersion = 53; enum class LoadStatus { Loaded, From 19bed84013a299568240ea9e761664ff81193007 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:46:06 -0700 Subject: [PATCH 25/34] fix(shader): give this PR's recompiler change its own disk cache format, 55 #8939 now takes 53, and 54 is held by #10489, so this PR takes 55. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index fc2b6bea030d..41fad42aa6c6 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 53; +inline constexpr std::uint32_t FormatVersion = 55; enum class LoadStatus { Loaded, From b6c114935d00371b791f76265aea1b78f055a1bb Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:47:24 -0700 Subject: [PATCH 26/34] fix(shader): give this PR's recompiler change its own disk cache format, 56 #9457 now takes 55, so this PR takes 56. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 41fad42aa6c6..3e43cbb22130 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 55; +inline constexpr std::uint32_t FormatVersion = 56; enum class LoadStatus { Loaded, From ee8767ff0de59e6db33a0c9ac941792b30915d77 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:58:55 -0700 Subject: [PATCH 27/34] test(shader): stop requiring a looped or five-step pointer chase to be refused The conditional pointer chase test required two valid shaders to throw "not a valid runtime value": one that repeats the chase in a loop and one with five conditional chase steps. Neither shape is implemented, and the test policy keeps tests from pinning valid but unimplemented shapes to a throw, so both checks, their helper and their shader words go. The taken and skipped steps, the moved link and the link outside the snapshot are still checked. --- .../tests/execution/ImageTable.cpp | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 24b4fcb8883e..166e97b9d978 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -459,29 +459,6 @@ alignas(256) constexpr std::array PointerChaseCode{ 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffe1u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; -alignas(256) constexpr std::array PointerChaseLoopCode{ - 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, - 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, - 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, - 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, - 0xbe960312u, 0xbf068016u, 0xbf850008u, 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b1eu, 0xfa000000u, - 0xbf8cc07fu, 0x80968116u, 0xbf82fff6u, 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, - 0x8afe3032u, 0xbf89ffe6u, 0xbefe042eu, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, - 0x80070408u, 0xbf810000u, -}; -alignas(256) constexpr std::array PointerChaseDeepCode{ - 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, - 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, - 0x060202f2u, 0x7e0402ffu, 0x3e000000u, 0xbf8cc07fu, 0x7d8800a0u, 0x7e140210u, 0x02061411u, 0xbeae047eu, - 0x7e280503u, 0xd4c20030u, 0x00020614u, 0xbeb22430u, 0x8f158414u, 0xf4240b02u, 0x2a000008u, 0xbf8cc07fu, - 0xbf0d8012u, 0xbf840006u, 0xf4080f16u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b1eu, 0xfa000000u, 0xbf8cc07fu, - 0xbf0d8112u, 0xbf840006u, 0xf4081016u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b20u, 0xfa000000u, 0xbf8cc07fu, - 0xbf0d8212u, 0xbf840006u, 0xf4081116u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b22u, 0xfa000000u, 0xbf8cc07fu, - 0xbf0d8312u, 0xbf840006u, 0xf4081216u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b24u, 0xfa000000u, 0xbf8cc07fu, - 0xbf0d8412u, 0xbf840006u, 0xf4081316u, 0xfa000060u, 0xbf8cc07fu, 0xf4240b26u, 0xfa000000u, 0xbf8cc07fu, - 0xf40c0916u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x01090401u, 0x8afe3032u, 0xbf89ffc9u, 0xbefe042eu, - 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, -}; alignas(256) constexpr std::array PointerChaseWordsCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0x8f108402u, 0xf4280404u, 0x20000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, @@ -2501,18 +2478,7 @@ void RunPointerChaseTests(AgcDriver::VulkanDevice& device) { Require(Faulted(outside) && outside.log.find("outside the snapshot") != std::string::npos, name + ": a link outside the snapshot did not fault as outside the snapshot:\n" + outside.log); Require(untouched(outside), name + ": a link outside the snapshot published copied output"); Require(outside.artifactId == taken.artifactId, name + ": a link outside the snapshot recompiled the shader"); - const auto refused = [&](std::span code, const std::string& what) { - std::string refusal; - try { - static_cast(run(code)); - } catch (const std::runtime_error& error) { - refusal = error.what(); - } - Require(refusal.find("not a valid runtime value") != std::string::npos, name + ": " + what + " was not refused by resource planning: " + refusal); - }; - refused(PointerChaseLoopCode, "a chase repeated in a loop"); - refused(PointerChaseDeepCode, "five conditional chase steps"); - std::puts("pointer table with a conditional V# chase: a T# at a record pointer chased up to twice through a V# at the pointer plus 0x60 checked for every taken and skipped step, a moved link and a link outside the snapshot, and a chase in a loop and five chase steps refused"); + std::puts("pointer table with a conditional V# chase: a T# at a record pointer chased up to twice through a V# at the pointer plus 0x60 checked for every taken and skipped step, a moved link and a link outside the snapshot"); } void RunPointerChaseWordsTests(AgcDriver::VulkanDevice& device) { From 4769d5a7456bafbed2409478255e17cea9f3f009 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:57:07 -0700 Subject: [PATCH 28/34] test(shader): drop the refusal wording from the nested table summary lines The nested image table and nested sampler table tests no longer check refusals: d908758281 dropped the shared V# and other-consumer T# cases, and the nested sampler refusal cases went before it. Their success lines now say only what they check. The nested image table line comes from #7223 (ad44bc41bb) and is in this branch through its merges of #7223. --- core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index be5f693cd094..0d375ca36406 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -1632,7 +1632,7 @@ void RunNestedRecordTests(AgcDriver::VulkanDevice& device, NestedCase selected) Require(Faulted(phiFault) && phiFault.log.find("outside the snapshot") != std::string::npos, "nested image table: the uncaptured Phi arm was assumed unreachable"); Require(std::all_of(phiFault.words.begin(), phiFault.words.end(), [](auto word) { return word == 0xdeadbeefu; }), "nested image table: an uncaptured pointer published copied output"); Require(phiFault.artifactId == phiValid.artifactId, "nested image table: the pointer choice was specialized on the CPU"); - std::puts("nested image table independent keys, alias, descriptor replacement, faults, budget and refusals checked"); + std::puts("nested image table independent keys, alias, descriptor replacement, faults and budget checked"); } void RunPointerChainRecordTests(AgcDriver::VulkanDevice& device) { @@ -1796,7 +1796,7 @@ void RunNestedSamplerTests(AgcDriver::VulkanDevice& device) { const bool clamp = keys[group * 2u + 1u] == 0u; RequireRed(sharedBudget, group, Lanes, [clamp](std::uint32_t x) { return std::bit_cast(FloatTexels[clamp ? Width - 1u : x]); }, "nested sampler table: aliased parent at the snapshot budget"); } - std::puts("nested sampler table: S# through record, pointer and V# checked on wave64 and wave32, with sharing, budgets and refusals"); + std::puts("nested sampler table: S# through record, pointer and V# checked on wave64 and wave32, with sharing and budgets"); } void RunPointerSamplerTests(AgcDriver::VulkanDevice& device) { From f4781de1f79f53c7f42ce2b1e6f566204a0c3c04 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:23:23 +0000 Subject: [PATCH 29/34] fix(shader): give this PR's recompiler change its own disk cache format, 63 yabanana's #7223 now takes 51, the number this PR had, and 52-62 are held by the open PRs: #10000 52, #8939 53, #10489 54, #9457 55, #9633 56, #9749 57, #10193 58, #9790 59, #9969 60, #10846 61, #7642 62. So this PR takes 63 and a cache written by #7223's recompiler is not read as this one's. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index dad5115a4d9c..cebadf9d7e69 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 51; +inline constexpr std::uint32_t FormatVersion = 63; enum class LoadStatus { Loaded, From 967f0dbebb8963842840ac8c10d2d13b99c0544a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:54:46 -0700 Subject: [PATCH 30/34] fix(shader): check the descriptor epoch for a lone nested sampler table The tracker now builds nested S# columns, whose heap source is itself a table column, but TableSnapshotter::Run() still widened hasIndirect for samplers only on column.indirect. A shader whose only table is a nested S# table therefore skipped the snapshot's own epoch check and retry and relied on the whole-snapshot revalidation alone. The sampler loop now uses the same test as the image loop. agc_shader_nested_sampler_snapshot_retry runs the nested retry check with an S# child column and a sampler as the only table. On Windows, agc_shader_memory_tests needs libc's directory on PATH, as every other case it runs gets; without it the new test exits with 0xc0000135 before it starts. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 48 +++++++++++++------ .../Optimization/src/ResourceMaterializer.cpp | 5 +- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d89ab1d66d36..bd64c14ca2d5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2805,6 +2805,11 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_nested_snapshot_retry COMMAND agc_shader_memory_tests --nested-snapshot-retry) set_tests_properties(agc_shader_nested_snapshot_retry PROPERTIES TIMEOUT 20) + add_test(NAME agc_shader_nested_sampler_snapshot_retry COMMAND agc_shader_memory_tests --nested-sampler-snapshot-retry) + set_tests_properties(agc_shader_nested_sampler_snapshot_retry PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_shader_nested_sampler_snapshot_retry PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) add_test(NAME agc_shader_region_merge COMMAND agc_shader_memory_tests --region-merge) set_tests_properties(agc_shader_region_merge PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4bc9a6480033..6d81c56fccd5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2656,12 +2656,19 @@ void verifyRegionMerge(bool strided = false, bool benchmark = false) { } -void verifyNestedSnapshotRetry() { +void verifyNestedSnapshotRetry(bool sampler) { using namespace ShaderRecompiler; constexpr std::uint32_t Parents = ImageTableAbi::MaxParentKeys / 2u + 1u; constexpr std::uint32_t Children = ImageTableAbi::MaxNestedKeys / 2u + 1u; - std::vector> leaves(Children, TableT(reinterpret_cast(0x100000000ull), TableFormat8888UNorm, TableType2D, 1u)); - const auto child = TableV(leaves.data(), 32u, Children); + const std::uint32_t leafDwords = sampler ? 4u : 8u; + const auto texture = TableT(reinterpret_cast(0x100000000ull), TableFormat8888UNorm, TableType2D, 1u); + const std::array pointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; + std::vector leaves; + for (std::uint32_t leaf = 0; leaf < Children; ++leaf) { + if (sampler) leaves.insert(leaves.end(), pointClamp.begin(), pointClamp.end()); + else leaves.insert(leaves.end(), texture.begin(), texture.end()); + } + const auto child = TableV(leaves.data(), leafDwords * 4u, Children); std::vector parents(Parents, reinterpret_cast(child.data())); const auto root = TableV(parents.data(), 8u, Parents); std::vector> values; @@ -2687,17 +2694,25 @@ void verifyNestedSnapshotRetry() { plan.descriptorSources[1].tableColumn = parent; TableColumn nested; nested.heapSource = 1u; - nested.stride = 32u; - nested.dwordCount = 8u; + nested.stride = leafDwords * 4u; + nested.dwordCount = leafDwords; + nested.sampler = sampler; plan.descriptorSources[2].tableColumn = nested; - ImageResource image; - image.source = 2u; - image.table = 0u; - image.resourceClass = ImageResourceClass::Sampled; - image.dimension = RdnaImageDimension::Dim2D; - image.read = true; - plan.info.images.push_back(image); - plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + if (sampler) { + SamplerResource resource; + resource.source = 2u; + resource.table = 0u; + plan.info.samplers.push_back(resource); + } else { + ImageResource image; + image.source = 2u; + image.table = 0u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + plan.info.images.push_back(image); + plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + } struct Memory { std::array, 3> ranges; std::uint64_t epoch = 0; @@ -9752,10 +9767,15 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--nested-snapshot-retry") { - verifyNestedSnapshotRetry(); + verifyNestedSnapshotRetry(false); std::cout << "Nested snapshot retry preserves parent and child coverage\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--nested-sampler-snapshot-retry") { + verifyNestedSnapshotRetry(true); + std::cout << "Nested sampler snapshot retry checks the descriptor epoch and preserves parent and child coverage\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--table-snapshot-reuse") { verifyTableSnapshotReuse(); verifyTableStampRace(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5adcf5f69d8f..0a721ee482d5 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -716,7 +716,10 @@ class TableSnapshotter { for (const auto& sampler : plan.info.samplers) { if (sampler.table != NoTable) { count = std::max(count, sampler.table + 1u); - if (sampler.source < plan.descriptorSources.size() && plan.descriptorSources[sampler.source].tableColumn.has_value() && plan.descriptorSources[sampler.source].tableColumn->indirect) hasIndirect = true; + if (sampler.source < plan.descriptorSources.size() && plan.descriptorSources[sampler.source].tableColumn.has_value()) { + const auto& column = *plan.descriptorSources[sampler.source].tableColumn; + hasIndirect |= column.indirect || plan.descriptorSources.at(column.heapSource).tableColumn.has_value(); + } } } if (count == 0u) { From 5fdf20fc3dee87142249645043b57b215d06a60b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:55:00 -0800 Subject: [PATCH 31/34] test(agc): put libc on PATH for both nested snapshot retry tests on Windows --- .../libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake index b67be4bcc64a..2bb77b7dfdf1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/cmake/agc_shader_memory_tests.cmake @@ -26,7 +26,7 @@ if(BUILD_TESTING) add_test(NAME agc_shader_nested_sampler_snapshot_retry COMMAND agc_shader_memory_tests --nested-sampler-snapshot-retry) set_tests_properties(agc_shader_nested_sampler_snapshot_retry PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(agc_shader_nested_sampler_snapshot_retry PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_nested_snapshot_retry agc_shader_nested_sampler_snapshot_retry PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) add_test(NAME agc_shader_region_merge COMMAND agc_shader_memory_tests --region-merge) From 49f6939fc0be3055e4b64a569b83cc1852916bd5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:59:54 -0800 Subject: [PATCH 32/34] fix(shader): give this PR's recompiler change its own disk cache format, 67 #8939 now takes 66, and no open pull request uses a number above it, so this PR takes 67. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 41fad42aa6c6..0f8a38b5a9f2 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 55; +inline constexpr std::uint32_t FormatVersion = 67; enum class LoadStatus { Loaded, From 280d6d6c445d1822a9c6d5256b3b48dbfe5d20f5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:40:59 -0800 Subject: [PATCH 33/34] fix(shader): give this PR's recompiler change its own disk cache format, 70 #9457 is at 67, so this PR takes 70, above it and every other open PR's number. --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 3e43cbb22130..749a82f98dc4 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 56; +inline constexpr std::uint32_t FormatVersion = 70; enum class LoadStatus { Loaded, From 014d1e30c3a298376e60842b641e9fbb69e186b6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:45:59 -0800 Subject: [PATCH 34/34] docs(shader): note that a nested child's T# skips registered views --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index cb885910793b..7950cb0f340c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -58,7 +58,7 @@ - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (T#s and S#s that `s_buffer_load` reads at `key * stride + addend` from a V#) are snapshotted when the dispatch or draw is recorded; a later CPU write to a record is not seen. - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. -- [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. +- [Nested image tables](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) support a V# loaded through a pointer from one scalar-buffer record origin, followed by an independently indexed T# or S#. A low-pointer Phi may merge that origin with constants; the GPU looks up the actual 64-bit pointer and faults if it was not captured. Other origins, shared V# consumers, T# or S# words read by anything but their descriptor handle, and deeper buffer nesting remain refused. A snapshot scans at most 524288 parent records and holds 1048576 inner key slots across its nested columns; equal V#s share a child snapshot within a column. The final image-table map is limited to 32 MiB. A T# reached through a nested child is materialized from its raw words, without registered `resourceViews`, so a 1D array past `maxImageArrayLayers` keeps its raw descriptor there. Synthetic readback covers the domains reported in #6191; complete title behavior remains unverified. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) T# or S# read through a pointer that a scalar-buffer record holds may have that pointer's low word merged by a Phi with constants, with words of a `Ballot` lane mask, and with reads of the same record word under another key. The snapshot then captures the pointer of every record, at most 65536, and the GPU looks up the actual 64-bit pointer; one that was not captured, such as a lane mask, faults when selected. Other origins of the low word remain refused. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 256 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead.