aboutsummaryrefslogtreecommitdiff
path: root/src/video_core
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-30 05:28:53 -0400
committerkraibse <33869366+kraibse@users.noreply.github.com>2026-10-30 17:27:04 +0100
commitd4ee7fcd5129e7ee32ff38a5aef9b449d5a756de (patch)
tree7cc2d65f2f2c8d385af00c664089291a67a4febb /src/video_core
parent940d44ca25628288f1b5f64c25c022b3a4ae5ce2 (diff)
vulkan: bind memory to sparse terrain arrays layer by layer
The written part of Mario Kart World's sparse terrain arrays is small: 13-26% of their blocks. The two 1.45 GB ASTC arrays were backed in full on the host, and the two 5.8 GB BC6H arrays, too large to back, stayed unbacked, so what the game streamed into them never showed. A color array of 32 MiB or more lying in one sparse region, in a format uploaded layer by layer (huge ASTC arrays decoded on the GPU, or formats sampled as they are), is now a sparse resident image (SparseLayers). When it is refreshed, the layers written since (or, for a new image, the layers any of whose blocks was written) get device memory bound with vkQueueBindSparse (under the submit lock, waited on, as the multi-range buffer cache does) and only those layers are uploaded. Unwritten layers read zero. One image binds at most 768 MiB (384 MiB on GPUs with less than 7 GiB); rendering into one binds all its layers. Mario Kart World, 180 s: the ASTC arrays hold 1.4 MiB per written layer, four BC6H layers (341 MiB) now have their data, device memory in use 4.8-5.0 GB instead of 6.0-7.1 GB with the texture cache below its collection threshold, 16.8-17.2 ms average frames, peak working set 13.4 GB.
Diffstat (limited to 'src/video_core')
-rw-r--r--src/video_core/nx2.h15
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp231
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h65
-rw-r--r--src/video_core/texture_cache/image_base.h3
-rw-r--r--src/video_core/texture_cache/texture_cache.h86
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h6
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h5
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp29
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h6
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.cpp11
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h4
11 files changed, 453 insertions, 8 deletions
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index 1b565e73d4..1a3bae93ec 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -123,6 +123,21 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) {
}
}
+/// True when [gpu_address, +size) lies in one sparse region.
+inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) {
+ if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) ||
+ gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] {
+ return false;
+ }
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ auto it = g_nx2_sparse_state.upper_bound(gpu_address);
+ if (it == g_nx2_sparse_state.begin()) {
+ return false;
+ }
+ --it;
+ return gpu_address + size <= it->first + it->second.size;
+}
+
/// The GPU renders into [gpu_address, +size): count its blocks as written, so images made there
/// later get memory (their contents live in the host image, not in guest memory).
inline void Nx2SparseMarkWritten(u64 gpu_address, u64 size) {
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 08a471cfd6..b77f9af5f8 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -16,7 +16,9 @@
#include <boost/container/small_vector.hpp>
#include <bit>
#include <numeric>
+#include "common/alignment.h"
#include "common/bit_util.h"
+#include "common/div_ceil.h"
#include "common/settings.h"
#include "video_core/renderer_vulkan/vk_texture_cache.h"
@@ -176,6 +178,31 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
info.type != ImageType::Linear && Tegra::Nx2SparseUnwritten(gpu_addr, size);
}
+// A large color array in a Switch 2 sparse region (title 1's terrain arrays: 13-26% of their
+// blocks written) gets memory by layer, as its layers are written: unbacked at first, then
+// Image::BindLayers. Only formats uploaded layer by layer: huge ASTC arrays (GPU decode) and
+// formats the host samples as they are.
+[[nodiscard]] bool UsesSparseLayers(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) {
+ if (!device.IsSparseResidencyImage2DSupported() || info.type != ImageType::e2D ||
+ info.resources.layers <= 1 || info.num_samples != 1 ||
+ VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) {
+ return false;
+ }
+ const bool layer_uploads =
+ IsHugeAstcArray(device, info) ||
+ (!IsPixelFormatASTC(info.format) &&
+ (!IsPixelFormatBCn(info.format) || device.IsOptimalBcnSupported()));
+ const u32 size = VideoCommon::CalculateGuestSizeInBytes(info);
+ return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size);
+}
+
+// The most device memory one SparseLayers image binds: 768 MiB, 384 MiB on GPUs with less than
+// 7 GiB (title 1's BC6H arrays are 85 MB a layer).
+[[nodiscard]] VkDeviceSize SparseLayersCap(const Device& device) {
+ return device.GetDeviceLocalMemory() < (u64{7} << 30) ? (VkDeviceSize{384} << 20)
+ : (VkDeviceSize{768} << 20);
+}
+
// The host format of a huge ASTC array: the GPU decoder's RGBA8 (sRGB kept for sampling).
[[nodiscard]] std::optional<VkFormat> HugeAstcHostFormat(const Device& device, const ImageInfo& info) {
if (!IsHugeAstcArray(device, info)) {
@@ -2037,16 +2064,19 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
? std::span<const VkFormat>{OpaqueAstcViewFormats}
: runtime->ViewFormats(info.format),
AstcHostFormat(runtime_.device, info),
- IsUnwrittenSparse(info, gpu_addr_))),
+ UsesSparseLayers(runtime_.device, info, gpu_addr_) ||
+ IsUnwrittenSparse(info, gpu_addr_))),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
flags |= VideoCommon::ImageFlagBits::Unbacked;
- // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing was
- // written there yet, so the first write gets it replaced.
const ImageInfo host_info = TrimmedHostInfo(runtime_.device, info, host_level_offset);
const u64 texels = u64{host_info.size.width} * host_info.size.height *
host_info.size.depth * static_cast<u64>(host_info.resources.layers);
- if (texels <= (u64{1} << 28)) {
+ if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) {
+ flags |= VideoCommon::ImageFlagBits::SparseLayers;
+ } else if (texels <= (u64{1} << 28)) {
+ // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing
+ // was written there yet, so the first write gets it replaced.
flags |= VideoCommon::ImageFlagBits::UnwrittenSparse;
}
}
@@ -2520,6 +2550,199 @@ VkImageView Image::StorageImageView(s32 level) noexcept {
return *view;
}
+bool Image::InitSparseLayers() {
+ const Device& device = runtime->device;
+ const VkImage handle = *original_image;
+ const auto requirements = device.GetLogical().GetImageSparseMemoryRequirements(handle);
+ const auto it = std::ranges::find_if(requirements, [](const auto& r) {
+ return (r.formatProperties.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0;
+ });
+ if (it == requirements.end()) {
+ return false;
+ }
+ SparseLayerMemory& s = sparse_layers;
+ s.allocator = &runtime->memory_allocator;
+ s.requirements = device.GetLogical().GetImageMemoryRequirements(handle);
+ s.sparse = *it;
+ u32 offset = host_level_offset;
+ const ImageInfo host = TrimmedHostInfo(device, info, offset);
+ s.width = host.size.width;
+ s.height = host.size.height;
+ s.levels = static_cast<u32>(host.resources.levels);
+ // Sparse block size in texels (drivers give it in texels or in compressed blocks).
+ const bool huge_astc = IsHugeAstcArray(device, info);
+ const u32 block_width = huge_astc ? 1 : VideoCore::Surface::DefaultBlockWidth(info.format);
+ const u32 block_height = huge_astc ? 1 : VideoCore::Surface::DefaultBlockHeight(info.format);
+ const u64 block_bytes = huge_astc ? 4 : VideoCore::Surface::BytesPerBlock(info.format);
+ const VkExtent3D granularity = it->formatProperties.imageGranularity;
+ s.tile_width = granularity.width;
+ s.tile_height = granularity.height;
+ const u64 tile = s.requirements.alignment;
+ if (u64{granularity.width / block_width} * (granularity.height / block_height) * block_bytes !=
+ tile &&
+ u64{granularity.width} * granularity.height * block_bytes == tile) {
+ s.tile_width *= block_width;
+ s.tile_height *= block_height;
+ }
+ const u32 tail_lod = (std::min)(it->imageMipTailFirstLod, s.levels);
+ VkDeviceSize bytes = 0;
+ for (u32 level = 0; level < tail_lod; ++level) {
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ bytes += u64{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height) *
+ tile;
+ }
+ if (tail_lod < s.levels &&
+ (it->formatProperties.flags & VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT) == 0) {
+ bytes += it->imageMipTailSize;
+ }
+ s.layer_bytes = Common::AlignUp(bytes, tile);
+ s.layer_memory.assign(static_cast<size_t>(info.resources.layers), VK_NULL_HANDLE);
+ LOG_INFO(Render_Vulkan,
+ "Sparse layered image {:#x}: {}x{} {} levels {} layers, {} KiB a layer, block {}x{}",
+ gpu_addr, s.width, s.height, s.levels, info.resources.layers, s.layer_bytes >> 10,
+ s.tile_width, s.tile_height);
+ return true;
+}
+
+std::vector<u32> Image::BindLayers(std::span<const u32> layers) {
+ SparseLayerMemory& s = sparse_layers;
+ std::vector<u32> bound;
+ if (s.layer_memory.empty()) {
+ return bound;
+ }
+ const Device& device = runtime->device;
+ const u64 tile = s.requirements.alignment;
+ const u32 tail_lod = (std::min)(s.sparse.imageMipTailFirstLod, s.levels);
+ const bool has_tail = tail_lod < s.levels;
+ const bool single_tail =
+ (s.sparse.formatProperties.flags & VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT) != 0;
+ const VkDeviceSize cap = SparseLayersCap(device);
+ std::vector<VkSparseImageMemoryBind> image_binds;
+ std::vector<VkSparseMemoryBind> opaque_binds;
+ for (const u32 layer : layers) {
+ if (s.layer_memory[layer] != VK_NULL_HANDLE) {
+ bound.push_back(layer);
+ continue;
+ }
+ if (s.bound_bytes + s.layer_bytes > cap) {
+ if (!std::exchange(s.warned, true)) {
+ LOG_WARNING(Render_Vulkan, "Sparse layered image {:#x}: {} MiB bound, more layers "
+ "stay without memory",
+ gpu_addr, s.bound_bytes >> 20);
+ }
+ continue;
+ }
+ const VkMemoryRequirements requirements{
+ .size = s.layer_bytes,
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ const VmaAllocation allocation = s.allocator->AllocateSparseMemory(requirements, memory, offset);
+ if (allocation == VK_NULL_HANDLE) {
+ LOG_WARNING(Render_Vulkan, "Sparse layered image {:#x}: out of device memory", gpu_addr);
+ break;
+ }
+ s.layer_memory[layer] = allocation;
+ s.bound_bytes += s.layer_bytes;
+ for (u32 level = 0; level < tail_lod; ++level) {
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ image_binds.push_back(VkSparseImageMemoryBind{
+ .subresource{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = level,
+ .arrayLayer = layer,
+ },
+ .offset{0, 0, 0},
+ .extent{width, height, 1},
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ offset += u64{Common::DivCeil(width, s.tile_width)} *
+ Common::DivCeil(height, s.tile_height) * tile;
+ }
+ if (has_tail && !single_tail) {
+ opaque_binds.push_back(VkSparseMemoryBind{
+ .resourceOffset = s.sparse.imageMipTailOffset + layer * s.sparse.imageMipTailStride,
+ .size = s.sparse.imageMipTailSize,
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ bound.push_back(layer);
+ }
+ if (has_tail && single_tail && s.tail_memory == VK_NULL_HANDLE && !bound.empty()) {
+ const VkMemoryRequirements requirements{
+ .size = Common::AlignUp(s.sparse.imageMipTailSize, tile),
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ s.tail_memory = s.allocator->AllocateSparseMemory(requirements, memory, offset);
+ if (s.tail_memory != VK_NULL_HANDLE) {
+ opaque_binds.push_back(VkSparseMemoryBind{
+ .resourceOffset = s.sparse.imageMipTailOffset,
+ .size = s.sparse.imageMipTailSize,
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ }
+ if (image_binds.empty() && opaque_binds.empty()) {
+ return bound;
+ }
+ const VkSparseImageMemoryBindInfo image_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(image_binds.size()),
+ .pBinds = image_binds.data(),
+ };
+ const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(opaque_binds.size()),
+ .pBinds = opaque_binds.data(),
+ };
+ const VkBindSparseInfo bind_info{
+ .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO,
+ .pNext = nullptr,
+ .waitSemaphoreCount = 0,
+ .pWaitSemaphores = nullptr,
+ .bufferBindCount = 0,
+ .pBufferBinds = nullptr,
+ .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U,
+ .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info,
+ .imageBindCount = image_binds.empty() ? 0U : 1U,
+ .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info,
+ .signalSemaphoreCount = 0,
+ .pSignalSemaphores = nullptr,
+ };
+ // Bound before the command buffer that uploads the layers is submitted (as the multi-range
+ // buffer cache does).
+ vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ });
+ VkResult result;
+ {
+ std::scoped_lock lock{scheduler->submit_mutex};
+ result = device.GetGraphicsQueue().BindSparse(bind_info, *fence);
+ }
+ if (result != VK_SUCCESS) {
+ LOG_ERROR(Render_Vulkan, "Sparse layered image {:#x}: binding failed ({})", gpu_addr,
+ static_cast<int>(result));
+ return {};
+ }
+ fence.Wait();
+ return bound;
+}
+
VkImageView Image::StorageImageLayerView(s32 level, u32 layer) {
const u32 layers = static_cast<u32>(info.resources.layers);
if (storage_layer_views.empty()) {
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 59f2936e06..13919f1270 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -385,6 +385,10 @@ public:
/// A storage view of one layer of a host level (decoding only some layers of an array).
VkImageView StorageImageLayerView(s32 level, u32 layer);
+ /// SparseLayers images: gives `layers` device memory where they have none yet. Returns the
+ /// layers that have memory afterwards (a cap per image can leave some out).
+ std::vector<u32> BindLayers(std::span<const u32> layers);
+
bool IsRescaled() const noexcept;
bool ScaleUp(bool ignore = false);
@@ -403,6 +407,67 @@ private:
Scheduler* scheduler{};
TextureCacheRuntime* runtime{};
+ /// Memory bound to the layers of a SparseLayers image. Declared before the images, so it is
+ /// released after them.
+ struct SparseLayerMemory {
+ SparseLayerMemory() = default;
+ SparseLayerMemory(const SparseLayerMemory&) = delete;
+ SparseLayerMemory& operator=(const SparseLayerMemory&) = delete;
+ SparseLayerMemory(SparseLayerMemory&& other) noexcept {
+ *this = std::move(other);
+ }
+ SparseLayerMemory& operator=(SparseLayerMemory&& other) noexcept {
+ Release();
+ allocator = std::exchange(other.allocator, nullptr);
+ requirements = other.requirements;
+ sparse = other.sparse;
+ width = other.width;
+ height = other.height;
+ levels = other.levels;
+ tile_width = other.tile_width;
+ tile_height = other.tile_height;
+ layer_bytes = other.layer_bytes;
+ layer_memory = std::move(other.layer_memory);
+ other.layer_memory.clear();
+ tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE);
+ bound_bytes = other.bound_bytes;
+ warned = other.warned;
+ return *this;
+ }
+ ~SparseLayerMemory() {
+ Release();
+ }
+ void Release() noexcept {
+ if (allocator == nullptr) {
+ return;
+ }
+ for (const VmaAllocation allocation : layer_memory) {
+ allocator->FreeSparseMemory(allocation);
+ }
+ allocator->FreeSparseMemory(tail_memory);
+ layer_memory.clear();
+ tail_memory = VK_NULL_HANDLE;
+ }
+
+ const MemoryAllocator* allocator{};
+ VkMemoryRequirements requirements{};
+ VkSparseImageMemoryRequirements sparse{};
+ u32 width{}; ///< Host level 0
+ u32 height{};
+ u32 levels{}; ///< Host levels
+ u32 tile_width{}; ///< Sparse block, in texels
+ u32 tile_height{};
+ VkDeviceSize layer_bytes{}; ///< One layer's levels before the mip tail, and its own tail
+ std::vector<VmaAllocation> layer_memory; ///< Per layer, null until bound
+ VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one
+ VkDeviceSize bound_bytes{};
+ bool warned{};
+ };
+ SparseLayerMemory sparse_layers;
+
+ /// Reads the image's sparse requirements; false when it can't be bound by layer.
+ bool InitSparseLayers();
+
vk::Image original_image;
vk::Image scaled_image;
diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h
index cfb839d17e..04254fb417 100644
--- a/src/video_core/texture_cache/image_base.h
+++ b/src/video_core/texture_cache/image_base.h
@@ -53,6 +53,9 @@ enum class ImageFlagBits : u32 {
/// Over Switch 2 sparse blocks nobody wrote: all zeros, so the host image has no memory
/// (Unbacked) until the first write, which makes a backed image replace it.
UnwrittenSparse = 1 << 21,
+ /// A large array over Switch 2 sparse blocks: unbacked, each layer gets device memory when
+ /// something is written to it (Image::BindLayers) and only those layers are uploaded.
+ SparseLayers = 1 << 22,
};
DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits)
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 168df50c8b..3de82d2eb4 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -7,6 +7,7 @@
#pragma once
#include <algorithm>
+#include <numeric>
#include <cstdlib>
#include <limits>
#include <map>
@@ -1306,6 +1307,10 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
TrackImage(image, image_id);
}
+ if (True(image.flags & ImageFlagBits::SparseLayers)) {
+ RefreshSparseLayers(image);
+ return;
+ }
if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) {
runtime.InsertUploadMemoryBarrier();
return;
@@ -1354,6 +1359,9 @@ bool TextureCache<P>::UploadsByLayer(const ImageBase& image) const noexcept {
// Title 1's terrain arrays (8192x8192x68 ASTC, 1.45 GB each) get a few layers streamed in at a
// time; re-decoding all of them for each took a 2 GiB staging buffer and stalled the GPU.
if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) {
+ if (True(image.flags & ImageFlagBits::SparseLayers)) {
+ return true;
+ }
return True(image.flags & ImageFlagBits::AcceleratedUpload) &&
image.info.type == ImageType::e2D && image.info.resources.layers > 1 &&
image.guest_size_bytes >= LayerUploadMinSize &&
@@ -1401,6 +1409,71 @@ bool TextureCache<P>::UploadWrittenLayers(Image& image) {
if (layers.empty() || layers.size() == num_layers) {
return false;
}
+ UploadLayers(image, layers);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+template <class P>
+void TextureCache<P>::RefreshSparseLayers(Image& image) {
+ if constexpr (requires { &Image::BindLayers; }) {
+ // The layers written since the last upload, or, for a new image, every layer any of whose
+ // blocks has been written.
+ const u32 num_layers = static_cast<u32>(image.info.resources.layers);
+ const u64 stride = image.info.layer_stride;
+ std::vector<u32> layers;
+ for (u32 layer = 0; layer < num_layers; ++layer) {
+ const bool written =
+ image.dirty_layers.empty()
+ ? !Tegra::Nx2SparseUnwritten(image.gpu_addr + layer * stride, stride)
+ : ((image.dirty_layers[layer / 64] >> (layer % 64)) & 1) != 0;
+ if (written) {
+ layers.push_back(layer);
+ }
+ }
+ image.dirty_layers.clear();
+ const std::vector<u32> bound = layers.empty() ? std::vector<u32>{} : image.BindLayers(layers);
+ if (bound.empty()) {
+ runtime.TransitionImageLayout(image);
+ return;
+ }
+ UploadLayers(image, bound);
+ runtime.InsertUploadMemoryBarrier();
+ }
+}
+
+template <class P>
+void TextureCache<P>::UploadLayers(Image& image, std::span<const u32> layers) {
+ if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) {
+ if (False(image.flags & ImageFlagBits::AcceleratedUpload)) {
+ // Plain formats: each layer unswizzled on its own into its layer of the image.
+ ImageInfo layer_info = image.info;
+ layer_info.resources.layers = 1;
+ const size_t layer_size = CalculateUnswizzledSizeBytes(layer_info);
+ auto staging = runtime.UploadStagingBuffer(layer_size * layers.size());
+ std::vector<BufferImageCopy> copies;
+ for (size_t i = 0; i < layers.size(); ++i) {
+ const GPUVAddr layer_addr = image.gpu_addr + u64{layers[i]} * image.info.layer_stride;
+ Tegra::Memory::GpuGuestMemory<u8, Tegra::Memory::GuestMemoryFlags::UnsafeRead>
+ swizzle_data(*gpu_memory, layer_addr, image.info.layer_stride,
+ &swizzle_data_buffer);
+ auto layer_copies =
+ UnswizzleImage(*gpu_memory, layer_addr, layer_info, swizzle_data,
+ staging.mapped_span.subspan(i * layer_size, layer_size));
+ for (BufferImageCopy copy : layer_copies) {
+ copy.buffer_offset += i * layer_size;
+ copy.image_subresource.base_layer = static_cast<s32>(layers[i]);
+ copies.push_back(copy);
+ }
+ }
+ image.UploadMemory(staging, copies);
+ ++images_uploaded;
+ uploaded_bytes += u64{image.info.layer_stride} * layers.size();
+ ++layer_uploads;
+ return;
+ }
// Each layer's mip chain from the first level the host keeps on (as UploadImageContents).
auto uploads = FullUploadSwizzles(image.info);
const size_t level_offset = uploads[image.host_level_offset].buffer_offset;
@@ -1420,9 +1493,6 @@ bool TextureCache<P>::UploadWrittenLayers(Image& image) {
++images_uploaded;
uploaded_bytes += tail * layers.size();
++layer_uploads;
- return true;
- } else {
- return false;
}
}
@@ -2524,7 +2594,8 @@ ImageViewId TextureCache<P>::FindOrEmplaceImageView(ImageId image_id, const Imag
if (const ImageViewId image_view_id = image.FindView(info); image_view_id) {
return image_view_id;
}
- if (True(image.flags & ImageFlagBits::Unbacked)) {
+ if (True(image.flags & ImageFlagBits::Unbacked) &&
+ False(image.flags & ImageFlagBits::SparseLayers)) {
// How the game uses an image too large to back (e.g. a sparse virtual texture).
LOG_INFO(HW_GPU, "View of unbacked image {:#x}: type {} format {} levels {}+{} layers {}+{}",
image.gpu_addr, static_cast<u32>(info.type), static_cast<u32>(info.format),
@@ -2924,6 +2995,13 @@ void TextureCache<P>::PrepareImage(ImageId image_id, bool is_modification, bool
}
}
if (is_modification) {
+ if constexpr (requires { &Image::BindLayers; }) {
+ if (True(image.flags & ImageFlagBits::SparseLayers)) {
+ std::vector<u32> layers(static_cast<size_t>(image.info.resources.layers));
+ std::iota(layers.begin(), layers.end(), 0U);
+ image.BindLayers(layers);
+ }
+ }
if (True(image.flags & ImageFlagBits::UnwrittenSparse) &&
False(image.flags & ImageFlagBits::Remapped)) {
// The GPU renders into it: these writes are lost, later uses get a backed image.
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index f4cc89f2ba..11e95f98b3 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -167,6 +167,12 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
/// Uploads the image's dirty layers. False when it needs a full upload instead.
bool UploadWrittenLayers(Image& image);
+ /// Gives the written layers of a SparseLayers image memory and uploads them.
+ void RefreshSparseLayers(Image& image);
+
+ /// Uploads `layers` of the image from guest memory.
+ void UploadLayers(Image& image, std::span<const u32> layers);
+
struct PendingUnswizzle {
ImageId image_id;
VideoCommon::ImageInfo info;
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index b1fa8db183..39ae25f43a 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -321,6 +321,11 @@ public:
return features.features.sparseBinding && graphics_family_sparse_binding;
}
+ /// 2D images whose memory can be bound in parts, on the graphics queue.
+ bool IsSparseResidencyImage2DSupported() const {
+ return IsSparseBindingSupported() && features.features.sparseResidencyImage2D;
+ }
+
/// Returns true for tile-based deferred renderers.
bool IsTiler() const {
switch (GetDriverID()) {
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
index 92f537aa3c..a0c96d1b45 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
@@ -295,6 +295,35 @@ vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) cons
device.GetDispatchLoader());
}
+VmaAllocation MemoryAllocator::AllocateSparseMemory(const VkMemoryRequirements &requirements,
+ VkDeviceMemory &memory,
+ VkDeviceSize &offset) const {
+ const VmaAllocationCreateInfo alloc_ci{
+ .flags = 0,
+ .usage = VMA_MEMORY_USAGE_UNKNOWN,
+ .requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
+ .preferredFlags = 0,
+ .memoryTypeBits = requirements.memoryTypeBits,
+ .pool = VK_NULL_HANDLE,
+ .pUserData = nullptr,
+ .priority = 0.f,
+ };
+ VmaAllocation allocation{};
+ VmaAllocationInfo info{};
+ if (vmaAllocateMemory(allocator, &requirements, &alloc_ci, &allocation, &info) != VK_SUCCESS) {
+ return VK_NULL_HANDLE;
+ }
+ memory = info.deviceMemory;
+ offset = info.offset;
+ return allocation;
+}
+
+void MemoryAllocator::FreeSparseMemory(VmaAllocation allocation) const {
+ if (allocation != VK_NULL_HANDLE) {
+ vmaFreeMemory(allocator, allocation);
+ }
+}
+
vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const {
// MESA will do memcpy() if not marked as host cached, so just force mark it for most buffers
auto const anv_flags = (usage == MemoryUsage::Stream
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h
index 99649b9aef..f3c6c177a2 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.h
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h
@@ -111,6 +111,12 @@ namespace Vulkan {
/// a null image when the device can't make one.
vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const;
+ /// Device memory to bind to part of a sparse resource. Null when there is none left.
+ VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements,
+ VkDeviceMemory &memory, VkDeviceSize &offset) const;
+
+ void FreeSparseMemory(VmaAllocation allocation) const;
+
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const;
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage,
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp
index bb43fe399f..dbaaf450a0 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.cpp
+++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp
@@ -219,6 +219,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept {
X(vkGetEventStatus);
X(vkGetFenceStatus);
X(vkGetImageMemoryRequirements);
+ X(vkGetImageSparseMemoryRequirements);
X(vkGetPipelineCacheData);
X(vkGetMemoryFdKHR);
#ifdef _WIN32
@@ -866,6 +867,16 @@ VkMemoryRequirements Device::GetImageMemoryRequirements(VkImage image) const noe
return requirements;
}
+std::vector<VkSparseImageMemoryRequirements> Device::GetImageSparseMemoryRequirements(
+ VkImage image) const {
+ u32 count = 0;
+ dld->vkGetImageSparseMemoryRequirements(handle, image, &count, nullptr);
+ std::vector<VkSparseImageMemoryRequirements> requirements(count);
+ dld->vkGetImageSparseMemoryRequirements(handle, image, &count, requirements.data());
+ requirements.resize(count);
+ return requirements;
+}
+
std::vector<VkPipelineExecutablePropertiesKHR> Device::GetPipelineExecutablePropertiesKHR(
VkPipeline pipeline) const {
const VkPipelineInfoKHR info{
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index 947150648d..3010368973 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -335,6 +335,7 @@ struct DeviceDispatch : InstanceDispatch {
PFN_vkGetEventStatus vkGetEventStatus{};
PFN_vkGetFenceStatus vkGetFenceStatus{};
PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements{};
+ PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements{};
PFN_vkGetPipelineCacheData vkGetPipelineCacheData{};
PFN_vkGetMemoryFdKHR vkGetMemoryFdKHR{};
#ifdef _WIN32
@@ -1111,6 +1112,9 @@ public:
VkMemoryRequirements GetImageMemoryRequirements(VkImage image) const noexcept;
+ std::vector<VkSparseImageMemoryRequirements> GetImageSparseMemoryRequirements(
+ VkImage image) const;
+
std::vector<VkPipelineExecutablePropertiesKHR> GetPipelineExecutablePropertiesKHR(
VkPipeline pipeline) const;