feat(agc): implement deferred faults for reads through null SRT pointers

Capturing a shader threw "null or misaligned address" when the flat
SRT walk read through a pointer loaded from memory that is null or
unmapped, even when the program skips that s_load. Such a read now
yields zero and a record with its pc and address, as does a slot whose
address needs it. The registered artifact checks one flag word per
guarded slot at each ReadConst; the capture appends the flags and the
records to the flattened SRT, and a set flag raises an Unmapped BDA
fault through the fault buffer and stops the invocation as a BDA read
does. Nothing per capture enters a key: poisoned and clean captures
share one variant and specialization. Descriptors, branch conditions,
uniform fills, table bases and user-data pointers that need such a
value still throw, and poisoned results stay out of the dispatch and
draw caches. The shader disk cache format becomes 21.
This commit is contained in:
Celegans12 2026-10-09 00:16:09 +02:00
parent 1be6f9f1c5
commit 56f8a62428
38 changed files with 715 additions and 24 deletions

View File

@ -82,6 +82,8 @@ struct DataMask {
enum class EntryOutcome { Equal, EqualData, Differing, Inaccessible, QueuedLabel, FlushingImage, PublishMoved, PendingMoved, ForgetMoved };
bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled);
}
#endif

View File

@ -85,6 +85,7 @@ private:
};
static bool read(void* context, std::uint64_t address, std::uint32_t* value);
static bool accessible(void* context, std::uint64_t address, std::uint64_t bytes);
Page& page(std::uint64_t base);
// Regions given at construction (the registered shader's code and header), referenced as given:

View File

@ -231,7 +231,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
recompileMs += phaseTiming.Elapsed();
phaseTiming.Phase(PhaseRecompile);
insertDispatch(address, key, noDispatchCache, profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming);
insertDispatch(address, key, noDispatchCache || !CacheableResult(*compiledResult), profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming);
}
if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult);

View File

@ -6,6 +6,10 @@
namespace AgcDriver::DriverDetail {
bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled) {
return compiled.poisonedSrtReads == 0u;
}
void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDispatchCache, bool profile, const std::shared_ptr<const ShaderSnapshot>& registeredShader, std::uint64_t forgetAtCapture, std::span<const ShaderRecompiler::MemoryRegion> memory, const std::shared_ptr<ShaderMemory>& shaderMemory, const std::vector<ShaderRecompiler::MemoryRegion>& captured, const std::shared_ptr<const ShaderRecompiler::ResourceCapture>& capture, const std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, const std::shared_ptr<DispatchEntry>& missedEntry, bool missedDiffering, std::shared_ptr<DispatchVariant>& attachVariant, DispatchPhaseTiming& phaseTiming) {
if (!noDispatchCache) {
auto fresh = std::make_shared<DispatchVariant>();

View File

@ -88,7 +88,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
std::uint64_t unstable = 0, mismatches = 0;
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& stageCapture = stageCaptures[i];
if (stageCapture.compiled == nullptr) continue;
if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue;
auto variant = std::make_shared<DispatchVariant>();
variant->compiled = stageCapture.compiled;
variant->shader = programs[i].snapshot;

View File

@ -201,6 +201,28 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
return true;
}
bool ShaderMemory::accessible(void* context, std::uint64_t address, std::uint64_t bytes) {
auto& self = *static_cast<ShaderMemory*>(context);
if (bytes == 0 || address > std::numeric_limits<std::uint64_t>::max() - bytes) return false;
const auto end = address + bytes;
while (address < end) {
const auto next = self.initial.upper_bound(address);
if (next != self.initial.begin()) {
const auto previous = std::prev(next);
if (address - previous->first < previous->second.size()) {
address = previous->first + previous->second.size();
continue;
}
}
const auto base = address & ~static_cast<std::uint64_t>(PageBytes - 1);
const auto pageEnd = std::min<std::uint64_t>(base + PageBytes, end);
const auto& page = self.page(base);
if (!page.wordwise && !page.valid.all() && !GuestMemory::Accessible(reinterpret_cast<const void*>(address), static_cast<std::size_t>(pageEnd - address))) return false;
address = pageEnd;
}
return true;
}
ShaderMemory::KnownValueCounts ShaderMemory::KnownValues() {
auto& totals = CaptureTotals();
return {totals.wordsKnown.load(std::memory_order_relaxed), totals.wordsKnownVerified.load(std::memory_order_relaxed), totals.wordsKnownMismatches.load(std::memory_order_relaxed)};
@ -233,6 +255,7 @@ std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::capture(c
runtime.userContext = this;
runtime.readMemory = &read;
runtime.readSpecializationMemory = &read;
runtime.accessible = &accessible;
auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
if (profile) {
totals.captureNanoseconds += NanosecondsSince(started);

View File

@ -15,8 +15,8 @@ void ShaderResources::prepareAddressBindings(std::span<const CompiledShader> sha
if (binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable) ++tables;
else ++faults;
}
const bool rectListFault = shader.stage == ShaderRecompiler::ShaderStage::TessellationControl && tables == 0 && faults == 1;
Require((tables == faults || rectListFault) && tables <= 1 && faults <= 1, "invalid BDA table and fault descriptors");
const bool faultOnly = tables == 0 && faults == 1 && shader.program->bdaAbiVersion == ShaderRecompiler::BdaAbi::Version;
Require((tables == faults || faultOnly) && tables <= 1 && faults <= 1, "invalid BDA table and fault descriptors");
Require(shader.program->bdaAbiVersion == (faults == 0 ? 0u : ShaderRecompiler::BdaAbi::Version), "incompatible BDA ABI version");
// Rect-list validation needs a fault buffer, but never accesses guest addresses.
usesBda = usesBda || tables != 0;

View File

@ -79,6 +79,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right
require(sameBindings(left.bindings, right.bindings), prefix + "bindings differ");
require(left.pushConstants == right.pushConstants, prefix + "push constants differ");
require(left.specialization == right.specialization, prefix + "specialization constants differ");
require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ");
require(left.vertexAttributes.size() == right.vertexAttributes.size(), prefix + "vertex attribute count differs");
for (std::size_t i = 0; i < left.vertexAttributes.size(); ++i) {
const auto& a = left.vertexAttributes[i];
@ -149,6 +150,7 @@ RecompileResult sampleResult() {
result.instanceOffsetConflict = true;
result.parameterExports = {0, 3, 7};
result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}};
result.poisonedSrtReads = 3;
result.variantId = 99;
return result;
}
@ -220,6 +222,7 @@ CompiledVariant sampleVariant() {
info.info.vertexOffsetSgpr = 6;
info.info.hasBitwiseXor = true;
info.info.usesDma = true;
info.info.usesFaultBuffer = true;
info.bindings.pushDataStartDword = 2;
info.bindings.memoryOffsetDword = 1;
info.bindings.memoryOffsetCount = 5;

View File

@ -29,6 +29,17 @@
#include <tuple>
#include <utility>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
namespace {
@ -1761,6 +1772,182 @@ void verifyFunctionLdsBound() {
require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array");
}
struct NestedRequest {
std::vector<std::uint32_t> code;
std::array<std::uint32_t, 2> userData{};
ShaderRecompiler::RecompileRequest request{};
NestedRequest(std::vector<std::uint32_t> words, const void* srt) : code(std::move(words)) {
using namespace ShaderRecompiler;
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(srt));
userData = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)};
request.shader = {ShaderStage::Compute, 0x60000u + static_cast<std::uint64_t>(code.size()) * 4u, code, 0, {}};
request.context.waveSize = 64;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target = BufferTarget();
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.fragmentShaderBarycentricEnabled = false;
request.layout.pushConstantSizeBytes = 128;
}
};
bool RegionsCover(const std::vector<ShaderRecompiler::MemoryRegion>& regions, const void* pointer, std::size_t bytes) {
auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer));
const auto end = address + bytes;
while (address < end) {
const auto region = std::find_if(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& candidate) { return address >= candidate.guestAddress && address < candidate.guestAddress + candidate.bytes.size(); });
if (region == regions.end()) return false;
address = region->guestAddress + region->bytes.size();
}
return true;
}
bool RegionsTouch(const std::vector<ShaderRecompiler::MemoryRegion>& regions, std::uint64_t address, std::size_t bytes) {
return std::any_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return region.guestAddress < address + bytes && address < region.guestAddress + region.bytes.size(); });
}
bool GuardRecords(const ShaderRecompiler::ResourceCapture& capture) {
const auto& plan = *capture.plan;
const auto& flat = capture.snapshot.flattenedSrt;
const auto& poison = capture.snapshot.srtPoison;
const auto flags = plan.srtReads.size();
const auto guards = plan.guardedSrtSlots.size();
if (flat.size() != flags + guards + 3u * poison.size()) return false;
for (std::size_t guard = 0; guard < guards; ++guard) {
const auto record = std::find_if(poison.begin(), poison.end(), [&](const ShaderRecompiler::SrtReadPoison& entry) { return entry.slot == plan.guardedSrtSlots[guard]; });
const auto expected = record == poison.end() ? 0u : static_cast<std::uint32_t>(record - poison.begin()) + 1u;
if (flat[flags + guard] != expected) return false;
}
for (std::size_t record = 0; record < poison.size(); ++record) {
const auto* words = flat.data() + flags + guards + 3u * record;
if (words[0] != poison[record].pc || words[1] != static_cast<std::uint32_t>(poison[record].address) || words[2] != static_cast<std::uint32_t>(poison[record].address >> 32u) || flat.at(poison[record].slot) != 0u) return false;
}
return true;
}
void verifyGuardedNullPointers() {
using namespace ShaderRecompiler;
constexpr std::size_t Reserved = 0x10000;
void* reserved = nullptr;
#ifdef _WIN32
reserved = VirtualAlloc(nullptr, Reserved, MEM_RESERVE, PAGE_NOACCESS);
#else
reserved = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (reserved == MAP_FAILED) reserved = nullptr;
#endif
require(reserved != nullptr, "guarded pointer: cannot reserve an inaccessible range");
const auto unmapped = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(reserved));
alignas(256) static std::array<std::uint32_t, 64> output{};
alignas(256) static std::array<std::uint32_t, 16> root{};
static std::uint32_t payload = 0x3f800000u;
static std::uint64_t indirect = 0;
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
root = {static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), static_cast<std::uint32_t>(sizeof(output)), 0x30027facu, 1u};
const auto point = [&](std::uint64_t address) {
root[6] = static_cast<std::uint32_t>(address);
root[7] = static_cast<std::uint32_t>(address >> 32u);
};
const auto payloadAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&payload));
const std::vector<std::uint32_t> branchCode{0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u};
const std::vector<std::uint32_t> chainCode{0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u,
0xe0701000u, 0x80010102u, 0xbf810000u};
const std::vector<std::uint32_t> descriptorCode{0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u,
0x80010002u, 0xbf810000u};
const auto slotPc = [](const IrResourcePlan& plan, std::uint32_t slot) { return plan.srtReads.at(slot).value->Flags<MemoryFlags>().pc; };
const auto faultBinding = [](const RecompileResult& result) {
return std::any_of(result.bindings.begin(), result.bindings.end(), [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::FaultBuffer; });
};
const auto samePipeline = [](const RecompileResult& left, const RecompileResult& right) {
return left.variantId == right.variantId && left.specializationId == right.specializationId && left.PipelineVariantId() == right.PipelineVariantId() && left.spirv == right.spirv;
};
NestedRequest branch(branchCode, root.data());
point(payloadAddress);
AgcDriver::ShaderMemory validMemory({});
const auto valid = validMemory.Capture(branch.request);
require(valid->snapshot.srtPoison.empty() && valid->plan->guardedSrtSlots.size() == 1u && slotPc(*valid->plan, valid->plan->guardedSrtSlots[0]) == 0x2cu && GuardRecords(*valid), "guarded pointer: a mapped nested pointer was poisoned, or its read has no guard");
require(valid->snapshot.flattenedSrt.at(valid->plan->guardedSrtSlots[0]) == payload, "guarded pointer: a mapped nested read does not hold its word");
branch.request.context.memory = validMemory.Regions();
const auto validResult = Recompile(branch.request, *valid);
require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer");
for (const auto pointer : {std::uint64_t{0}, unmapped}) {
for (const auto condition : {1u, 0u}) {
point(pointer);
root[4] = condition;
branch.request.context.memory = {};
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(branch.request);
const auto& poison = capture->snapshot.srtPoison;
require(poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: the nested read was not poisoned at its pc and address");
require(GuardRecords(*capture), "guarded pointer: the poison record is not where the guard reads it");
const auto regions = memory.Regions();
require(!RegionsTouch(regions, pointer, 4u) && RegionsCover(regions, root.data() + 6, 8u), "guarded pointer: the captured regions are wrong");
branch.request.context.memory = regions;
const auto result = Recompile(branch.request, *capture);
require(result->poisonedSrtReads == 1u && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the poisoned capture does not report through the fault buffer");
require(samePipeline(*result, *validResult), "guarded pointer: poison changed the variant, the specialization or the module");
const auto replay = Recompile(branch.request);
require(replay.poisonedSrtReads == 1u && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison");
verifyResult(*result, replay);
#if ANYPS5_ENABLE_SPIRV_TOOLS
static_cast<void>(ValidateAndOptimizeSpirv(result->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion));
#endif
}
}
root[4] = 1u;
NestedRequest chain(chainCode, root.data());
point(unmapped);
AgcDriver::ShaderMemory chainMemory({});
const auto chained = chainMemory.Capture(chain.request);
const auto& chainPoison = chained->snapshot.srtPoison;
require(chainPoison.size() == 3u && GuardRecords(*chained), "guarded pointer: a pointer read through an unmapped pointer did not poison its two words and the read through it");
for (const auto& poison : chainPoison) {
const auto own = slotPc(*chained->plan, poison.slot);
const bool pointerWord = own == 0x2cu && (poison.address == unmapped || poison.address == unmapped + 4u);
const bool throughPointer = own == 0x38u && poison.address == unmapped;
require(poison.pc == 0x2cu && (pointerWord || throughPointer), "guarded pointer: a derived poison does not name the inaccessible read");
}
chain.request.context.memory = chainMemory.Regions();
const auto chainResult = Recompile(chain.request, *chained);
indirect = 0;
point(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&indirect)));
AgcDriver::ShaderMemory nullChainMemory({});
const auto nullChain = nullChainMemory.Capture(chain.request);
require(nullChain->snapshot.srtPoison.size() == 1u && nullChain->snapshot.srtPoison[0].pc == 0x38u && nullChain->snapshot.srtPoison[0].address == 0u && GuardRecords(*nullChain), "guarded pointer: a null pointer read from memory did not poison the read through it");
chain.request.context.memory = nullChainMemory.Regions();
const auto nullChainResult = Recompile(chain.request, *nullChain);
indirect = payloadAddress;
AgcDriver::ShaderMemory mappedChainMemory({});
const auto mappedChain = mappedChainMemory.Capture(chain.request);
require(mappedChain->snapshot.srtPoison.empty() && mappedChain->plan->guardedSrtSlots.size() == 3u && GuardRecords(*mappedChain), "guarded pointer: a mapped pointer chain was poisoned, or its reads have no guards");
chain.request.context.memory = mappedChainMemory.Regions();
const auto mappedChainResult = Recompile(chain.request, *mappedChain);
require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads");
require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module");
NestedRequest descriptor(descriptorCode, root.data());
point(0u);
AgcDriver::ShaderMemory descriptorMemory({});
expectFailure([&] { static_cast<void>(descriptorMemory.Capture(descriptor.request)); }, "null or misaligned address", "guarded pointer: a V# loaded through a null nested pointer was accepted");
NestedRequest rootless(branchCode, nullptr);
AgcDriver::ShaderMemory rootlessMemory({});
expectFailure([&] { static_cast<void>(rootlessMemory.Capture(rootless.request)); }, "null or misaligned address", "guarded pointer: a null user-data pointer was accepted");
point(0u);
#ifdef _WIN32
VirtualFree(reserved, 0, MEM_RELEASE);
#else
munmap(reserved, Reserved);
#endif
}
int main(int argc, char** argv) {
try {
using namespace ShaderRecompiler;
@ -1793,6 +1980,7 @@ int main(int argc, char** argv) {
verifyTwoLaneUniformValues();
verifyBdaReadFallbackFunctions();
verifyFunctionLdsBound();
verifyGuardedNullPointers();
#if ANYPS5_ENABLE_SPIRV_TOOLS
const std::vector<std::uint32_t> minimalSpirv{
0x07230203u, 0x00010000u, 0u, 5u, 0u,
@ -1979,8 +2167,18 @@ int main(int argc, char** argv) {
expectFailure([&] { static_cast<void>(PrepareResourceProgram(request)); }, "shader user data exceeds the scalar register bank", "user data overran scalar register bank");
request.context.userDataBaseRegister = 8;
request.context.memory = {};
AgcDriver::ShaderMemory guarded({});
const auto poisoned = guarded.Capture(request);
require(poisoned->snapshot.srtPoison.size() == 1u && poisoned->snapshot.srtPoison[0].pc == 8u && poisoned->snapshot.srtPoison[0].address == 0u && GuardRecords(*poisoned), "null nested pointer was not poisoned at its read");
const auto guardedRegions = guarded.Regions();
request.context.memory = guardedRegions;
const auto poisonedVertex = Recompile(request, *poisoned);
require(poisonedVertex->poisonedSrtReads == 1u && poisonedVertex->bdaAbiVersion == BdaAbi::Version && poisonedVertex->PipelineVariantId() == first.PipelineVariantId() && std::any_of(poisonedVertex->bindings.begin(), poisonedVertex->bindings.end(), [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::FaultBuffer; }), "a vertex shader reading through a null nested pointer has no fault buffer or another variant");
request.context.memory = {};
const std::array<std::uint32_t, 2> nullUserData{};
request.context.userData = nullUserData;
AgcDriver::ShaderMemory invalid({});
expectFailure([&] { invalid.Capture(request); }, "null or misaligned address", "null nested pointer was accepted");
expectFailure([&] { invalid.Capture(request); }, "null or misaligned address", "null user-data pointer was accepted");
std::cout << "Shader memory capture, strict validation and deterministic replay passed\n";
return 0;
} catch (const std::exception& error) {

View File

@ -0,0 +1,244 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <io.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Lanes = 64;
constexpr std::uint32_t Default = 0x11111111u;
constexpr std::uint32_t Payload = 0x3f800000u;
alignas(256) std::array<std::uint32_t, Lanes> Output{};
alignas(256) std::array<std::uint32_t, 16> Root{};
alignas(256) std::array<std::uint32_t, 4> Data{Payload, 0u, 0u, 0u};
alignas(256) std::array<std::uint64_t, 1> Indirect{};
alignas(256) constexpr std::array<std::uint32_t, 19> BranchCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u,
0xe0701000u, 0x80010102u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 21> ExecCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0x7da80008u, 0xbf880004u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu,
0x7e02020cu, 0xbefe04c1u, 0xe0701000u, 0x80010102u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 21> EmptyExecCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0x7da80008u, 0xbf800000u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu,
0x7e02020cu, 0xbefe04c1u, 0xe0701000u, 0x80010102u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 22> ChainCode{
0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u,
0xbf8cc07fu, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000307u, 0xfa000000u,
0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u,
};
class StderrCapture {
public:
StderrCapture() {
std::fflush(stderr);
file = std::tmpfile();
Require(file != nullptr, "cannot open a temporary file for stderr");
#ifdef _WIN32
saved = _dup(_fileno(stderr));
_dup2(_fileno(file), _fileno(stderr));
#else
saved = dup(fileno(stderr));
dup2(fileno(file), fileno(stderr));
#endif
}
std::string Finish() {
std::fflush(stderr);
#ifdef _WIN32
_dup2(saved, _fileno(stderr));
_close(saved);
#else
dup2(saved, fileno(stderr));
close(saved);
#endif
std::rewind(file);
std::string text;
char buffer[4096];
for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read);
std::fclose(file);
std::fwrite(text.data(), 1, text.size(), stderr);
return text;
}
private:
std::FILE* file = nullptr;
int saved = -1;
};
struct Outcome {
std::vector<std::uint32_t> words;
std::string log;
std::uint64_t variantId = 0;
std::uint64_t pipelineVariantId = 0;
std::uint32_t poisoned = 0;
bool cacheable = false;
};
template <std::size_t CodeWords>
Outcome Run(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, CodeWords>& code, std::uint64_t pointer, std::uint32_t guard) {
const auto output = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Output.data()));
Root = {};
Root[0] = static_cast<std::uint32_t>(output);
Root[1] = static_cast<std::uint32_t>((output >> 32u) & 0xffffu);
Root[2] = static_cast<std::uint32_t>(sizeof(Output));
Root[3] = 0x30027facu;
Root[4] = guard;
Root[6] = static_cast<std::uint32_t>(pointer);
Root[7] = static_cast<std::uint32_t>(pointer >> 32u);
std::fill(Output.begin(), Output.end(), 0xdeadbeefu);
const auto root = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Root.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(root), static_cast<std::uint32_t>(root >> 32u)};
const std::span<const std::uint32_t> span(code);
const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(span.data()), span, 0, {}},
{64, 0, userData, compute, std::nullopt, std::nullopt, {}},
device.Target(),
{0, 0, 0, 128}
};
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
const auto regions = memory.Regions();
request.context.memory = regions;
const auto result = ShaderRecompiler::Recompile(request, *capture);
Outcome outcome;
outcome.variantId = result->variantId;
outcome.pipelineVariantId = result->PipelineVariantId();
outcome.poisoned = result->poisonedSrtReads;
outcome.cacheable = AgcDriver::DriverDetail::CacheableResult(*result);
StderrCapture log;
try {
device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(span.data()));
device.WaitIdle();
} catch (...) {
static_cast<void>(log.Finish());
throw;
}
outcome.log = log.Finish();
outcome.words.assign(Output.begin(), Output.end());
return outcome;
}
bool Faulted(const Outcome& outcome) {
return outcome.log.find("BDA access failed") != std::string::npos;
}
std::string Fault(std::uint64_t address, std::uint32_t pc) {
char text[96];
std::snprintf(text, sizeof(text), "address=0x%llx instruction=0x%x bytes=4", static_cast<unsigned long long>(address), pc);
return text;
}
void RequireWords(const Outcome& outcome, std::uint32_t lanes, std::uint32_t inside, const std::string& what) {
for (std::uint32_t lane = 0; lane < Lanes; ++lane) {
const auto expected = lane < lanes ? inside : Default;
Require(outcome.words[lane] == expected, what + ": lane " + std::to_string(lane) + " wrote " + std::to_string(outcome.words[lane]) + ", expected " + std::to_string(expected));
}
}
void RunTests(AgcDriver::VulkanDevice& device) {
const auto valid = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Data.data()));
void* reserved = nullptr;
#ifdef _WIN32
reserved = VirtualAlloc(nullptr, 65536, MEM_RESERVE, PAGE_NOACCESS);
#else
reserved = mmap(nullptr, 65536, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (reserved == MAP_FAILED) reserved = nullptr;
#endif
Require(reserved != nullptr, "cannot reserve an inaccessible range");
const auto unmapped = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(reserved));
const auto taken = Run(device, BranchCode, valid, 1u);
Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "guarded pointer: a mapped pointer faulted, was poisoned or left the caches:\n" + taken.log);
RequireWords(taken, Lanes, Payload, "guarded pointer: a mapped pointer under a taken branch");
const auto skippedNull = Run(device, BranchCode, 0u, 0u);
Require(!Faulted(skippedNull) && skippedNull.poisoned == 1u && !skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted or stayed cacheable:\n" + skippedNull.log);
RequireWords(skippedNull, 0u, 0u, "guarded pointer: a skipped read through a null pointer");
const auto takenNull = Run(device, BranchCode, 0u, 1u);
Require(takenNull.pipelineVariantId == skippedNull.pipelineVariantId && takenNull.pipelineVariantId == taken.pipelineVariantId && takenNull.variantId == taken.variantId, "guarded pointer: the poison or the branch condition changed the pipeline variant");
Require(Faulted(takenNull) && takenNull.log.find(Fault(0u, 0x2cu)) != std::string::npos && takenNull.log.find("reason=1") != std::string::npos, "guarded pointer: a read through a null pointer that runs did not fault at its pc and address:\n" + takenNull.log);
Require(std::all_of(takenNull.words.begin(), takenNull.words.end(), [](std::uint32_t word) { return word == 0u || word == 0xdeadbeefu; }), "guarded pointer: a faulting read did not return zero");
const auto takenUnmapped = Run(device, BranchCode, unmapped, 1u);
Require(Faulted(takenUnmapped) && takenUnmapped.log.find(Fault(unmapped, 0x2cu)) != std::string::npos, "guarded pointer: a read through an unmapped pointer that runs did not fault at its address:\n" + takenUnmapped.log);
Require(takenUnmapped.pipelineVariantId == taken.pipelineVariantId, "guarded pointer: an unmapped pointer changed the pipeline variant");
const auto someLanes = Run(device, ExecCode, valid, 5u);
Require(!Faulted(someLanes), "guarded pointer: a mapped pointer under EXEC faulted:\n" + someLanes.log);
RequireWords(someLanes, 5u, Payload, "guarded pointer: a mapped pointer read under EXEC");
const auto noLanes = Run(device, ExecCode, 0u, 0u);
Require(!Faulted(noLanes) && noLanes.poisoned == 1u, "guarded pointer: a null pointer read skipped by s_cbranch_execz faulted:\n" + noLanes.log);
RequireWords(noLanes, 0u, 0u, "guarded pointer: a read skipped by s_cbranch_execz");
const auto oneLane = Run(device, ExecCode, 0u, 1u);
Require(Faulted(oneLane) && oneLane.log.find(Fault(0u, 0x34u)) != std::string::npos, "guarded pointer: a null pointer read with one EXEC lane did not fault:\n" + oneLane.log);
const auto emptyMapped = Run(device, EmptyExecCode, valid, 0u);
Require(!Faulted(emptyMapped), "guarded pointer: a mapped pointer read with an empty EXEC faulted:\n" + emptyMapped.log);
RequireWords(emptyMapped, 0u, 0u, "guarded pointer: a mapped pointer read with an empty EXEC");
const auto emptyNull = Run(device, EmptyExecCode, 0u, 0u);
Require(Faulted(emptyNull) && emptyNull.log.find(Fault(0u, 0x34u)) != std::string::npos, "guarded pointer: a null pointer read that runs with an empty EXEC did not fault:\n" + emptyNull.log);
Require(noLanes.pipelineVariantId == someLanes.pipelineVariantId && oneLane.pipelineVariantId == someLanes.pipelineVariantId && emptyNull.pipelineVariantId == emptyMapped.pipelineVariantId, "guarded pointer: the poison changed an EXEC program's pipeline variant");
const auto chainSkipped = Run(device, ChainCode, 0u, 0u);
Require(chainSkipped.poisoned == 3u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log);
const auto chainTaken = Run(device, ChainCode, 0u, 1u);
Require(Faulted(chainTaken) && chainTaken.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a read through a pointer read through a null pointer did not fault at the first read:\n" + chainTaken.log);
Indirect[0] = valid;
const auto chainMapped = Run(device, ChainCode, static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Indirect.data())), 1u);
Require(!Faulted(chainMapped) && chainMapped.poisoned == 0u && chainMapped.cacheable && chainMapped.pipelineVariantId == chainTaken.pipelineVariantId && chainSkipped.pipelineVariantId == chainTaken.pipelineVariantId, "guarded pointer: a chain's poison changed its pipeline variant, or a mapped chain faulted:\n" + chainMapped.log);
RequireWords(chainMapped, Lanes, Payload, "guarded pointer: a read through a mapped pointer chain");
#ifdef _WIN32
VirtualFree(reserved, 0, MEM_RELEASE);
#else
munmap(reserved, 65536);
#endif
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
RunTests(*device);
std::puts("guarded null pointer tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -50,6 +50,14 @@ struct SrtRead {
bool operator==(const SrtRead& other) const = default;
};
struct SrtReadPoison {
std::uint32_t slot = 0;
std::uint32_t pc = 0;
std::uint64_t address = 0;
bool operator==(const SrtReadPoison& other) const = default;
};
struct ResourceBlock {
IrValue* condition = nullptr;
std::vector<std::uint32_t> successors;

View File

@ -41,6 +41,7 @@ struct ResourceSnapshot {
std::vector<std::uint32_t> flattenedSrt;
std::vector<std::uint32_t> userData;
UniformFill uniformFill;
std::vector<SrtReadPoison> srtPoison;
};
struct UniformFillPlan {
@ -63,6 +64,8 @@ struct IrResourcePlan {
std::vector<ResourceBlock> controlFlow;
std::vector<std::uint32_t> materializationSources;
std::vector<SrtRead> srtReads;
std::vector<std::uint32_t> guardedSrtSlots;
std::uint32_t srtGuardOffset = 0;
std::vector<std::uint8_t> cleanFlatSlots;
// One byte per srtReads slot, 1 when the CPU walk never consumes the slot's value (see
// Detail::ComputePureFlatSlots): a driver may reuse a capture whose words differ only there.

View File

@ -33,6 +33,7 @@ struct ShaderInfo {
bool hasBitwiseXor = false;
bool usesDma = false;
bool bdaWrites = false;
bool usesFaultBuffer = false;
bool dispatchThreadLimit = false;
bool operator==(const ShaderInfo& other) const = default;

View File

@ -16,6 +16,7 @@ public:
private:
static bool ReadGuestMemory(void* userContext, std::uint64_t address, std::uint32_t* value);
static bool Covered(void* userContext, std::uint64_t address, std::uint64_t bytes);
std::vector<MemoryRegion> regions;
};

View File

@ -11,6 +11,7 @@
namespace ShaderRecompiler {
using SrtMemoryReader = bool (*)(void* userData, std::uint64_t address, std::uint32_t* value);
using SrtMemoryProbe = bool (*)(void* userData, std::uint64_t address, std::uint64_t bytes);
// The guest addresses a walk dereferenced (Detail::Evaluator::EvaluateRawRead): the leaf read of
// each pure flat slot (IrResourcePlan::pureFlatSlots) as (flat offset, address), set by the
@ -30,6 +31,7 @@ struct SrtRuntime {
void* userContext = nullptr;
SrtMemoryReader readSpecializationMemory = nullptr;
SrtReadTrace* readTrace = nullptr;
SrtMemoryProbe accessible = nullptr;
};
enum class RuntimeValueType {
@ -44,7 +46,7 @@ public:
void EvaluateUniformValues(const IrResourcePlan& program, std::span<IrValue* const> values, const SrtRuntime& runtime, std::span<std::uint32_t> results) const;
void EvaluateDescriptorSource(const IrResourcePlan& program, std::uint32_t source, const SrtRuntime& runtime, DescriptorValue& result) const;
void EvaluateDescriptorSources(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results) const;
void EvaluateRuntimeSources(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources) const;
void EvaluateRuntimeSources(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources, std::vector<SrtReadPoison>* poison = nullptr) const;
void Walk(const IrResourcePlan& program, const SrtRuntime& runtime, std::vector<std::uint32_t>& flat) const;
};

View File

@ -15,7 +15,7 @@ namespace ShaderRecompiler::Detail {
// Why the last EvaluateRuntimeSourcesImpl call on this thread returned false.
const std::string& RuntimeSourceFailureReason();
bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, bool evaluateFlat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources);
bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, bool evaluateFlat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources, std::vector<SrtReadPoison>* poison = nullptr);
}

View File

@ -66,12 +66,18 @@ private:
std::size_t _count = 0;
};
struct InaccessibleRead {
const IrValue* read = nullptr;
std::uint64_t address = 0;
};
class Evaluator {
public:
Evaluator(const IrResourcePlan& program, const SrtRuntime& runtime, std::span<const std::uint8_t> cleanFlatSlots = {}, Evaluator* cleanEvaluator = nullptr, IrValue* activeMask = nullptr) : _program(program), _runtime(runtime), _cleanFlatSlots(cleanFlatSlots), _cleanEvaluator(cleanEvaluator), _activeMask(activeMask != nullptr ? activeMask->Resolve() : nullptr) {}
bool Evaluate(IrValue* value, std::uint32_t& result);
bool EvaluateWide(IrValue* raw, std::uint64_t& result);
void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; }
private:
static float Float32(std::uint64_t bits);
@ -88,6 +94,7 @@ private:
std::span<const std::uint8_t> _cleanFlatSlots;
Evaluator* _cleanEvaluator = nullptr;
IrValue* _activeMask = nullptr;
InaccessibleRead* _inaccessible = nullptr;
EvaluatedValues _cache;
std::vector<IrValue*> _visiting;
};

View File

@ -16,6 +16,7 @@ namespace ShaderRecompiler::Detail {
// fill) has no pure slot. Reachability over-approximates what the evaluators consume, so an
// unknown opcode only makes more slots impure.
std::vector<std::uint8_t> ComputePureFlatSlots(const IrResourcePlan& plan);
std::vector<std::uint32_t> ComputeGuardedFlatSlots(const IrResourcePlan& plan);
}

View File

@ -6,6 +6,7 @@
namespace ShaderRecompiler::Detail {
bool IsRawRead(const IrResourcePlan& program, const IrValue& inst);
bool LoadedFromMemory(const IrValue& handle);
bool IsDescriptorHandle(IrOpcode opcode);
bool IsRuntimeSelect(IrOpcode opcode);
bool IsRuntimeUniformOp(IrOpcode opcode);

View File

@ -161,6 +161,8 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin
}
if (info.usesDma) {
addBinding(next, DescriptorBindingKind::BdaPagetable);
}
if (info.usesDma || info.usesFaultBuffer) {
addBinding(next, DescriptorBindingKind::FaultBuffer);
}

View File

@ -64,6 +64,29 @@ bool RequestMemoryView::ReadGuestMemory(void* userContext, std::uint64_t address
return true;
}
bool RequestMemoryView::Covered(void* userContext, std::uint64_t address, std::uint64_t bytes) {
const auto* self = static_cast<const RequestMemoryView*>(userContext);
if (bytes > std::numeric_limits<std::uint64_t>::max() - address) {
return false;
}
const std::uint64_t end = address + bytes;
while (address < end) {
const auto it = std::upper_bound(self->regions.begin(), self->regions.end(), address, [](std::uint64_t addressValue, const MemoryRegion& region) {
return addressValue < region.guestAddress;
});
if (it == self->regions.begin()) {
return false;
}
const MemoryRegion& region = *std::prev(it);
const std::uint64_t regionEnd = region.guestAddress + region.bytes.size();
if (address >= regionEnd) {
return false;
}
address = regionEnd;
}
return true;
}
SrtRuntime RequestMemoryView::MakeRuntime(std::span<const std::uint32_t> userData, std::uint64_t shaderBase) {
SrtRuntime runtime;
runtime.userData = userData;
@ -71,6 +94,7 @@ SrtRuntime RequestMemoryView::MakeRuntime(std::span<const std::uint32_t> userDat
runtime.userContext = this;
runtime.readMemory = &ReadGuestMemory;
runtime.readSpecializationMemory = &ReadGuestMemory;
runtime.accessible = &Covered;
return runtime;
}

View File

@ -444,7 +444,7 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime,
std::vector<DescriptorValue> values;
std::vector<std::uint8_t> activeSources;
walker.EvaluateRuntimeSources(plan, plan.materializationSources, runtime, values, snapshot.flattenedSrt, plan.cleanFlatSlots, activeSources);
walker.EvaluateRuntimeSources(plan, plan.materializationSources, runtime, values, snapshot.flattenedSrt, plan.cleanFlatSlots, activeSources, &snapshot.srtPoison);
std::size_t cursor = 0;
if (values.size() < plan.info.buffers.size()) {
@ -593,6 +593,19 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, c
}
}
void materializeSrtGuards(const IrResourcePlan& plan, ResourceSnapshot& snapshot) {
const auto& guarded = plan.guardedSrtSlots;
const auto flags = snapshot.flattenedSrt.size();
snapshot.flattenedSrt.resize(flags + guarded.size(), 0u);
for (std::uint32_t record = 0u; record < snapshot.srtPoison.size(); ++record) {
const auto& poison = snapshot.srtPoison[record];
const auto found = std::lower_bound(guarded.begin(), guarded.end(), poison.slot);
if (found == guarded.end() || *found != poison.slot) throw std::runtime_error("SRT read at flat offset " + std::to_string(poison.slot) + " reads inaccessible memory but has no guard");
snapshot.flattenedSrt[flags + static_cast<std::size_t>(found - guarded.begin())] = record + 1u;
snapshot.flattenedSrt.insert(snapshot.flattenedSrt.end(), {poison.pc, static_cast<std::uint32_t>(poison.address), static_cast<std::uint32_t>(poison.address >> 32u)});
}
}
}
std::vector<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageResource& image) {
@ -780,6 +793,9 @@ void ResourceMaterializer::ApplyStaticInterface(IrProgram& program, bool nativeS
sampler.depthCompare = sampler.depthCompare || images[pair.image].depthCompare;
}
resources.info.images = std::move(images);
resources.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(resources);
resources.srtGuardOffset = mappingOffset;
if (!resources.guardedSrtSlots.empty()) resources.info.usesFaultBuffer = true;
PrepareImageModes(resources.info);
}
@ -893,6 +909,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const
}
for (const auto& sampler : plan.info.samplers) addSource(sampler.source);
plan.pureFlatSlots = Detail::ComputePureFlatSlots(plan);
plan.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(plan);
return plan;
}
@ -915,6 +932,7 @@ void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtR
}
const auto started = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
materializeTables(plan, nextSnapshot, tables);
materializeSrtGuards(plan, nextSnapshot);
if (MaterializeProfiled()) specializationNanoseconds.fetch_add(static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed);
snapshot = std::move(nextSnapshot);
reportBindless();

View File

@ -44,7 +44,7 @@ const DescriptorSource* Source(const IrResourcePlan& program, std::uint32_t sour
}
bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, bool evaluateFlat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources) {
bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, bool evaluateFlat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources, std::vector<SrtReadPoison>* poison) {
failureReason().clear();
static const bool debug = std::getenv("APS5_SRT_DEBUG") != nullptr;
if (debug) {
@ -117,8 +117,14 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const s
evaluated.push_back(value);
}
std::vector<std::uint32_t> flattened;
std::vector<SrtReadPoison> poisoned;
if (evaluateFlat) {
flattened.resize(program.srtReads.size());
InaccessibleRead inaccessible;
if (poison != nullptr) {
evaluator.ReportInaccessibleReads(&inaccessible);
cleanEvaluator.ReportInaccessibleReads(&inaccessible);
}
for (const auto& read : program.srtReads) {
const bool clean = read.flatOffset < cleanFlatSlots.size() && cleanFlatSlots[read.flatOffset] != 0u;
auto& selected = clean ? cleanEvaluator : evaluator;
@ -131,8 +137,15 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const s
trace->leaf = read.value->Resolve();
trace->leafSlot = read.flatOffset;
}
inaccessible = {};
const bool evaluated = read.flatOffset < flattened.size() && selected.Evaluate(read.value, flattened[read.flatOffset]);
if (pure) trace->leaf = nullptr;
if (!evaluated && inaccessible.read != nullptr) {
if (debug) std::fprintf(stderr, "[srt] flat offset %u reads inaccessible 0x%llx at pc 0x%x: poisoned\n", read.flatOffset, static_cast<unsigned long long>(inaccessible.address), inaccessible.read->Flags<MemoryFlags>().pc);
flattened[read.flatOffset] = 0u;
poisoned.push_back({read.flatOffset, inaccessible.read->Flags<MemoryFlags>().pc, inaccessible.address});
continue;
}
if (!evaluated) {
return Fail(std::string(clean ? "clean " : "") + "SRT read at flat offset " + std::to_string(read.flatOffset) + ": " + DescribeValue(read.value, 4));
}
@ -143,6 +156,9 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span<const s
if (evaluateFlat) {
flat = std::move(flattened);
}
if (poison != nullptr) {
*poison = std::move(poisoned);
}
return true;
}

View File

@ -152,6 +152,10 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) {
return false;
}
}
if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle)) {
*_inaccessible = {&inst, address};
return false;
}
if (auto* trace = _runtime.readTrace; trace != nullptr) {
if (&inst == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address);
else trace->otherReads.push_back(address);
@ -187,6 +191,7 @@ bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) {
case IrOpcode::Phi: return EvaluatePhi(inst, result);
case IrOpcode::ReadFirstLane: {
Evaluator active(_program, _runtime, _cleanFlatSlots, _cleanEvaluator, inst.Argument(1));
active.ReportInaccessibleReads(_inaccessible);
return active.EvaluateWide(inst.Argument(0), result);
}
case IrOpcode::BitCastU32F32:

View File

@ -1,4 +1,5 @@
#include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp"
#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp"
#include <algorithm>
#include <unordered_map>
@ -61,4 +62,47 @@ std::vector<std::uint8_t> ComputePureFlatSlots(const IrResourcePlan& plan) {
return pure;
}
std::vector<std::uint32_t> ComputeGuardedFlatSlots(const IrResourcePlan& plan) {
std::unordered_map<const IrValue*, std::vector<const IrValue*>> users;
std::unordered_set<const IrValue*> seen;
std::vector<const IrValue*> pending;
std::vector<const IrValue*> poisonable;
for (const auto& read : plan.srtReads) {
if (read.value != nullptr) pending.push_back(read.value->Resolve());
}
while (!pending.empty()) {
const IrValue* value = pending.back();
pending.pop_back();
if (!seen.insert(value).second) continue;
const IrValue* handle = value->ArgumentCount() != 0u && value->Argument(0) != nullptr ? value->Argument(0)->Resolve() : nullptr;
if (IsRawRead(plan, *value) && handle != nullptr && LoadedFromMemory(*handle)) poisonable.push_back(value);
const auto follow = [&](const IrValue* argument) {
if (argument == nullptr) return;
argument = argument->Resolve();
users[argument].push_back(value);
pending.push_back(argument);
};
for (std::size_t i = 0; i < value->ArgumentCount(); ++i) follow(value->Argument(i));
if (value->Opcode() == IrOpcode::ReadConst && value->ArgumentCount() >= 2 && value->Argument(1) != nullptr) {
const IrValue* slot = value->Argument(1)->Resolve();
if (slot->HasImmediate() && slot->Type() == IrType::U32 && slot->ImmediateU32() < plan.srtReads.size()) follow(plan.srtReads[slot->ImmediateU32()].value);
}
}
std::unordered_set<const IrValue*> reaching(poisonable.begin(), poisonable.end());
while (!poisonable.empty()) {
const IrValue* value = poisonable.back();
poisonable.pop_back();
const auto found = users.find(value);
if (found == users.end()) continue;
for (const IrValue* user : found->second) {
if (reaching.insert(user).second) poisonable.push_back(user);
}
}
std::vector<std::uint32_t> guarded;
for (std::uint32_t slot = 0; slot < plan.srtReads.size(); ++slot) {
if (plan.srtReads[slot].value != nullptr && reaching.contains(plan.srtReads[slot].value->Resolve())) guarded.push_back(slot);
}
return guarded;
}
}

View File

@ -1,5 +1,8 @@
#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp"
#include <algorithm>
#include <vector>
namespace ShaderRecompiler::Detail {
bool IsRawRead(const IrResourcePlan& program, const IrValue& inst) {
@ -15,6 +18,24 @@ bool IsRawRead(const IrResourcePlan& program, const IrValue& inst) {
return (op == IrOpcode::LoadAddressU32 && kind == ResourceKind::ScalarAddress) || (op == IrOpcode::ReadConstBuffer && kind == ResourceKind::ScalarBuffer);
}
bool LoadedFromMemory(const IrValue& handle) {
std::vector<const IrValue*> pending;
std::vector<const IrValue*> visited;
for (std::size_t index = 0; index < handle.ArgumentCount(); ++index) pending.push_back(handle.Argument(index));
while (!pending.empty()) {
const IrValue* value = pending.back();
pending.pop_back();
if (value == nullptr) continue;
value = value->Resolve();
if (value->HasImmediate() || std::find(visited.begin(), visited.end(), value) != visited.end()) continue;
visited.push_back(value);
const auto opcode = value->Opcode();
if (opcode == IrOpcode::ReadConst || opcode == IrOpcode::LoadAddressU32 || opcode == IrOpcode::ReadConstBuffer) return true;
for (std::size_t index = 0; index < value->ArgumentCount(); ++index) pending.push_back(value->Argument(index));
}
return false;
}
bool IsDescriptorHandle(IrOpcode opcode) {
switch (opcode) {
case IrOpcode::GetBufferResource:

View File

@ -49,8 +49,8 @@ void SrtWalker::EvaluateDescriptorSources(const IrResourcePlan& program, std::sp
}
}
void SrtWalker::EvaluateRuntimeSources(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources) const {
if (!Detail::EvaluateRuntimeSourcesImpl(program, sources, runtime, results, flat, true, cleanFlatSlots, activeSources)) {
void SrtWalker::EvaluateRuntimeSources(const IrResourcePlan& program, std::span<const std::uint32_t> sources, const SrtRuntime& runtime, std::vector<DescriptorValue>& results, std::vector<std::uint32_t>& flat, std::span<const std::uint8_t> cleanFlatSlots, std::vector<std::uint8_t>& activeSources, std::vector<SrtReadPoison>* poison) const {
if (!Detail::EvaluateRuntimeSourcesImpl(program, sources, runtime, results, flat, true, cleanFlatSlots, activeSources, poison)) {
throw std::runtime_error("SrtWalker::EvaluateRuntimeSources failed to evaluate runtime sources: " + Detail::RuntimeSourceFailureReason());
}
}

View File

@ -344,7 +344,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra
result.variantId = nextVariantId();
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u;
result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u;
result.memoryOffsetDword = bindings.layout.memoryOffsetDword;
result.shaderDataDwords = bindings.layout.ShaderDataDwords();
result.imageMetadataDword = bindings.layout.ImageMetadataDword();
@ -680,6 +680,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request));
result.bindings = std::move(bindings.bindings);
result.pushConstants = std::move(bindings.pushConstants);
result.poisonedSrtReads = static_cast<std::uint32_t>(snapshot.srtPoison.size());
return result;
}

View File

@ -462,6 +462,7 @@ struct ShaderInvocation {
std::vector<DescriptorBinding> bindings;
std::vector<std::byte> pushConstants;
std::vector<VertexAttribute> vertexAttributes;
std::uint32_t poisonedSrtReads = 0;
};
struct RecompileResult : CompiledShaderArtifact, ShaderInvocation {

View File

@ -65,14 +65,14 @@ namespace ShaderRecompiler::ShaderDiskCache {
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(CompiledShaderArtifact) == 184, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(ShaderInvocation) == 104, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(RecompileResult) == 296, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(DescriptorBinding) == 448, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder");
static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder");
static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder");
static_assert(sizeof(CompiledShaderInfo) == 336, "CompiledShaderInfo changed: update the info encoder");
static_assert(sizeof(ShaderInfo) == 224, "ShaderInfo changed: update the info encoder");
static_assert(sizeof(CompiledShaderInfo) == 344, "CompiledShaderInfo changed: update the info encoder");
static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder");
static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder");
static_assert(sizeof(ImageResource) == 112, "ImageResource changed: update the info encoder");
static_assert(sizeof(SamplerResource) == 16, "SamplerResource changed: update the info encoder");
@ -374,6 +374,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) {
out.Value(attribute.fetchIndex);
out.Value(attribute.formatComponents);
});
writer.Value(invocation.poisonedSrtReads);
}
void decodeInvocation(Reader& reader, ShaderInvocation& invocation) {
@ -394,6 +395,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) {
in.Value(attribute.fetchIndex);
in.Value(attribute.formatComponents);
});
reader.Value(invocation.poisonedSrtReads);
}
void encodeLayout(Writer& writer, const IrBindingLayout& layout) {
@ -518,6 +520,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) {
writer.Value(info.hasBitwiseXor);
writer.Value(info.usesDma);
writer.Value(info.bdaWrites);
writer.Value(info.usesFaultBuffer);
writer.Value(info.dispatchThreadLimit);
encodeLayout(writer, compiled.bindings);
}
@ -620,6 +623,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) {
reader.Value(info.hasBitwiseXor);
reader.Value(info.usesDma);
reader.Value(info.bdaWrites);
reader.Value(info.usesFaultBuffer);
reader.Value(info.dispatchThreadLimit);
decodeLayout(reader, compiled.bindings);
}

View File

@ -12,7 +12,7 @@
namespace ShaderRecompiler::ShaderDiskCache {
inline constexpr std::uint32_t FormatVersion = 20;
inline constexpr std::uint32_t FormatVersion = 21;
enum class LoadStatus {
Loaded,

View File

@ -16,6 +16,7 @@ std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index);
void DefineBdaFaultFunction(SpirvEmitterState& state);
void DefineBdaDwordReadFunctions(SpirvEmitterState& state);
void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason);
void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason);
void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason);
void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target);
// Whether a faulting BDA access may end its invocation. Programs with workgroup barriers must keep

View File

@ -29,16 +29,20 @@ std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index) {
void DefineBdaFaultFunction(SpirvEmitterState& state) {
const auto u32 = TypeU32(state);
const auto u64 = TypeScalarU64(state);
const bool words = !state.program.Info().usesDma;
const auto addressType = words ? u32 : TypeScalarU64(state);
state.bdaFaultFunction = state.module.AllocateId();
state.module.AddName(state.bdaFaultFunction, "record_bda_fault");
state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.bdaFaultFunction, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, TypeVoid(state), u32, u64, u32, u32));
const auto type = words ? state.module.Type(spv::OpTypeFunction, TypeVoid(state), u32, addressType, u32, u32, u32) : state.module.Type(spv::OpTypeFunction, TypeVoid(state), u32, addressType, u32, u32);
state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.bdaFaultFunction, spv::FunctionControlDontInlineMask, type);
const auto reason = state.module.AllocateId();
const auto address = state.module.AllocateId();
const auto addressHigh = words ? state.module.AllocateId() : 0u;
const auto bytes = state.module.AllocateId();
const auto instruction = state.module.AllocateId();
state.module.AddFunction(spv::OpFunctionParameter, u32, reason);
state.module.AddFunction(spv::OpFunctionParameter, u64, address);
state.module.AddFunction(spv::OpFunctionParameter, addressType, address);
if (words) state.module.AddFunction(spv::OpFunctionParameter, u32, addressHigh);
state.module.AddFunction(spv::OpFunctionParameter, u32, bytes);
state.module.AddFunction(spv::OpFunctionParameter, u32, instruction);
EmitLabel(state, state.module.AllocateId());
@ -53,8 +57,8 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) {
state.module.AddFunction(spv::OpStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, index)), value);
};
store(1, reason);
store(2, Unary(state, spv::OpUConvert, TypeU32(state), address));
store(3, Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, 32u))));
store(2, words ? address : Unary(state, spv::OpUConvert, TypeU32(state), address));
store(3, words ? addressHigh : Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, 32u))));
store(4, bytes);
store(5, ConstantU32(state, static_cast<std::uint32_t>(state.program.Resources().stage)));
store(6, instruction);
@ -67,10 +71,21 @@ void DefineBdaFaultFunction(SpirvEmitterState& state) {
}
void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) {
if (state.bdaFaultFunction == 0) throw std::runtime_error("BDA fault function is missing");
if (state.bdaFaultFunction == 0 || !state.program.Info().usesDma) throw std::runtime_error("BDA fault function is missing");
state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast<std::uint32_t>(reason)), address, bytes, instruction);
}
void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) {
if (state.program.Info().usesDma) {
const auto u64 = TypeScalarU64(state);
const auto high = Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, addressHigh), BdaConstant(state, 32u));
RecordBdaFault(state, Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, addressLow), high), bytes, instruction, reason);
return;
}
if (state.bdaFaultFunction == 0) throw std::runtime_error("BDA fault function is missing");
state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast<std::uint32_t>(reason)), addressLow, addressHigh, bytes, instruction);
}
void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) {
const auto failed = state.module.AllocateId();
const auto next = state.module.AllocateId();

View File

@ -157,8 +157,8 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) {
}
void DefineGetBdaPointer(SpirvEmitterState& state) {
if (state.faultBufferVariable != 0) DefineBdaFaultFunction(state);
if (!state.program.Info().usesDma) return;
DefineBdaFaultFunction(state);
state.bdaPointerFunction = DefineBdaLookup(state, "get_bda_pointer", true);
if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false);
DefineBdaDwordReadFunctions(state);

View File

@ -167,6 +167,7 @@ std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, s
}
void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target) {
if (program.Info().usesFaultBuffer && target.bdaAbiVersion != BdaAbi::Version) throw std::runtime_error("the fault buffer needs the BDA fault ABI, which the target lacks");
if (!program.Info().usesDma) return;
if (target.bdaAbiVersion != BdaAbi::Version) throw std::runtime_error("unsupported BDA ABI version");
for (const auto capability : {spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}) {

View File

@ -85,7 +85,8 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) {
const auto pc = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, TypeU32(state), pc, state.loopGuardPc);
EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), pc, ConstantU32(state, 0u)), [&] {
RecordBdaFault(state, BdaConstant(state, state.program.Resources().shaderHash), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit);
const auto hash = state.program.Resources().shaderHash;
RecordBdaFaultWords(state, ConstantU32(state, static_cast<std::uint32_t>(hash)), ConstantU32(state, static_cast<std::uint32_t>(hash >> 32u)), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit);
});
}
if (OrderedPixelShader(state)) {

View File

@ -1020,6 +1020,41 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) {
if (state.flattenedSrtVariable == 0) {
ctx.Fail(inst, "requires the flattened SRT descriptor");
}
const auto& resources = state.program.Resources();
const auto& guarded = resources.guardedSrtSlots;
if (!guarded.empty()) {
const IrValue* slot = inst.Argument(1)->Resolve();
if (!slot->HasImmediate()) {
ctx.Fail(inst, "reads the flattened SRT at a runtime slot while some slots are guarded");
}
const auto found = std::lower_bound(guarded.begin(), guarded.end(), slot->ImmediateU32());
if (found != guarded.end() && *found == slot->ImmediateU32()) {
if (state.faultBufferVariable == 0) {
ctx.Fail(inst, "reads a guarded SRT slot without a fault buffer");
}
const auto u32 = TypeU32(state);
const auto load = [&](std::uint32_t index) {
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.flattenedSrtVariable, ConstantU32(state, 0u), index);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, u32, value, pointer);
return value;
};
const auto guard = static_cast<std::uint32_t>(found - guarded.begin());
const auto flag = load(ConstantU32(state, resources.srtGuardOffset + guard));
const auto poisoned = Binary(state, spv::OpINotEqual, TypeBool(state), flag, ConstantU32(state, 0u));
EmitIfCondition(state, poisoned, [&] {
const auto records = resources.srtGuardOffset + static_cast<std::uint32_t>(guarded.size());
const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u)));
const auto word = [&](std::uint32_t offset) { return load(Binary(state, spv::OpIAdd, u32, record, ConstantU32(state, offset))); };
const auto pc = word(0u);
const auto low = word(1u);
const auto high = word(2u);
RecordBdaFaultWords(state, low, high, ConstantU32(state, 4u), pc, BdaAbi::FaultReason::Unmapped);
});
StopBdaInvocationIf(state, poisoned);
}
}
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.flattenedSrtVariable, ConstantU32(state, 0u), ctx.Arg(inst, 1));
const auto value = state.module.AllocateId();

View File

@ -271,6 +271,9 @@ Throughout the project, every function at every stage either **does exactly what
- [Samples and gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB.
- [Bindless image table](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB.
- The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either.
- 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 [descriptor, 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, even if the console skips the read.
- On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit.
- [EXEC read as a value](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes.
- [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR.