[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:
Herman S.
2025-10-31 21:20:37 +09:00
committed by Radosław Gliński
parent 93adb2bb95
commit e2c33686cc
8 changed files with 482 additions and 124 deletions

View File

@@ -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, &copy_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, &copy_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_));