[Vulkan] Bind shared memory persistently for texture loads and resolves

Co-authored-by: Herman S. <429230+has207@users.noreply.github.com>
This commit is contained in:
goldislead
2026-08-05 21:14:00 -07:00
committed by Radosław Gliński
parent cb240560df
commit 7101021150
3 changed files with 243 additions and 85 deletions

View File

@@ -1184,12 +1184,22 @@ bool VulkanRenderTargetCache::Resolve(
} else {
// TODO(Triang3l): Switching between descriptors if exceeding
// maxStorageBufferRange.
// TODO(Triang3l): Use a single 512 MB shared memory binding if
// possible.
// Bind the whole shared memory buffer persistently when possible
// (passing the destination byte offset via dest_base) instead of
// allocating and writing a per-resolve descriptor. Scaled resolves
// write to separate scaled buffers, so they use transient descriptors.
// Decided per resolve, as native copies write to shared memory even
// with resolution scaling on.
const bool use_persistent_dest =
texture_cache.shared_memory_persistent_descriptor_set() !=
VK_NULL_HANDLE &&
!copy_dest_scaled;
VkDescriptorSet descriptor_set_dest =
command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
use_persistent_dest
? texture_cache.shared_memory_persistent_descriptor_set()
: command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
if (descriptor_set_dest != VK_NULL_HANDLE) {
// Write the destination descriptor.
VkDescriptorBufferInfo write_descriptor_set_dest_buffer_info;
@@ -1256,7 +1266,7 @@ bool VulkanRenderTargetCache::Resolve(
}
}
if (!scaled_buffer_ready) {
if (!scaled_buffer_ready && !use_persistent_dest) {
// Write unscaled or native resolves to shared memory.
if (copy_dest_scaled) {
XELOGW(
@@ -1273,22 +1283,24 @@ bool VulkanRenderTargetCache::Resolve(
resolve_info.copy_dest_base +
resolve_info.copy_dest_extent_length;
}
VkWriteDescriptorSet write_descriptor_set_dest;
write_descriptor_set_dest.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_dest.pNext = nullptr;
write_descriptor_set_dest.dstSet = descriptor_set_dest;
write_descriptor_set_dest.dstBinding = 0;
write_descriptor_set_dest.dstArrayElement = 0;
write_descriptor_set_dest.descriptorCount = 1;
write_descriptor_set_dest.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_dest.pImageInfo = nullptr;
write_descriptor_set_dest.pBufferInfo =
&write_descriptor_set_dest_buffer_info;
write_descriptor_set_dest.pTexelBufferView = nullptr;
dfn.vkUpdateDescriptorSets(device, 1, &write_descriptor_set_dest, 0,
nullptr);
if (!use_persistent_dest) {
VkWriteDescriptorSet write_descriptor_set_dest;
write_descriptor_set_dest.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_dest.pNext = nullptr;
write_descriptor_set_dest.dstSet = descriptor_set_dest;
write_descriptor_set_dest.dstBinding = 0;
write_descriptor_set_dest.dstArrayElement = 0;
write_descriptor_set_dest.descriptorCount = 1;
write_descriptor_set_dest.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_dest.pImageInfo = nullptr;
write_descriptor_set_dest.pBufferInfo =
&write_descriptor_set_dest_buffer_info;
write_descriptor_set_dest.pTexelBufferView = nullptr;
dfn.vkUpdateDescriptorSets(device, 1, &write_descriptor_set_dest, 0,
nullptr);
}
// Submit the resolve.
if (!scaled_buffer_ready) {
@@ -1345,11 +1357,15 @@ bool VulkanRenderTargetCache::Resolve(
sizeof(copy_shader_constants.dest_relative),
&copy_shader_constants.dest_relative);
} else {
// TODO(Triang3l): Proper dest_base in case of one 512 MB shared
// memory binding, or multiple shared memory bindings in case of
// TODO(Triang3l): Multiple shared memory bindings in case of
// splitting due to maxStorageBufferRange overflow.
copy_shader_constants.dest_base -=
uint32_t(write_descriptor_set_dest_buffer_info.offset);
if (!use_persistent_dest) {
// The descriptor is offset to the destination, so make dest_base
// relative to it. With the whole buffer bound persistently,
// dest_base stays the absolute byte offset.
copy_shader_constants.dest_base -=
uint32_t(write_descriptor_set_dest_buffer_info.offset);
}
command_buffer.CmdVkPushConstants(
copy_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0,
sizeof(copy_shader_constants), &copy_shader_constants);

View File

@@ -475,6 +475,10 @@ VulkanTextureCache::~VulkanTextureCache() {
if (load_pipeline_layout_ != VK_NULL_HANDLE) {
dfn.vkDestroyPipelineLayout(device, load_pipeline_layout_, nullptr);
}
if (shared_memory_persistent_descriptor_pool_ != VK_NULL_HANDLE) {
dfn.vkDestroyDescriptorPool(
device, shared_memory_persistent_descriptor_pool_, nullptr);
}
// Textures memory is allocated using the Vulkan Memory Allocator, destroy all
// textures before destroying VMA.
@@ -1345,6 +1349,13 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
const VkDevice device = vulkan_device->device();
VulkanSharedMemory& vulkan_shared_memory =
static_cast<VulkanSharedMemory&>(shared_memory());
// Bind the whole shared memory buffer persistently when possible (passing the
// texture's byte offset via guest_offset) instead of allocating and writing
// per-load source descriptors. Scaled resolve textures read from separate
// scaled buffers, so they always use transient descriptors.
const bool use_persistent_source =
shared_memory_persistent_descriptor_set_ != VK_NULL_HANDLE &&
!texture_key.scaled_resolve;
std::array<VkWriteDescriptorSet, 3> write_descriptor_sets;
uint32_t write_descriptor_set_count = 0;
VkDescriptorSet descriptor_set_dest =
@@ -1380,12 +1391,16 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
VkDescriptorBufferInfo write_descriptor_set_source_base_buffer_info;
VkDescriptorBufferInfo write_descriptor_set_source_mips_buffer_info;
if (level_first == 0) {
descriptor_set_source_base =
command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
if (!descriptor_set_source_base) {
return false;
if (use_persistent_source) {
descriptor_set_source_base = shared_memory_persistent_descriptor_set_;
} else {
descriptor_set_source_base =
command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
if (!descriptor_set_source_base) {
return false;
}
}
if (texture_key.scaled_resolve) {
// For scaled textures, read from scaled resolve buffers
@@ -1434,7 +1449,7 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
guest_address);
return false;
}
} else {
} else if (!use_persistent_source) {
// Regular unscaled texture - use shared memory
write_descriptor_set_source_base_buffer_info.buffer =
vulkan_shared_memory.buffer();
@@ -1445,57 +1460,96 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
write_descriptor_set_source_base_buffer_info.range =
xe::align(vulkan_texture.GetGuestBaseSize(), uint32_t(16));
}
VkWriteDescriptorSet& write_descriptor_set_source_base =
write_descriptor_sets[write_descriptor_set_count++];
write_descriptor_set_source_base.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_source_base.pNext = nullptr;
write_descriptor_set_source_base.dstSet = descriptor_set_source_base;
write_descriptor_set_source_base.dstBinding = 0;
write_descriptor_set_source_base.dstArrayElement = 0;
write_descriptor_set_source_base.descriptorCount = 1;
write_descriptor_set_source_base.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_source_base.pImageInfo = nullptr;
write_descriptor_set_source_base.pBufferInfo =
&write_descriptor_set_source_base_buffer_info;
write_descriptor_set_source_base.pTexelBufferView = nullptr;
}
// For scaled resolve textures, we don't load mips from buffers - they will
// be generated via blit from the base level. For unscaled textures, load
// mips from shared memory as usual.
if (level_last != 0 && !texture_key.scaled_resolve) {
descriptor_set_source_mips =
command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
if (!descriptor_set_source_mips) {
return false;
if (!use_persistent_source) {
VkWriteDescriptorSet& write_descriptor_set_source_base =
write_descriptor_sets[write_descriptor_set_count++];
write_descriptor_set_source_base.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_source_base.pNext = nullptr;
write_descriptor_set_source_base.dstSet = descriptor_set_source_base;
write_descriptor_set_source_base.dstBinding = 0;
write_descriptor_set_source_base.dstArrayElement = 0;
write_descriptor_set_source_base.descriptorCount = 1;
write_descriptor_set_source_base.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_source_base.pImageInfo = nullptr;
write_descriptor_set_source_base.pBufferInfo =
&write_descriptor_set_source_base_buffer_info;
write_descriptor_set_source_base.pTexelBufferView = nullptr;
}
}
// Set up the mips source: scaled resolve buffer or shared memory.
if (level_last != 0) {
if (use_persistent_source) {
descriptor_set_source_mips = shared_memory_persistent_descriptor_set_;
} else {
descriptor_set_source_mips =
command_processor_.AllocateSingleTransientDescriptor(
VulkanCommandProcessor::SingleTransientDescriptorLayout ::
kStorageBufferCompute);
if (!descriptor_set_source_mips) {
return false;
}
if (texture_key.scaled_resolve) {
// Scaled resolved mips live in the scaled buffer, like the base.
uint32_t guest_address = texture_key.mip_page << 12;
uint32_t guest_size = vulkan_texture.GetGuestMipsSize();
if (EnsureScaledResolveMemoryCommitted(guest_address, guest_size) &&
MakeScaledResolveRangeCurrent(guest_address, guest_size)) {
VkBuffer scaled_buffer = GetCurrentScaledResolveBuffer();
if (scaled_buffer != VK_NULL_HANDLE) {
uint32_t draw_resolution_scale_area =
draw_resolution_scale_x() * draw_resolution_scale_y();
uint64_t scaled_offset =
uint64_t(guest_address) * draw_resolution_scale_area;
uint64_t buffer_relative_offset =
scaled_offset - GetCurrentScaledResolveBufferBaseOffset();
write_descriptor_set_source_mips_buffer_info.buffer = scaled_buffer;
write_descriptor_set_source_mips_buffer_info.offset =
buffer_relative_offset;
write_descriptor_set_source_mips_buffer_info.range = xe::align(
guest_size * draw_resolution_scale_area, uint32_t(16));
} else {
XELOGE(
"Scaled resolve texture load: Failed to get current scaled "
"buffer for mips at 0x{:08X}",
guest_address);
return false;
}
} else {
XELOGE(
"Scaled resolve texture load: Failed to make range current for "
"mips at 0x{:08X}",
guest_address);
return false;
}
} else {
// Regular unscaled texture - use shared memory
write_descriptor_set_source_mips_buffer_info.buffer =
vulkan_shared_memory.buffer();
write_descriptor_set_source_mips_buffer_info.offset =
texture_key.mip_page << 12;
// Align (primarily the last row of a linear packed mip tail) because
// shaders use up to 16-byte loads for multiple blocks at once.
write_descriptor_set_source_mips_buffer_info.range =
xe::align(vulkan_texture.GetGuestMipsSize(), uint32_t(16));
}
VkWriteDescriptorSet& write_descriptor_set_source_mips =
write_descriptor_sets[write_descriptor_set_count++];
write_descriptor_set_source_mips.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_source_mips.pNext = nullptr;
write_descriptor_set_source_mips.dstSet = descriptor_set_source_mips;
write_descriptor_set_source_mips.dstBinding = 0;
write_descriptor_set_source_mips.dstArrayElement = 0;
write_descriptor_set_source_mips.descriptorCount = 1;
write_descriptor_set_source_mips.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_source_mips.pImageInfo = nullptr;
write_descriptor_set_source_mips.pBufferInfo =
&write_descriptor_set_source_mips_buffer_info;
write_descriptor_set_source_mips.pTexelBufferView = nullptr;
}
// Regular unscaled texture - use shared memory
write_descriptor_set_source_mips_buffer_info.buffer =
vulkan_shared_memory.buffer();
write_descriptor_set_source_mips_buffer_info.offset = texture_key.mip_page
<< 12;
// Align (primarily the last row of a linear packed mip tail) because
// shaders use up to 16-byte loads for multiple blocks at once.
write_descriptor_set_source_mips_buffer_info.range =
xe::align(vulkan_texture.GetGuestMipsSize(), uint32_t(16));
VkWriteDescriptorSet& write_descriptor_set_source_mips =
write_descriptor_sets[write_descriptor_set_count++];
write_descriptor_set_source_mips.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write_descriptor_set_source_mips.pNext = nullptr;
write_descriptor_set_source_mips.dstSet = descriptor_set_source_mips;
write_descriptor_set_source_mips.dstBinding = 0;
write_descriptor_set_source_mips.dstArrayElement = 0;
write_descriptor_set_source_mips.descriptorCount = 1;
write_descriptor_set_source_mips.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
write_descriptor_set_source_mips.pImageInfo = nullptr;
write_descriptor_set_source_mips.pBufferInfo =
&write_descriptor_set_source_mips_buffer_info;
write_descriptor_set_source_mips.pTexelBufferView = nullptr;
}
if (write_descriptor_set_count) {
dfn.vkUpdateDescriptorSets(device, write_descriptor_set_count,
@@ -1541,8 +1595,13 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
kLoadDescriptorSetIndexSource, 1, &descriptor_set_source, 0, nullptr);
}
// TODO(Triang3l): guest_offset relative to the storage buffer origin.
load_constants.guest_offset = 0;
// With the whole buffer bound persistently, guest_offset is relative to the
// buffer origin. With a per-load source descriptor, it's already offset to
// the texture's base or mip page, so it stays relative to that.
load_constants.guest_offset =
use_persistent_source
? ((is_base ? texture_key.base_page : texture_key.mip_page) << 12)
: 0;
if (!is_base) {
load_constants.guest_offset +=
guest_layout.mip_offsets_bytes[level] *
@@ -2521,6 +2580,71 @@ bool VulkanTextureCache::Initialize() {
return false;
}
// If the whole shared memory buffer fits within maxStorageBufferRange, create
// a persistent descriptor set binding it for texture load sources, so
// per-load transient source descriptors don't need to be allocated and
// written. The texture's byte offset is passed via guest_offset instead, as
// on Direct3D 12. When the buffer doesn't fit, the per-load sub-range
// descriptors are used.
if (device_properties.maxStorageBufferRange >= SharedMemory::kBufferSize) {
VkDescriptorPoolSize shared_memory_persistent_pool_size;
shared_memory_persistent_pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
shared_memory_persistent_pool_size.descriptorCount = 1;
VkDescriptorPoolCreateInfo shared_memory_persistent_pool_create_info;
shared_memory_persistent_pool_create_info.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
shared_memory_persistent_pool_create_info.pNext = nullptr;
shared_memory_persistent_pool_create_info.flags = 0;
shared_memory_persistent_pool_create_info.maxSets = 1;
shared_memory_persistent_pool_create_info.poolSizeCount = 1;
shared_memory_persistent_pool_create_info.pPoolSizes =
&shared_memory_persistent_pool_size;
if (dfn.vkCreateDescriptorPool(
device, &shared_memory_persistent_pool_create_info, nullptr,
&shared_memory_persistent_descriptor_pool_) == VK_SUCCESS) {
VkDescriptorSetAllocateInfo shared_memory_persistent_set_allocate_info;
shared_memory_persistent_set_allocate_info.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
shared_memory_persistent_set_allocate_info.pNext = nullptr;
shared_memory_persistent_set_allocate_info.descriptorPool =
shared_memory_persistent_descriptor_pool_;
shared_memory_persistent_set_allocate_info.descriptorSetCount = 1;
shared_memory_persistent_set_allocate_info.pSetLayouts =
&load_descriptor_set_layout_storage_buffer;
if (dfn.vkAllocateDescriptorSets(
device, &shared_memory_persistent_set_allocate_info,
&shared_memory_persistent_descriptor_set_) == VK_SUCCESS) {
VkDescriptorBufferInfo shared_memory_persistent_buffer_info;
shared_memory_persistent_buffer_info.buffer =
static_cast<VulkanSharedMemory&>(shared_memory()).buffer();
shared_memory_persistent_buffer_info.offset = 0;
shared_memory_persistent_buffer_info.range = SharedMemory::kBufferSize;
VkWriteDescriptorSet shared_memory_persistent_write;
shared_memory_persistent_write.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
shared_memory_persistent_write.pNext = nullptr;
shared_memory_persistent_write.dstSet =
shared_memory_persistent_descriptor_set_;
shared_memory_persistent_write.dstBinding = 0;
shared_memory_persistent_write.dstArrayElement = 0;
shared_memory_persistent_write.descriptorCount = 1;
shared_memory_persistent_write.descriptorType =
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
shared_memory_persistent_write.pImageInfo = nullptr;
shared_memory_persistent_write.pBufferInfo =
&shared_memory_persistent_buffer_info;
shared_memory_persistent_write.pTexelBufferView = nullptr;
dfn.vkUpdateDescriptorSets(device, 1, &shared_memory_persistent_write,
0, nullptr);
} else {
dfn.vkDestroyDescriptorPool(
device, shared_memory_persistent_descriptor_pool_, nullptr);
shared_memory_persistent_descriptor_pool_ = VK_NULL_HANDLE;
shared_memory_persistent_descriptor_set_ = VK_NULL_HANDLE;
}
}
}
// Load pipelines, only the ones needed for the formats that will be used.
bool load_shaders_needed[kLoadShaderCount] = {};

View File

@@ -97,6 +97,14 @@ class VulkanTextureCache final : public TextureCache {
xenos::FetchOpDimension dimension,
bool is_signed);
// Descriptor set (kStorageBufferCompute layout) binding the whole shared
// memory buffer for compute load/store, or VK_NULL_HANDLE if the buffer
// doesn't fit in maxStorageBufferRange. When valid, the byte offset into the
// buffer must be supplied via push constants. Shared with resolve.
VkDescriptorSet shared_memory_persistent_descriptor_set() const {
return shared_memory_persistent_descriptor_set_;
}
SamplerParameters GetSamplerParameters(
const VulkanShader::SamplerBinding& binding) const;
@@ -441,6 +449,16 @@ class VulkanTextureCache final : public TextureCache {
std::array<VkPipeline, kLoadShaderCount> load_pipelines_{};
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_{};
// Persistent descriptor binding the whole shared memory buffer
// (kStorageBufferCompute layout) for compute load/store, so per-operation
// transient descriptors don't need to be allocated and written. Only created
// when the buffer fits in maxStorageBufferRange. The byte offset into the
// buffer is passed via push constants instead. Used as the source of texture
// loads here, and shared as the destination of resolves in the render target
// cache.
VkDescriptorPool shared_memory_persistent_descriptor_pool_ = VK_NULL_HANDLE;
VkDescriptorSet shared_memory_persistent_descriptor_set_ = VK_NULL_HANDLE;
// If both images can be placed in the same allocation, it's one allocation,
// otherwise it's two separate.
std::array<VkDeviceMemory, 2> null_images_memory_{};