From 024cb1fedaac34a8d9ee86f4cef78f75895b7fdf Mon Sep 17 00:00:00 2001 From: raghav314 Date: Sat, 10 Oct 2026 11:13:08 +0530 Subject: [PATCH] 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 --- .../Execution/src/VulkanDevice.cpp | 7 +++++-- .../Graphics/include/ShaderResources.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++-- .../Graphics/src/ShaderResources.cpp | 20 ++++++++++++++++--- .../libSceAgcDriver/tests/BdaResources.cpp | 16 ++++++++++++++- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5f2e43115..660622656 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -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->device = state->device; recipe->templateRef = resources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 28370a397..737327b9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -188,6 +188,8 @@ public: bool Reusable() const { return reusable; } static bool NeverReusable(std::span shaders); const std::vector>& 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 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 344261741..77d53b380 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1498,7 +1498,8 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr }); 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9ac9ff9bd..0fbb3ef2a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1563,12 +1563,21 @@ std::atomic 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 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(elements), static_cast(elements - readOnly), static_cast(readOnly), static_cast(counters.notesSkipped.load())); } +void ShaderResources::CheckFaults() const { + if (bda) bda->CheckFault(); +} + void ShaderResources::WriteBackBuffers() { if (bda) bda->CheckFault(); if (SkipWriteBack()) return; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 932eb95ac..43db11be7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -697,7 +697,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { const std::array stages{{{ShaderRecompiler::ShaderStage::TessellationControl, &control, 0}}}; const std::array 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 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(&compiled, 1)) && !resources.Reusable(), "an address-based build is reusable");