diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index 40b097715..3af412e80 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -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) { diff --git a/src/xenia/gpu/d3d12/d3d12_shared_memory.cc b/src/xenia/gpu/d3d12/d3d12_shared_memory.cc index 64bc28b82..f87b61352 100644 --- a/src/xenia/gpu/d3d12/d3d12_shared_memory.cc +++ b/src/xenia/gpu/d3d12/d3d12_shared_memory.cc @@ -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()) { diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index ed4415659..41fc005bc 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -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) { diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.h b/src/xenia/gpu/d3d12/d3d12_texture_cache.h index 347772bb6..e0ff3c60a 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.h +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.h @@ -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, diff --git a/src/xenia/gpu/texture_cache.cc b/src/xenia/gpu/texture_cache.cc index 15fa967ca..f043c6468 100644 --- a/src/xenia/gpu/texture_cache.cc +++ b/src/xenia/gpu/texture_cache.cc @@ -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( diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index 1d30ef136..381adbb3f 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -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_; } diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 59e88e87b..fb3378bd1 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -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( *register_file_, *memory_, trace_writer_, draw_resolution_scale_x, draw_resolution_scale_y, *this); diff --git a/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc b/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc index 923135029..8113b6a52 100644 --- a/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_render_target_cache.cc @@ -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 = diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc index c39746c2c..a50c7ec2b 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc @@ -15,6 +15,7 @@ #include #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 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(result)); + ShutdownSparseScaledResolve(); + return true; // Fall back to simple buffers + } + + scaled_resolve_sparse_buffers_[i] = + std::make_unique(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(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 diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.h b/src/xenia/gpu/vulkan/vulkan_texture_cache.h index 472dc35ed..7e8f5525c 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.h @@ -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 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* sampler_used_first_ = nullptr; std::pair* sampler_used_last_ = nullptr; - // Scaled resolve buffer storage + // Scaled resolve buffer storage (simple non-overlapping, fallback path) std::vector 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, + kMaxScaledResolveSparseBuffers> + scaled_resolve_sparse_buffers_; + // 16MB heaps that can be mapped to multiple buffer regions + std::vector 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