aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/core/hle/service/nx2_gsv.h3
-rw-r--r--src/video_core/nx2.h76
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp30
-rw-r--r--src/video_core/texture_cache/image_base.h3
-rw-r--r--src/video_core/texture_cache/texture_cache.h14
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp29
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h4
7 files changed, 147 insertions, 12 deletions
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index f3cc87ac4d..9d00b81368 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -549,6 +549,7 @@ private:
// BackSparseWrite gives a block memory before the GPU writes into it.
m_sparse_regions[start] = {size, std::vector<u8>(Common::DivCeil(size, SparseBlockSize))};
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size);
+ Tegra::Nx2SparseRegionCreated(start, size, SparseBlockSize);
Tegra::Nx2AddSparseSpan(start, size);
LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it",
start, last);
@@ -580,6 +581,7 @@ private:
FreeReleasedRegions();
if (last > first) {
if (m_sparse_regions.erase(first) != 0) {
+ Tegra::Nx2SparseRegionDestroyed(first);
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1);
} else {
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1);
@@ -723,6 +725,7 @@ private:
region.blocks[block] = BlockBacked;
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address,
block_size);
+ Tegra::Nx2SparseBlockWritten(start, block);
m_sparse_backed_bytes += block_size;
if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) {
LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only "
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index 58c9b17bcf..1b565e73d4 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -7,6 +7,7 @@
#include <array>
#include <atomic>
#include <functional>
+#include <map>
#include <mutex>
#include <vector>
@@ -93,6 +94,81 @@ inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) {
}
}
+// Which blocks of each sparse region have been written (got memory), for the texture cache: an
+// image over blocks nobody wrote is all zeros and needs no device memory. Its own lock, taken last:
+// the texture cache asks with its lock held, and nx2:gpu updates it holding none of the GPU's.
+struct Nx2SparseRegionState {
+ u64 size;
+ u64 block_size;
+ std::vector<bool> written;
+};
+inline std::mutex g_nx2_sparse_state_mutex;
+inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state;
+
+inline void Nx2SparseRegionCreated(u64 gpu_address, u64 size, u64 block_size) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ g_nx2_sparse_state[gpu_address] = {size, block_size,
+ std::vector<bool>((size + block_size - 1) / block_size)};
+}
+
+inline void Nx2SparseRegionDestroyed(u64 gpu_address) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ g_nx2_sparse_state.erase(gpu_address);
+}
+
+inline void Nx2SparseBlockWritten(u64 region_address, u64 block) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ if (const auto it = g_nx2_sparse_state.find(region_address); it != g_nx2_sparse_state.end()) {
+ it->second.written[block] = true;
+ }
+}
+
+/// 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) {
+ 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;
+ }
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ for (auto& [start, region] : g_nx2_sparse_state) {
+ const u64 end = start + region.size;
+ if (gpu_address >= end || gpu_address + size <= start) {
+ continue;
+ }
+ const u64 first = ((std::max)(gpu_address, start) - start) / region.block_size;
+ const u64 last = ((std::min)(gpu_address + size, end) - 1 - start) / region.block_size;
+ for (u64 block = first; block <= last; ++block) {
+ region.written[block] = true;
+ }
+ }
+}
+
+/// True when [gpu_address, +size) lies in one sparse region and none of it has been written.
+inline bool Nx2SparseUnwritten(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;
+ const auto& [start, region] = *it;
+ if (gpu_address + size > start + region.size) {
+ return false;
+ }
+ const u64 last = (gpu_address + size - 1 - start) / region.block_size;
+ for (u64 block = (gpu_address - start) / region.block_size; block <= last; ++block) {
+ if (region.written[block]) {
+ return false;
+ }
+ }
+ return true;
+}
+
/// GPU VA in the first page that mirrors a release at `address`, or 0.
inline u64 Nx2SemaphoreMirror(u64 address) {
for (const auto& region : g_nx2_semaphore_regions) {
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 27f0e58321..08a471cfd6 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -20,6 +20,7 @@
#include "common/settings.h"
#include "video_core/renderer_vulkan/vk_texture_cache.h"
+#include "video_core/nx2.h"
#include "video_core/gpu_logging/gpu_logging.h"
#include "video_core/engines/fermi_2d.h"
@@ -165,6 +166,16 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return bytes * info.resources.layers > (u64{1} << 30);
}
+// A large image over Switch 2 sparse blocks nobody has written (title 1's terrain layer pools, 2.9
+// GB of R8/RG16/R16 that stay empty in the attract demo) is all zeros: it gets no device memory
+// until a write makes the texture cache replace it.
+[[nodiscard]] bool IsUnwrittenSparse(const ImageInfo& info, GPUVAddr gpu_addr) {
+ constexpr u32 MinSize = 32U << 20;
+ const u32 size = VideoCommon::CalculateGuestSizeInBytes(info);
+ return size >= MinSize && info.type != ImageType::e1D && info.type != ImageType::Buffer &&
+ info.type != ImageType::Linear && Tegra::Nx2SparseUnwritten(gpu_addr, size);
+}
+
// 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)) {
@@ -310,7 +321,8 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
[[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator,
const ImageInfo& info, std::span<const VkFormat> view_formats,
- std::optional<VkFormat> format_override = {}) {
+ std::optional<VkFormat> format_override = {},
+ bool unbacked = false) {
if (info.type == ImageType::Buffer) {
return vk::Image{};
}
@@ -338,6 +350,11 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
image_ci.pNext = &image_format_list;
}
}
+ if (unbacked) {
+ if (vk::Image image = allocator.CreateUnbackedImage(image_ci)) {
+ return image;
+ }
+ }
return allocator.CreateImage(image_ci);
}
@@ -2019,10 +2036,19 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
: IsOpaqueBc1Astc(runtime_.device, info)
? std::span<const VkFormat>{OpaqueAstcViewFormats}
: runtime->ViewFormats(info.format),
- AstcHostFormat(runtime_.device, info))),
+ AstcHostFormat(runtime_.device, info),
+ 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)) {
+ flags |= VideoCommon::ImageFlagBits::UnwrittenSparse;
+ }
}
if (host_level_offset != 0) {
flags |= VideoCommon::ImageFlagBits::TrimmedLevels;
diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h
index 20d3883130..cfb839d17e 100644
--- a/src/video_core/texture_cache/image_base.h
+++ b/src/video_core/texture_cache/image_base.h
@@ -50,6 +50,9 @@ enum class ImageFlagBits : u32 {
/// in VRAM); uploads skip the rest, downloads are never done.
TrimmedLevels = 1 << 19,
ZeroFilled = 1 << 20, ///< Cleared to zero blocks before its first asynchronous decode.
+ /// 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,
};
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 d582f520f7..168df50c8b 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -28,6 +28,7 @@
#include "video_core/texture_cache/samples_helper.h"
#include "video_core/texture_cache/texture_cache_base.h"
#include "video_core/texture_cache/util.h"
+#include "video_core/nx2.h"
#include "video_core/nx2_profile.h"
#include "video_core/textures/decoders.h"
@@ -717,6 +718,11 @@ FramebufferId TextureCache<P>::GetFramebufferId(const RenderTargets& key) {
template <class P>
void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
ForEachImageInRegion(cpu_addr, size, [this, cpu_addr, size](ImageId image_id, Image& image) {
+ if (True(image.flags & ImageFlagBits::UnwrittenSparse)) {
+ // Written at last: a backed image replaces it where it is used next.
+ image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped;
+ return;
+ }
if (UploadsByLayer(image)) {
MarkLayersWritten(image, cpu_addr, size);
return;
@@ -2918,6 +2924,14 @@ void TextureCache<P>::PrepareImage(ImageId image_id, bool is_modification, bool
}
}
if (is_modification) {
+ 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.
+ LOG_WARNING(HW_GPU, "GPU write into unwritten sparse image {:#x}; replacing it",
+ image.gpu_addr);
+ Tegra::Nx2SparseMarkWritten(image.gpu_addr, image.guest_size_bytes);
+ image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped;
+ }
MarkModification(image);
}
lru_cache.Touch(image.lru_index, frame_tick);
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
index 8bf692b00e..92f537aa3c 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
@@ -248,21 +248,15 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat
}
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && ci.imageType != VK_IMAGE_TYPE_1D) {
// Too big to back at all, e.g. title 1's sparse virtual texture arrays (8192x8192,
- // 68 layers, ~23 GB): on the console only resident tiles have memory. Create a sparse
- // resident image with nothing bound, so views and copies still work; reads of it return
- // zero and writes are dropped, instead of aborting.
- VkImageCreateInfo sparse_ci = ci;
- sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT;
- VkImage sparse{};
- if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr,
- &sparse) == VK_SUCCESS) {
+ // 68 layers, ~23 GB): on the console only resident tiles have memory. Use an image with
+ // nothing bound, so views and copies still work, instead of aborting.
+ if (vk::Image sparse = CreateUnbackedImage(ci)) {
LOG_WARNING(Render_Vulkan,
"Image {}x{}x{} ({} levels, {} layers, format {}) does not fit; using an "
"unbacked sparse image",
ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels,
ci.arrayLayers, static_cast<u32>(ci.format));
- return vk::Image(sparse, ci.usage, *device.GetLogical(), allocator, VK_NULL_HANDLE,
- device.GetDispatchLoader());
+ return sparse;
}
}
if (result != VK_SUCCESS) {
@@ -286,6 +280,21 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat
device.GetDispatchLoader());
}
+vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) const {
+ if (ci.imageType == VK_IMAGE_TYPE_1D) {
+ return vk::Image{};
+ }
+ VkImageCreateInfo sparse_ci = ci;
+ sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT;
+ VkImage sparse{};
+ if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr,
+ &sparse) != VK_SUCCESS) {
+ return vk::Image{};
+ }
+ return vk::Image(sparse, ci.usage, *device.GetLogical(), allocator, VK_NULL_HANDLE,
+ device.GetDispatchLoader());
+}
+
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 fa7d4b41c3..99649b9aef 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.h
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h
@@ -107,6 +107,10 @@ namespace Vulkan {
/// images would otherwise leave holes in the shared blocks).
vk::Image CreateImage(const VkImageCreateInfo &ci, bool dedicated = false) const;
+ /// A sparse resident image with nothing bound (reads return zero, writes are dropped), or
+ /// a null image when the device can't make one.
+ vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const;
+
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const;
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage,