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) <noreply@anthropic.com>
This commit is contained in:
parent
7d40a57d18
commit
3cb7ccc805
|
|
@ -12,6 +12,7 @@
|
|||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
|
@ -114,7 +115,14 @@ public:
|
|||
std::vector<Snapshot> snapshots;
|
||||
~DrawBindings();
|
||||
};
|
||||
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder) const;
|
||||
struct MovedBuffer {
|
||||
std::size_t allocation;
|
||||
std::uint64_t address;
|
||||
std::size_t size;
|
||||
std::vector<std::uint32_t> words;
|
||||
};
|
||||
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}) const;
|
||||
std::optional<std::vector<MovedBuffer>> MovedReadOnlyBuffers(std::span<const CompiledShader> 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<std::uint32_t> ContentKey(const CompiledShader& shader, bool dataWords = true);
|
||||
static std::vector<std::uint32_t> 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
|
||||
|
|
|
|||
|
|
@ -538,6 +538,11 @@ std::set<std::uint32_t> 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<const CompiledShader> 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<const Compi
|
|||
append64(reinterpret_cast<std::uint64_t>(context.device));
|
||||
key.push_back(static_cast<std::uint32_t>(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<std::uint32_t>(part.size()));
|
||||
key.insert(key.end(), part.begin(), part.end());
|
||||
}
|
||||
|
|
@ -867,6 +872,7 @@ std::shared_ptr<StorageTexture> refreshResidentTarget(const Context& context, co
|
|||
// stages repeat, or a fresh build.
|
||||
struct ResolvedResources {
|
||||
std::shared_ptr<ShaderResources> resources;
|
||||
std::vector<ShaderResources::MovedBuffer> 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<std::vector<ShaderResources::MovedBuffer>> 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<const VkImageView> targetViews;
|
||||
std::vector<std::shared_ptr<StorageTexture>> targets;
|
||||
const IndirectRecord* indirect = nullptr;
|
||||
std::span<const ShaderResources::MovedBuffer> 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<ShaderResources>(context, shaders, state.color, draw.indexAddress, static_cast<std::size_t>(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<DrawRecipe>();
|
||||
recipe->device = context.device;
|
||||
recipe->templateRef = resources;
|
||||
|
|
|
|||
|
|
@ -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<unsigned long long>(descriptors.layoutHits), static_cast<unsigned long long>(descriptors.layoutMisses), static_cast<unsigned long long>(descriptors.sets), static_cast<unsigned long long>(descriptors.pools), static_cast<unsigned long long>(samplerHits), static_cast<unsigned long long>(samplerMisses));
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords) {
|
||||
std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) {
|
||||
Require(shader.program != nullptr, "missing compiled shader");
|
||||
const auto& program = *shader.program;
|
||||
std::vector<std::uint32_t> key;
|
||||
|
|
@ -1132,7 +1132,17 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
|
|||
};
|
||||
for (const auto& binding : program.bindings) {
|
||||
key.insert(key.end(), {static_cast<std::uint32_t>(binding.kind), static_cast<std::uint32_t>(binding.role), binding.descriptorSet, binding.binding, binding.count, binding.readOnly ? 1u : 0u, binding.imageShape.has_value() ? static_cast<std::uint32_t>(*binding.imageShape) + 1u : 0u, static_cast<std::uint32_t>(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<std::size_t>(binding.count) * 4u) {
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto* words = binding.guestDescriptor.data() + static_cast<std::size_t>(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::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder) const {
|
||||
std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedReadOnlyBuffers(std::span<const CompiledShader> shaders, Recorder& recorder) const {
|
||||
std::vector<MovedBuffer> 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<std::size_t>(binding.count) * 4u) return std::nullopt;
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto* words = binding.guestDescriptor.data() + static_cast<std::size_t>(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<std::size_t>(size) + item.adjustment;
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(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<std::size_t>(size)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
|
||||
if (_set == VK_NULL_HANDLE || usesBda) return {};
|
||||
const auto reads = guestMemory.InPlaceReads();
|
||||
auto result = std::make_shared<DrawBindings>();
|
||||
std::vector<std::size_t> 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<Buffer>(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<std::byte>(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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue