[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:
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user