diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 37c69f17f..e43c71c88 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -99,7 +99,7 @@ bool GetGPUSetting(GPUSetting setting) { return false; } -ReadbackResolveMode GetReadbackResolveMode() { +static ReadbackResolveMode ParseReadbackResolveMode() { const std::string& mode = cvars::readback_resolve; if (mode == "full") { return ReadbackResolveMode::kFull; @@ -129,6 +129,8 @@ CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system, write_ptr_index_event_(xe::threading::Event::CreateAutoResetEvent(false)), write_ptr_index_(0) { assert_not_null(write_ptr_index_event_); + // Parse and cache readback resolve mode once + cached_readback_resolve_mode_ = ParseReadbackResolveMode(); } CommandProcessor::~CommandProcessor() = default; diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index f2bf000d4..97a3437b6 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -43,7 +43,6 @@ enum class ReadbackResolveMode { void SaveGPUSetting(GPUSetting setting, uint64_t value); bool GetGPUSetting(GPUSetting setting); -ReadbackResolveMode GetReadbackResolveMode(); void SetReadbackResolveMode(const std::string& mode); class GraphicsSystem; @@ -105,6 +104,11 @@ class CommandProcessor { virtual void ClearCaches(); + // Get cached readback resolve mode (avoids string parsing every frame) + ReadbackResolveMode GetReadbackResolveMode() const { + return cached_readback_resolve_mode_; + } + // "Desired" is for the external thread managing the post-processing effect. SwapPostEffect GetDesiredSwapPostEffect() const { return swap_post_effect_desired_; @@ -167,7 +171,7 @@ 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 size_t kMaxReadbackBuffers = 256; static constexpr uint64_t kReadbackBufferEvictionAgeFrames = 60; // Progressive alignment for readback buffers to avoid wasting memory @@ -329,6 +333,10 @@ class CommandProcessor { SwapPostEffect swap_post_effect_desired_ = SwapPostEffect::kNone; SwapPostEffect swap_post_effect_actual_ = SwapPostEffect::kNone; + // Cached readback resolve mode (parsed once from string cvar) + ReadbackResolveMode cached_readback_resolve_mode_ = + ReadbackResolveMode::kFast; + private: reg::DC_LUT_30_COLOR gamma_ramp_256_entry_table_[256] = {}; reg::DC_LUT_PWL_DATA gamma_ramp_pwl_rgb_[128][3] = {}; diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index c1a62b0e3..37cdb88ed 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -1622,6 +1622,13 @@ void D3D12CommandProcessor::ShutdownContext() { AwaitAllQueueOperationsCompletion(); for (auto& pair : readback_buffers_) { + for (int i = 0; i < 2; i++) { + if (pair.second.buffers[i] != nullptr) { + if (pair.second.mapped_data[i] != nullptr) { + pair.second.buffers[i]->Unmap(0, nullptr); + } + } + } ui::d3d12::util::ReleaseAndNull(pair.second.buffers[0]); ui::d3d12::util::ReleaseAndNull(pair.second.buffers[1]); } @@ -3051,6 +3058,31 @@ void D3D12CommandProcessor::InitializeTrace() { } } +void D3D12CommandProcessor::EvictOldReadbackBuffers( + std::unordered_map& buffer_map) { + if (frame_current_ <= kReadbackBufferEvictionAgeFrames) { + return; + } + + for (auto it = buffer_map.begin(); it != buffer_map.end();) { + if (it->second.last_used_frame < + frame_current_ - kReadbackBufferEvictionAgeFrames) { + // Unmap and release both buffers + for (int i = 0; i < 2; i++) { + if (it->second.buffers[i] != nullptr) { + if (it->second.mapped_data[i] != nullptr) { + it->second.buffers[i]->Unmap(0, nullptr); + } + it->second.buffers[i]->Release(); + } + } + it = buffer_map.erase(it); + } else { + ++it; + } + } +} + bool D3D12CommandProcessor::IssueCopy() { #if XE_GPU_FINE_GRAINED_DRAW_SCOPES SCOPE_profile_cpu_f("gpu"); @@ -3098,8 +3130,11 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { } // Create a key for this specific resolve operation + // We only copy on cache miss now, so no need for frame bucketing + // to avoid stale data - we never copy stale data anymore uint64_t resolve_key = MakeReadbackResolveKey(written_address, written_length); + ReadbackBuffer& rb = readback_buffers_[resolve_key]; rb.last_used_frame = frame_current_; @@ -3119,11 +3154,26 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { provider.GetHeapFlagCreateNotZeroed(), &buffer_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&buffer)))) { + // Unmap and release old buffer if (rb.buffers[write_index] != nullptr) { + if (rb.mapped_data[write_index] != nullptr) { + rb.buffers[write_index]->Unmap(0, nullptr); + rb.mapped_data[write_index] = nullptr; + } rb.buffers[write_index]->Release(); } rb.buffers[write_index] = buffer; rb.sizes[write_index] = size; + + // Map the new buffer persistently + D3D12_RANGE read_range = {0, size}; + if (SUCCEEDED( + buffer->Map(0, &read_range, &rb.mapped_data[write_index]))) { + // Successfully mapped + } else { + XELOGE("Failed to persistently map readback buffer"); + rb.mapped_data[write_index] = nullptr; + } } else { XELOGE("Failed to create a {} MB readback buffer", size >> 20); return true; @@ -3155,10 +3205,12 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { // Read from the appropriate buffer ID3D12Resource* read_source = rb.buffers[read_index]; + bool is_cache_miss = false; // 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])) { + is_cache_miss = true; read_source = rb.buffers[write_index]; read_index = write_index; if (!AwaitAllQueueOperationsCompletion()) { @@ -3166,21 +3218,19 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { } } - 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); - } + // Only copy on cache miss (when we have fresh data from GPU sync) + // On cache hit, we'd be copying stale data from previous frame + if (is_cache_miss && read_source != nullptr && + written_length <= rb.sizes[read_index] && + rb.mapped_data[read_index] != nullptr) { + auto physaddr = memory_->TranslatePhysical(written_address); + memory::vastcpy(physaddr, (uint8_t*)rb.mapped_data[read_index], + written_length); } + + // Swap buffer index for next time this specific resolve address is used + // This way next time we write to the other buffer and read from this one + rb.current_index = 1 - rb.current_index; } } else { return false; @@ -3258,11 +3308,26 @@ void D3D12CommandProcessor::IssueDraw_MemexportReadbackFastPath( &ui::d3d12::util::kHeapPropertiesReadback, provider.GetHeapFlagCreateNotZeroed(), &buffer_desc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&buffer)))) { + // Unmap and release old buffer if (rb.buffers[write_index] != nullptr) { + if (rb.mapped_data[write_index] != nullptr) { + rb.buffers[write_index]->Unmap(0, nullptr); + rb.mapped_data[write_index] = nullptr; + } rb.buffers[write_index]->Release(); } rb.buffers[write_index] = buffer; rb.sizes[write_index] = size; + + // Map the new buffer persistently + D3D12_RANGE read_range = {0, size}; + if (SUCCEEDED( + buffer->Map(0, &read_range, &rb.mapped_data[write_index]))) { + // Successfully mapped + } else { + XELOGE("Failed to persistently map memexport readback buffer"); + rb.mapped_data[write_index] = nullptr; + } } else { XELOGE("Failed to create a {} MB memexport readback buffer", size >> 20); return; @@ -3284,36 +3349,37 @@ void D3D12CommandProcessor::IssueDraw_MemexportReadbackFastPath( // Use delayed sync (read from previous frame's buffer) uint32_t read_index = 1 - write_index; - ID3D12Resource* read_source = rb.buffers[read_index]; + bool is_cache_miss = false; // If previous buffer doesn't exist or is too small, fall back to sync // This happens on first use or buffer resize - subsequent frames will be fast - if (read_source == nullptr || memexport_total_size > rb.sizes[read_index]) { - read_source = rb.buffers[write_index]; + if (rb.buffers[read_index] == nullptr || + memexport_total_size > rb.sizes[read_index]) { + is_cache_miss = true; read_index = write_index; if (!AwaitAllQueueOperationsCompletion()) { return; } } - // Read from buffer (previous frame if available, current frame with sync as - // fallback) - D3D12_RANGE readback_range; - readback_range.Begin = 0; - readback_range.End = memexport_total_size; - void* readback_mapping; - if (SUCCEEDED(read_source->Map(0, &readback_range, &readback_mapping))) { + // Only copy on cache miss (when we have fresh data from GPU sync) + // On cache hit, we'd be copying stale data from previous frame + if (is_cache_miss && rb.buffers[read_index] != nullptr && + memexport_total_size <= rb.sizes[read_index] && + rb.mapped_data[read_index] != nullptr) { const uint8_t* readback_bytes = - reinterpret_cast(readback_mapping); + static_cast(rb.mapped_data[read_index]); for (const draw_util::MemExportRange& memexport_range : memexport_ranges_) { memory::vastcpy( memory_->TranslatePhysical(memexport_range.base_address_dwords << 2), const_cast(readback_bytes), memexport_range.size_bytes); readback_bytes += memexport_range.size_bytes; } - D3D12_RANGE readback_write_range = {}; - read_source->Unmap(0, &readback_write_range); } + + // Swap buffer index for next time this specific memexport address is used + // This way next time we write to the other buffer and read from this one + rb.current_index = 1 - rb.current_index; } void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) { @@ -3499,61 +3565,6 @@ 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; - } - } - } - - // Swap all memexport readback buffers for delayed sync (one frame behind) - for (auto& pair : memexport_readback_buffers_) { - pair.second.current_index = 1 - pair.second.current_index; - } - - // Evict old memexport readback buffers - if (memexport_readback_buffers_.size() > kMaxReadbackBuffers) { - for (auto it = memexport_readback_buffers_.begin(); - it != memexport_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 = memexport_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_)); @@ -3699,6 +3710,10 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) { closed_frame_submissions_[(frame_current_++) % kQueueFrames] = submission_current_ - 1; + // Evict old readback buffers once per frame + EvictOldReadbackBuffers(readback_buffers_); + EvictOldReadbackBuffers(memexport_readback_buffers_); + if (cache_clear_requested_ && AwaitAllQueueOperationsCompletion()) { cache_clear_requested_ = false; diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index e8dfa9b88..a3f5800fb 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -326,6 +326,7 @@ class D3D12CommandProcessor final : public CommandProcessor { bool IssueCopy_ReadbackResolvePath(); void IssueDraw_MemexportReadbackFullPath(uint32_t memexport_total_size); void IssueDraw_MemexportReadbackFastPath(uint32_t memexport_total_size); + void InitializeTrace() override; private: @@ -698,9 +699,15 @@ class D3D12CommandProcessor final : public CommandProcessor { struct ReadbackBuffer { ID3D12Resource* buffers[2] = {nullptr, nullptr}; uint32_t sizes[2] = {0, 0}; + void* mapped_data[2] = {nullptr, nullptr}; // Persistent mappings uint32_t current_index = 0; uint64_t last_used_frame = 0; }; + + // Helper to evict old readback buffers from a cache map + void EvictOldReadbackBuffers( + std::unordered_map& buffer_map); + // Map: (written_address << 32 | written_length) -> ReadbackBuffer std::unordered_map readback_buffers_; diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 09c1e97a9..d845e63af 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -1080,6 +1080,11 @@ void VulkanCommandProcessor::ShutdownContext() { // Clean up all readback buffers. for (auto& pair : readback_buffers_) { + for (int i = 0; i < 2; i++) { + if (pair.second.mapped_data[i] != nullptr) { + dfn.vkUnmapMemory(device, pair.second.memories[i]); + } + } ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device, pair.second.buffers[0]); ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device, @@ -1093,6 +1098,11 @@ void VulkanCommandProcessor::ShutdownContext() { // Clean up all memexport readback buffers. for (auto& pair : memexport_readback_buffers_) { + for (int i = 0; i < 2; i++) { + if (pair.second.mapped_data[i] != nullptr) { + dfn.vkUnmapMemory(device, pair.second.memories[i]); + } + } ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device, pair.second.buffers[0]); ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device, @@ -2810,13 +2820,13 @@ void VulkanCommandProcessor::IssueDraw_MemexportReadbackFastPath( uint64_t memexport_key = MakeReadbackResolveKey( memexport_ranges_[0].base_address_dwords, memexport_total_size); - ReadbackBuffer& rb = memexport_readback_buffers_[memexport_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(); + ReadbackBuffer& rb = memexport_readback_buffers_[memexport_key]; + rb.last_used_frame = frame_current_; + uint32_t write_index = rb.current_index; uint32_t size = AlignReadbackBufferSize(memexport_total_size); @@ -2883,6 +2893,10 @@ void VulkanCommandProcessor::IssueDraw_MemexportReadbackFastPath( } // Clean up old buffer if exists + if (rb.mapped_data[write_index] != nullptr) { + dfn.vkUnmapMemory(device, rb.memories[write_index]); + rb.mapped_data[write_index] = nullptr; + } if (rb.buffers[write_index] != VK_NULL_HANDLE) { dfn.vkDestroyBuffer(device, rb.buffers[write_index], nullptr); } @@ -2893,6 +2907,15 @@ void VulkanCommandProcessor::IssueDraw_MemexportReadbackFastPath( rb.buffers[write_index] = new_buffer; rb.memories[write_index] = new_memory; rb.sizes[write_index] = size; + + // Map the new buffer persistently + if (dfn.vkMapMemory(device, new_memory, 0, size, 0, + &rb.mapped_data[write_index]) != VK_SUCCESS) { + XELOGE( + "VulkanCommandProcessor: Failed to persistently map memexport " + "readback buffer"); + rb.mapped_data[write_index] = nullptr; + } } VkBuffer shared_memory_buffer = shared_memory_->buffer(); @@ -2916,38 +2939,68 @@ void VulkanCommandProcessor::IssueDraw_MemexportReadbackFastPath( // Use delayed sync (read from previous frame's buffer) uint32_t read_index = 1 - write_index; - VkBuffer read_source = rb.buffers[read_index]; - VkDeviceMemory read_memory = rb.memories[read_index]; + bool is_cache_miss = false; // If previous buffer doesn't exist or is too small, fall back to sync // This happens on first use or buffer resize - subsequent frames will be fast - if (read_source == VK_NULL_HANDLE || + if (rb.buffers[read_index] == VK_NULL_HANDLE || memexport_total_size > rb.sizes[read_index]) { - read_source = rb.buffers[write_index]; - read_memory = rb.memories[write_index]; + is_cache_miss = true; read_index = write_index; if (!AwaitAllQueueOperationsCompletion()) { return; } } - // Read from buffer (previous frame if available, current frame with sync as - // fallback) - void* mapped_data; - if (dfn.vkMapMemory(device, read_memory, 0, memexport_total_size, 0, - &mapped_data) == VK_SUCCESS) { - if (mapped_data) { - const uint8_t* readback_bytes = static_cast(mapped_data); - for (const draw_util::MemExportRange& memexport_range : - memexport_ranges_) { - memory::vastcpy( - memory_->TranslatePhysical(memexport_range.base_address_dwords - << 2), - const_cast(readback_bytes), memexport_range.size_bytes); - readback_bytes += memexport_range.size_bytes; + // Only copy on cache miss (when we have fresh data from GPU sync) + // On cache hit, we'd be copying stale data from previous frame + if (is_cache_miss && rb.buffers[read_index] != VK_NULL_HANDLE && + memexport_total_size <= rb.sizes[read_index] && + rb.mapped_data[read_index] != nullptr) { + const uint8_t* readback_bytes = + static_cast(rb.mapped_data[read_index]); + for (const draw_util::MemExportRange& memexport_range : memexport_ranges_) { + memory::vastcpy( + memory_->TranslatePhysical(memexport_range.base_address_dwords << 2), + const_cast(readback_bytes), memexport_range.size_bytes); + readback_bytes += memexport_range.size_bytes; + } + } + + // Swap buffer index for next time this specific memexport address is used + // This way next time we write to the other buffer and read from this one + rb.current_index = 1 - rb.current_index; +} + +void VulkanCommandProcessor::EvictOldReadbackBuffers( + std::unordered_map& buffer_map) { + if (frame_current_ <= kReadbackBufferEvictionAgeFrames) { + return; + } + + 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 = buffer_map.begin(); it != buffer_map.end();) { + if (it->second.last_used_frame < + frame_current_ - kReadbackBufferEvictionAgeFrames) { + // Unmap and release both buffers + for (int i = 0; i < 2; i++) { + if (it->second.mapped_data[i] != nullptr) { + dfn.vkUnmapMemory(device, it->second.memories[i]); + } + if (it->second.buffers[i] != VK_NULL_HANDLE) { + dfn.vkDestroyBuffer(device, it->second.buffers[i], nullptr); + } + if (it->second.memories[i] != VK_NULL_HANDLE) { + dfn.vkFreeMemory(device, it->second.memories[i], nullptr); + } } + it = buffer_map.erase(it); + } else { + ++it; } - dfn.vkUnmapMemory(device, read_memory); } } @@ -2995,13 +3048,14 @@ bool VulkanCommandProcessor::IssueCopy() { // 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(); + 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); @@ -3067,6 +3121,10 @@ bool VulkanCommandProcessor::IssueCopy() { } // Clean up old buffer if exists + if (rb.mapped_data[write_index] != nullptr) { + dfn.vkUnmapMemory(device, rb.memories[write_index]); + rb.mapped_data[write_index] = nullptr; + } if (rb.buffers[write_index] != VK_NULL_HANDLE) { dfn.vkDestroyBuffer(device, rb.buffers[write_index], nullptr); } @@ -3077,6 +3135,15 @@ bool VulkanCommandProcessor::IssueCopy() { rb.buffers[write_index] = new_buffer; rb.memories[write_index] = new_memory; rb.sizes[write_index] = size; + + // Map the new buffer persistently + if (dfn.vkMapMemory(device, new_memory, 0, size, 0, + &rb.mapped_data[write_index]) != VK_SUCCESS) { + XELOGE( + "VulkanCommandProcessor: Failed to persistently map readback " + "buffer"); + rb.mapped_data[write_index] = nullptr; + } } VkBuffer shared_memory_buffer = shared_memory_->buffer(); @@ -3109,11 +3176,12 @@ bool VulkanCommandProcessor::IssueCopy() { } } - // Read from the appropriate buffer + bool is_cache_miss = false; // 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])) { + is_cache_miss = true; read_index = write_index; if (!AwaitAllQueueOperationsCompletion()) { XELOGE( @@ -3123,28 +3191,19 @@ bool VulkanCommandProcessor::IssueCopy() { } } - 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"); - } + // Only copy on cache miss (when we have fresh data from GPU sync) + // On cache hit, we'd be copying stale data from previous frame + if (is_cache_miss && rb.buffers[read_index] != VK_NULL_HANDLE && + written_length <= rb.sizes[read_index] && + rb.mapped_data[read_index] != nullptr) { + uint8_t* dest_ptr = memory_->TranslatePhysical(written_address); + memory::vastcpy(dest_ptr, + static_cast(rb.mapped_data[read_index]), + written_length); } + + // Swap buffer index for next time this specific resolve address is used + rb.current_index = 1 - rb.current_index; } return true; @@ -3459,75 +3518,6 @@ 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; - } - } - } - - // Swap all memexport readback buffers for delayed sync (one frame behind) - for (auto& pair : memexport_readback_buffers_) { - pair.second.current_index = 1 - pair.second.current_index; - } - - // Evict old memexport readback buffers - if (memexport_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 = memexport_readback_buffers_.begin(); - it != memexport_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 = memexport_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_)); @@ -3828,6 +3818,10 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { closed_frame_submissions_[(frame_current_++) % kMaxFramesInFlight] = GetCurrentSubmission() - 1; + // Evict old readback buffers once per frame + EvictOldReadbackBuffers(readback_buffers_); + EvictOldReadbackBuffers(memexport_readback_buffers_); + if (cache_clear_requested_ && AwaitAllQueueOperationsCompletion()) { cache_clear_requested_ = false; diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.h b/src/xenia/gpu/vulkan/vulkan_command_processor.h index 954192c7a..0fb01bb84 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.h +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.h @@ -754,9 +754,15 @@ class VulkanCommandProcessor final : public CommandProcessor { VkBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; VkDeviceMemory memories[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; uint32_t sizes[2] = {0, 0}; + void* mapped_data[2] = {nullptr, nullptr}; // Persistent mappings uint32_t current_index = 0; uint64_t last_used_frame = 0; }; + + // Helper to evict old readback buffers from a cache map + void EvictOldReadbackBuffers( + std::unordered_map& buffer_map); + // Map: (written_address << 32 | written_length) -> ReadbackBuffer std::unordered_map readback_buffers_;