From 6aee0ff2a43c4da4dd76d77463cb5fafe4facb09 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Wed, 4 Nov 2026 14:41:32 +0100 Subject: [PATCH] perf(shader): skip fault checks dominated by the same check An invalid-descriptor fault check of a GPU-descriptor buffer access, and the swizzle-selector check of a formatted load, is not emitted when an identical check on the same descriptor under the same execution guard sits earlier in the same IR block or in a dominating block. The fault record, the invocation stop and the host error stay the same; only the pc stored for a fault that two invocations race to record can differ. APS5_NO_FAULT_CHECK_DEDUP restores every check. Co-Authored-By: Claude Opus 5.5 --- .../SpirvBackend/SpirvEmitterState.hpp | 7 ++ .../include/SpirvBackend/SpirvModule.hpp | 1 + .../SpirvBackend/SpirvRuntimeBuffer.hpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 2 + .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 98 +++++++++++++++++-- docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/shader.md | 1 + 7 files changed, 104 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 3adafd732e..faba616acf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -149,6 +149,13 @@ struct SpirvEmitterState { std::vector memoryByteOffsets; std::map, std::uint32_t> formattedBufferFunctions; std::map, std::uint32_t> formattedGpuBufferFunctions; + struct FaultCheckEntry { + const IrBlock* block = nullptr; + std::uint32_t guard = 0; + }; + std::map, std::vector> faultChecks; + std::unordered_map faultDominators; + bool faultCheckDedup = false; struct BufferLengthCacheEntry { std::array descriptor{}; std::uint32_t variable = 0; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index a793b54a0e..7c15a8dfcf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -59,6 +59,7 @@ public: void AddSubgroupBallot(std::uint32_t type, std::uint32_t result, std::uint32_t scope, std::uint32_t predicate); void BeginHelperFunction(); void EndHelperFunction(); + [[nodiscard]] bool InHelperFunction() const { return inHelperFunction; } [[nodiscard]] SpirvDeferredPhi AddDeferredPhi(std::uint32_t type, std::uint32_t result, std::size_t incomingCount); void PatchDeferredPhi(SpirvDeferredPhi phi, std::size_t incoming, std::uint32_t value, std::uint32_t parent); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvRuntimeBuffer.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvRuntimeBuffer.hpp index a76f780378..b2bc80efe9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvRuntimeBuffer.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvRuntimeBuffer.hpp @@ -6,6 +6,7 @@ namespace ShaderRecompiler { +void PlanFaultCheckDominators(SpirvEmitterState& state, const IrProgram& program); std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components); void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components); std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 892a2ae2a3..20dec7d4d0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -2,6 +2,7 @@ #include "PipelineSpecialization.hpp" #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvRuntimeBuffer.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include #include @@ -851,6 +852,7 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu state.module.AddFunction(spv::OpStore, state.loopGuardVisits, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, state.loopGuardPc, ConstantU32(state, 0u)); } + PlanFaultCheckDominators(state, program); state.module.AddFunction(spv::OpBranch, context.Label(blocks.front())); static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr; if (debug) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 5d3bf630b3..46a1842e81 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -9,7 +9,9 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include +#include #include +#include #include #include #include @@ -107,7 +109,35 @@ void faultIf(SpirvValueEmitContext& context, const IrValue& instruction, std::ui StopBdaInvocationIf(state, condition); } -RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& instruction) { +bool faultBlockDominates(const SpirvEmitterState& state, const IrBlock* dominator, const IrBlock* block) { + while (block != nullptr) { + if (block == dominator) return true; + const auto found = state.faultDominators.find(block); + if (found == state.faultDominators.end() || found->second == block) return false; + block = found->second; + } + return false; +} + +bool faultCheckCovered(SpirvEmitterState& state, std::vector key, std::uint32_t guard) { + if (!state.faultCheckDedup || state.currentBlock == nullptr || state.module.InHelperFunction()) return false; + auto& entries = state.faultChecks[std::move(key)]; + for (const auto& entry : entries) { + if (entry.guard != 0u && entry.guard != guard) continue; + if (faultBlockDominates(state, entry.block, state.currentBlock)) return true; + } + entries.push_back({state.currentBlock, guard}); + return false; +} + +std::vector faultCheckKey(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t check) { + const auto* handle = instruction.Argument(0)->Resolve(); + const auto& memory = context.Memory(instruction); + const bool based = memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic; + return {check, memory.resource, memory.gpuDescriptor ? 1u : 0u, based ? 1u : 0u, static_cast(memory.kind), context.Arg(*handle, 0u), context.Arg(*handle, 1u), context.Arg(*handle, 2u), context.Arg(*handle, 3u)}; +} + +RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t guard) { auto& state = context.state; const auto* handle = instruction.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#"); @@ -139,7 +169,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst invalid = Binary(state, spv::OpLogicalOr, TypeBool(state), invalid, addTid); } } - if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); + const bool covered = memory.gpuDescriptor && memory.kind != ResourceKind::ScalarBuffer && faultCheckCovered(state, faultCheckKey(context, instruction, 8u), guard); + if (memory.gpuDescriptor && !covered) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); auto baseHigh = runtime ? word1 : context.Arg(*handle, 1u); if (!runtime && GcnGuestShaders()) baseHigh = Binary(state, spv::OpBitwiseAnd, u32, baseHigh, ConstantU32(state, GcnBufferWord1Mask)); @@ -712,7 +743,7 @@ std::uint32_t formattedDispatch(SpirvValueEmitContext& context, const IrValue& i const auto selector = field(state, resource.word3, component * 3u, 3u); invalid = Binary(state, spv::OpLogicalOr, TypeBool(state), invalid, Binary(state, spv::OpLogicalOr, TypeBool(state), equal(state, selector, ConstantU32(state, 2u)), equal(state, selector, ConstantU32(state, 3u)))); } - faultIf(context, instruction, invalid, resource.base, 0u, BdaAbi::FaultReason::InvalidDescriptor); + if (!faultCheckCovered(state, faultCheckKey(context, instruction, 9u + components), context.Arg(instruction, instruction.ArgumentCount() - 1u))) faultIf(context, instruction, invalid, resource.base, 0u, BdaAbi::FaultReason::InvalidDescriptor); } const auto* word = instruction.Argument(0)->Resolve()->Argument(3)->Resolve(); if (memory.gpuDescriptor && word->HasImmediate()) { @@ -945,13 +976,66 @@ std::optional coherentPair(SpirvValueEmitContext& context, const I } +void PlanFaultCheckDominators(SpirvEmitterState& state, const IrProgram& program) { + state.faultChecks.clear(); + state.faultDominators.clear(); + state.faultCheckDedup = std::getenv("APS5_NO_FAULT_CHECK_DEDUP") == nullptr; + if (!state.faultCheckDedup) return; + const auto& order = program.BlockOrder(); + if (order.empty() || !order.front()->Predecessors().empty()) return; + std::vector postorder; + std::unordered_map number; + std::vector> stack{{order.front(), 0u}}; + std::unordered_map seen{{order.front(), true}}; + while (!stack.empty()) { + auto& [block, next] = stack.back(); + const auto& successors = block->Successors(); + if (next < successors.size()) { + const IrBlock* successor = successors[next++]; + if (!seen[successor]) { + seen[successor] = true; + stack.push_back({successor, 0u}); + } + continue; + } + number[block] = static_cast(postorder.size()); + postorder.push_back(block); + stack.pop_back(); + } + auto& idom = state.faultDominators; + idom[order.front()] = order.front(); + const auto intersect = [&](const IrBlock* left, const IrBlock* right) { + while (left != right) { + while (number.at(left) < number.at(right)) left = idom.at(left); + while (number.at(right) < number.at(left)) right = idom.at(right); + } + return left; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto at = postorder.rbegin(); at != postorder.rend(); ++at) { + const IrBlock* block = *at; + if (block == order.front()) continue; + const IrBlock* chosen = nullptr; + for (const IrBlock* predecessor : block->Predecessors()) { + if (!idom.contains(predecessor)) continue; + chosen = chosen == nullptr ? predecessor : intersect(predecessor, chosen); + } + if (chosen != nullptr && (!idom.contains(block) || idom.at(block) != chosen)) { + idom[block] = chosen; + changed = true; + } + } + } +} + std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t components) { auto& state = context.state; const auto& memory = context.Memory(instruction); const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); return EmitValueOrDefaultIfCondition(state, context.Arg(instruction, instruction.ArgumentCount() - 1u), type, zero, [&] { - const auto resource = descriptor(context, instruction); + const auto resource = descriptor(context, instruction, context.Arg(instruction, instruction.ArgumentCount() - 1u)); if (memory.formatted) return EmitValueOrDefaultIfCondition(state, resource.valid, type, zero, [&] { return formattedDispatch(context, instruction, resource, components, false, 0u); }); const auto readComponents = [&] { std::array values{}; @@ -978,7 +1062,7 @@ void EmitRuntimeBufferStore(SpirvValueEmitContext& context, const IrValue& instr auto& state = context.state; const auto& memory = context.Memory(instruction); EmitIfCondition(state, context.Arg(instruction, instruction.ArgumentCount() - 1u), [&] { - const auto resource = descriptor(context, instruction); + const auto resource = descriptor(context, instruction, context.Arg(instruction, instruction.ArgumentCount() - 1u)); if (memory.formatted) { EmitIfCondition(state, resource.valid, [&] { static_cast(formattedDispatch(context, instruction, resource, components, true, storeData(context, instruction))); }); return; @@ -1010,7 +1094,7 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const auto& state = context.state; if (std::ranges::find(state.supportedCapabilities, spv::CapabilityInt64) == state.supportedCapabilities.end()) context.Fail(instruction, "scalar buffer bounds require shaderInt64"); state.module.EmitCapability(spv::CapabilityInt64); - const auto resource = descriptor(context, instruction); + const auto resource = descriptor(context, instruction, 0u); const auto u64 = TypeScalarU64(state); const auto size = Select(state, u64, equal(state, resource.stride, ConstantU32(state, 0u)), Unary(state, spv::OpUConvert, u64, resource.records), Binary(state, spv::OpIMul, u64, Unary(state, spv::OpUConvert, u64, resource.stride), Unary(state, spv::OpUConvert, u64, resource.records))); const auto address = EmitAddU32(state, context.Arg(instruction, 1u), ConstantU32(state, context.Memory(instruction).offset)); @@ -1047,7 +1131,7 @@ static std::uint32_t RuntimeBufferAtomic(SpirvValueEmitContext& context, const I const auto scalar = wide ? TypeScalarU64(state) : TypeU32(state); const auto zero = wide ? ConstantU64(state, 0u) : ConstantU32(state, 0u); return EmitValueOrDefaultIfCondition(state, active, type, zero, [&] { - const auto resource = descriptor(context, instruction); + const auto resource = descriptor(context, instruction, active); const auto access = address(context, instruction, resource, 0u, bits / 8u); return EmitValueOrDefaultIfCondition(state, access.inBounds, type, zero, [&] { if (!context.Memory(instruction).gpuDescriptor) { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 1e13b52662..d237a1463e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -46,6 +46,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_BUFFER_SUBSTITUTES` | presence | `core/shader/recompiler/Recompiler.cpp` | | specializes a null buffer V# with its own words instead of the fields its slot had the first time a dispatch of the same program bound it | | `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | | `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | +| `APS5_NO_FAULT_CHECK_DEDUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp` | | emits every invalid-descriptor fault check of a GPU-descriptor buffer access instead of skipping one that an identical check on the same descriptor under the same execution guard already covers in the same IR block or a dominating block; read once per program | | `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | | `APS5_NO_MASKED_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes every result memo miss instead of patching the buffer descriptors, user data and flattened SRT of a cached result whose snapshot differs only in those words | | `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 03f6b6d33e..d66bfc615f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -250,3 +250,4 @@ - [NGG host subgroup layout](../../../core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp) maps wave64 mesh programs to 32- or 64-lane host subgroups; narrower host subgroups have no verified wave64 mapping. The layered-volume mesh execution regression requires those supported widths. Vertex execution and mesh compilation remain separate checks; compile-only coverage does not establish execution on narrower hosts. - [T#s with an unset format field](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (shader recompiler) - a directly bound sampled read that is neither written nor atomic decodes like a null descriptor when its TYPE is an image type the decode implements and its format field names no format (0 or 0x1ff: the zero-format and 0xffffffff-filled unbound slots PPSA02721 samples), in the compile-time decode and the materialization; a reserved or non-image TYPE keeps its rejection however its format reads, as do storage and atomic images with an unset format, a TYPE the driver does not decode, and an image-table entry. What the console returns is not measured; the null decode follows the all-zero descriptor rule. The driver's [IsNullSampledTextureResource](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) applies the same rule to sampled consumers alone; storage bindings and written-buffer tracking keep the base-and-type null rule. +- [Fault check dedup](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) - the pc stored for an invalid-descriptor fault differs from the unmerged module when two invocations race to record the first fault; the pc of a skipped check is never recorded. The formatted swizzle-selector check and loops are not covered by tests. Each IR block is assumed to be emitted once. Checks are not merged across blocks when the entry block has predecessors, in helper functions or in blocks the dominator pass does not reach; scalar buffer reads have no check to merge. `APS5_NO_FAULT_CHECK_DEDUP` restores every check. Closes when: a test covers the swizzle-selector check and a loop.