[GPU] Optimize readback resolve and memexport fast path.

Remove string comparisons on every frame for cvar check, use persistent
buffers instead of mapping/unmapping on every call, increase cache size
to better match actual usage, rework eviction strategy and skip copies of
cached data.
This commit is contained in:
Herman S.
2025-11-17 14:43:47 +09:00
parent 6c6bd88ec3
commit f76c2e0bf4
6 changed files with 233 additions and 201 deletions
+3 -1
View File
@@ -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;
+10 -2
View File
@@ -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] = {};
+97 -82
View File
@@ -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<uint64_t, ReadbackBuffer>& 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<const uint8_t*>(readback_mapping);
static_cast<const uint8_t*>(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<uint8_t*>(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;
@@ -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<uint64_t, ReadbackBuffer>& buffer_map);
// Map: (written_address << 32 | written_length) -> ReadbackBuffer
std::unordered_map<uint64_t, ReadbackBuffer> readback_buffers_;
+110 -116
View File
@@ -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<const uint8_t*>(mapped_data);
for (const draw_util::MemExportRange& memexport_range :
memexport_ranges_) {
memory::vastcpy(
memory_->TranslatePhysical(memexport_range.base_address_dwords
<< 2),
const_cast<uint8_t*>(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<const uint8_t*>(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<uint8_t*>(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<uint64_t, ReadbackBuffer>& 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<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");
}
// 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<uint8_t*>(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;
@@ -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<uint64_t, ReadbackBuffer>& buffer_map);
// Map: (written_address << 32 | written_length) -> ReadbackBuffer
std::unordered_map<uint64_t, ReadbackBuffer> readback_buffers_;