From 3cb7ccc8059cb28347baece4e75d7b40efeb3459 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sat, 3 Oct 2026 01:33:05 +0200 Subject: [PATCH] perf(agc): draw templates rebind moved read-only buffers and data words The draw resource key holds each read-only buffer's V# base and size and every ShaderData/FlattenedSrt word, so draws whose buffers move per frame (constant and vertex rings) never hit a template. The key now leaves those out; a hit checks every moved read-only buffer (accessible, no pending or shadowed writes over it) and rebinds it from its new address with the template's push adjustment, and data words that changed get a fresh data buffer with the template's patches. A template whose buffers cannot be rebound counts as a miss and is built fresh. APS5_NO_MOVED_BUFFER_TEMPLATES=1 keys the words as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/ShaderResources.hpp | 12 ++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 27 +++++- .../Graphics/src/ShaderResources.cpp | 92 +++++++++++++++++-- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index b48c42c3c..0ae88286a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -114,7 +115,14 @@ public: std::vector snapshots; ~DrawBindings(); }; - std::shared_ptr PrepareDrawBindings(Recorder& recorder) const; + struct MovedBuffer { + std::size_t allocation; + std::uint64_t address; + std::size_t size; + std::vector words; + }; + std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; + std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs @@ -155,7 +163,7 @@ public: // Without `dataWords` the ShaderData and FlattenedSrt descriptor words stay out of the key // (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); + static std::vector ContentKey(const CompiledShader& shader, bool dataWords = true, bool movableBuffers = false); // 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/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1c9e47706..0859fe202 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -538,6 +538,11 @@ std::set CachedFragmentOutputs(const Context& context, std::span< // draw), so a target or index ring that moves per frame does not miss on every draw. That guards a // workload with such rings; in the profiled menu stage every draw was DRAW_INDEX_AUTO (index range // 0) onto one fixed target, so its misses come from the stages' descriptor words themselves. +bool MovableBuffers() { + static const bool enabled = std::getenv("APS5_NO_MOVED_BUFFER_TEMPLATES") == nullptr; + return enabled; +} + ResourceCache::Key DrawResourceKey(const Context& context, std::span shaders, const ColorTarget& target, std::uint64_t indexAddress, std::uint64_t indexBytes, bool ranges) { ResourceCache::Key key{0xffffffffu}; const auto append64 = [&](std::uint64_t value) { @@ -547,7 +552,7 @@ ResourceCache::Key DrawResourceKey(const Context& context, std::span(context.device)); key.push_back(static_cast(shaders.size())); for (const auto& shader : shaders) { - const auto part = ShaderResources::ContentKey(shader); + const auto part = ShaderResources::ContentKey(shader, true, MovableBuffers()); key.push_back(static_cast(part.size())); key.insert(key.end(), part.begin(), part.end()); } @@ -867,6 +872,7 @@ std::shared_ptr refreshResidentTarget(const Context& context, co // stages repeat, or a fresh build. struct ResolvedResources { std::shared_ptr resources; + std::vector moved; ResourceCache::Key contentKey; bool cacheable = false; const ShaderResources::BuildTiming* built = nullptr; @@ -890,11 +896,18 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat if (resolved.cacheable) { resolved.contentKey = DrawResourceKey(context, shaders, state.color, draw.indexAddress, indexBytes, !trimKey); if (auto cached = SharedResourceCache().Find(resolved.contentKey)) { - if (cached->Revalidate(shaders)) { + const bool valid = cached->Revalidate(shaders); + auto* recorder = Recorder::Active(); + std::optional> moved; + if (valid && recorder != nullptr) moved = cached->MovedReadOnlyBuffers(shaders, *recorder); + if (moved.has_value()) { if (trimKey) CheckBufferAliases(shaders, state.color, draw.indexAddress, indexBytes); resolved.resources = std::move(cached); + resolved.moved = std::move(*moved); outcome.kind = KindTemplateHit; countCache(&DrawProfile::cacheHits); + } else if (valid) { + countCache(&DrawProfile::cacheMisses); } else { SharedResourceCache().Remove(resolved.contentKey); countCache(&DrawProfile::cacheInvalidated); @@ -1105,6 +1118,7 @@ struct RecordedDraw { std::span targetViews; std::vector> targets; const IndirectRecord* indirect = nullptr; + std::span moved; bool listed = false; bool completion = false; bool waited = false; @@ -1244,7 +1258,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); // A queued label store over such memory likewise (Recorder::RecordStore). if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); - const auto drawBindings = resources.PrepareDrawBindings(*recorder); + const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); const bool capture = CaptureInputsEnabled(); const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey); outcome.passContinued = continued; @@ -1582,6 +1596,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; const bool lean = recorded && !drawTransitions; + if (!lean && !resolved.moved.empty()) { + resolved.resources = std::make_shared(context, shaders, state.color, draw.indexAddress, static_cast(indexBytes), snapshots); + resolved.moved.clear(); + } APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline"); // The state is copied only when a mask must change. auto masked = maskedState(state, inputs.fragmentOutputs); @@ -1599,6 +1617,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& RecordedDraw record; record.recorder = recorder; record.resources = resources; + record.moved = resolved.moved; record.pipeline = pipeline; record.framebuffer = framebuffer; record.targetViews = targetViews; @@ -1612,7 +1631,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty()) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 9c5bbcf75..1ec052417 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1109,7 +1109,7 @@ void ShaderResources::reportDescriptorCaches() const { std::fprintf(stderr, "[descriptors] layouts %llu hits / %llu created, sets %llu from %llu pools, samplers %llu hits / %llu created\n", static_cast(descriptors.layoutHits), static_cast(descriptors.layoutMisses), static_cast(descriptors.sets), static_cast(descriptors.pools), static_cast(samplerHits), static_cast(samplerMisses)); } -std::vector ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords) { +std::vector ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) { Require(shader.program != nullptr, "missing compiled shader"); const auto& program = *shader.program; std::vector key; @@ -1132,7 +1132,17 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha }; for (const auto& binding : program.bindings) { key.insert(key.end(), {static_cast(binding.kind), static_cast(binding.role), binding.descriptorSet, binding.binding, binding.count, binding.readOnly ? 1u : 0u, binding.imageShape.has_value() ? static_cast(*binding.imageShape) + 1u : 0u, static_cast(binding.guestDescriptor.size())}); - if (dataWords || !DataRole(binding.role)) key.insert(key.end(), binding.guestDescriptor.begin(), binding.guestDescriptor.end()); + if (movableBuffers && binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers && binding.guestDescriptor.size() == static_cast(binding.count) * 4u) { + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + const bool empty = words[2] == 0 || (words[0] == 0 && (words[1] & 0xffffu) == 0); + if (written || empty) key.insert(key.end(), words, words + 4); + else key.insert(key.end(), {0u, words[1] & 0xffff0000u, 0u, words[3]}); + } + } else if ((dataWords && !movableBuffers) || !DataRole(binding.role)) { + key.insert(key.end(), binding.guestDescriptor.begin(), binding.guestDescriptor.end()); + } packBits(binding.imageWritten); packBits(binding.samplerDepthCompare); packBits(binding.imageDepthCompare); @@ -2562,18 +2572,84 @@ ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); } -std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder) const { +std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { + std::vector moved; + if (_set == VK_NULL_HANDLE || usesBda) return moved; + for (const auto& shader : shaders) { + if (shader.program == nullptr) return std::nullopt; + for (const auto& binding : shader.program->bindings) { + if (DataRole(binding.role)) { + const auto kept = std::find_if(bindings.begin(), bindings.end(), [&](const Binding& item) { return item.layout.binding == binding.binding; }); + if (kept == bindings.end() || kept->allocations.size() != 1) return std::nullopt; + const auto index = kept->allocations.front(); + const auto& item = allocations[index]; + if (item.guest || item.buffer == nullptr || item.size != binding.guestDescriptor.size() * sizeof(std::uint32_t)) return std::nullopt; + const bool patched = std::any_of(dataPatches.begin(), dataPatches.end(), [&](const DataPatch& patch) { return patch.allocation == index; }); + const bool same = !item.dataWords.empty() ? item.dataWords == binding.guestDescriptor : !patched && std::memcmp(item.buffer->Bytes().data(), binding.guestDescriptor.data(), item.size) == 0; + if (same) continue; + if (item.dataWords.empty() && patched) return std::nullopt; + moved.push_back({index, 0, item.size, binding.guestDescriptor}); + continue; + } + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + const auto kept = std::find_if(bindings.begin(), bindings.end(), [&](const Binding& item) { return item.layout.binding == binding.binding; }); + if (kept == bindings.end() || kept->allocations.size() != binding.count || binding.guestDescriptor.size() != static_cast(binding.count) * 4u) return std::nullopt; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const auto address = descriptor.Base48(); + const auto size = descriptor.GetSize(); + const auto index = kept->allocations[element]; + const auto& item = allocations[index]; + const bool empty = size == 0 || address == 0; + if (!item.guest) { + if (empty) continue; + return std::nullopt; + } + if (item.address == address && item.size == size) continue; + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + if (written || empty || item.written || size > context.limits.maxStorageBufferRange) return std::nullopt; + const auto begin = address - item.adjustment; + const auto bytes = static_cast(size) + item.adjustment; + if (!GuestMemory::Accessible(reinterpret_cast(begin), bytes)) return std::nullopt; + if (guestMemory.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes) || PendingStorageOverlaps(begin, bytes, nullptr) || AnyShadowedOverlaps(begin, bytes)) return std::nullopt; + moved.push_back({index, address, static_cast(size)}); + } + } + } + return moved; +} + +std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if (_set == VK_NULL_HANDLE || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); auto result = std::make_shared(); std::vector selected; for (std::size_t index = 0; index < allocations.size(); ++index) { const auto& item = allocations[index]; - if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; - const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; }); - if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue; - const auto begin = item.address - item.adjustment; - const auto bytes = item.size + item.adjustment; + const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); + if (override != moved.end() && !override->words.empty()) { + auto buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + } + selected.push_back(index); + result->snapshots.push_back({0, std::move(buffer)}); + continue; + } + std::uint64_t address = item.address; + std::size_t size = item.size; + if (override != moved.end()) { + address = override->address; + size = override->size; + } else { + if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; + const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; }); + if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue; + } + const auto begin = address - item.adjustment; + const auto bytes = size + item.adjustment; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const auto generation = GuestMemory::CollectWrites(begin, bytes); auto buffer = recorder.ReusableDrawSnapshot(begin, bytes);