feat(agc): count which key component differs on resource-content cache misses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlxcBTWandClaude Opus 5.5 committed 2026-10-13 21:04:44 +02:00
1 parent dacd78525e
commit 5cf0aa48d9
4 files changed
+298 -3

No files matched your search

@@ -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:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
tests/Submit.cpp
)
@@ -13,6 +13,7 @@
#include <map>
#include <memory>
#include <optional>
#include <span>
#include <mutex>
#include <string>
#include <unordered_map>
@@ -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<bool, static_cast<std::size_t>(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<std::uint32_t> ContentKey(const CompiledShader& shader, bool dataWords = true, bool movableBuffers = false);
static bool DiffContentKeys(std::span<const std::uint32_t> previous, std::span<const std::uint32_t> 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
@@ -1810,6 +1810,88 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
return key;
}
namespace {
using Component = ContentKeyComponent;
bool LabelShaderKey(std::span<const std::uint32_t> key, std::size_t at, std::size_t end, bool movable, std::vector<std::uint8_t>& 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<std::ptrdiff_t>(first), labels.begin() + static_cast<std::ptrdiff_t>(first + count), static_cast<std::uint8_t>(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<ShaderRecompiler::DescriptorRole>(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<Component>(static_cast<std::size_t>(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<std::uint8_t>(component);
}
at += words;
for (int flags = 0; flags < 9; ++flags) {
if (at >= end) return false;
const auto count = 1 + (static_cast<std::size_t>(key[at]) + 31) / 32;
if (!label(at, count, Component::Other)) return false;
at += count;
}
if (at >= end) return false;
const auto samplers = static_cast<std::size_t>(key[at]);
if (!label(at, 1 + samplers, Component::Sampler)) return false;
at += 1 + samplers;
}
return at == end;
}
bool LabelContentKey(std::span<const std::uint32_t> key, bool draw, bool movable, std::vector<std::uint8_t>& labels) {
labels.assign(key.size(), static_cast<std::uint8_t>(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<std::uint8_t>(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<std::size_t>(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<std::uint8_t>(Component::DrawTarget);
return true;
}
}
bool ShaderResources::DiffContentKeys(std::span<const std::uint32_t> previous, std::span<const std::uint32_t> key, bool draw, ContentKeyDiff& diff) {
diff = {};
std::vector<std::uint8_t> 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<std::size_t>(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<const std::uint32_t> 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<std::uint64_t, static_cast<std::size_t>(ContentKeyComponent::Count)> differing{};
};
struct ChurnCounts {
std::array<std::uint64_t, 8> roles{};
std::array<std::uint64_t, 4> 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<std::uint64_t, ResourceCache::Key> lastByVariant;
ChurnCounts dispatch;
ChurnCounts draws;
std::unordered_map<std::uint64_t, std::uint32_t> 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<const char*, static_cast<std::size_t>(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<ShaderResources> 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<unsigned long long>(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<unsigned long long>(value); };
const auto part = [&](ContentKeyComponent component) { return count(window.differing[static_cast<std::size_t>(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<ShaderResources> resources, std::vector<std::shared_ptr<ShaderResources>>* evicted) {
@@ -0,0 +1,119 @@
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include <cstdio>
#include <optional>
#include <stdexcept>
#include <vector>
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<std::uint32_t> words;
std::uint32_t count = 1;
};
std::vector<std::uint32_t> shaderKey(bool dataWords, bool movable, const std::vector<Binding>& bindings) {
std::vector<std::uint32_t> key{dataWords ? 1u : 0u, 0u, 7u, 0u, static_cast<std::uint32_t>(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<std::uint32_t>(binding.role), 0u, 0u, binding.count, 0u, 0u, static_cast<std::uint32_t>(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<std::uint32_t> drawKey(std::uint32_t device, const std::vector<std::uint32_t>& stage, std::uint32_t target) {
std::vector<std::uint32_t> key{0xffffffffu, device, 0u, 1u, static_cast<std::uint32_t>(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<Binding> 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<ContentKeyComponent> component, const char* message) {
for (std::size_t i = 0; i < diff.size(); ++i) {
check(diff[i] == (component && i == static_cast<std::size_t>(*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<std::size_t>(ContentKeyComponent::BufferWord0)] && diff[static_cast<std::size_t>(ContentKeyComponent::BufferWord3)] && !diff[static_cast<std::size_t>(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<std::uint32_t>& key, std::optional<ContentKeyComponent> 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;
}
}