From 5cf0aa48d95f21d31e37fbdd3721fea691ef1cdc Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:04:44 +0200 Subject: [PATCH] feat(agc): count which key component differs on resource-content cache misses Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 23 +++ .../Graphics/include/ShaderResources.hpp | 6 + .../Graphics/src/ShaderResources.cpp | 153 +++++++++++++++++- .../prx/libSceAgcDriver/tests/KeyMiss.cpp | 119 ++++++++++++++ 4 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..bf23fd834a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -348,6 +348,29 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_driver_key_miss_tests EXCLUDE_FROM_ALL tests/KeyMiss.cpp) +target_include_directories(agc_driver_key_miss_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_key_miss_tests PRIVATE + libSceAgcDriver + libc + libkernel +) +set_target_properties(agc_driver_key_miss_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_key_miss_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_key_miss_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_key_miss COMMAND agc_driver_key_miss_tests) + set_tests_properties(agc_driver_key_miss PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_key_miss PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL tests/Submit.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 51d1b86941..74f5074184 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +107,10 @@ private: Stats stats; }; +enum class ContentKeyComponent : std::size_t { Layout, BufferWord0, BufferWord1, BufferWord2, BufferWord3, Image, Sampler, ShaderData, Other, DrawTarget, Count }; + +using ContentKeyDiff = std::array(ContentKeyComponent::Count)>; + class ShaderResources { public: ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes); @@ -195,6 +200,7 @@ public: // (their count and size remain): a compute template then serves dispatches whose constants // differ, and the hit refreshes its data buffers with the dispatch's words (RefreshData). static std::vector ContentKey(const CompiledShader& shader, bool dataWords = true, bool movableBuffers = false); + static bool DiffContentKeys(std::span previous, std::span key, bool draw, ContentKeyDiff& diff); // Records the shader's ShaderData and FlattenedSrt words into this object's data buffers // (vkCmdUpdateBuffer, a transfer write the caller's pre-dispatch barrier makes visible; a // buffer already holding the words is left alone). Returns whether anything was recorded. With diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 552145a607..64292bc1e9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1810,6 +1810,88 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha return key; } +namespace { + +using Component = ContentKeyComponent; + +bool LabelShaderKey(std::span key, std::size_t at, std::size_t end, bool movable, std::vector& labels) { + const auto label = [&](std::size_t first, std::size_t count, Component component) { + if (first + count > end) return false; + std::fill(labels.begin() + static_cast(first), labels.begin() + static_cast(first + count), static_cast(component)); + return true; + }; + if (!label(at, 5, Component::Layout)) return false; + const bool dataWords = key[at] != 0; + const auto bindings = key[at + 4]; + at += 5; + for (std::uint32_t binding = 0; binding < bindings; ++binding) { + if (!label(at, 8, Component::Layout)) return false; + const auto role = static_cast(key[at + 1]); + const auto size = key[at + 7]; + at += 8; + const auto words = DataRole(role) && !(dataWords && !movable) ? 0u : size; + if (at + words > end) return false; + for (std::uint32_t i = 0; i < words; ++i) { + auto component = Component::Other; + if (role == ShaderRecompiler::DescriptorRole::GuestBuffers) component = static_cast(static_cast(Component::BufferWord0) + i % 4u); + else if (role == ShaderRecompiler::DescriptorRole::GuestImages) component = Component::Image; + else if (role == ShaderRecompiler::DescriptorRole::GuestSamplers) component = Component::Sampler; + else if (DataRole(role)) component = Component::ShaderData; + labels[at + i] = static_cast(component); + } + at += words; + for (int flags = 0; flags < 9; ++flags) { + if (at >= end) return false; + const auto count = 1 + (static_cast(key[at]) + 31) / 32; + if (!label(at, count, Component::Other)) return false; + at += count; + } + if (at >= end) return false; + const auto samplers = static_cast(key[at]); + if (!label(at, 1 + samplers, Component::Sampler)) return false; + at += 1 + samplers; + } + return at == end; +} + +bool LabelContentKey(std::span key, bool draw, bool movable, std::vector& labels) { + labels.assign(key.size(), static_cast(Component::Layout)); + if (!draw) return LabelShaderKey(key, 0, key.size(), movable, labels); + if (key.size() < 4) return false; + std::fill(labels.begin() + 1, labels.begin() + 3, static_cast(Component::Other)); + std::size_t at = 4; + for (std::uint32_t stage = 0; stage < key[3]; ++stage) { + if (at >= key.size()) return false; + const auto size = static_cast(key[at]); + ++at; + if (at + size > key.size() || !LabelShaderKey(key, at, at + size, movable, labels)) return false; + at += size; + } + if (at >= key.size()) return false; + for (auto tail = at + 1; tail < key.size(); ++tail) labels[tail] = static_cast(Component::DrawTarget); + return true; +} + +} + +bool ShaderResources::DiffContentKeys(std::span previous, std::span key, bool draw, ContentKeyDiff& diff) { + diff = {}; + std::vector labels; + bool labelled = false; + for (const bool movable : {draw, false}) { + labelled = LabelContentKey(key, draw, movable, labels); + if (labelled) break; + } + if (!labelled || previous.size() != key.size()) { + diff[static_cast(Component::Layout)] = true; + return false; + } + for (std::size_t i = 0; i < key.size(); ++i) { + if (previous[i] != key[i]) diff[labels[i]] = true; + } + return true; +} + bool MatchesContentKey(const CompiledShader& shader, std::span key) { std::size_t cursor = 0; bool equal = true; @@ -2503,6 +2585,14 @@ namespace { // seen for the same shader variant(s) and the first differing word is charged to its binding's role // (and to its index within the V# or T# element), printed as [rescache] every 10 s: it says whether // V# bases of ring allocations, SRT words or image descriptors churn. +struct KeyMissWindow { + std::uint64_t compared = 0; + std::uint64_t noCandidate = 0; + std::uint64_t unlabelled = 0; + std::uint64_t identical = 0; + std::array(ContentKeyComponent::Count)> differing{}; +}; + struct ChurnCounts { std::array roles{}; std::array bufferWords{}; @@ -2511,6 +2601,7 @@ struct ChurnCounts { std::uint64_t sameKey = 0; std::uint64_t layout = 0; std::uint64_t drawWords = 0; + KeyMissWindow window; }; // Dispatch keys and draw keys are counted apart: what churns a draw's key decides the draw steps. @@ -2519,9 +2610,21 @@ struct ChurnProfile { std::unordered_map lastByVariant; ChurnCounts dispatch; ChurnCounts draws; + std::unordered_map tracedByVariant; + std::size_t tracedLines = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +constexpr std::size_t MaxTracedLines = 256; +constexpr std::uint32_t MaxTracedLinesPerVariant = 4; + +const char* componentName(std::size_t component) { + constexpr std::array(ContentKeyComponent::Count)> names{"layout", "V# word 0", "V# word 1", "V# word 2", "V# word 3", "image", "sampler", "shader-data", "other", "draw target/index"}; + return names[component]; +} + +void traceMiss(ChurnProfile& churn, bool draw, std::uint64_t variants, const ContentKeyDiff* parts); + ChurnProfile& Churn() { static ChurnProfile profile; return profile; @@ -2584,9 +2687,28 @@ std::shared_ptr ResourceCache::Find(const Key& key) { return found->second->second; } +namespace { + +void traceMiss(ChurnProfile& churn, bool draw, std::uint64_t variants, const ContentKeyDiff* parts) { + if (churn.tracedLines >= MaxTracedLines || ++churn.tracedByVariant[variants] > MaxTracedLinesPerVariant) return; + ++churn.tracedLines; + std::string changed = parts == nullptr ? "first-seen" : ""; + for (std::size_t i = 0; parts != nullptr && i < parts->size(); ++i) { + if (!(*parts)[i]) continue; + if (!changed.empty()) changed += ", "; + changed += componentName(i); + } + if (changed.empty()) changed = "no-component-change"; + const auto hashed = (variants ^ 0x9e3779b97f4a7c15ull) * 0xff51afd7ed558ccdull; + AgcDriver::ProfilePrint_nid_no_patch("[keymiss] resource-content %s shader set %016llx: %s\n", draw ? "draw" : "dispatch", static_cast(hashed ^ (hashed >> 33u)), changed.c_str()); +} + +} + void ResourceCache::noteMiss(const Key& key) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile || key.size() < 5) return; + static const bool trace = std::getenv("APS5_TRACE_RESOURCE_KEY_MISSES") != nullptr; + if ((!profile && !trace) || key.size() < 5) return; // The variant(s) the key belongs to: a compute key starts with the data-words flag, the stage // and its variant, a draw key with a marker, the device and its stages' keys (Draw.cpp // DrawResourceKey). @@ -2607,6 +2729,8 @@ void ResourceCache::noteMiss(const Key& key) { const auto found = churn.lastByVariant.find(variants); if (found == churn.lastByVariant.end()) { ++counts.firstSeen; + ++counts.window.noCandidate; + if (trace) traceMiss(churn, draw, variants, nullptr); churn.lastByVariant.emplace(variants, key); } else { const auto& previous = found->second; @@ -2627,6 +2751,17 @@ void ResourceCache::noteMiss(const Key& key) { std::size_t at = 0; if (!chargeShaderKey(counts, key, at, diff)) ++counts.layout; } + ContentKeyDiff parts; + ++counts.window.compared; + if (!ShaderResources::DiffContentKeys(previous, key, draw, parts)) ++counts.window.unlabelled; + bool anyPart = false; + for (std::size_t i = 0; i < parts.size(); ++i) { + if (!parts[i]) continue; + ++counts.window.differing[i]; + anyPart = true; + } + if (!anyPart) ++counts.window.identical; + if (trace) traceMiss(churn, draw, variants, &parts); found->second = key; } const auto now = std::chrono::steady_clock::now(); @@ -2651,8 +2786,20 @@ void ResourceCache::noteMiss(const Key& key) { line += item; AgcDriver::ProfilePrint_nid_no_patch("%s\n", line.c_str()); }; - report("dispatch", churn.dispatch); - report("draws", churn.draws); + if (profile) { + report("dispatch", churn.dispatch); + report("draws", churn.draws); + } + const auto keyMiss = [](const char* what, KeyMissWindow& window) { + const auto count = [](std::uint64_t value) { return static_cast(value); }; + const auto part = [&](ContentKeyComponent component) { return count(window.differing[static_cast(component)]); }; + AgcDriver::ProfilePrint_nid_no_patch("[keymiss] resource-content %s (10 s): misses %llu; differing: layout %llu, V# word0 %llu, V# word1 %llu, V# word2 %llu, V# word3 %llu, image %llu, sampler %llu, shader-data %llu, other %llu, draw target/index %llu; no-candidate %llu; identical %llu; not comparable %llu\n", what, count(window.compared + window.noCandidate), part(ContentKeyComponent::Layout), part(ContentKeyComponent::BufferWord0), part(ContentKeyComponent::BufferWord1), part(ContentKeyComponent::BufferWord2), part(ContentKeyComponent::BufferWord3), part(ContentKeyComponent::Image), part(ContentKeyComponent::Sampler), part(ContentKeyComponent::ShaderData), part(ContentKeyComponent::Other), part(ContentKeyComponent::DrawTarget), count(window.noCandidate), count(window.identical), count(window.unlabelled)); + window = KeyMissWindow{}; + }; + if (trace) { + keyMiss("dispatch", churn.dispatch.window); + keyMiss("draw", churn.draws.window); + } } void ResourceCache::Insert(const Key& key, std::shared_ptr resources, std::vector>* evicted) { diff --git a/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp b/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp new file mode 100644 index 0000000000..e2dfb0db44 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/KeyMiss.cpp @@ -0,0 +1,119 @@ +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include +#include +#include +#include + +using namespace AgcDriver; +using Graphics::ContentKeyComponent; +using Graphics::ContentKeyDiff; +using Graphics::ShaderResources; +using ShaderRecompiler::DescriptorRole; + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Binding { + DescriptorRole role; + std::vector words; + std::uint32_t count = 1; +}; + +std::vector shaderKey(bool dataWords, bool movable, const std::vector& bindings) { + std::vector key{dataWords ? 1u : 0u, 0u, 7u, 0u, static_cast(bindings.size())}; + for (const auto& binding : bindings) { + const bool data = binding.role == DescriptorRole::ShaderData || binding.role == DescriptorRole::FlattenedSrt; + for (const auto word : {0u, static_cast(binding.role), 0u, 0u, binding.count, 0u, 0u, static_cast(binding.words.size())}) key.push_back(word); + const bool emitted = (movable && binding.role == DescriptorRole::GuestBuffers && binding.words.size() == binding.count * 4u) || (dataWords && !movable) || !data; + if (emitted) key.insert(key.end(), binding.words.begin(), binding.words.end()); + for (int flags = 0; flags < 9; ++flags) key.push_back(0u); + key.push_back(0u); + } + return key; +} + +std::vector drawKey(std::uint32_t device, const std::vector& stage, std::uint32_t target) { + std::vector key{0xffffffffu, device, 0u, 1u, static_cast(stage.size())}; + key.insert(key.end(), stage.begin(), stage.end()); + key.push_back(1u); + for (const auto word : {target, 0u, 0x100u, 0u, 0u, 0u, 0u, 0u}) key.push_back(word); + return key; +} + +std::vector bindings() { + return {{DescriptorRole::GuestBuffers, {10, 11, 12, 13}}, {DescriptorRole::GuestImages, {20, 21, 22, 23, 24, 25, 26, 27}}, {DescriptorRole::GuestSamplers, {30, 31, 32, 33}}, {DescriptorRole::ShaderData, {40, 41, 42, 43}}}; +} + +void expectOnly(const ContentKeyDiff& diff, std::optional component, const char* message) { + for (std::size_t i = 0; i < diff.size(); ++i) { + check(diff[i] == (component && i == static_cast(*component)), message); + } +} + +void testDispatchKey() { + ContentKeyDiff diff; + const auto base = shaderKey(true, false, bindings()); + check(ShaderResources::DiffContentKeys(base, base, false, diff), "equal keys were not comparable"); + expectOnly(diff, std::nullopt, "equal keys reported a difference"); + const auto change = [&](std::size_t binding, std::size_t word, ContentKeyComponent expected, const char* message) { + auto changed = bindings(); + changed[binding].words[word] += 1; + check(ShaderResources::DiffContentKeys(base, shaderKey(true, false, changed), false, diff), message); + expectOnly(diff, expected, message); + }; + change(0, 0, ContentKeyComponent::BufferWord0, "V# word 0"); + change(0, 1, ContentKeyComponent::BufferWord1, "V# word 1"); + change(0, 2, ContentKeyComponent::BufferWord2, "V# word 2"); + change(0, 3, ContentKeyComponent::BufferWord3, "V# word 3"); + change(1, 5, ContentKeyComponent::Image, "image descriptor word"); + change(2, 1, ContentKeyComponent::Sampler, "sampler word"); + change(3, 2, ContentKeyComponent::ShaderData, "shader data word"); + auto two = bindings(); + two[0].words[0] += 1; + two[0].words[3] += 1; + check(ShaderResources::DiffContentKeys(base, shaderKey(true, false, two), false, diff), "two components"); + check(diff[static_cast(ContentKeyComponent::BufferWord0)] && diff[static_cast(ContentKeyComponent::BufferWord3)] && !diff[static_cast(ContentKeyComponent::BufferWord1)], "two components in one miss are both counted"); + auto fewer = bindings(); + fewer.pop_back(); + check(!ShaderResources::DiffContentKeys(base, shaderKey(true, false, fewer), false, diff), "different shapes were compared"); + expectOnly(diff, ContentKeyComponent::Layout, "shape change is a layout change"); + const auto noData = shaderKey(false, false, bindings()); + auto changedData = bindings(); + changedData[3].words[0] += 1; + check(ShaderResources::DiffContentKeys(noData, shaderKey(false, false, changedData), false, diff), "data words left out"); + expectOnly(diff, std::nullopt, "data words are not part of the key without dataWords"); +} + +void testDrawKey() { + ContentKeyDiff diff; + const auto base = drawKey(5, shaderKey(true, true, bindings()), 0x1000); + const auto compare = [&](const std::vector& key, std::optional expected, const char* message) { + check(ShaderResources::DiffContentKeys(base, key, true, diff), message); + expectOnly(diff, expected, message); + }; + compare(drawKey(5, shaderKey(true, true, bindings()), 0x2000), ContentKeyComponent::DrawTarget, "draw target"); + compare(drawKey(6, shaderKey(true, true, bindings()), 0x1000), ContentKeyComponent::Other, "device"); + auto buffer = bindings(); + buffer[0].words[0] += 1; + compare(drawKey(5, shaderKey(true, true, buffer), 0x1000), ContentKeyComponent::BufferWord0, "draw V# word 0"); + auto data = bindings(); + data[3].words[0] += 1; + compare(drawKey(5, shaderKey(true, true, data), 0x1000), std::nullopt, "movable draw keys leave data words out"); +} + +} + +int main() { + try { + testDispatchKey(); + testDrawKey(); + std::puts("Key miss attribution passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +}