diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index dc00c9d5e..f9548ba5c 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -50,7 +50,7 @@ DECLARE_bool(guide_button); DECLARE_bool(clear_memory_page_state); -DECLARE_bool(readback_resolve); +DECLARE_string(readback_resolve); DECLARE_bool(readback_memexport); @@ -1832,10 +1832,10 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey( xe::threading::Sleep(delay); break; case ButtonFunctions::ReadbackResolve: - ToggleGPUSetting(GPUSetting::ReadbackResolve); + CycleReadbackResolve(); - notificationTitle = "Toggle Readback Resolve"; - notificationDesc = cvars::readback_resolve ? "Enabled" : "Disabled"; + notificationTitle = "Readback Resolve Mode"; + notificationDesc = cvars::readback_resolve; // Extra Sleep xe::threading::Sleep(delay); @@ -2019,15 +2019,23 @@ void EmulatorWindow::ToggleGPUSetting(gpu::GPUSetting setting) { SaveGPUSetting(GPUSetting::ClearMemoryPageState, !cvars::clear_memory_page_state); break; - case GPUSetting::ReadbackResolve: - SaveGPUSetting(GPUSetting::ReadbackResolve, !cvars::readback_resolve); - break; case GPUSetting::ReadbackMemexport: SaveGPUSetting(GPUSetting::ReadbackMemexport, !cvars::readback_memexport); break; } } +void EmulatorWindow::CycleReadbackResolve() { + const std::string& current = cvars::readback_resolve; + if (current == "fast") { + gpu::SetReadbackResolveMode("full"); + } else if (current == "full") { + gpu::SetReadbackResolveMode("none"); + } else { + gpu::SetReadbackResolveMode("fast"); + } +} + void EmulatorWindow::DisplayHotKeysConfig() { std::string msg = ""; std::string msg_passthru = ""; @@ -2066,8 +2074,7 @@ void EmulatorWindow::DisplayHotKeysConfig() { msg.insert(0, msg_passthru); msg += "\n"; - msg += "Readback Resolve: " + - xe::string_util::BoolToString(cvars::readback_resolve); + msg += "Readback Resolve: " + cvars::readback_resolve; msg += "\n"; msg += "Clear Memory Page State: " + diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index de0958902..e748b91a7 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -280,6 +280,7 @@ class EmulatorWindow { bool vibrate = true); void GamepadHotKeys(); void ToggleGPUSetting(gpu::GPUSetting setting); + void CycleReadbackResolve(); void DisplayHotKeysConfig(); static std::string CanonicalizeFileExtension( diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 64d556f7a..6e31cca24 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -48,11 +48,12 @@ DEFINE_bool(clear_memory_page_state, false, "for 'Team Ninja' Games to fix missing character models)", "GPU"); -DEFINE_bool( - readback_resolve, false, - "Read render-to-texture results on the CPU. This may be " - "needed in some games, for instance, for screenshots in saved games, but " - "causes mid-frame synchronization, so it has a huge performance impact.", +DEFINE_string( + readback_resolve, "fast", + "Controls CPU readback of render-to-texture resolve results.\n" + " fast: Read from previous frame (1 frame delay, no GPU stall - default)\n" + " full: Wait for GPU to finish (accurate but slow, GPU-CPU sync stall)\n" + " none: Disable readback completely (some games render better without it)", "GPU"); DEFINE_bool( @@ -73,9 +74,6 @@ void SaveGPUSetting(GPUSetting setting, uint64_t value) { case GPUSetting::ClearMemoryPageState: OVERRIDE_bool(clear_memory_page_state, static_cast(value)); break; - case GPUSetting::ReadbackResolve: - OVERRIDE_bool(readback_resolve, static_cast(value)); - break; case GPUSetting::ReadbackMemexport: OVERRIDE_bool(readback_memexport, static_cast(value)); break; @@ -86,14 +84,28 @@ bool GetGPUSetting(GPUSetting setting) { switch (setting) { case GPUSetting::ClearMemoryPageState: return cvars::clear_memory_page_state; - case GPUSetting::ReadbackResolve: - return cvars::readback_resolve; case GPUSetting::ReadbackMemexport: return cvars::readback_memexport; } return false; } +ReadbackResolveMode GetReadbackResolveMode() { + const std::string& mode = cvars::readback_resolve; + if (mode == "full") { + return ReadbackResolveMode::kFull; + } else if (mode == "none") { + return ReadbackResolveMode::kDisabled; + } else { + // Default to "fast" for any unrecognized value + return ReadbackResolveMode::kFast; + } +} + +void SetReadbackResolveMode(const std::string& mode) { + OVERRIDE_string(readback_resolve, mode); +} + using namespace xe::gpu::xenos; CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system, diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index b8f7d4d56..f2bf000d4 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -33,14 +33,18 @@ class ByteStream; namespace gpu { -enum class GPUSetting { - ClearMemoryPageState, - ReadbackResolve, - ReadbackMemexport +enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport }; + +enum class ReadbackResolveMode { + kDisabled, // No readback (none) + kFast, // Delayed sync, 1 frame behind (fast) + kFull // Immediate sync with GPU stall (full) }; void SaveGPUSetting(GPUSetting setting, uint64_t value); bool GetGPUSetting(GPUSetting setting); +ReadbackResolveMode GetReadbackResolveMode(); +void SetReadbackResolveMode(const std::string& mode); class GraphicsSystem; class Shader; @@ -162,6 +166,27 @@ class CommandProcessor { static constexpr uint32_t kReadbackBufferSizeIncrement = 16 * 1024 * 1024; + // Eviction policy constants for readback buffer cache + static constexpr size_t kMaxReadbackBuffers = 64; + static constexpr uint64_t kReadbackBufferEvictionAgeFrames = 60; + + // Progressive alignment for readback buffers to avoid wasting memory + static inline uint32_t AlignReadbackBufferSize(uint32_t size) { + if (size < 1 * 1024 * 1024) { + return xe::align(size, 256u * 1024u); // 256KB for < 1MB + } else if (size < 4 * 1024 * 1024) { + return xe::align(size, 1u * 1024u * 1024u); // 1MB for < 4MB + } else { + return xe::align(size, kReadbackBufferSizeIncrement); // 16MB for >= 4MB + } + } + + // Generate a cache key for a specific resolve operation + static inline uint64_t MakeReadbackResolveKey(uint32_t address, + uint32_t length) { + return (uint64_t(address) << 32) | uint64_t(length); + } + void WorkerThreadMain(); virtual bool SetupContext() = 0; virtual void ShutdownContext() = 0; diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index 0d917fde8..b6fed8a12 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -1613,8 +1613,14 @@ bool D3D12CommandProcessor::SetupContext() { void D3D12CommandProcessor::ShutdownContext() { AwaitAllQueueOperationsCompletion(); - ui::d3d12::util::ReleaseAndNull(readback_buffer_); - readback_buffer_size_ = 0; + for (auto& pair : readback_buffers_) { + ui::d3d12::util::ReleaseAndNull(pair.second.buffers[0]); + ui::d3d12::util::ReleaseAndNull(pair.second.buffers[1]); + } + readback_buffers_.clear(); + + ui::d3d12::util::ReleaseAndNull(memexport_readback_buffer_); + memexport_readback_buffer_size_ = 0; ui::d3d12::util::ReleaseAndNull(scratch_buffer_); scratch_buffer_size_ = 0; @@ -2987,7 +2993,7 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, memexport_range.base_address_dwords << 2, memexport_range.size_bytes, false); } - if (GetGPUSetting(GPUSetting::ReadbackResolve)) { + if (GetGPUSetting(GPUSetting::ReadbackMemexport)) { // Read the exported data on the CPU. uint32_t memexport_total_size = 0; for (const draw_util::MemExportRange& memexport_range : @@ -3067,7 +3073,8 @@ bool D3D12CommandProcessor::IssueCopy() { if (!BeginSubmission(true)) { return false; } - if (!GetGPUSetting(GPUSetting::ReadbackResolve)) { + ReadbackResolveMode readback_mode = GetReadbackResolveMode(); + if (readback_mode == ReadbackResolveMode::kDisabled) { uint32_t written_address, written_length; return render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, written_address, @@ -3075,7 +3082,6 @@ bool D3D12CommandProcessor::IssueCopy() { } else { return IssueCopy_ReadbackResolvePath(); } - return true; } XE_NOINLINE bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { @@ -3083,32 +3089,113 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { if (render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, written_address, written_length)) { if (!texture_cache_->IsDrawResolutionScaled() && written_length) { - // Read the resolved data on the CPU. - ID3D12Resource* readback_buffer = RequestReadbackBuffer(written_length); - if (readback_buffer != nullptr) { - shared_memory_->UseAsCopySource(); - SubmitBarriers(); - ID3D12Resource* shared_memory_buffer = shared_memory_->GetBuffer(); - deferred_command_list_.D3DCopyBufferRegion( - readback_buffer, 0, shared_memory_buffer, written_address, - written_length); - if (AwaitAllQueueOperationsCompletion()) { - D3D12_RANGE readback_range; - readback_range.Begin = 0; - readback_range.End = written_length; - void* readback_mapping; - if (SUCCEEDED(readback_buffer->Map(0, &readback_range, - &readback_mapping))) { - // chrispy: this memcpy needs to be optimized as much as possible - - auto physaddr = memory_->TranslatePhysical(written_address); - memory::vastcpy(physaddr, (uint8_t*)readback_mapping, - written_length); - D3D12_RANGE readback_write_range = {}; - readback_buffer->Unmap(0, &readback_write_range); + // 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; } } } + + 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_; + + 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]) { + const ui::d3d12::D3D12Provider& provider = GetD3D12Provider(); + ID3D12Device* device = provider.GetDevice(); + D3D12_RESOURCE_DESC buffer_desc; + ui::d3d12::util::FillBufferResourceDesc(buffer_desc, size, + D3D12_RESOURCE_FLAG_NONE); + ID3D12Resource* buffer; + if (SUCCEEDED(device->CreateCommittedResource( + &ui::d3d12::util::kHeapPropertiesReadback, + provider.GetHeapFlagCreateNotZeroed(), &buffer_desc, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&buffer)))) { + if (rb.buffers[write_index] != nullptr) { + rb.buffers[write_index]->Release(); + } + rb.buffers[write_index] = buffer; + rb.sizes[write_index] = size; + } else { + XELOGE("Failed to create a {} MB readback buffer", size >> 20); + return true; + } + } + + // Copy resolved data to current frame's buffer + shared_memory_->UseAsCopySource(); + SubmitBarriers(); + ID3D12Resource* shared_memory_buffer = shared_memory_->GetBuffer(); + deferred_command_list_.D3DCopyBufferRegion( + rb.buffers[write_index], 0, shared_memory_buffer, written_address, + written_length); + + ReadbackResolveMode readback_mode = GetReadbackResolveMode(); + 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()) { + return true; + } + } + + // Read from the appropriate buffer + ID3D12Resource* read_source = rb.buffers[read_index]; + + // If using delayed sync but previous buffer doesn't exist, use current + // buffer with sync as fallback + if (use_delayed_sync && + (read_source == nullptr || written_length > rb.sizes[read_index])) { + read_source = rb.buffers[write_index]; + read_index = write_index; + if (!AwaitAllQueueOperationsCompletion()) { + return true; + } + } + + if (read_source != nullptr && written_length <= rb.sizes[read_index]) { + D3D12_RANGE readback_range; + readback_range.Begin = 0; + readback_range.End = written_length; + void* readback_mapping; + if (SUCCEEDED( + read_source->Map(0, &readback_range, &readback_mapping))) { + // Memory accessibility already checked at the start of this function + // chrispy: this memcpy needs to be optimized as much as possible + auto physaddr = memory_->TranslatePhysical(written_address); + memory::vastcpy(physaddr, (uint8_t*)readback_mapping, written_length); + D3D12_RANGE readback_write_range = {}; + read_source->Unmap(0, &readback_write_range); + } + } } } else { return false; @@ -3298,6 +3385,34 @@ bool D3D12CommandProcessor::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) { + 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 + if (it->second.buffers[0] != nullptr) { + it->second.buffers[0]->Release(); + } + if (it->second.buffers[1] != nullptr) { + it->second.buffers[1]->Release(); + } + it = readback_buffers_.erase(it); + } else { + ++it; + } + } + } + // Reset bindings that depend on the data stored in the pools. std::memset(current_float_constant_map_vertex_, 0, sizeof(current_float_constant_map_vertex_)); @@ -5077,8 +5192,10 @@ ID3D12Resource* D3D12CommandProcessor::RequestReadbackBuffer(uint32_t size) { if (size == 0) { return nullptr; } - size = xe::align(size, kReadbackBufferSizeIncrement); - if (size > readback_buffer_size_) { + + size = AlignReadbackBufferSize(size); + + if (size > memexport_readback_buffer_size_) { const ui::d3d12::D3D12Provider& provider = GetD3D12Provider(); ID3D12Device* device = provider.GetDevice(); D3D12_RESOURCE_DESC buffer_desc; @@ -5092,13 +5209,13 @@ ID3D12Resource* D3D12CommandProcessor::RequestReadbackBuffer(uint32_t size) { XELOGE("Failed to create a {} MB readback buffer", size >> 20); return nullptr; } - if (readback_buffer_ != nullptr) { - readback_buffer_->Release(); + if (memexport_readback_buffer_ != nullptr) { + memexport_readback_buffer_->Release(); } - readback_buffer_ = buffer; - readback_buffer_size_ = size; + memexport_readback_buffer_ = buffer; + memexport_readback_buffer_size_ = size; } - return readback_buffer_; + return memexport_readback_buffer_; } void D3D12CommandProcessor::WriteGammaRampSRV( diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index d2c8e7054..ca015cc49 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -692,8 +692,19 @@ class D3D12CommandProcessor final : public CommandProcessor { D3D12_RESOURCE_STATES scratch_buffer_state_; bool scratch_buffer_used_ = false; - ID3D12Resource* readback_buffer_ = nullptr; - uint32_t readback_buffer_size_ = 0; + // Per-resolve double-buffered readback for delayed sync + struct ReadbackBuffer { + ID3D12Resource* buffers[2] = {nullptr, nullptr}; + uint32_t sizes[2] = {0, 0}; + uint32_t current_index = 0; + uint64_t last_used_frame = 0; + }; + // Map: (written_address << 32 | written_length) -> ReadbackBuffer + std::unordered_map readback_buffers_; + + // Simple single buffer for memexport (always syncs, no double-buffering) + ID3D12Resource* memexport_readback_buffer_ = nullptr; + uint32_t memexport_readback_buffer_size_ = 0; // The current fixed-function drawing state. D3D12_VIEWPORT ff_viewport_; diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 0a262f065..91229391b 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -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(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(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_)); diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.h b/src/xenia/gpu/vulkan/vulkan_command_processor.h index 2aec81f9b..29a1f88ac 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.h +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.h @@ -754,10 +754,21 @@ class VulkanCommandProcessor final : public CommandProcessor { // Temporary storage for memexport stream constants used in the draw. std::vector memexport_ranges_; - // Readback buffer for CPU access to resolved data - VkBuffer readback_buffer_ = VK_NULL_HANDLE; - VkDeviceMemory readback_buffer_memory_ = VK_NULL_HANDLE; - uint32_t readback_buffer_size_ = 0; + // Per-resolve double-buffered readback for delayed sync + struct ReadbackBuffer { + VkBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + VkDeviceMemory memories[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + uint32_t sizes[2] = {0, 0}; + uint32_t current_index = 0; + uint64_t last_used_frame = 0; + }; + // Map: (written_address << 32 | written_length) -> ReadbackBuffer + std::unordered_map readback_buffers_; + + // Simple single buffer for memexport (always syncs, no double-buffering) + VkBuffer memexport_readback_buffer_ = VK_NULL_HANDLE; + VkDeviceMemory memexport_readback_buffer_memory_ = VK_NULL_HANDLE; + uint32_t memexport_readback_buffer_size_ = 0; }; } // namespace vulkan