fix(agc): preserve per-draw input buffer contents until gpu completion

This commit is contained in:
boykopovar 2026-09-28 13:35:53 +03:00
parent 76f1d48e60
commit 9f1c680b38
4 changed files with 142 additions and 9 deletions

View File

@ -101,6 +101,20 @@ public:
ShaderResources& operator=(const ShaderResources&) = delete;
VkDescriptorSetLayout Layout() const;
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
struct DrawBindings {
DrawBindings() = default;
DrawBindings(const DrawBindings&) = delete;
DrawBindings& operator=(const DrawBindings&) = delete;
struct Snapshot {
std::uint64_t address;
std::shared_ptr<Buffer> buffer;
};
DescriptorCache* cache = nullptr;
DescriptorCache::SetAllocation allocation;
std::vector<Snapshot> snapshots;
~DrawBindings();
};
std::shared_ptr<DrawBindings> PrepareDrawBindings(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

View File

@ -1005,7 +1005,7 @@ bool CaptureInputsEnabled() {
return enabled;
}
void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer commands, const ShaderResources& resources, std::uint64_t target) {
void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer commands, const ShaderResources& resources, const std::shared_ptr<ShaderResources::DrawBindings>& bindings, std::uint64_t target) {
struct Sample {
std::uint64_t address;
std::size_t offset;
@ -1018,22 +1018,33 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c
const auto batch = static_cast<unsigned long long>(recorder.Submissions() + 1);
std::vector<Sample> samples;
std::size_t total = 0;
const auto addSample = [&](std::uint64_t address, std::size_t bytes, VkBuffer source, VkDeviceSize offset, const std::byte* expected) {
if (bytes == 0 || bytes > 512) return;
Require(offset % 4 == 0 && bytes % 4 == 0, "capture input is not aligned for a Vulkan buffer copy");
Require(total + bytes <= 65536, "capture inputs exceed 64 KiB per draw");
Sample sample{address, total, std::vector<std::byte>(bytes), source, offset};
std::memcpy(sample.expected.data(), expected, bytes);
total += bytes;
samples.push_back(std::move(sample));
};
if (bindings != nullptr) {
for (const auto& snapshot : bindings->snapshots) {
const auto bytes = snapshot.buffer->Bytes();
addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data());
}
}
for (const auto& [begin, end] : resources.InPlaceReads()) {
Require(end >= begin, "invalid capture input range");
const auto bytes = static_cast<std::size_t>(end - begin);
if (bytes == 0 || bytes > 512) continue;
if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue;
if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) {
CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast<unsigned long long>(begin), bytes);
continue;
}
const auto* imported = HostImportFor(context, begin, bytes);
Require(imported != nullptr, "capture input has no host import");
Require(begin % 4 == 0 && bytes % 4 == 0, "capture input is not aligned for a Vulkan buffer copy");
Require(total + bytes <= 65536, "capture inputs exceed 64 KiB per draw");
Sample sample{begin, total, std::vector<std::byte>(bytes), imported->buffer, begin - imported->base};
std::memcpy(sample.expected.data(), reinterpret_cast<const void*>(begin), bytes);
total += bytes;
samples.push_back(std::move(sample));
addSample(begin, bytes, imported->buffer, begin - imported->base, reinterpret_cast<const std::byte*>(begin));
}
CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast<unsigned long long>(target), samples.size(), total);
if (samples.empty()) return;
@ -1097,12 +1108,13 @@ 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 bool capture = CaptureInputsEnabled();
const bool continued = !capture && !readsTarget && !gpuIndirect && recorder->ContinuesRenderPass(passKey);
outcome.passContinued = continued;
outcome.passBegun = !continued;
const auto commands = continued ? recorder->CommandsInRenderPass() : recorder->Commands();
if (capture) captureInputs(context, *recorder, commands, resources, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress);
if (capture) captureInputs(context, *recorder, commands, resources, drawBindings, record.targets.empty() || record.targets.front() == nullptr ? 0 : record.targets.front()->Descriptor().baseAddress);
// The draw's [gputime] class range: from its first barrier to the pass's trailing barrier (a
// continued draw lies inside its pass's range).
const auto drawTiming = !continued ? recorder->BeginGpuTiming(CommandClass::Draw) : Recorder::NoTiming;
@ -1134,7 +1146,12 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state.viewport, state.scissor);
}
APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK");
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout());
if (drawBindings != nullptr) {
const auto set = drawBindings->allocation.set;
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr);
} else {
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout());
}
APS5_LOG_CHARS_OUT_DEBUG("Resources bound");
if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes);
else record.pipeline->PushConstants(commands, shaders);

View File

@ -2462,6 +2462,70 @@ VkDescriptorSetLayout ShaderResources::Layout() const {
return _layout;
}
ShaderResources::DrawBindings::~DrawBindings() {
if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation);
}
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder) 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;
auto buffer = std::make_shared<Buffer>(context, item.size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::memcpy(buffer->Bytes().data(), reinterpret_cast<const void*>(item.address), item.size);
selected.push_back(index);
result->snapshots.push_back({item.address, std::move(buffer)});
CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast<unsigned long long>(recorder.Submissions() + 1), static_cast<unsigned long long>(item.address), item.size);
}
if (selected.empty()) return {};
Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache");
std::map<VkDescriptorType, std::uint32_t> counts;
for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount;
std::vector<VkDescriptorPoolSize> sizes;
for (const auto& [type, count] : counts) sizes.push_back({type, count});
result->cache = context.descriptorCache;
result->allocation = result->cache->Allocate(_layout, sizes);
Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed");
std::vector<VkCopyDescriptorSet> copies;
for (const auto& binding : bindings) {
VkCopyDescriptorSet copy{VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET};
copy.srcSet = _set;
copy.srcBinding = binding.layout.binding;
copy.dstSet = result->allocation.set;
copy.dstBinding = binding.layout.binding;
copy.descriptorCount = binding.layout.descriptorCount;
copies.push_back(copy);
}
const auto update = context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets");
update(context.device, 0, nullptr, static_cast<std::uint32_t>(copies.size()), copies.data());
std::vector<VkDescriptorBufferInfo> infos;
infos.reserve(selected.size());
for (const auto& snapshot : result->snapshots) infos.push_back({snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()});
std::vector<VkWriteDescriptorSet> writes;
for (const auto& binding : bindings) {
for (std::size_t element = 0; element < binding.allocations.size(); ++element) {
const auto found = std::find(selected.begin(), selected.end(), binding.allocations[element]);
if (found == selected.end()) continue;
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = result->allocation.set;
write.dstBinding = binding.layout.binding;
write.dstArrayElement = static_cast<std::uint32_t>(element);
write.descriptorCount = 1;
write.descriptorType = binding.layout.descriptorType;
write.pBufferInfo = &infos[static_cast<std::size_t>(found - selected.begin())];
writes.push_back(write);
}
}
update(context.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0, nullptr);
recorder.Keep(result);
return result;
}
void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const {
if (_set == VK_NULL_HANDLE) return;
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr);

View File

@ -653,6 +653,44 @@ void resourceReadTests(const Device& device, Recorder& recorder) {
Require(!recorder.PendingReadOverlaps(element, 16), "the build's read refuses after its batch ran");
recorder.Sync();
}
std::weak_ptr<ShaderResources::DrawBindings> snapshotLifetime;
{
auto snapshotContext = context;
DescriptorCache cache(snapshotContext);
snapshotContext.descriptorCache = &cache;
Recorder snapshotRecorder(snapshotContext);
snapshotRecorder.Activate();
ShaderResources resources(snapshotContext, compute);
auto first = resources.PrepareDrawBindings(snapshotRecorder);
Require(first != nullptr && first->snapshots.size() == 1, "read-only draw input was not snapshotted");
std::memset(reinterpret_cast<void*>(element), 0x22, elementBytes);
auto second = resources.PrepareDrawBindings(snapshotRecorder);
Require(second != nullptr && second->snapshots.size() == 1, "cached draw input was not snapshotted again");
Require(first->allocation.set != second->allocation.set, "in-flight draws share mutable descriptor bindings");
std::memset(reinterpret_cast<void*>(element), 0x33, elementBytes);
auto downloaded = std::make_shared<Buffer>(snapshotContext, elementBytes * 2, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = snapshotRecorder.Commands();
RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
const auto copy = snapshotContext.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
VkBufferCopy region{0, 0, elementBytes};
copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, &region);
region.dstOffset = elementBytes;
copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, &region);
RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
snapshotLifetime = first;
first.reset();
second.reset();
Require(!snapshotLifetime.expired(), "draw snapshot was released before its GPU batch");
snapshotRecorder.Sync();
const auto contents = downloaded->Bytes();
Require(std::all_of(contents.begin(), contents.begin() + elementBytes, [](std::byte value) { return value == std::byte{0x11}; }), "first draw observed overwritten input");
Require(std::all_of(contents.begin() + elementBytes, contents.end(), [](std::byte value) { return value == std::byte{0x22}; }), "second draw observed overwritten input");
snapshotRecorder.NotePendingWrite(element, elementBytes);
Require(resources.PrepareDrawBindings(snapshotRecorder) == nullptr, "GPU-produced input was replaced with stale CPU memory");
snapshotRecorder.Sync();
}
Require(snapshotLifetime.expired(), "draw snapshots outlived their recorder");
recorder.Activate();
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);