From 0adbdcf45c059bd271a475d87f296a3dc8f95b72 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 12 Oct 2026 01:45:31 +1300 Subject: [PATCH] perf(agc): cache a draw key the second time it is seen A draw whose key has no entry captured its stages, built a variant per stage (a copy of every captured region) and inserted a draw entry. A key that never comes back paid for the variants and the entry for nothing, and its entry pushed a live one out of the cache. Draws whose user data changes every frame are such keys. The driver now remembers the keys it has seen once, in a table of four-way buckets holding four times the cache's size in keys. A key gets its entry at its second sight; a key that already has an entry still takes new variants at once. A full bucket replaces its oldest key, so keys that share a bucket do not push each other out until more than four of them come between two sights of one. Test: agc_driver_draw_cache_reuse draws three times. The first two draws find no entry, one entry is inserted, and the third draw hits it. A sighting table of one bucket keeps two keys drawn in turn and replaces its oldest key when full. --- .../include/Driver/Draw/DrawCache.hpp | 13 ++++++++ .../Execution/include/Driver/Driver.hpp | 2 ++ .../Execution/src/Driver/Draw/DrawCache.cpp | 31 +++++++++++++++++++ .../Execution/src/Driver/Draw/DrawCapture.cpp | 2 +- .../tests/execution/DrawCacheReuse.cpp | 24 +++++++++++--- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 5fa283462..859344a12 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -75,6 +75,19 @@ struct DrawEntryCounters { std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +class DrawKeySightings { +public: + static constexpr std::size_t Ways = 4; + explicit DrawKeySightings(std::size_t keys); + bool SeenBefore(std::uint64_t key); + +private: + std::vector keys; + std::vector filled; + std::vector next; + std::size_t bucketMask = 0; +}; + enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ce483328b..c305a1c93 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -115,6 +115,7 @@ private: static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode); + bool admitDrawKey(std::uint64_t key); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected); std::shared_ptr findDrawRecipe(std::uint64_t key, const std::vector>& stages); void attachDrawRecipe(std::uint64_t key, const std::vector>& stages, std::shared_ptr recipe); @@ -310,6 +311,7 @@ private: std::list drawOrder; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; + std::optional drawKeySightings; DrawEntryCounters drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 7844679fd..31803fd0f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include +#include #include namespace AgcDriver::DriverDetail { @@ -56,6 +57,36 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) { } } +DrawKeySightings::DrawKeySightings(std::size_t keyCount) { + Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket"); + const auto buckets = std::bit_ceil(keyCount / Ways); + keys.assign(buckets * Ways, 0); + filled.assign(buckets, 0); + next.assign(buckets, 0); + bucketMask = buckets - 1; +} + +bool DrawKeySightings::SeenBefore(std::uint64_t key) { + auto mixed = key; + mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull; + mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull; + mixed ^= mixed >> 31u; + const auto bucket = static_cast(mixed) & bucketMask; + auto* ways = keys.data() + bucket * Ways; + if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true; + ways[next[bucket]] = key; + next[bucket] = static_cast((next[bucket] + 1u) % Ways); + if (filled[bucket] < Ways) ++filled[bucket]; + return false; +} + +bool Driver::admitDrawKey(std::uint64_t key) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawCache.contains(key)) return true; + if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); + return drawKeySightings->SeenBefore(key); +} + void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 3c6931fd2..b4f9d0cd5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st } void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) { + if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) { phaseTiming.Phase(DrawRowVectors); std::uint64_t unstable = 0, mismatches = 0; for (std::size_t i = 0; i < programs.size(); ++i) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 93c8237c2..23fbd3392 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -18,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using AgcDriver::DriverDetail::DrawMiss; +using AgcDriver::DriverDetail::DrawKeySightings; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; @@ -48,7 +49,7 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawTwice() { +std::vector DrawThrice() { const auto color = reinterpret_cast(ColorMemory.data()); const auto vertex = reinterpret_cast(VertexCode.data()); const auto pixel = reinterpret_cast(PixelCode.data()); @@ -83,28 +84,41 @@ std::vector DrawTwice() { for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); Append(commands, 0x2d, {3, 2}); Append(commands, 0x2d, {3, 2}); + Append(commands, 0x2d, {3, 2}); return commands; } -void SecondDrawHitsTheCache(bool libraries) { +void ThirdDrawHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - auto commands = DrawTwice(); + auto commands = DrawThrice(); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); AgcDriverWaitIdle_nid_postfix(); const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; const auto layout = counters.misses[static_cast(DrawMiss::Layout)]; - Require(counters.lookups == 2 && counters.absent == 1, mode + ": the two draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 2 lookups with 1 absent"); + Require(counters.lookups == 3 && counters.absent == 2, mode + ": the three draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 3 lookups with 2 absent"); + Require(counters.inserts == 1, mode + ": the three draws inserted " + std::to_string(counters.inserts) + " draw entries, expected 1: a key is cached the second time it is seen"); Require(layout == 0, mode + ": the repeated draw missed the draw cache on its push offset (" + std::to_string(layout) + " layout misses)"); Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void SightingsKeepCollidingKeys() { + DrawKeySightings sightings(DrawKeySightings::Ways); + Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); + Require(!sightings.SeenBefore(11) && !sightings.SeenBefore(12), "an unseen draw key was admitted"); + Require(sightings.SeenBefore(11) && sightings.SeenBefore(12) && sightings.SeenBefore(11), "two keys in one bucket, drawn in turn, were not admitted at their second sight"); + Require(!sightings.SeenBefore(13) && !sightings.SeenBefore(14), "an unseen draw key was admitted"); + Require(!sightings.SeenBefore(0), "the oldest key of a full bucket was kept over the newest"); + Require(sightings.SeenBefore(12) && sightings.SeenBefore(13) && sightings.SeenBefore(14), "a full bucket lost a key newer than the one it replaced"); +} + } int main() { try { + SightingsKeepCollidingKeys(); bool libraries = false; { const auto device = OpenVulkanTestDevice(); @@ -112,7 +126,7 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; - SecondDrawHitsTheCache(libraries); + ThirdDrawHitsTheCache(libraries); AgcDriverShutdown_nid_postfix(); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0;