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.
This commit is contained in:
Dean Galvin 2026-10-12 01:45:31 +13:00
parent 60fb618708
commit 0adbdcf45c
5 changed files with 66 additions and 6 deletions

View File

@ -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<std::uint64_t> keys;
std::vector<std::uint8_t> filled;
std::vector<std::uint8_t> next;
std::size_t bucketMask = 0;
};
enum class DrawVerdict { Drawn, Nothing, Rejected };
struct StageCapture {

View File

@ -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<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode);
bool admitDrawKey(std::uint64_t key);
void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected);
std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages);
void attachDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages, std::shared_ptr<const DrawRecipe> recipe);
@ -310,6 +311,7 @@ private:
std::list<std::uint64_t> drawOrder;
std::mutex drawCacheMutex;
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
std::optional<DrawKeySightings> drawKeySightings;
DrawEntryCounters drawEntryCounters;

View File

@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
#include <algorithm>
#include <bit>
#include <cstdlib>
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<std::size_t>(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<std::uint8_t>((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<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode) {
std::lock_guard cacheLock(drawCacheMutex);
auto& counters = drawEntryCounters;

View File

@ -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<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& 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) {

View File

@ -18,6 +18,7 @@ namespace {
using AgcDriver::Graphics::Require;
using AgcDriver::DriverDetail::DrawMiss;
using AgcDriver::DriverDetail::DrawKeySightings;
alignas(256) constexpr std::array<std::uint32_t, 8> VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u};
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
@ -48,7 +49,7 @@ void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::ini
commands.insert(commands.end(), payload);
}
std::vector<std::uint32_t> DrawTwice() {
std::vector<std::uint32_t> DrawThrice() {
const auto color = reinterpret_cast<std::uintptr_t>(ColorMemory.data());
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
@ -83,28 +84,41 @@ std::vector<std::uint32_t> 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<std::uint32_t>(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<std::size_t>(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;