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 <[email protected]>
This commit is contained in:
Alex ColliniandClaude Opus 5.5 committed 2026-11-04 15:00:27 +01:00
1 parent b11c7bf84f
commit 6aee0ff2a4
7 files changed
+104 -7

No files matched your search

@@ -149,6 +149,13 @@ struct SpirvEmitterState {
std::vector<std::uint32_t> memoryByteOffsets;
std::map<std::array<std::uint32_t, 6>, std::uint32_t> formattedBufferFunctions;
std::map<std::array<std::uint32_t, 6>, std::uint32_t> formattedGpuBufferFunctions;
struct FaultCheckEntry {
const IrBlock* block = nullptr;
std::uint32_t guard = 0;
};
std::map<std::vector<std::uint32_t>, std::vector<FaultCheckEntry>> faultChecks;
std::unordered_map<const IrBlock*, const IrBlock*> faultDominators;
bool faultCheckDedup = false;
struct BufferLengthCacheEntry {
std::array<const IrValue*, 4> descriptor{};
std::uint32_t variable = 0;
@@ -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);
@@ -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);
@@ -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 <cstdio>
#include <cstdlib>
@@ -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) {
@@ -9,7 +9,9 @@
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <span>
#include <unordered_map>
#include <utility>
#include <string>
#include <vector>
@@ -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<std::uint32_t> 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<std::uint32_t> 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<std::uint32_t>(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> 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<const IrBlock*> postorder;
std::unordered_map<const IrBlock*, std::uint32_t> number;
std::vector<std::pair<const IrBlock*, std::size_t>> stack{{order.front(), 0u}};
std::unordered_map<const IrBlock*, bool> 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<std::uint32_t>(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<std::uint32_t, 4> 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<void>(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) {
+1
View File
@@ -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 |
+1
View File
@@ -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.