[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

@@ -50,7 +50,7 @@ DECLARE_bool(guide_button);
DECLARE_bool(clear_memory_page_state); DECLARE_bool(clear_memory_page_state);
DECLARE_bool(readback_resolve); DECLARE_string(readback_resolve);
DECLARE_bool(readback_memexport); DECLARE_bool(readback_memexport);
@@ -1832,10 +1832,10 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
xe::threading::Sleep(delay); xe::threading::Sleep(delay);
break; break;
case ButtonFunctions::ReadbackResolve: case ButtonFunctions::ReadbackResolve:
ToggleGPUSetting(GPUSetting::ReadbackResolve); CycleReadbackResolve();
notificationTitle = "Toggle Readback Resolve"; notificationTitle = "Readback Resolve Mode";
notificationDesc = cvars::readback_resolve ? "Enabled" : "Disabled"; notificationDesc = cvars::readback_resolve;
// Extra Sleep // Extra Sleep
xe::threading::Sleep(delay); xe::threading::Sleep(delay);
@@ -2019,15 +2019,23 @@ void EmulatorWindow::ToggleGPUSetting(gpu::GPUSetting setting) {
SaveGPUSetting(GPUSetting::ClearMemoryPageState, SaveGPUSetting(GPUSetting::ClearMemoryPageState,
!cvars::clear_memory_page_state); !cvars::clear_memory_page_state);
break; break;
case GPUSetting::ReadbackResolve:
SaveGPUSetting(GPUSetting::ReadbackResolve, !cvars::readback_resolve);
break;
case GPUSetting::ReadbackMemexport: case GPUSetting::ReadbackMemexport:
SaveGPUSetting(GPUSetting::ReadbackMemexport, !cvars::readback_memexport); SaveGPUSetting(GPUSetting::ReadbackMemexport, !cvars::readback_memexport);
break; 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() { void EmulatorWindow::DisplayHotKeysConfig() {
std::string msg = ""; std::string msg = "";
std::string msg_passthru = ""; std::string msg_passthru = "";
@@ -2066,8 +2074,7 @@ void EmulatorWindow::DisplayHotKeysConfig() {
msg.insert(0, msg_passthru); msg.insert(0, msg_passthru);
msg += "\n"; msg += "\n";
msg += "Readback Resolve: " + msg += "Readback Resolve: " + cvars::readback_resolve;
xe::string_util::BoolToString(cvars::readback_resolve);
msg += "\n"; msg += "\n";
msg += "Clear Memory Page State: " + msg += "Clear Memory Page State: " +

View File

@@ -280,6 +280,7 @@ class EmulatorWindow {
bool vibrate = true); bool vibrate = true);
void GamepadHotKeys(); void GamepadHotKeys();
void ToggleGPUSetting(gpu::GPUSetting setting); void ToggleGPUSetting(gpu::GPUSetting setting);
void CycleReadbackResolve();
void DisplayHotKeysConfig(); void DisplayHotKeysConfig();
static std::string CanonicalizeFileExtension( static std::string CanonicalizeFileExtension(

View File

@@ -48,11 +48,12 @@ DEFINE_bool(clear_memory_page_state, false,
"for 'Team Ninja' Games to fix missing character models)", "for 'Team Ninja' Games to fix missing character models)",
"GPU"); "GPU");
DEFINE_bool( DEFINE_string(
readback_resolve, false, readback_resolve, "fast",
"Read render-to-texture results on the CPU. This may be " "Controls CPU readback of render-to-texture resolve results.\n"
"needed in some games, for instance, for screenshots in saved games, but " " fast: Read from previous frame (1 frame delay, no GPU stall - default)\n"
"causes mid-frame synchronization, so it has a huge performance impact.", " 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"); "GPU");
DEFINE_bool( DEFINE_bool(
@@ -73,9 +74,6 @@ void SaveGPUSetting(GPUSetting setting, uint64_t value) {
case GPUSetting::ClearMemoryPageState: case GPUSetting::ClearMemoryPageState:
OVERRIDE_bool(clear_memory_page_state, static_cast<bool>(value)); OVERRIDE_bool(clear_memory_page_state, static_cast<bool>(value));
break; break;
case GPUSetting::ReadbackResolve:
OVERRIDE_bool(readback_resolve, static_cast<bool>(value));
break;
case GPUSetting::ReadbackMemexport: case GPUSetting::ReadbackMemexport:
OVERRIDE_bool(readback_memexport, static_cast<bool>(value)); OVERRIDE_bool(readback_memexport, static_cast<bool>(value));
break; break;
@@ -86,14 +84,28 @@ bool GetGPUSetting(GPUSetting setting) {
switch (setting) { switch (setting) {
case GPUSetting::ClearMemoryPageState: case GPUSetting::ClearMemoryPageState:
return cvars::clear_memory_page_state; return cvars::clear_memory_page_state;
case GPUSetting::ReadbackResolve:
return cvars::readback_resolve;
case GPUSetting::ReadbackMemexport: case GPUSetting::ReadbackMemexport:
return cvars::readback_memexport; return cvars::readback_memexport;
} }
return false; 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; using namespace xe::gpu::xenos;
CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system, CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system,

View File

@@ -33,14 +33,18 @@ class ByteStream;
namespace gpu { namespace gpu {
enum class GPUSetting { enum class GPUSetting { ClearMemoryPageState, ReadbackMemexport };
ClearMemoryPageState,
ReadbackResolve, enum class ReadbackResolveMode {
ReadbackMemexport 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); void SaveGPUSetting(GPUSetting setting, uint64_t value);
bool GetGPUSetting(GPUSetting setting); bool GetGPUSetting(GPUSetting setting);
ReadbackResolveMode GetReadbackResolveMode();
void SetReadbackResolveMode(const std::string& mode);
class GraphicsSystem; class GraphicsSystem;
class Shader; class Shader;
@@ -162,6 +166,27 @@ class CommandProcessor {
static constexpr uint32_t kReadbackBufferSizeIncrement = 16 * 1024 * 1024; 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(); void WorkerThreadMain();
virtual bool SetupContext() = 0; virtual bool SetupContext() = 0;
virtual void ShutdownContext() = 0; virtual void ShutdownContext() = 0;

View File

@@ -1613,8 +1613,14 @@ bool D3D12CommandProcessor::SetupContext() {
void D3D12CommandProcessor::ShutdownContext() { void D3D12CommandProcessor::ShutdownContext() {
AwaitAllQueueOperationsCompletion(); AwaitAllQueueOperationsCompletion();
ui::d3d12::util::ReleaseAndNull(readback_buffer_); for (auto& pair : readback_buffers_) {
readback_buffer_size_ = 0; 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_); ui::d3d12::util::ReleaseAndNull(scratch_buffer_);
scratch_buffer_size_ = 0; 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, memexport_range.base_address_dwords << 2, memexport_range.size_bytes,
false); false);
} }
if (GetGPUSetting(GPUSetting::ReadbackResolve)) { if (GetGPUSetting(GPUSetting::ReadbackMemexport)) {
// Read the exported data on the CPU. // Read the exported data on the CPU.
uint32_t memexport_total_size = 0; uint32_t memexport_total_size = 0;
for (const draw_util::MemExportRange& memexport_range : for (const draw_util::MemExportRange& memexport_range :
@@ -3067,7 +3073,8 @@ bool D3D12CommandProcessor::IssueCopy() {
if (!BeginSubmission(true)) { if (!BeginSubmission(true)) {
return false; return false;
} }
if (!GetGPUSetting(GPUSetting::ReadbackResolve)) { ReadbackResolveMode readback_mode = GetReadbackResolveMode();
if (readback_mode == ReadbackResolveMode::kDisabled) {
uint32_t written_address, written_length; uint32_t written_address, written_length;
return render_target_cache_->Resolve(*memory_, *shared_memory_, return render_target_cache_->Resolve(*memory_, *shared_memory_,
*texture_cache_, written_address, *texture_cache_, written_address,
@@ -3075,7 +3082,6 @@ bool D3D12CommandProcessor::IssueCopy() {
} else { } else {
return IssueCopy_ReadbackResolvePath(); return IssueCopy_ReadbackResolvePath();
} }
return true;
} }
XE_NOINLINE XE_NOINLINE
bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() {
@@ -3083,32 +3089,113 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() {
if (render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, if (render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_,
written_address, written_length)) { written_address, written_length)) {
if (!texture_cache_->IsDrawResolutionScaled() && written_length) { if (!texture_cache_->IsDrawResolutionScaled() && written_length) {
// Read the resolved data on the CPU. // Early check: if destination memory is not accessible, skip all the
ID3D12Resource* readback_buffer = RequestReadbackBuffer(written_length); // expensive GPU readback work.
if (readback_buffer != nullptr) { VirtualHeap* physical_heap = memory_->GetPhysicalHeap();
shared_memory_->UseAsCopySource(); bool memory_accessible = false;
SubmitBarriers(); if (physical_heap) {
ID3D12Resource* shared_memory_buffer = shared_memory_->GetBuffer(); HeapAllocationInfo alloc_info;
deferred_command_list_.D3DCopyBufferRegion( if (physical_heap->QueryRegionInfo(written_address, &alloc_info) &&
readback_buffer, 0, shared_memory_buffer, written_address, (alloc_info.state & kMemoryAllocationCommit) &&
written_length); (alloc_info.protect & kMemoryProtectWrite)) {
if (AwaitAllQueueOperationsCompletion()) { uint32_t end_address = written_address + written_length;
D3D12_RANGE readback_range; uint32_t region_end =
readback_range.Begin = 0; alloc_info.base_address + alloc_info.region_size;
readback_range.End = written_length; if (end_address <= region_end) {
void* readback_mapping; memory_accessible = true;
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);
} }
} }
} }
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 { } else {
return false; return false;
@@ -3298,6 +3385,34 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
if (is_opening_frame) { if (is_opening_frame) {
frame_open_ = true; 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. // Reset bindings that depend on the data stored in the pools.
std::memset(current_float_constant_map_vertex_, 0, std::memset(current_float_constant_map_vertex_, 0,
sizeof(current_float_constant_map_vertex_)); sizeof(current_float_constant_map_vertex_));
@@ -5077,8 +5192,10 @@ ID3D12Resource* D3D12CommandProcessor::RequestReadbackBuffer(uint32_t size) {
if (size == 0) { if (size == 0) {
return nullptr; 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(); const ui::d3d12::D3D12Provider& provider = GetD3D12Provider();
ID3D12Device* device = provider.GetDevice(); ID3D12Device* device = provider.GetDevice();
D3D12_RESOURCE_DESC buffer_desc; 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); XELOGE("Failed to create a {} MB readback buffer", size >> 20);
return nullptr; return nullptr;
} }
if (readback_buffer_ != nullptr) { if (memexport_readback_buffer_ != nullptr) {
readback_buffer_->Release(); memexport_readback_buffer_->Release();
} }
readback_buffer_ = buffer; memexport_readback_buffer_ = buffer;
readback_buffer_size_ = size; memexport_readback_buffer_size_ = size;
} }
return readback_buffer_; return memexport_readback_buffer_;
} }
void D3D12CommandProcessor::WriteGammaRampSRV( void D3D12CommandProcessor::WriteGammaRampSRV(

View File

@@ -692,8 +692,19 @@ class D3D12CommandProcessor final : public CommandProcessor {
D3D12_RESOURCE_STATES scratch_buffer_state_; D3D12_RESOURCE_STATES scratch_buffer_state_;
bool scratch_buffer_used_ = false; bool scratch_buffer_used_ = false;
ID3D12Resource* readback_buffer_ = nullptr; // Per-resolve double-buffered readback for delayed sync
uint32_t readback_buffer_size_ = 0; 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<uint64_t, ReadbackBuffer> 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. // The current fixed-function drawing state.
D3D12_VIEWPORT ff_viewport_; D3D12_VIEWPORT ff_viewport_;

View File

@@ -1087,12 +1087,24 @@ void VulkanCommandProcessor::ShutdownContext() {
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device, ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
gamma_ramp_buffer_memory_); 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, ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device,
readback_buffer_); memexport_readback_buffer_);
ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device, ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device,
readback_buffer_memory_); memexport_readback_buffer_memory_);
readback_buffer_size_ = 0; memexport_readback_buffer_size_ = 0;
ui::vulkan::util::DestroyAndNullHandle( ui::vulkan::util::DestroyAndNullHandle(
dfn.vkDestroyDescriptorPool, device, dfn.vkDestroyDescriptorPool, device,
@@ -2690,7 +2702,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
if (AwaitAllQueueOperationsCompletion()) { if (AwaitAllQueueOperationsCompletion()) {
// Map staging buffer and copy to guest memory. // Map staging buffer and copy to guest memory.
void* mapped_data; void* mapped_data;
if (dfn.vkMapMemory(device, readback_buffer_memory_, 0, if (dfn.vkMapMemory(device, memexport_readback_buffer_memory_, 0,
memexport_total_size, 0, memexport_total_size, 0,
&mapped_data) == VK_SUCCESS) { &mapped_data) == VK_SUCCESS) {
if (mapped_data) { if (mapped_data) {
@@ -2708,7 +2720,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type,
"VulkanCommandProcessor: Failed to map readback buffer " "VulkanCommandProcessor: Failed to map readback buffer "
"(mapped_data is null)"); "(mapped_data is null)");
} }
dfn.vkUnmapMemory(device, readback_buffer_memory_); dfn.vkUnmapMemory(device, memexport_readback_buffer_memory_);
} else { } else {
XELOGE( XELOGE(
"VulkanCommandProcessor: Failed to map readback buffer memory " "VulkanCommandProcessor: Failed to map readback buffer memory "
@@ -2741,56 +2753,183 @@ bool VulkanCommandProcessor::IssueCopy() {
return false; return false;
} }
// CPU readback resolve path (if enabled). // CPU readback resolve path (if not disabled).
if (GetGPUSetting(GPUSetting::ReadbackResolve) && ReadbackResolveMode readback_mode = GetReadbackResolveMode();
if (readback_mode != ReadbackResolveMode::kDisabled &&
!texture_cache_->IsDrawResolutionScaled() && written_length > 0) { !texture_cache_->IsDrawResolutionScaled() && written_length > 0) {
VkBuffer readback_buffer = RequestReadbackBuffer(written_length); // Early check: if destination memory is not accessible, skip all the
if (readback_buffer != VK_NULL_HANDLE) { // expensive GPU readback work.
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); VirtualHeap* physical_heap = memory_->GetPhysicalHeap();
const ui::vulkan::VulkanDevice::Functions& dfn = bool memory_accessible = false;
vulkan_device->functions(); if (physical_heap) {
const VkDevice device = vulkan_device->device(); HeapAllocationInfo alloc_info;
if (physical_heap->QueryRegionInfo(written_address, &alloc_info) &&
VkBuffer shared_memory_buffer = shared_memory_->buffer(); (alloc_info.state & kMemoryAllocationCommit) &&
(alloc_info.protect & kMemoryProtectWrite)) {
// Ensure shared memory is ready for transfer. uint32_t end_address = written_address + written_length;
shared_memory_->Use(VulkanSharedMemory::Usage::kRead); uint32_t region_end = alloc_info.base_address + alloc_info.region_size;
if (end_address <= region_end) {
// Copy GPU buffer → staging buffer. memory_accessible = true;
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");
} }
} 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( XELOGE(
"VulkanCommandProcessor: Failed to complete queue operations for " "VulkanCommandProcessor: Failed to complete queue operations for "
"resolve readback"); "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; 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* const vulkan_device = GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device(); const VkDevice device = vulkan_device->device();
@@ -2868,17 +3007,17 @@ VkBuffer VulkanCommandProcessor::RequestReadbackBuffer(uint32_t size) {
} }
// Destroy old buffer if it exists. // Destroy old buffer if it exists.
if (readback_buffer_ != VK_NULL_HANDLE) { if (memexport_readback_buffer_ != VK_NULL_HANDLE) {
dfn.vkDestroyBuffer(device, readback_buffer_, nullptr); dfn.vkDestroyBuffer(device, memexport_readback_buffer_, nullptr);
dfn.vkFreeMemory(device, readback_buffer_memory_, nullptr); dfn.vkFreeMemory(device, memexport_readback_buffer_memory_, nullptr);
} }
readback_buffer_ = new_buffer; memexport_readback_buffer_ = new_buffer;
readback_buffer_memory_ = new_memory; memexport_readback_buffer_memory_ = new_memory;
readback_buffer_size_ = size; memexport_readback_buffer_size_ = size;
} }
return readback_buffer_; return memexport_readback_buffer_;
} }
void VulkanCommandProcessor::InitializeTrace() { void VulkanCommandProcessor::InitializeTrace() {
@@ -3107,6 +3246,41 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
if (is_opening_frame) { if (is_opening_frame) {
frame_open_ = true; 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. // Reset bindings that depend on transient data.
std::memset(current_float_constant_map_vertex_, 0, std::memset(current_float_constant_map_vertex_, 0,
sizeof(current_float_constant_map_vertex_)); sizeof(current_float_constant_map_vertex_));

View File

@@ -754,10 +754,21 @@ class VulkanCommandProcessor final : public CommandProcessor {
// Temporary storage for memexport stream constants used in the draw. // Temporary storage for memexport stream constants used in the draw.
std::vector<draw_util::MemExportRange> memexport_ranges_; std::vector<draw_util::MemExportRange> memexport_ranges_;
// Readback buffer for CPU access to resolved data // Per-resolve double-buffered readback for delayed sync
VkBuffer readback_buffer_ = VK_NULL_HANDLE; struct ReadbackBuffer {
VkDeviceMemory readback_buffer_memory_ = VK_NULL_HANDLE; VkBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
uint32_t readback_buffer_size_ = 0; 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<uint64_t, ReadbackBuffer> 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 } // namespace vulkan