fix(agc): preserve per-draw input buffer contents until gpu completion
This commit is contained in:
parent
76f1d48e60
commit
9f1c680b38
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, ®ion);
|
||||
region.dstOffset = elementBytes;
|
||||
copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion);
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue