[Vulkan] Add resolution scaling support to Vulkan backend.

Largely similar to D3D12 implementation but more simple buffer
management and no mips scaling. Includes both FBO and FSI
rendering paths.

Tested on Linux with 2x2 and 3x3 running smoothly.
This commit is contained in:
Herman S.
2025-08-27 20:44:15 +09:00
committed by Radosław Gliński
parent 2de1b0b2dd
commit d430342d93
11 changed files with 563 additions and 53 deletions

View File

@@ -472,6 +472,16 @@ VulkanTextureCache::~VulkanTextureCache() {
// textures before destroying VMA.
DestroyAllTextures(true);
// Clean up scaled resolve buffers before destroying VMA
// The command processor should ensure all GPU operations are complete
// before the texture cache is destroyed
for (ScaledResolveBuffer& buffer : scaled_resolve_buffers_) {
if (buffer.buffer != VK_NULL_HANDLE) {
vmaDestroyBuffer(vma_allocator_, buffer.buffer, buffer.allocation);
}
}
scaled_resolve_buffers_.clear();
if (vma_allocator_ != VK_NULL_HANDLE) {
vmaDestroyAllocator(vma_allocator_);
}
@@ -891,6 +901,7 @@ VkImageView VulkanTextureCache::RequestSwapTexture(
return VK_NULL_HANDLE;
}
if (!LoadTextureData(*texture)) {
XELOGE("Failed to load texture data for swap texture");
return VK_NULL_HANDLE;
}
texture->MarkAsUsed();
@@ -920,6 +931,13 @@ VkImageView VulkanTextureCache::RequestSwapTexture(
return texture_view;
}
bool VulkanTextureCache::IsScaledResolveSupportedForFormat(
TextureKey key) const {
// Check if the format has a valid host format pair, meaning we can handle it
const HostFormatPair& host_format_pair = GetHostFormatPair(key);
return host_format_pair.format_unsigned.format != VK_FORMAT_UNDEFINED;
}
bool VulkanTextureCache::IsSignedVersionSeparateForFormat(
TextureKey key) const {
const HostFormatPair& host_format_pair = GetHostFormatPair(key);
@@ -1257,7 +1275,6 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
write_descriptor_set_dest.pTexelBufferView = nullptr;
}
// TODO(Triang3l): Use a single 512 MB shared memory binding if possible.
// TODO(Triang3l): Scaled resolve buffer bindings.
// Aligning because if the data for a vector in a storage buffer is provided
// partially, the value read may still be (0, 0, 0, 0), and small (especially
// linear) textures won't be loaded correctly.
@@ -1275,12 +1292,69 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
if (!descriptor_set_source_base) {
return false;
}
write_descriptor_set_source_base_buffer_info.buffer =
vulkan_shared_memory.buffer();
write_descriptor_set_source_base_buffer_info.offset = texture_key.base_page
<< 12;
write_descriptor_set_source_base_buffer_info.range =
xe::align(vulkan_texture.GetGuestBaseSize(), source_length_alignment);
if (texture_key.scaled_resolve) {
// For scaled textures, read from scaled resolve buffers
uint32_t guest_address = texture_key.base_page << 12;
uint32_t guest_size = vulkan_texture.GetGuestBaseSize();
// Ensure the scaled buffer exists
if (EnsureScaledResolveMemoryCommitted(guest_address, guest_size)) {
// Make the range current
if (MakeScaledResolveRangeCurrent(guest_address, guest_size)) {
VkBuffer scaled_buffer = GetCurrentScaledResolveBuffer();
if (scaled_buffer != VK_NULL_HANDLE) {
// Calculate offset within the scaled buffer
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 = 0;
if (scaled_resolve_current_buffer_index_ <
scaled_resolve_buffers_.size()) {
const ScaledResolveBuffer& current_buffer =
scaled_resolve_buffers_[scaled_resolve_current_buffer_index_];
buffer_relative_offset =
scaled_offset - current_buffer.range_start_scaled;
}
write_descriptor_set_source_base_buffer_info.buffer = scaled_buffer;
write_descriptor_set_source_base_buffer_info.offset =
buffer_relative_offset;
write_descriptor_set_source_base_buffer_info.range =
xe::align(guest_size * draw_resolution_scale_area,
source_length_alignment);
} else {
XELOGE(
"Scaled resolve texture load: Failed to get current scaled "
"buffer for texture at 0x{:08X}",
guest_address);
return false;
}
} else {
XELOGE(
"Scaled resolve texture load: Failed to make range current for "
"texture at 0x{:08X}",
guest_address);
return false;
}
} else {
XELOGE(
"Scaled resolve texture load: Failed to ensure scaled memory for "
"texture at 0x{:08X}",
guest_address);
return false;
}
} else {
// Regular unscaled texture - use shared memory
write_descriptor_set_source_base_buffer_info.buffer =
vulkan_shared_memory.buffer();
write_descriptor_set_source_base_buffer_info.offset =
texture_key.base_page << 12;
write_descriptor_set_source_base_buffer_info.range =
xe::align(vulkan_texture.GetGuestBaseSize(), source_length_alignment);
}
VkWriteDescriptorSet& write_descriptor_set_source_base =
write_descriptor_sets[write_descriptor_set_count++];
write_descriptor_set_source_base.sType =
@@ -1305,6 +1379,10 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
if (!descriptor_set_source_mips) {
return false;
}
// TODO: Implement scaled mips support similar to D3D12.
// Currently mips are always loaded from unscaled shared memory even when
// the base texture is scaled. D3D12 properly handles scaled mips in
// D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl.
write_descriptor_set_source_mips_buffer_info.buffer =
vulkan_shared_memory.buffer();
write_descriptor_set_source_mips_buffer_info.offset = texture_key.mip_page
@@ -1696,10 +1774,7 @@ VulkanTextureCache::VulkanTextureCache(
: TextureCache(register_file, shared_memory, draw_resolution_scale_x,
draw_resolution_scale_y),
command_processor_(command_processor),
guest_shader_pipeline_stages_(guest_shader_pipeline_stages) {
// TODO(Triang3l): Support draw resolution scaling.
assert_true(draw_resolution_scale_x == 1 && draw_resolution_scale_y == 1);
}
guest_shader_pipeline_stages_(guest_shader_pipeline_stages) {}
bool VulkanTextureCache::Initialize() {
const ui::vulkan::VulkanDevice* const vulkan_device =
@@ -2652,6 +2727,160 @@ xenos::ClampMode VulkanTextureCache::NormalizeClampMode(
return clamp_mode;
}
bool VulkanTextureCache::EnsureScaledResolveMemoryCommitted(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2) {
if (!IsDrawResolutionScaled()) {
return true;
}
if (length_unscaled == 0) {
return true;
}
if (start_unscaled > SharedMemory::kBufferSize ||
(SharedMemory::kBufferSize - start_unscaled) < length_unscaled) {
return false;
}
uint32_t draw_resolution_scale_area =
draw_resolution_scale_x() * draw_resolution_scale_y();
uint64_t start_scaled = uint64_t(start_unscaled) * draw_resolution_scale_area;
uint64_t length_scaled_alignment_bits =
(UINT64_C(1) << length_scaled_alignment_log2) - 1;
uint64_t length_scaled =
(uint64_t(length_unscaled) * draw_resolution_scale_area +
length_scaled_alignment_bits) &
~length_scaled_alignment_bits;
// Check if any existing buffer covers this range
bool range_covered = false;
for (const ScaledResolveBuffer& buffer : scaled_resolve_buffers_) {
if (buffer.range_start_scaled <= start_scaled &&
(buffer.range_start_scaled + buffer.range_length_scaled) >=
(start_scaled + length_scaled)) {
// This buffer covers the requested range
scaled_resolve_current_range_start_scaled_ = buffer.range_start_scaled;
scaled_resolve_current_range_length_scaled_ = buffer.range_length_scaled;
range_covered = true;
break;
}
}
if (!range_covered) {
// Need to create a new buffer or extend an existing one
// For simplicity and to avoid fragmentation, we'll use a fixed-size buffer
// approach similar to D3D12 (but smaller - 256MB chunks instead of 2GB)
constexpr uint64_t kBufferSize = 256 * 1024 * 1024; // 256MB per buffer
// Round up the range to cover complete buffer chunks
uint64_t buffer_start = (start_scaled / kBufferSize) * kBufferSize;
uint64_t buffer_end =
((start_scaled + length_scaled + kBufferSize - 1) / kBufferSize) *
kBufferSize;
uint64_t buffer_size = buffer_end - buffer_start;
// Check again if this expanded range is covered
bool expanded_range_covered = false;
for (const ScaledResolveBuffer& buffer : scaled_resolve_buffers_) {
if (buffer.range_start_scaled <= buffer_start &&
(buffer.range_start_scaled + buffer.range_length_scaled) >=
buffer_end) {
scaled_resolve_current_range_start_scaled_ = buffer.range_start_scaled;
scaled_resolve_current_range_length_scaled_ =
buffer.range_length_scaled;
expanded_range_covered = true;
break;
}
}
if (!expanded_range_covered) {
// Limit the number of buffers to prevent unbounded growth
constexpr size_t kMaxBuffers = 32; // Maximum 8GB total (32 * 256MB)
if (scaled_resolve_buffers_.size() >= kMaxBuffers) {
// Reuse the least recently used buffer
// For now, just reuse the first buffer (simple LRU would be better)
ScaledResolveBuffer& reused_buffer = scaled_resolve_buffers_[0];
reused_buffer.range_start_scaled = buffer_start;
reused_buffer.range_length_scaled = buffer_size;
scaled_resolve_current_range_start_scaled_ = buffer_start;
scaled_resolve_current_range_length_scaled_ = buffer_size;
} else {
ScaledResolveBuffer new_buffer;
new_buffer.size = buffer_size;
VkBufferCreateInfo buffer_create_info = {};
buffer_create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_create_info.size = new_buffer.size;
buffer_create_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo allocation_create_info = {};
allocation_create_info.usage = VMA_MEMORY_USAGE_GPU_ONLY;
VkResult result = vmaCreateBuffer(
vma_allocator_, &buffer_create_info, &allocation_create_info,
&new_buffer.buffer, &new_buffer.allocation, nullptr);
if (result != VK_SUCCESS) {
XELOGE(
"VulkanTextureCache: Failed to create scaled resolve buffer: {}",
static_cast<int>(result));
return false;
}
new_buffer.range_start_scaled = buffer_start;
new_buffer.range_length_scaled = buffer_size;
scaled_resolve_buffers_.push_back(new_buffer);
scaled_resolve_current_range_start_scaled_ = buffer_start;
scaled_resolve_current_range_length_scaled_ = buffer_size;
}
}
}
return true;
}
bool VulkanTextureCache::MakeScaledResolveRangeCurrent(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2) {
if (!IsDrawResolutionScaled()) {
return false;
}
// First ensure the memory is committed (creates buffers if needed)
if (!EnsureScaledResolveMemoryCommitted(start_unscaled, length_unscaled,
length_scaled_alignment_log2)) {
return false;
}
uint32_t draw_resolution_scale_area =
draw_resolution_scale_x() * draw_resolution_scale_y();
uint64_t start_scaled = uint64_t(start_unscaled) * draw_resolution_scale_area;
// Find which buffer contains this range
for (size_t i = 0; i < scaled_resolve_buffers_.size(); ++i) {
const ScaledResolveBuffer& buffer = scaled_resolve_buffers_[i];
if (start_scaled >= buffer.range_start_scaled &&
start_scaled <
(buffer.range_start_scaled + buffer.range_length_scaled)) {
scaled_resolve_current_buffer_index_ = i;
return true;
}
}
return false;
}
VkBuffer VulkanTextureCache::GetCurrentScaledResolveBuffer() const {
if (scaled_resolve_current_buffer_index_ >= scaled_resolve_buffers_.size()) {
return VK_NULL_HANDLE;
}
return scaled_resolve_buffers_[scaled_resolve_current_buffer_index_].buffer;
}
} // namespace vulkan
} // namespace gpu
} // namespace xe