From 7b9c7ca5821d32a3e9d232013a0014b7d7f52439 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:43:06 +0200 Subject: [PATCH 1/5] fix(recompiler): leave out descriptor sources behind a branch the capture decides A compute kernel that picks a buffer or an image path from an SRT word reads the same address as a V# in one path and as a T# in another. The resource tracker now records which block uses each descriptor source and builds the resource control flow from the structured block terminators, so a source whose only uses sit behind a conditional branch with a clean, evaluable condition is left inactive (bound as zero words, not validated) when the capture takes the other path. A condition that reads inaccessible memory is not evaluable instead of throwing. PPSA15246 (Teardown) stopped on "runtime image descriptor is invalid" for the untaken path of such a kernel. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 76 ++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 89 +++++++++++++++++++ .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 10 ++- 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 57596b5d62d..00d9c38c974 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -610,6 +610,80 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } +// A T# used only behind a branch on a word of the SRT: with the branch skipping the image, the +// T# is not validated (it may hold anything); with the branch taken it is. +void verifyGuardedDescriptorSources() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 0u, 0u, 0u, 0u, 0u}; + const std::array sampler{0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + // s_load_dword s16 (mode, SRT + 0x50); s_load_dwordx4 s[20:23] (S#) and s[24:27] (output V#); + // s_waitcnt; s_cmp_eq_u32 s16, 0; s_cbranch_scc1 over the image block; the image block is + // s_load_dwordx8 s[8:15] (T#), s_waitcnt, image_sample_lz v[0:3], s[8:15], s[20:23], s_waitcnt; + // buffer_store_dword v0, s[24:27]; s_endpgm. + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + fill(invalid, 0u); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "guarded sources: the image and its sampler were not planned"); + require(!plan->controlFlow.empty(), "guarded sources: the branch around the image was not planned"); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: the skipped T# was not left null"); + } + fill(invalid, 1u); + { + AgcDriver::ShaderMemory memory({}); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); + } + fill(valid, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "guarded sources: the T# behind a taken branch was not read"); + } + fill(valid, 0u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: a valid T# behind a skipped branch was read"); + } +} + // A compute program sampling a T# loaded from a table buffer at a runtime key (a bindless image // table): mode M enumerates the keys from the material records, mode T binds the whole table. void verifyBindlessTable() { @@ -3097,6 +3171,7 @@ void verifyGuardedNullPointers() { const auto tableAddress = static_cast(reinterpret_cast(table.data())); vertexRoot[2] = static_cast(tableAddress); vertexRoot[3] = static_cast(tableAddress >> 32u); + vertexRoot[0] = 1u; AgcDriver::ShaderMemory mappedVertexMemory({}); const auto mappedVertex = mappedVertexMemory.Capture(vertex.request); require(mappedVertex->snapshot.srtPoison.empty() && mappedVertex->snapshot.buffers.size() == 1u && mappedVertex->snapshot.buffers[0].dwords[0] == table[4], "guarded pointer: a vertex V# behind a mapped pointer was poisoned or not read"); @@ -3188,6 +3263,7 @@ int main(int argc, char** argv) { verifyOutOfRangeConstantReads(); verifySignedMinimum(); verifyPureFlatSlots(); + verifyGuardedDescriptorSources(); verifyBindlessTable(); verifyDescriptorPhis(); verifyLaneSelectedDescriptors(); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 1ca667d9799..4cafa88f90e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -114,11 +114,20 @@ class Tracker { } SplitDescriptorPhis(); PlanIndirectImages(); + std::unordered_map blockIndices; + for (const IrBlock* block : m_program.BlockOrder()) { + blockIndices.emplace(block, static_cast(blockIndices.size())); + } + m_blockSources.assign(blockIndices.size(), {}); for (auto& block : m_program.Blocks()) { + const auto found = blockIndices.find(block.get()); + m_currentBlock = found == blockIndices.end() ? std::numeric_limits::max() : found->second; for (IrValue* inst : block->Instructions()) { Collect(*inst); } } + m_currentBlock = std::numeric_limits::max(); + PlanResourceControlFlow(); if (m_bdaTraces > bdaTraceLimit) std::fprintf(stderr, "[bda] %u more address accesses in this program not shown\n", m_bdaTraces - bdaTraceLimit); LinkImageAliases(); for (const auto& patch : m_handlePatches) { @@ -241,13 +250,91 @@ class Tracker { same = same && EquivalentValue(m_program.Resources(), current.dwords[i], descriptor.dwords[i]); } if (same) { + NoteBlockSource(candidate); return candidate; } } m_sources.push_back(descriptor); + NoteBlockSource(static_cast(m_sources.size() - 1)); return static_cast(m_sources.size() - 1); } + void NoteBlockSource(std::uint32_t source) { + if (m_currentBlock >= m_blockSources.size()) { + return; + } + auto& sources = m_blockSources[m_currentBlock]; + if (std::ranges::find(sources, source) == sources.end()) { + sources.push_back(source); + } + } + + // A descriptor source that every path of the program reaches stays active; one that is used + // only behind a conditional branch can be left out when the branch condition is evaluable at + // capture time (see EvaluateRuntimeSourcesImpl), so a descriptor the untaken path would read + // as another resource kind is not validated. The edges are the structured terminators of the + // block metadata; a block whose terminator is not a plain or conditional branch keeps all of + // its CFG successors, so no source is ever dropped for a path the walk cannot decide. + void PlanResourceControlFlow() { + const auto& order = m_program.BlockOrder(); + const auto& info = m_program.Metadata().blockInfo; + if (order.empty() || order.size() != info.size()) { + return; + } + std::unordered_map indexOf; + std::unordered_map idToIndex; + for (std::uint32_t index = 0; index < order.size(); index++) { + indexOf.emplace(order[index], index); + idToIndex.emplace(info[index].id, index); + } + std::vector controlFlow(order.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + auto& block = controlFlow[index]; + const auto& terminator = info[index].terminator; + const auto target = [&](std::uint32_t id) { return idToIndex.find(id); }; + const auto trueEdge = target(terminator.trueBlock); + const auto falseEdge = target(terminator.falseBlock); + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && info[index].condition != nullptr && trueEdge != idToIndex.end() && falseEdge != idToIndex.end()) { + block.condition = info[index].condition; + block.successors = {trueEdge->second, falseEdge->second}; + } else if (terminator.kind != TerminatorKind::Return) { + for (const IrBlock* successor : order[index]->Successors()) { + const auto found = indexOf.find(successor); + if (found == indexOf.end()) { + return; + } + block.successors.push_back(found->second); + } + } + block.sources = m_blockSources[index]; + } + std::vector unconditional(order.size()); + for (std::uint32_t index = 0; index < order.size() && unconditional[index] == 0u;) { + unconditional[index] = 1u; + if (controlFlow[index].successors.size() != 1u) { + break; + } + index = controlFlow[index].successors.front(); + } + std::vector reached(m_sources.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + for (const auto source : controlFlow[index].sources) { + if (unconditional[index] != 0u) { + reached[source] = 1u; + } + } + } + bool guarded = false; + for (const auto& block : controlFlow) { + for (const auto source : block.sources) { + guarded = guarded || reached[source] == 0u; + } + } + if (guarded) { + m_program.Resources().controlFlow = std::move(controlFlow); + } + } + static bool immediateU32(IrValue* value, std::uint32_t& result) { value = value->Resolve(); if (!value->HasImmediate() || value->Type() != IrType::U32) { @@ -1384,6 +1471,8 @@ class Tracker { ShaderInfo m_info; IrBuilder m_builder; std::vector m_sources; + std::vector> m_blockSources; + std::uint32_t m_currentBlock = std::numeric_limits::max(); std::vector m_handlePatches; std::vector m_memoryPatches; // APS5_TRACE_BDA: address accesses seen by Collect (the first bdaTraceLimit are printed). diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index bc89e71e9e0..1f4f84eb0d9 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -85,7 +86,14 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span Date: Sun, 11 Oct 2026 21:44:35 +0200 Subject: [PATCH 2/5] style(recompiler): drop the comment above the resource control flow planner Co-Authored-By: Claude Sonnet 5.5 --- core/shader/recompiler/Optimization/src/ResourceTracker.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 4cafa88f90e..2890479af88 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -269,12 +269,6 @@ class Tracker { } } - // A descriptor source that every path of the program reaches stays active; one that is used - // only behind a conditional branch can be left out when the branch condition is evaluable at - // capture time (see EvaluateRuntimeSourcesImpl), so a descriptor the untaken path would read - // as another resource kind is not validated. The edges are the structured terminators of the - // block metadata; a block whose terminator is not a plain or conditional branch keeps all of - // its CFG successors, so no source is ever dropped for a path the walk cannot decide. void PlanResourceControlFlow() { const auto& order = m_program.BlockOrder(); const auto& info = m_program.Metadata().blockInfo; From 37003b7a38e43a1f701bbff825c6d203469db373 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:03:49 +0200 Subject: [PATCH 3/5] fix(recompiler): track which branch uses each image, not only each descriptor source Several image instructions of different dimensions on the same T# address share one descriptor source, so a source stays active as soon as one of them is taken and the others are still checked against the T#. Record the images a block uses as well, deactivate the images used only behind a branch the capture does not take, and bind them as null descriptors. PPSA15246 (Teardown) then passes the "incompatible with the static runtime image interface" check of the three image_store variants that are not taken. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 16 +++++++++++ .../IrMetadata/ControlFlowInfo.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 5 ++++ .../Optimization/src/ResourceTracker.cpp | 28 +++++++++++++++++-- .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 8 +++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 00d9c38c974..18e2acf614b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -682,6 +682,22 @@ void verifyGuardedDescriptorSources() { const auto capture = memory.Capture(request); require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: a valid T# behind a skipped branch was read"); } + + // Two image_store instructions of different dimensions on the same T# address, one per branch of the mode word. + const std::vector storeCode{0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}; + auto storeRequest = request; + storeRequest.shader = {ShaderStage::Compute, 0x23000u, storeCode, 0, {}}; + const auto compileStore = [&](std::uint32_t mode) { + fill(valid, mode); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(storeRequest); + storeRequest.context.memory = memory.Regions(); + return Recompile(storeRequest, *capture); + }; + const auto storePlan = GetResourcePlan(storeRequest); + require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#"); + static_cast(compileStore(1u)); + expectFailure([&] { static_cast(compileStore(0u)); }, "incompatible with the static runtime image interface", "guarded images: a store of the wrong dimension behind a taken branch was accepted"); } // A compute program sampling a T# loaded from a table buffer at a runtime key (a bindless image diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index eed5b95514c..f9df378d48d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -62,6 +62,7 @@ struct ResourceBlock { IrValue* condition = nullptr; std::vector successors; std::vector sources; + std::vector images; }; } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 53118b192a2..0d0fc45d801 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -487,6 +487,11 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } + const auto activity = plan.descriptorSources.size() + i; + if (activity < activeSources.size() && activeSources[activity] == 0u) { + descriptor = DescriptorValue{}; + descriptor.dwordCount = 8u; + } if (!validImageDescriptor(descriptor, image.r128) && !nullImageDescriptor(descriptor)) { throw std::runtime_error("runtime image descriptor is invalid"); } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 2890479af88..6d5e2d6f88e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -119,6 +119,7 @@ class Tracker { blockIndices.emplace(block, static_cast(blockIndices.size())); } m_blockSources.assign(blockIndices.size(), {}); + m_blockImages.assign(blockIndices.size(), {}); for (auto& block : m_program.Blocks()) { const auto found = blockIndices.find(block.get()); m_currentBlock = found == blockIndices.end() ? std::numeric_limits::max() : found->second; @@ -259,6 +260,16 @@ class Tracker { return static_cast(m_sources.size() - 1); } + void NoteBlockImage(std::uint32_t image) { + if (m_currentBlock >= m_blockImages.size()) { + return; + } + auto& images = m_blockImages[m_currentBlock]; + if (std::ranges::find(images, image) == images.end()) { + images.push_back(image); + } + } + void NoteBlockSource(std::uint32_t source) { if (m_currentBlock >= m_blockSources.size()) { return; @@ -301,6 +312,7 @@ class Tracker { } } block.sources = m_blockSources[index]; + block.images = m_blockImages[index]; } std::vector unconditional(order.size()); for (std::uint32_t index = 0; index < order.size() && unconditional[index] == 0u;) { @@ -311,11 +323,16 @@ class Tracker { index = controlFlow[index].successors.front(); } std::vector reached(m_sources.size()); + std::vector reachedImages(m_info.images.size()); for (std::uint32_t index = 0; index < order.size(); index++) { + if (unconditional[index] == 0u) { + continue; + } for (const auto source : controlFlow[index].sources) { - if (unconditional[index] != 0u) { - reached[source] = 1u; - } + reached[source] = 1u; + } + for (const auto image : controlFlow[index].images) { + reachedImages[image] = 1u; } } bool guarded = false; @@ -323,6 +340,9 @@ class Tracker { for (const auto source : block.sources) { guarded = guarded || reached[source] == 0u; } + for (const auto image : block.images) { + guarded = guarded || reachedImages[image] == 0u; + } } if (guarded) { m_program.Resources().controlFlow = std::move(controlFlow); @@ -1411,6 +1431,7 @@ class Tracker { GetHandle(inst.Argument(0), IrOpcode::GetImageResource, 8, handle, source); } resource = AddImage(source, memory, op, flags.pc); + NoteBlockImage(resource); if (resource == std::numeric_limits::max()) { fail("image resource limit exceeded"); } @@ -1466,6 +1487,7 @@ class Tracker { IrBuilder m_builder; std::vector m_sources; std::vector> m_blockSources; + std::vector> m_blockImages; std::uint32_t m_currentBlock = std::numeric_limits::max(); std::vector m_handlePatches; std::vector m_memoryPatches; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 1f4f84eb0d9..e96dd416d71 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -64,13 +64,16 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span active; if (evaluateFlat) { - active.assign(program.descriptorSources.size(), 1u); + active.assign(program.descriptorSources.size() + program.info.images.size(), 1u); } if (evaluateFlat && !program.controlFlow.empty()) { for (const auto& block : program.controlFlow) { for (const auto source : block.sources) { active.at(source) = 0u; } + for (const auto image : block.images) { + active.at(program.descriptorSources.size() + image) = 0u; + } } std::vector visited(program.controlFlow.size()); std::vector pending {0}; @@ -85,6 +88,9 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span Date: Mon, 12 Oct 2026 02:53:53 +0200 Subject: [PATCH 4/5] fix(recompiler): evaluate pruning conditions without throwing on null roots A branch condition that reads through a null user-data pointer is now treated as not evaluable instead of being caught, so every successor stays active. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 7 ------- .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 17 ++++++++--------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e2acf614b..112e80af0c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -610,8 +610,6 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } -// A T# used only behind a branch on a word of the SRT: with the branch skipping the image, the -// T# is not validated (it may hold anything); with the branch taken it is. void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -637,10 +635,6 @@ void verifyGuardedDescriptorSources() { const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; RecompileRequest request{}; - // s_load_dword s16 (mode, SRT + 0x50); s_load_dwordx4 s[20:23] (S#) and s[24:27] (output V#); - // s_waitcnt; s_cmp_eq_u32 s16, 0; s_cbranch_scc1 over the image block; the image block is - // s_load_dwordx8 s[8:15] (T#), s_waitcnt, image_sample_lz v[0:3], s[8:15], s[20:23], s_waitcnt; - // buffer_store_dword v0, s[24:27]; s_endpgm. const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; request.context.waveSize = 32; @@ -683,7 +677,6 @@ void verifyGuardedDescriptorSources() { require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: a valid T# behind a skipped branch was read"); } - // Two image_store instructions of different dimensions on the same T# address, one per branch of the mode word. const std::vector storeCode{0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}; auto storeRequest = request; storeRequest.shader = {ShaderStage::Compute, 0x23000u, storeCode, 0, {}}; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index e96dd416d71..690bad731a8 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -77,6 +76,10 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span visited(program.controlFlow.size()); std::vector pending {0}; + InaccessibleRead conditionRead; + std::uint32_t conditionNullRoots = 0; + cleanEvaluator.ReportInaccessibleReads(&conditionRead); + cleanEvaluator.ReportNullRootReads(&conditionNullRoots); while (!pending.empty()) { const auto index = pending.back(); pending.pop_back(); @@ -92,20 +95,16 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span Date: Mon, 12 Oct 2026 20:59:12 +0200 Subject: [PATCH 5/5] docs(debt): move the resource tracker entries to debt/shader.md Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/shader.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1fd56400a03..d28e018661d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -12,6 +12,7 @@ ### Functional +- The [resource tracker](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans a [descriptor source](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) used only behind a conditional branch as inactive when the branch condition reads only clean (specialization) memory and so is evaluable at capture time; an inactive source is bound as zero words and not validated. Other conditions (EXEC, VCC, SCC set by vector or non-clean data, loop headers) leave every source active, as before, and a branch on a value that changes between dispatches is a specialization key like any other clean slot. PPSA15246 has a copy kernel that selects a buffer or an image path from an SRT word and reads the same address as a V# or a T#; before, the untaken path's descriptor was validated and threw. Whether the console would also accept an invalid descriptor in a path it does not take is assumed, not measured - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. - [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. @@ -36,7 +37,7 @@ - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) reads the initial rounding fields from `GuestContext::floatMode`; missing metadata retains the legacy round-to-nearest-even assumption. Immediate rounding writes are accepted only when they preserve that state. State-changing or SGPR-sourced MODE writes throw until runtime mode tracking and affected arithmetic are implemented. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. -- A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. +- A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw.