perf(agc): cache resources whose only completion work is the fault check

A draw or dispatch whose shaders have guarded descriptor slots (or a rect
list) but no BDA table gets a fault buffer, and with it NeedsCompletion(),
which kept its resources out of the resource cache, and NeverReusable
skipped the cache lookup for it. In PPSA36082 nearly every draw in the hub
is such a build, so the cache served none of them.

Such an object has no table to revalidate, no stores through BDA and no
copied writes, so it is now reusable. Each use of a reused object checks
the fault record when its batch completes (CheckFaults) instead of
running the once-only write-back; draw recipes over such a template
register the same check, and dispatch recipes still skip templates with
completion work. NeverReusable now refuses what noteReusable refuses: a
BDA table, or a fault buffer with APS5_LOOP_GUARD set (its fault resets
the buffer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
raghav314 2026-10-10 11:13:08 +05:30 committed by Adrià Franch
parent 1f2161bf5c
commit 024cb1feda
5 changed files with 43 additions and 8 deletions

View File

@ -3585,7 +3585,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
recordStep(PhaseRecordMarks);
// Only copied written buffers (and BDA fault checks) need work once the GPU is done; without them
// the batch can signal its labels from the GPU.
if (resources.NeedsCompletion()) {
if (resources.NeedsCompletion() && resources.Reusable()) {
auto kept = record.resources;
recorder.OnComplete([kept] { kept->CheckFaults(); });
} else if (resources.NeedsCompletion()) {
// Listed for DispatchIndirect until the write-back ran (a non-reusable object, so once).
auto writers = state->copiedWriters;
auto kept = record.resources;
@ -3873,7 +3876,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
// object the resource cache serves under this content key (reusable: no lease, no copied
// writes, every direct region import- or mirror-served), so a hit's proof is the template's
// Revalidate and nothing needs completion work.
if (recipeOut != nullptr && DispatchRecipes() && cacheable && resources->Reusable() && objects != nullptr && !contentKey.empty()) {
if (recipeOut != nullptr && DispatchRecipes() && cacheable && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) {
auto recipe = std::make_shared<Recipe>();
recipe->device = state->device;
recipe->templateRef = resources;

View File

@ -188,6 +188,8 @@ public:
bool Reusable() const { return reusable; }
static bool NeverReusable(std::span<const CompiledShader> shaders);
const std::vector<std::shared_ptr<Texture>>& SampledTextures() const { return textures; }
bool ChecksFaults() const { return bda != nullptr; }
void CheckFaults() const;
// `shaders` are the stages the object was built from, in build order (a recorded draw's vertex
// and fragment stages, or one compute stage): their bindings are walked like the build did.
// How a Revalidate proved (or refused) the object, for the [recipe] line: the proof path taken

View File

@ -1498,7 +1498,8 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>
});
writers->push_back(resources);
} else if (completion) {
recorder.OnComplete([resources] { resources->WriteBackBuffers(); });
if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); });
else recorder.OnComplete([resources] { resources->WriteBackBuffers(); });
}
for (const auto& resident : residents) {
if (resident != nullptr) resident->MarkDirty();
@ -2421,7 +2422,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
SharedResourceCache().Touch(recipe.key);
countCache(&DrawProfile::cacheHits);
timer.phase(PhaseLookup);
outcome.completion = false;
outcome.completion = resources->ChecksFaults();
outcome.recorded = true;
// The pipeline is the recipe's while the pipeline store still holds it (its device's teardown
// or its eviction bound drops it: a miss). The framebuffer is the recipe's while the pipeline's
@ -2435,6 +2436,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
RecordedDraw record;
record.recorder = recorder;
record.completion = outcome.completion;
record.resources = std::move(resources);
record.pipeline = std::move(pipeline);
record.framebuffer = std::move(framebuffer);

View File

@ -1563,12 +1563,21 @@ std::atomic<std::uint64_t> resourceCacheTouches{0};
}
namespace {
bool LoopGuardEnabled() {
static const bool enabled = std::getenv("APS5_LOOP_GUARD") != nullptr;
return enabled;
}
}
void ShaderResources::noteReusable() {
// Taken whether or not the object turns out reusable: the lookups' records are this build's.
captureValidation();
reusable = false;
directRegions.clear();
if (NeedsCompletion() || HoldsLease()) return;
if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return;
if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.size > MaxRefreshBytes; })) return;
const auto regions = guestMemory.DirectRegions();
if (!regions.has_value()) return;
@ -1586,8 +1595,9 @@ void ShaderResources::noteReusable() {
}
bool ShaderResources::NeverReusable(std::span<const CompiledShader> shaders) {
return std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) {
return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; });
const bool loopGuard = LoopGuardEnabled();
return std::any_of(shaders.begin(), shaders.end(), [loopGuard](const CompiledShader& shader) {
return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [loopGuard](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || (loopGuard && binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer); });
});
}
@ -3446,6 +3456,10 @@ void ShaderResources::MarkGpuWrites(Recorder& recorder) {
AgcDriver::ProfilePrint_nid_no_patch("[buffers] descriptor elements bound: %llu total, %llu written, %llu read-only; %llu pending-write notes skipped\n", static_cast<unsigned long long>(elements), static_cast<unsigned long long>(elements - readOnly), static_cast<unsigned long long>(readOnly), static_cast<unsigned long long>(counters.notesSkipped.load()));
}
void ShaderResources::CheckFaults() const {
if (bda) bda->CheckFault();
}
void ShaderResources::WriteBackBuffers() {
if (bda) bda->CheckFault();
if (SkipWriteBack()) return;

View File

@ -697,7 +697,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
const std::array<CompiledShader, 1> stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}};
const std::array<GuestMemorySnapshot, 1> unusedSnapshots{{{0, source}}};
ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, unusedSnapshots);
Require(ShaderResources::NeverReusable(stages) && !resources.Reusable(), "a fault-buffer build is reusable");
Require(!ShaderResources::NeverReusable(stages), "a fault-buffer build is refused the resource cache");
Require(!resources.Reusable(), "a fault-buffer build with copied writes is reusable");
Require(resources.NeedsCompletion() && !resources.UsesAddressTable(), "a fault-buffer build reaches guest memory through the BDA table");
const auto fault = access.bytes(access.descriptor(5).buffer);
for (const auto byte : fault) Require(byte == std::byte{}, "rect-list fault buffer was not initialized");
@ -710,6 +711,19 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
resources.WriteBack();
Require(guest[1] == changed, "rect-list fault-only path lost guest buffer writes");
}
{
ShaderRecompiler::RecompileResult control;
control.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version;
control.bindings = {binding(Role::FaultBuffer, 5)};
const std::array<CompiledShader, 1> stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}};
ShaderResources resources(disabled, stages, ColorTarget{}, 0, 0, 0, {});
Require(!ShaderResources::NeverReusable(stages) && resources.Reusable() && resources.ChecksFaults(), "a fault-only build is not reusable");
resources.CheckFaults();
const auto fault = access.bytes(access.descriptor(5).buffer);
const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, 0, 0, 0, 0, 0};
std::memcpy(fault.data(), &report, sizeof(report));
reject([&] { resources.CheckFaults(); }, "rect-list requires");
}
{
ShaderResources resources(context, compiled, snapshots);
Require(ShaderResources::NeverReusable(std::span<const CompiledShader>(&compiled, 1)) && !resources.Reusable(), "an address-based build is reusable");