[Vulkan] Implement tiled_shared_memory for resolution scaling

Adds support for >3x resolution scaling to vulkan by implementing
sparse scaled resolve similar to the d3d12 implementation.
This commit is contained in:
Herman S.
2025-12-22 11:58:13 +09:00
parent a092551735
commit 5897de3eaf
10 changed files with 508 additions and 77 deletions

View File

@@ -851,8 +851,11 @@ bool D3D12CommandProcessor::SetupContext() {
TextureCache::GetConfigDrawResolutionScale(draw_resolution_scale_x,
draw_resolution_scale_y);
if (!D3D12TextureCache::ClampDrawResolutionScaleToMaxSupported(
draw_resolution_scale_x, draw_resolution_scale_y, provider)) {
bool has_tiled_resources =
provider.GetTiledResourcesTier() >= D3D12_TILED_RESOURCES_TIER_1;
if (!TextureCache::ClampDrawResolutionScaleToMaxSupported(
draw_resolution_scale_x, draw_resolution_scale_y, has_tiled_resources,
provider.GetVirtualAddressBitsPerResource())) {
draw_resolution_scale_not_clamped = false;
}
if (!draw_resolution_scale_not_clamped) {

View File

@@ -18,14 +18,8 @@
#include "xenia/gpu/d3d12/d3d12_command_processor.h"
#include "xenia/ui/d3d12/d3d12_util.h"
DEFINE_bool(d3d12_tiled_shared_memory, true,
"Enable tiled resources for shared memory emulation. Disabling "
"them increases video memory usage - a 512 MB buffer is created - "
"but allows graphics debuggers that don't support tiled resources "
"to work.",
"D3D12");
DECLARE_bool(gpu_allow_invalid_upload_range);
DECLARE_bool(tiled_shared_memory);
namespace xe {
namespace gpu {
@@ -50,7 +44,7 @@ bool D3D12SharedMemory::Initialize() {
ui::d3d12::util::FillBufferResourceDesc(
buffer_desc, kBufferSize, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
buffer_state_ = D3D12_RESOURCE_STATE_COPY_DEST;
if (cvars::d3d12_tiled_shared_memory &&
if (cvars::tiled_shared_memory &&
provider.GetTiledResourcesTier() !=
D3D12_TILED_RESOURCES_TIER_NOT_SUPPORTED &&
!provider.GetGraphicsAnalysis()) {

View File

@@ -846,40 +846,6 @@ void D3D12TextureCache::WriteSampler(SamplerParameters parameters,
device->CreateSampler(&desc, handle);
}
bool D3D12TextureCache::ClampDrawResolutionScaleToMaxSupported(
uint32_t& scale_x, uint32_t& scale_y,
const ui::d3d12::D3D12Provider& provider) {
bool was_clamped;
if (provider.GetTiledResourcesTier() < D3D12_TILED_RESOURCES_TIER_1) {
was_clamped = scale_x > 1 || scale_y > 1;
scale_x = 1;
scale_y = 1;
return !was_clamped;
}
// Limit to the virtual address space available for a resource.
was_clamped = false;
uint32_t virtual_address_bits_per_resource =
provider.GetVirtualAddressBitsPerResource();
while (scale_x > 1 || scale_y > 1) {
uint64_t highest_scaled_address =
uint64_t(SharedMemory::kBufferSize) * (scale_x * scale_y) - 1;
if (uint32_t(64) - xe::lzcnt(highest_scaled_address) <=
virtual_address_bits_per_resource) {
break;
}
// When reducing from a square size, prefer decreasing the horizontal
// resolution as vertical resolution difference is visible more clearly in
// perspective.
was_clamped = true;
if (scale_x >= scale_y) {
--scale_x;
} else {
--scale_y;
}
}
return !was_clamped;
}
bool D3D12TextureCache::EnsureScaledResolveMemoryCommitted(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2) {

View File

@@ -126,10 +126,6 @@ class D3D12TextureCache final : public TextureCache {
void WriteSampler(SamplerParameters parameters,
D3D12_CPU_DESCRIPTOR_HANDLE handle) const;
// Returns whether the actual scale is not smaller than the requested one.
static bool ClampDrawResolutionScaleToMaxSupported(
uint32_t& scale_x, uint32_t& scale_y,
const ui::d3d12::D3D12Provider& provider);
// Ensures the tiles backing the range in the buffers are allocated.
bool EnsureScaledResolveMemoryCommitted(
uint32_t start_unscaled, uint32_t length_unscaled,

View File

@@ -12,17 +12,19 @@
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/profiling.h"
#include "xenia/gpu/gpu_flags.h"
#include "xenia/gpu/shared_memory.h"
DEFINE_int32(
draw_resolution_scale_x, 1,
"Integer pixel width scale used for scaling the rendering resolution "
"opaquely to the game.\n"
"1, 2 and 3 may be supported, but support of anything above 1 depends on "
"the device properties, such as whether it supports sparse binding / tiled "
"resources, the number of virtual address bits per resource, and other "
"factors.\n"
"Values from 1 to 7 may be supported, depending on device capabilities. "
"Requires sparse binding (Vulkan) or tiled resources (D3D12) for scales "
"above 1x1. The emulator will automatically clamp to the maximum supported "
"scale if the requested value exceeds device limits.\n"
"Various effects and parts of game rendering pipelines may work "
"incorrectly as pixels become ambiguous from the game's perspective and "
"because half-pixel offset (which normally doesn't affect coverage when "
@@ -59,6 +61,10 @@ DEFINE_uint32(
"textures - so with 2x2 resolution scaling, the soft limit will be 360 + "
"96 MB, and with 3x3, it will be 360 + 216 MB.",
"GPU");
DEFINE_bool(tiled_shared_memory, true,
"Enable tiled/sparse resources for efficient large address space "
"support. Disable for graphics debugger compatibility.",
"GPU");
namespace xe {
namespace gpu {
@@ -187,6 +193,43 @@ bool TextureCache::GetConfigDrawResolutionScale(uint32_t& x_out,
return clamped_x == config_x && clamped_y == config_y;
}
bool TextureCache::ClampDrawResolutionScaleToMaxSupported(
uint32_t& scale_x, uint32_t& scale_y, bool sparse_bind_supported,
uint32_t virtual_address_bits_per_resource) {
// Without sparse/tiled resource support, resolution scaling is not possible
// because the scaled address space exceeds what simple buffers can handle.
if (!sparse_bind_supported) {
bool was_clamped = scale_x > 1 || scale_y > 1;
scale_x = 1;
scale_y = 1;
return !was_clamped;
}
// With sparse binding, limit based on virtual address space if specified.
bool was_clamped = false;
if (virtual_address_bits_per_resource > 0) {
while (scale_x > 1 || scale_y > 1) {
uint64_t highest_scaled_address =
uint64_t(SharedMemory::kBufferSize) * (scale_x * scale_y) - 1;
if (uint32_t(64) - xe::lzcnt(highest_scaled_address) <=
virtual_address_bits_per_resource) {
break;
}
// When reducing from a square size, prefer decreasing the horizontal
// resolution as vertical resolution difference is visible more clearly in
// perspective.
was_clamped = true;
if (scale_x >= scale_y) {
--scale_x;
} else {
--scale_y;
}
}
}
return !was_clamped;
}
void TextureCache::ClearCache() { DestroyAllTextures(); }
void TextureCache::CompletedSubmissionUpdated(

View File

@@ -69,6 +69,14 @@ class TextureCache {
// Returns whether the actual scale is not smaller than the requested one.
static bool GetConfigDrawResolutionScale(uint32_t& x_out, uint32_t& y_out);
// Clamps the resolution scale based on device capabilities.
// sparse_bind_supported: whether the device supports sparse/tiled resources
// virtual_address_bits: max bits for virtual address per resource (0 = no
// limit) Returns true if scale was not clamped.
static bool ClampDrawResolutionScaleToMaxSupported(
uint32_t& scale_x, uint32_t& scale_y, bool sparse_bind_supported,
uint32_t virtual_address_bits_per_resource = 0);
uint32_t draw_resolution_scale_x() const { return draw_resolution_scale_x_; }
uint32_t draw_resolution_scale_y() const { return draw_resolution_scale_y_; }

View File

@@ -276,10 +276,26 @@ bool VulkanCommandProcessor::SetupContext() {
<< shared_memory_binding_count_log2;
// Requires the transient descriptor set layouts.
// Get draw resolution scale using the same method as D3D12
// Get draw resolution scale and clamp based on device capabilities
uint32_t draw_resolution_scale_x, draw_resolution_scale_y;
TextureCache::GetConfigDrawResolutionScale(draw_resolution_scale_x,
draw_resolution_scale_y);
bool draw_resolution_scale_not_clamped =
TextureCache::GetConfigDrawResolutionScale(draw_resolution_scale_x,
draw_resolution_scale_y);
// Check if sparse binding is supported for resolution scaling
bool has_sparse_binding = device_properties.sparseBinding &&
device_properties.sparseResidencyBuffer;
if (!TextureCache::ClampDrawResolutionScaleToMaxSupported(
draw_resolution_scale_x, draw_resolution_scale_y, has_sparse_binding,
0)) {
draw_resolution_scale_not_clamped = false;
}
if (!draw_resolution_scale_not_clamped) {
XELOGW(
"The requested draw resolution scale is not supported by the device or "
"the emulator, reducing to {}x{}",
draw_resolution_scale_x, draw_resolution_scale_y);
}
render_target_cache_ = std::make_unique<VulkanRenderTargetCache>(
*register_file_, *memory_, trace_writer_, draw_resolution_scale_x,
draw_resolution_scale_y, *this);

View File

@@ -1156,17 +1156,9 @@ bool VulkanRenderTargetCache::Resolve(const Memory& memory,
draw_resolution_scale_x() * draw_resolution_scale_y();
uint64_t scaled_offset =
uint64_t(dest_address) * draw_resolution_scale_area;
// Get the buffer's base offset to calculate relative offset
uint64_t buffer_relative_offset = 0;
size_t buffer_index =
texture_cache.GetScaledResolveCurrentBufferIndex();
auto* buffer_info =
texture_cache.GetScaledResolveBufferInfo(buffer_index);
if (buffer_info) {
buffer_relative_offset =
scaled_offset - buffer_info->range_start_scaled;
}
uint64_t buffer_relative_offset =
scaled_offset -
texture_cache.GetCurrentScaledResolveBufferBaseOffset();
write_descriptor_set_dest_buffer_info.buffer = scaled_buffer;
write_descriptor_set_dest_buffer_info.offset =

View File

@@ -15,6 +15,7 @@
#include <utility>
#include "xenia/base/assert.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/profiling.h"
@@ -27,6 +28,8 @@
#include "xenia/ui/vulkan/vulkan_mem_alloc.h"
#include "xenia/ui/vulkan/vulkan_util.h"
DECLARE_bool(tiled_shared_memory);
namespace xe {
namespace gpu {
namespace vulkan {
@@ -477,7 +480,10 @@ VulkanTextureCache::~VulkanTextureCache() {
// textures before destroying VMA.
DestroyAllTextures(true);
// Clean up scaled resolve buffers before destroying VMA
// Clean up sparse scaled resolve resources (heaps and sparse buffers)
ShutdownSparseScaledResolve();
// Clean up simple 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_) {
@@ -1323,15 +1329,8 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
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;
}
uint64_t buffer_relative_offset =
scaled_offset - GetCurrentScaledResolveBufferBaseOffset();
write_descriptor_set_source_base_buffer_info.buffer = scaled_buffer;
write_descriptor_set_source_base_buffer_info.offset =
@@ -2668,6 +2667,12 @@ bool VulkanTextureCache::Initialize() {
max_anisotropy_ = xenos::AnisoFilter::kDisabled;
}
// Initialize sparse scaled resolve buffers if draw resolution scaling is
// enabled and sparse binding is supported.
if (!InitializeSparseScaledResolve()) {
return false;
}
return true;
}
@@ -2743,6 +2748,13 @@ bool VulkanTextureCache::EnsureScaledResolveMemoryCommitted(
return true;
}
// Dispatch to sparse implementation if supported
if (sparse_scaled_resolve_supported_) {
return EnsureScaledResolveMemoryCommittedSparse(
start_unscaled, length_unscaled, length_scaled_alignment_log2);
}
// Simple non-overlapping buffer implementation (fallback)
if (length_unscaled == 0) {
return true;
}
@@ -2859,6 +2871,13 @@ bool VulkanTextureCache::MakeScaledResolveRangeCurrent(
return false;
}
// Dispatch to sparse implementation if supported
if (sparse_scaled_resolve_supported_) {
return MakeScaledResolveRangeCurrentSparse(start_unscaled, length_unscaled,
length_scaled_alignment_log2);
}
// Simple non-overlapping buffer implementation (fallback)
// First ensure the memory is committed (creates buffers if needed)
if (!EnsureScaledResolveMemoryCommitted(start_unscaled, length_unscaled,
length_scaled_alignment_log2)) {
@@ -2893,12 +2912,340 @@ bool VulkanTextureCache::MakeScaledResolveRangeCurrent(
}
VkBuffer VulkanTextureCache::GetCurrentScaledResolveBuffer() const {
if (sparse_scaled_resolve_supported_) {
if (scaled_resolve_current_buffer_index_ < kMaxScaledResolveSparseBuffers &&
scaled_resolve_sparse_buffers_[scaled_resolve_current_buffer_index_]) {
return scaled_resolve_sparse_buffers_
[scaled_resolve_current_buffer_index_]
->buffer();
}
return VK_NULL_HANDLE;
}
if (scaled_resolve_current_buffer_index_ >= scaled_resolve_buffers_.size()) {
return VK_NULL_HANDLE;
}
return scaled_resolve_buffers_[scaled_resolve_current_buffer_index_].buffer;
}
// Sparse scaled resolve helper functions
size_t VulkanTextureCache::GetScaledResolveSparseBufferCount() const {
// Each buffer is 2GB, and buffer N covers [N GB .. (N+2) GB)
// So for an address space of X bytes, we need ceil((X-1) / 1GB) buffers
// (or 0 if X <= 2GB, but we handle that with max)
uint64_t address_space = uint64_t(SharedMemory::kBufferSize) *
draw_resolution_scale_x() *
draw_resolution_scale_y();
// Number of buffers needed: ceil((address_space - 1) / 1GB)
// For 3x3 (4.5GB): (4.5GB - 1) / 1GB = 3.5 -> 4 buffers
// For 2x2 (2GB): (2GB - 1) / 1GB = 0.99 -> 1 buffer
// For 1x1 (512MB): (512MB - 1) / 1GB = 0 -> but we need at least 1
if (address_space <= kScaledResolveSparseBufferSize) {
return 1;
}
return size_t((address_space - 1) >> 30);
}
std::array<size_t, 2> VulkanTextureCache::GetPossibleScaledResolveBufferIndices(
uint64_t address_scaled) const {
// Given an address, find which buffers could contain it.
// Buffer N covers [N GB .. (N+2) GB), so address A could be in:
// - Buffer floor(A/1GB) if it exists (address is in [N GB .. (N+1) GB) part)
// - Buffer floor(A/1GB) - 1 if it exists (address is in [(N-1)+1 GB .. N+1
// GB) part)
size_t gb_index = size_t(address_scaled >> 30);
size_t buffer_count = GetScaledResolveSparseBufferCount();
size_t max_buffer_index = buffer_count > 0 ? buffer_count - 1 : 0;
// First possible buffer: the one starting at this GB
size_t buffer_a = std::min(gb_index, max_buffer_index);
// Second possible buffer: the one starting 1GB earlier (if it exists)
size_t buffer_b = gb_index > 0 ? std::min(gb_index - 1, max_buffer_index)
: max_buffer_index + 1; // Invalid
return {buffer_a, buffer_b};
}
bool VulkanTextureCache::InitializeSparseScaledResolve() {
if (!IsDrawResolutionScaled()) {
return true;
}
if (!cvars::tiled_shared_memory) {
XELOGI("VulkanTextureCache: Sparse scaled resolve disabled by CVAR");
return true;
}
const ui::vulkan::VulkanDevice* vulkan_device =
command_processor_.GetVulkanDevice();
const ui::vulkan::VulkanDevice::Properties& device_properties =
vulkan_device->properties();
if (!device_properties.sparseBinding ||
!device_properties.sparseResidencyBuffer) {
XELOGI(
"VulkanTextureCache: Sparse binding not supported, using simple "
"buffers");
return true;
}
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
VkDevice device = vulkan_device->device();
size_t buffer_count = GetScaledResolveSparseBufferCount();
XELOGI(
"VulkanTextureCache: Creating {} sparse scaled resolve buffers (2GB "
"each)",
buffer_count);
// Create the sparse buffers
for (size_t i = 0; i < buffer_count; ++i) {
VkBufferCreateInfo buffer_create_info = {};
buffer_create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_create_info.flags = VK_BUFFER_CREATE_SPARSE_BINDING_BIT |
VK_BUFFER_CREATE_SPARSE_RESIDENCY_BIT;
buffer_create_info.size = kScaledResolveSparseBufferSize;
buffer_create_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer buffer;
VkResult result =
dfn.vkCreateBuffer(device, &buffer_create_info, nullptr, &buffer);
if (result != VK_SUCCESS) {
XELOGE("VulkanTextureCache: Failed to create sparse buffer {}: error {}",
i, static_cast<int>(result));
ShutdownSparseScaledResolve();
return true; // Fall back to simple buffers
}
scaled_resolve_sparse_buffers_[i] =
std::make_unique<ScaledResolveSparseBuffer>(buffer);
}
// Get memory requirements for the sparse buffers
VkMemoryRequirements memory_requirements;
dfn.vkGetBufferMemoryRequirements(device,
scaled_resolve_sparse_buffers_[0]->buffer(),
&memory_requirements);
// Find a suitable device-local memory type
if (!xe::bit_scan_forward(memory_requirements.memoryTypeBits &
vulkan_device->memory_types().device_local,
&scaled_resolve_memory_type_)) {
XELOGE(
"VulkanTextureCache: Failed to find suitable memory type for sparse "
"buffers");
ShutdownSparseScaledResolve();
return true;
}
// Calculate the number of heaps we might need
uint64_t address_space = uint64_t(SharedMemory::kBufferSize) *
draw_resolution_scale_x() *
draw_resolution_scale_y();
uint32_t max_heap_count =
uint32_t((address_space + kScaledResolveHeapSize - 1) >>
kScaledResolveHeapSizeLog2);
scaled_resolve_heaps_.resize(max_heap_count, VK_NULL_HANDLE);
scaled_resolve_heap_count_ = 0;
sparse_scaled_resolve_supported_ = true;
XELOGI(
"VulkanTextureCache: Sparse scaled resolve initialized with {} buffers, "
"max {} heaps",
buffer_count, max_heap_count);
return true;
}
void VulkanTextureCache::ShutdownSparseScaledResolve() {
const ui::vulkan::VulkanDevice* vulkan_device =
command_processor_.GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
VkDevice device = vulkan_device->device();
// Free all heaps
for (VkDeviceMemory heap : scaled_resolve_heaps_) {
if (heap != VK_NULL_HANDLE) {
dfn.vkFreeMemory(device, heap, nullptr);
}
}
scaled_resolve_heaps_.clear();
scaled_resolve_heap_count_ = 0;
// Destroy sparse buffers
for (auto& buffer : scaled_resolve_sparse_buffers_) {
if (buffer) {
dfn.vkDestroyBuffer(device, buffer->buffer(), nullptr);
buffer.reset();
}
}
scaled_resolve_memory_type_ = UINT32_MAX;
sparse_scaled_resolve_supported_ = false;
}
void VulkanTextureCache::BindHeapToOverlappingBuffers(uint32_t heap_index,
VkDeviceMemory heap) {
// Calculate the address this heap covers
uint64_t heap_address = uint64_t(heap_index) << kScaledResolveHeapSizeLog2;
// Find which buffers this heap should be bound to
auto [buffer_a, buffer_b] =
GetPossibleScaledResolveBufferIndices(heap_address);
size_t buffer_count = GetScaledResolveSparseBufferCount();
// Bind to the first possible buffer
if (buffer_a < buffer_count && scaled_resolve_sparse_buffers_[buffer_a]) {
uint64_t offset_in_buffer =
heap_address - (uint64_t(buffer_a) << 30); // buffer_a * 1GB
VkSparseMemoryBind bind = {};
bind.resourceOffset = offset_in_buffer;
bind.size = kScaledResolveHeapSize;
bind.memory = heap;
bind.memoryOffset = 0;
bind.flags = 0;
command_processor_.SparseBindBuffer(
scaled_resolve_sparse_buffers_[buffer_a]->buffer(), 1, &bind,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
}
// Bind to the second possible buffer (if different and valid)
if (buffer_b < buffer_count && buffer_b != buffer_a &&
scaled_resolve_sparse_buffers_[buffer_b]) {
uint64_t offset_in_buffer =
heap_address - (uint64_t(buffer_b) << 30); // buffer_b * 1GB
VkSparseMemoryBind bind = {};
bind.resourceOffset = offset_in_buffer;
bind.size = kScaledResolveHeapSize;
bind.memory = heap;
bind.memoryOffset = 0;
bind.flags = 0;
command_processor_.SparseBindBuffer(
scaled_resolve_sparse_buffers_[buffer_b]->buffer(), 1, &bind,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
}
}
bool VulkanTextureCache::EnsureScaledResolveMemoryCommittedSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2) {
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;
uint64_t end_scaled = start_scaled + length_scaled;
// Calculate which heaps we need
uint32_t heap_first = uint32_t(start_scaled >> kScaledResolveHeapSizeLog2);
uint32_t heap_last = uint32_t((end_scaled - 1) >> kScaledResolveHeapSizeLog2);
const ui::vulkan::VulkanDevice* vulkan_device =
command_processor_.GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
VkDevice device = vulkan_device->device();
// Allocate and bind any heaps we need
for (uint32_t heap_index = heap_first; heap_index <= heap_last;
++heap_index) {
if (heap_index >= scaled_resolve_heaps_.size()) {
XELOGE("VulkanTextureCache: Heap index {} out of range (max {})",
heap_index, scaled_resolve_heaps_.size());
return false;
}
if (scaled_resolve_heaps_[heap_index] == VK_NULL_HANDLE) {
// Allocate a new heap
VkMemoryAllocateInfo allocate_info = {};
allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate_info.allocationSize = kScaledResolveHeapSize;
allocate_info.memoryTypeIndex = scaled_resolve_memory_type_;
VkDeviceMemory heap;
VkResult result =
dfn.vkAllocateMemory(device, &allocate_info, nullptr, &heap);
if (result != VK_SUCCESS) {
XELOGE("VulkanTextureCache: Failed to allocate heap {}: error {}",
heap_index, static_cast<int>(result));
return false;
}
scaled_resolve_heaps_[heap_index] = heap;
++scaled_resolve_heap_count_;
// Bind this heap to all overlapping buffers
BindHeapToOverlappingBuffers(heap_index, heap);
}
}
return true;
}
bool VulkanTextureCache::MakeScaledResolveRangeCurrentSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2) {
// First ensure the memory is committed
if (!EnsureScaledResolveMemoryCommittedSparse(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;
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;
uint64_t end_scaled = start_scaled + length_scaled;
// Find a buffer that contains the entire range
// Each buffer is 2GB and covers [buffer_index GB .. (buffer_index + 2) GB)
auto [buffer_a, buffer_b] =
GetPossibleScaledResolveBufferIndices(start_scaled);
auto [end_buffer_a, end_buffer_b] = GetPossibleScaledResolveBufferIndices(
end_scaled > 0 ? end_scaled - 1 : 0);
// Check if buffer_a can contain both start and end
size_t chosen_buffer = SIZE_MAX;
if (buffer_a == end_buffer_a || buffer_a == end_buffer_b) {
chosen_buffer = buffer_a;
} else if (buffer_b == end_buffer_a || buffer_b == end_buffer_b) {
chosen_buffer = buffer_b;
}
if (chosen_buffer == SIZE_MAX ||
chosen_buffer >= GetScaledResolveSparseBufferCount()) {
XELOGE("VulkanTextureCache: No buffer can contain range [{:X}, {:X})",
start_scaled, end_scaled);
return false;
}
scaled_resolve_current_buffer_index_ = chosen_buffer;
return true;
}
} // namespace vulkan
} // namespace gpu
} // namespace xe

View File

@@ -122,6 +122,7 @@ class VulkanTextureCache final : public TextureCache {
xenos::TextureFormat& format_out);
// Scaled resolve buffer management (for use by VulkanRenderTargetCache)
// Simple non-overlapping buffer (fallback when sparse binding unavailable)
struct ScaledResolveBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE;
@@ -130,6 +131,24 @@ class VulkanTextureCache final : public TextureCache {
uint64_t range_length_scaled = 0;
};
// Sparse buffer wrapper for overlapping 2GB windows
class ScaledResolveSparseBuffer {
public:
explicit ScaledResolveSparseBuffer(VkBuffer buffer) : buffer_(buffer) {}
VkBuffer buffer() const { return buffer_; }
private:
VkBuffer buffer_ = VK_NULL_HANDLE;
};
// Constants for sparse scaled resolve
static constexpr uint32_t kScaledResolveHeapSizeLog2 = 24; // 16MB heaps
static constexpr uint32_t kScaledResolveHeapSize =
uint32_t(1) << kScaledResolveHeapSizeLog2;
static constexpr uint64_t kScaledResolveSparseBufferSize =
uint64_t(2) << 30; // 2GB per buffer
// Public scaled resolve buffer methods for use by VulkanRenderTargetCache
bool EnsureScaledResolveMemoryCommittedPublic(
uint32_t start_unscaled, uint32_t length_unscaled,
@@ -144,6 +163,20 @@ class VulkanTextureCache final : public TextureCache {
VkBuffer GetCurrentScaledResolveBuffer() const;
// Returns the base scaled address that the current buffer starts at.
// For sparse buffers: buffer N starts at N GB (N << 30)
// For simple buffers: returns the buffer's range_start_scaled
uint64_t GetCurrentScaledResolveBufferBaseOffset() const {
if (sparse_scaled_resolve_supported_) {
return uint64_t(scaled_resolve_current_buffer_index_) << 30;
}
if (scaled_resolve_current_buffer_index_ < scaled_resolve_buffers_.size()) {
return scaled_resolve_buffers_[scaled_resolve_current_buffer_index_]
.range_start_scaled;
}
return 0;
}
size_t GetScaledResolveCurrentBufferIndex() const {
return scaled_resolve_current_buffer_index_;
}
@@ -352,6 +385,20 @@ class VulkanTextureCache final : public TextureCache {
xenos::ClampMode NormalizeClampMode(xenos::ClampMode clamp_mode) const;
// Sparse scaled resolve helper functions
bool InitializeSparseScaledResolve();
void ShutdownSparseScaledResolve();
size_t GetScaledResolveSparseBufferCount() const;
std::array<size_t, 2> GetPossibleScaledResolveBufferIndices(
uint64_t address_scaled) const;
bool EnsureScaledResolveMemoryCommittedSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2);
bool MakeScaledResolveRangeCurrentSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2);
void BindHeapToOverlappingBuffers(uint32_t heap_index, VkDeviceMemory heap);
VulkanCommandProcessor& command_processor_;
VkPipelineStageFlags guest_shader_pipeline_stages_;
@@ -392,12 +439,31 @@ class VulkanTextureCache final : public TextureCache {
std::pair<const SamplerParameters, Sampler>* sampler_used_first_ = nullptr;
std::pair<const SamplerParameters, Sampler>* sampler_used_last_ = nullptr;
// Scaled resolve buffer storage
// Scaled resolve buffer storage (simple non-overlapping, fallback path)
std::vector<ScaledResolveBuffer> scaled_resolve_buffers_;
// Current scaled resolve range tracking
uint64_t scaled_resolve_current_range_start_scaled_ = 0;
uint64_t scaled_resolve_current_range_length_scaled_ = 0;
size_t scaled_resolve_current_buffer_index_ = SIZE_MAX;
// Sparse scaled resolve (overlapping 2GB windows)
bool sparse_scaled_resolve_supported_ = false;
// 2GB overlapping sparse buffers - buffer N covers [N GB ... (N+2) GB)
// For 3x3 scale (4.5GB), need 4 buffers: 0:[0-2GB), 1:[1-3GB), 2:[2-4GB),
// 3:[3-4.5GB)
static constexpr size_t kMaxScaledResolveSparseBuffers =
(uint64_t(SharedMemory::kBufferSize) * kMaxDrawResolutionScaleAlongAxis *
kMaxDrawResolutionScaleAlongAxis -
1) >>
30;
std::array<std::unique_ptr<ScaledResolveSparseBuffer>,
kMaxScaledResolveSparseBuffers>
scaled_resolve_sparse_buffers_;
// 16MB heaps that can be mapped to multiple buffer regions
std::vector<VkDeviceMemory> scaled_resolve_heaps_;
uint32_t scaled_resolve_heap_count_ = 0;
// Memory type for sparse allocations
uint32_t scaled_resolve_memory_type_ = UINT32_MAX;
};
} // namespace vulkan