[GPU] Double-buffer readback_resolve.
Introduce "fast" readback resolve that reads from previous frame's resolve buffers, quick swap between buffers each frame to avoid copies so minimal performance impact and mostly correct bahavior. Checks if previous frame had a buffer for the current resolve and falls through to the slow path, which also allows to support "screenshot" features in the games that do that without stalling on normal resolve operations. Re-enabled readback_memexport as separate feature, was previously bundled with readback_resolve (probably not intentionally) and ensures destination address is writeable to avoid memory access related crashes and unnecessary work. readback_resolve cvar changes from bool to string ternary with "none", "fast" and "full" options, defaulting to the new "fast" mode.
This commit is contained in:
committed by
Radosław Gliński
parent
93adb2bb95
commit
e2c33686cc
@@ -1087,12 +1087,24 @@ void VulkanCommandProcessor::ShutdownContext() {
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
gamma_ramp_buffer_memory_);
|
||||
|
||||
// Clean up readback buffer.
|
||||
// Clean up all readback buffers.
|
||||
for (auto& pair : readback_buffers_) {
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
|
||||
pair.second.buffers[0]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
pair.second.memories[0]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
|
||||
pair.second.buffers[1]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
pair.second.memories[1]);
|
||||
}
|
||||
readback_buffers_.clear();
|
||||
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
|
||||
readback_buffer_);
|
||||
memexport_readback_buffer_);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
readback_buffer_memory_);
|
||||
readback_buffer_size_ = 0;
|
||||
memexport_readback_buffer_memory_);
|
||||
memexport_readback_buffer_size_ = 0;
|
||||
|
||||
ui::vulkan::util::DestroyAndNullHandle(
|
||||
dfn.vkDestroyDescriptorPool, device,
|
||||
@@ -2690,7 +2702,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
|
||||
if (AwaitAllQueueOperationsCompletion()) {
|
||||
// Map staging buffer and copy to guest memory.
|
||||
void* mapped_data;
|
||||
if (dfn.vkMapMemory(device, readback_buffer_memory_, 0,
|
||||
if (dfn.vkMapMemory(device, memexport_readback_buffer_memory_, 0,
|
||||
memexport_total_size, 0,
|
||||
&mapped_data) == VK_SUCCESS) {
|
||||
if (mapped_data) {
|
||||
@@ -2708,7 +2720,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
|
||||
"VulkanCommandProcessor: Failed to map readback buffer "
|
||||
"(mapped_data is null)");
|
||||
}
|
||||
dfn.vkUnmapMemory(device, readback_buffer_memory_);
|
||||
dfn.vkUnmapMemory(device, memexport_readback_buffer_memory_);
|
||||
} else {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to map readback buffer memory "
|
||||
@@ -2741,56 +2753,183 @@ bool VulkanCommandProcessor::IssueCopy() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// CPU readback resolve path (if enabled).
|
||||
if (GetGPUSetting(GPUSetting::ReadbackResolve) &&
|
||||
// CPU readback resolve path (if not disabled).
|
||||
ReadbackResolveMode readback_mode = GetReadbackResolveMode();
|
||||
if (readback_mode != ReadbackResolveMode::kDisabled &&
|
||||
!texture_cache_->IsDrawResolutionScaled() && written_length > 0) {
|
||||
VkBuffer readback_buffer = RequestReadbackBuffer(written_length);
|
||||
if (readback_buffer != VK_NULL_HANDLE) {
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn =
|
||||
vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
||||
VkBuffer shared_memory_buffer = shared_memory_->buffer();
|
||||
|
||||
// Ensure shared memory is ready for transfer.
|
||||
shared_memory_->Use(VulkanSharedMemory::Usage::kRead);
|
||||
|
||||
// Copy GPU buffer → staging buffer.
|
||||
VkBufferCopy copy_region = {};
|
||||
copy_region.srcOffset = written_address;
|
||||
copy_region.dstOffset = 0;
|
||||
copy_region.size = written_length;
|
||||
|
||||
deferred_command_buffer_.CmdVkCopyBuffer(
|
||||
shared_memory_buffer, readback_buffer, 1, ©_region);
|
||||
|
||||
// Wait for GPU to finish (SYNCHRONIZATION STALL - major performance
|
||||
// hit!).
|
||||
if (AwaitAllQueueOperationsCompletion()) {
|
||||
// Map staging buffer and copy to guest memory.
|
||||
void* mapped_data;
|
||||
if (dfn.vkMapMemory(device, readback_buffer_memory_, 0, written_length,
|
||||
0, &mapped_data) == VK_SUCCESS) {
|
||||
if (mapped_data) {
|
||||
memory::vastcpy(memory_->TranslatePhysical(written_address),
|
||||
static_cast<uint8_t*>(mapped_data), written_length);
|
||||
} else {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to map readback buffer "
|
||||
"(mapped_data is null)");
|
||||
}
|
||||
dfn.vkUnmapMemory(device, readback_buffer_memory_);
|
||||
} else {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to map readback buffer memory "
|
||||
"for "
|
||||
"resolve");
|
||||
// Early check: if destination memory is not accessible, skip all the
|
||||
// expensive GPU readback work.
|
||||
VirtualHeap* physical_heap = memory_->GetPhysicalHeap();
|
||||
bool memory_accessible = false;
|
||||
if (physical_heap) {
|
||||
HeapAllocationInfo alloc_info;
|
||||
if (physical_heap->QueryRegionInfo(written_address, &alloc_info) &&
|
||||
(alloc_info.state & kMemoryAllocationCommit) &&
|
||||
(alloc_info.protect & kMemoryProtectWrite)) {
|
||||
uint32_t end_address = written_address + written_length;
|
||||
uint32_t region_end = alloc_info.base_address + alloc_info.region_size;
|
||||
if (end_address <= region_end) {
|
||||
memory_accessible = true;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if (!memory_accessible) {
|
||||
// Destination memory not accessible, skip readback entirely
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a key for this specific resolve operation
|
||||
uint64_t resolve_key =
|
||||
MakeReadbackResolveKey(written_address, written_length);
|
||||
ReadbackBuffer& rb = readback_buffers_[resolve_key];
|
||||
rb.last_used_frame = frame_current_;
|
||||
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
||||
uint32_t write_index = rb.current_index;
|
||||
uint32_t size = AlignReadbackBufferSize(written_length);
|
||||
|
||||
// Allocate/resize write buffer if needed
|
||||
if (size > rb.sizes[write_index]) {
|
||||
// Create buffer with TRANSFER_DST usage for copying from GPU.
|
||||
VkBufferCreateInfo buffer_info = {};
|
||||
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||
buffer_info.size = size;
|
||||
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
|
||||
VkBuffer new_buffer;
|
||||
if (dfn.vkCreateBuffer(device, &buffer_info, nullptr, &new_buffer) !=
|
||||
VK_SUCCESS) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to create readback buffer of {} MB",
|
||||
size >> 20);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get memory requirements.
|
||||
VkMemoryRequirements memory_requirements;
|
||||
dfn.vkGetBufferMemoryRequirements(device, new_buffer,
|
||||
&memory_requirements);
|
||||
|
||||
// Allocate HOST_VISIBLE | HOST_CACHED | HOST_COHERENT memory for
|
||||
// readback.
|
||||
const uint32_t memory_type_index = ui::vulkan::util::ChooseMemoryType(
|
||||
vulkan_device->memory_types(), memory_requirements.memoryTypeBits,
|
||||
ui::vulkan::util::MemoryPurpose::kReadback);
|
||||
|
||||
if (memory_type_index == UINT32_MAX) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to find memory type for readback "
|
||||
"buffer");
|
||||
dfn.vkDestroyBuffer(device, new_buffer, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
VkMemoryAllocateInfo memory_info = {};
|
||||
memory_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||
memory_info.allocationSize = memory_requirements.size;
|
||||
memory_info.memoryTypeIndex = memory_type_index;
|
||||
|
||||
VkDeviceMemory new_memory;
|
||||
if (dfn.vkAllocateMemory(device, &memory_info, nullptr, &new_memory) !=
|
||||
VK_SUCCESS) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to allocate readback buffer "
|
||||
"memory");
|
||||
dfn.vkDestroyBuffer(device, new_buffer, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bind memory to buffer.
|
||||
if (dfn.vkBindBufferMemory(device, new_buffer, new_memory, 0) !=
|
||||
VK_SUCCESS) {
|
||||
XELOGE("VulkanCommandProcessor: Failed to bind readback buffer memory");
|
||||
dfn.vkFreeMemory(device, new_memory, nullptr);
|
||||
dfn.vkDestroyBuffer(device, new_buffer, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clean up old buffer if exists
|
||||
if (rb.buffers[write_index] != VK_NULL_HANDLE) {
|
||||
dfn.vkDestroyBuffer(device, rb.buffers[write_index], nullptr);
|
||||
}
|
||||
if (rb.memories[write_index] != VK_NULL_HANDLE) {
|
||||
dfn.vkFreeMemory(device, rb.memories[write_index], nullptr);
|
||||
}
|
||||
|
||||
rb.buffers[write_index] = new_buffer;
|
||||
rb.memories[write_index] = new_memory;
|
||||
rb.sizes[write_index] = size;
|
||||
}
|
||||
|
||||
VkBuffer shared_memory_buffer = shared_memory_->buffer();
|
||||
|
||||
// Ensure shared memory is ready for transfer.
|
||||
shared_memory_->Use(VulkanSharedMemory::Usage::kRead);
|
||||
|
||||
// Copy GPU buffer → staging buffer.
|
||||
VkBufferCopy copy_region = {};
|
||||
copy_region.srcOffset = written_address;
|
||||
copy_region.dstOffset = 0;
|
||||
copy_region.size = written_length;
|
||||
|
||||
deferred_command_buffer_.CmdVkCopyBuffer(
|
||||
shared_memory_buffer, rb.buffers[write_index], 1, ©_region);
|
||||
|
||||
bool use_delayed_sync = (readback_mode == ReadbackResolveMode::kFast);
|
||||
uint32_t read_index = write_index;
|
||||
|
||||
if (use_delayed_sync) {
|
||||
// Use previous frame's data (avoid stall)
|
||||
read_index = 1 - write_index;
|
||||
} else {
|
||||
// Wait for GPU to finish (accurate but slow)
|
||||
if (!AwaitAllQueueOperationsCompletion()) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to complete queue operations for "
|
||||
"resolve readback");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Read from the appropriate buffer
|
||||
// If using delayed sync but previous buffer doesn't exist, use current
|
||||
// buffer with sync as fallback
|
||||
if (use_delayed_sync && (rb.buffers[read_index] == VK_NULL_HANDLE ||
|
||||
written_length > rb.sizes[read_index])) {
|
||||
read_index = write_index;
|
||||
if (!AwaitAllQueueOperationsCompletion()) {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to complete queue operations for "
|
||||
"resolve readback fallback");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rb.buffers[read_index] != VK_NULL_HANDLE &&
|
||||
written_length <= rb.sizes[read_index]) {
|
||||
void* mapped_data;
|
||||
if (dfn.vkMapMemory(device, rb.memories[read_index], 0, written_length, 0,
|
||||
&mapped_data) == VK_SUCCESS) {
|
||||
if (mapped_data) {
|
||||
// Memory accessibility already checked at the start of this function
|
||||
uint8_t* dest_ptr = memory_->TranslatePhysical(written_address);
|
||||
memory::vastcpy(dest_ptr, static_cast<uint8_t*>(mapped_data),
|
||||
written_length);
|
||||
} else {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to map readback buffer "
|
||||
"(mapped_data is null)");
|
||||
}
|
||||
dfn.vkUnmapMemory(device, rb.memories[read_index]);
|
||||
} else {
|
||||
XELOGE(
|
||||
"VulkanCommandProcessor: Failed to map readback buffer memory for "
|
||||
"resolve");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2803,9 +2942,9 @@ VkBuffer VulkanCommandProcessor::RequestReadbackBuffer(uint32_t size) {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
size = xe::align(size, kReadbackBufferSizeIncrement);
|
||||
size = AlignReadbackBufferSize(size);
|
||||
|
||||
if (size > readback_buffer_size_) {
|
||||
if (size > memexport_readback_buffer_size_) {
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
@@ -2868,17 +3007,17 @@ VkBuffer VulkanCommandProcessor::RequestReadbackBuffer(uint32_t size) {
|
||||
}
|
||||
|
||||
// Destroy old buffer if it exists.
|
||||
if (readback_buffer_ != VK_NULL_HANDLE) {
|
||||
dfn.vkDestroyBuffer(device, readback_buffer_, nullptr);
|
||||
dfn.vkFreeMemory(device, readback_buffer_memory_, nullptr);
|
||||
if (memexport_readback_buffer_ != VK_NULL_HANDLE) {
|
||||
dfn.vkDestroyBuffer(device, memexport_readback_buffer_, nullptr);
|
||||
dfn.vkFreeMemory(device, memexport_readback_buffer_memory_, nullptr);
|
||||
}
|
||||
|
||||
readback_buffer_ = new_buffer;
|
||||
readback_buffer_memory_ = new_memory;
|
||||
readback_buffer_size_ = size;
|
||||
memexport_readback_buffer_ = new_buffer;
|
||||
memexport_readback_buffer_memory_ = new_memory;
|
||||
memexport_readback_buffer_size_ = size;
|
||||
}
|
||||
|
||||
return readback_buffer_;
|
||||
return memexport_readback_buffer_;
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::InitializeTrace() {
|
||||
@@ -3107,6 +3246,41 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
|
||||
if (is_opening_frame) {
|
||||
frame_open_ = true;
|
||||
|
||||
// Swap all readback buffers for delayed sync (one frame behind)
|
||||
for (auto& pair : readback_buffers_) {
|
||||
pair.second.current_index = 1 - pair.second.current_index;
|
||||
}
|
||||
|
||||
// Evict old readback buffers only when map gets too large to prevent
|
||||
// unbounded memory growth. Don't do this every frame as it's expensive.
|
||||
if (readback_buffers_.size() > kMaxReadbackBuffers) {
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn =
|
||||
vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
||||
for (auto it = readback_buffers_.begin();
|
||||
it != readback_buffers_.end();) {
|
||||
// Evict if not used recently
|
||||
if (frame_current_ > kReadbackBufferEvictionAgeFrames &&
|
||||
it->second.last_used_frame <
|
||||
frame_current_ - kReadbackBufferEvictionAgeFrames) {
|
||||
// Release both buffers and memories
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
|
||||
it->second.buffers[0]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
it->second.memories[0]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
|
||||
it->second.buffers[1]);
|
||||
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
|
||||
it->second.memories[1]);
|
||||
it = readback_buffers_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset bindings that depend on transient data.
|
||||
std::memset(current_float_constant_map_vertex_, 0,
|
||||
sizeof(current_float_constant_map_vertex_));
|
||||
|
||||
Reference in New Issue
Block a user