aboutsummaryrefslogtreecommitdiff
path: root/src/video_core
diff options
context:
space:
mode:
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;