mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[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:
@@ -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;
|
||||
|
||||
@@ -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] = {};
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user