aboutsummaryrefslogtreecommitdiff
path: root/src/video_core
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 00:36:38 -0400
committerkraibse <33869366+kraibse@users.noreply.github.com>2026-11-01 17:29:56 +0100
commit1c794af56407f324eedad99478388ba0c9cf9ad6 (patch)
tree09c4f7e4e892a902c9f2613b63de8668986f5d66 /src/video_core
parenta855e8373f5575fddd172ec7ffc56a8c3904c3fb (diff)
texture_cache: back large sparse 2D images by sparse block
Mario Kart World's nine world-sized masks (R8 16384x16384, RG16 and R16 8192x8192, 170-341 MB each) live in sparse regions the game maps its own memory into, a few MiB of each. Since a mapped tile counts as written, the texture cache backed and uploaded each mask in full: about 2.9 GB of device memory, and a whole-mask upload for every tile the game streamed in. Such images (single layer, one texel per block, colour, in a sparse region, 32 MiB or more) are now unbacked SparseTiles images. The texture cache records the guest ranges written to them; Image::BindTiles gives the host sparse blocks under those ranges memory, and only those rectangles are unswizzled, a row of whole GOB blocks at a time, and uploaded. A new image starts with the tiles the game maps there (nx2:gpu 216 now records them per 64 KiB), or the written blocks where it maps none. Unmapped tiles keep their blocks and are loaded as zeros. Checked against the full unswizzle on 400 ordinary game images (row padding, mip chains, odd sizes): no row differs. Not on AMD drivers, which cannot sample images with unbound memory. Mario Kart World title screen, 150 s, against backing the masks whole: dedicated VRAM 6.6 -> 6.2 GB, shared 1.0 -> 0.3 GB, peak 8.2 -> 6.9 GB.
Diffstat (limited to 'src/video_core')
-rw-r--r--src/video_core/nx2.h60
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp210
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h30
-rw-r--r--src/video_core/texture_cache/image_base.h7
-rw-r--r--src/video_core/texture_cache/texture_cache.h65
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h9
-rw-r--r--src/video_core/texture_cache/types.h9
-rw-r--r--src/video_core/texture_cache/util.cpp204
-rw-r--r--src/video_core/texture_cache/util.h21
9 files changed, 615 insertions, 0 deletions
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index 824dc76044..318607384b 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -9,6 +9,7 @@
#include <functional>
#include <map>
#include <mutex>
+#include <utility>
#include <vector>
#include "common/common_types.h"
@@ -101,7 +102,13 @@ struct Nx2SparseRegionState {
u64 size;
u64 block_size;
std::vector<bool> written;
+ /// Once the game maps its own memory into the region (nx2:gpu 216): which tiles it maps.
+ /// Only those hold data then (writes elsewhere are dropped, as on the console).
+ std::vector<bool> mapped;
};
+
+/// The granularity of the game's sparse tiles (nx2:gpu 216 maps 64 KiB pages).
+inline constexpr u64 Nx2SparseTileSize = 64ULL << 10;
inline std::mutex g_nx2_sparse_state_mutex;
inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state;
@@ -129,6 +136,59 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) {
}
}
+/// The game maps its own memory over [offset, +size) of the region at region_address (mapped), or
+/// unmaps it there.
+inline void Nx2SparseTilesMapped(u64 region_address, u64 offset, u64 size, bool mapped) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ const auto it = g_nx2_sparse_state.find(region_address);
+ if (it == g_nx2_sparse_state.end() || size == 0) {
+ return;
+ }
+ auto& region = it->second;
+ if (region.mapped.empty()) {
+ region.mapped.resize((region.size + Nx2SparseTileSize - 1) / Nx2SparseTileSize);
+ }
+ const u64 last = (std::min)((offset + size - 1) / Nx2SparseTileSize, u64{region.mapped.size() - 1});
+ for (u64 tile = offset / Nx2SparseTileSize; tile <= last; ++tile) {
+ region.mapped[tile] = mapped;
+ }
+}
+
+/// The parts of [gpu_address, +size), which lies in one sparse region, that can hold data: the
+/// tiles the game maps there if it maps any, otherwise the blocks written. As (offset from
+/// gpu_address, size) runs.
+inline std::vector<std::pair<u64, u64>> Nx2SparseDataRanges(u64 gpu_address, u64 size) {
+ std::vector<std::pair<u64, u64>> ranges;
+ 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 ranges;
+ }
+ --it;
+ const auto& [start, region] = *it;
+ if (size == 0 || gpu_address + size > start + region.size) {
+ return ranges;
+ }
+ const bool by_tile = !region.mapped.empty();
+ const u64 unit = by_tile ? Nx2SparseTileSize : region.block_size;
+ const std::vector<bool>& present = by_tile ? region.mapped : region.written;
+ const u64 begin = gpu_address - start;
+ const u64 end = begin + size;
+ for (u64 index = begin / unit; index * unit < end && index < present.size(); ++index) {
+ if (!present[index]) {
+ continue;
+ }
+ const u64 run_begin = (std::max)(index * unit, begin) - begin;
+ const u64 run_end = (std::min)((index + 1) * unit, end) - begin;
+ if (!ranges.empty() && ranges.back().first + ranges.back().second == run_begin) {
+ ranges.back().second += run_end - run_begin;
+ } else {
+ ranges.emplace_back(run_begin, run_end - run_begin);
+ }
+ }
+ return ranges;
+}
+
/// 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) ||
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 3294af9c22..947bbebe88 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -201,6 +201,23 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size);
}
+// A large single 2D image in a Switch 2 sparse region (title 1's world-sized R8/RG16/R16 masks,
+// 170-341 MB, of which the game maps a few MiB) gets memory by sparse block where the guest memory
+// under it can hold data: unbacked at first, then Image::BindTiles. Formats of one texel per
+// block that the host samples as they are.
+[[nodiscard]] bool UsesSparseTiles(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) {
+ if (!device.CanUseUnbackedSparseImages() || !device.IsSparseResidencyImage2DSupported() ||
+ info.type != ImageType::e2D || info.resources.layers != 1 || info.num_samples != 1 ||
+ info.size.depth != 1 || info.block.width != 0 ||
+ VideoCore::Surface::DefaultBlockWidth(info.format) != 1 ||
+ VideoCore::Surface::DefaultBlockHeight(info.format) != 1 ||
+ VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) {
+ return false;
+ }
+ const u32 size = VideoCommon::CalculateGuestSizeInBytes(info);
+ return 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) {
@@ -2093,6 +2110,7 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
: runtime->ViewFormats(info.format),
AstcHostFormat(runtime_.device, info),
UsesSparseLayers(runtime_.device, info, gpu_addr_) ||
+ UsesSparseTiles(runtime_.device, info, gpu_addr_) ||
IsUnwrittenSparse(runtime_.device, info, gpu_addr_))),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
@@ -2102,6 +2120,8 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
host_info.size.depth * static_cast<u64>(host_info.resources.layers);
if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) {
flags |= VideoCommon::ImageFlagBits::SparseLayers;
+ } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) {
+ flags |= VideoCommon::ImageFlagBits::SparseTiles;
} 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.
@@ -2633,6 +2653,196 @@ bool Image::InitSparseLayers() {
return true;
}
+bool Image::InitSparseTiles() {
+ 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.requirements = device.GetLogical().GetImageMemoryRequirements(handle);
+ const VkExtent3D granularity = it->formatProperties.imageGranularity;
+ const u32 bytes = VideoCore::Surface::BytesPerBlock(info.format);
+ // Rectangles are uploaded by whole GOB columns (64 bytes across).
+ if (granularity.width == 0 || granularity.height == 0 || (granularity.width * bytes) % 64 != 0 ||
+ u64{granularity.width} * granularity.height * bytes != s.requirements.alignment) {
+ return false;
+ }
+ s.allocator = &runtime->memory_allocator;
+ s.sparse = *it;
+ s.width = info.size.width;
+ s.height = info.size.height;
+ s.levels = static_cast<u32>(info.resources.levels);
+ s.tile_width = granularity.width;
+ s.tile_height = granularity.height;
+ s.tail_level = (std::min)(it->imageMipTailFirstLod, s.levels);
+ s.level_tiles.resize(s.tail_level);
+ for (u32 level = 0; level < s.tail_level; ++level) {
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ s.level_tiles[level].assign(
+ size_t{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height),
+ VK_NULL_HANDLE);
+ }
+ LOG_INFO(Render_Vulkan, "Sparse tiled image {:#x}: {}x{} {} levels, block {}x{}, mip tail from {}",
+ gpu_addr, s.width, s.height, s.levels, s.tile_width, s.tile_height, s.tail_level);
+ return true;
+}
+
+bool Image::BindTiles(std::span<const VideoCommon::LevelRect> rects) {
+ SparseLayerMemory& s = sparse_layers;
+ if (s.allocator == nullptr) {
+ return false;
+ }
+ const Device& device = runtime->device;
+ const u64 tile = s.requirements.alignment;
+ const VkMemoryRequirements tile_requirements{
+ .size = tile,
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ std::vector<VkSparseImageMemoryBind> image_binds;
+ std::vector<VkSparseMemoryBind> opaque_binds;
+ std::vector<VmaAllocation*> fresh; // Blocks given memory by this call
+ bool fresh_tail = false;
+ // A failed bind leaves the blocks as they were, so the next call tries them again.
+ const auto roll_back = [&] {
+ for (VmaAllocation* const allocation : fresh) {
+ s.allocator->FreeSparseMemory(*allocation);
+ *allocation = VK_NULL_HANDLE;
+ s.bound_bytes -= tile;
+ }
+ if (fresh_tail) {
+ s.allocator->FreeSparseMemory(s.tail_memory);
+ s.tail_memory = VK_NULL_HANDLE;
+ s.bound_bytes -= Common::AlignUp(s.sparse.imageMipTailSize, tile);
+ }
+ };
+ bool needs_tail = false;
+ for (const VideoCommon::LevelRect& rect : rects) {
+ const u32 level = static_cast<u32>(rect.level);
+ if (level >= s.tail_level) {
+ needs_tail = true;
+ continue;
+ }
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ const u32 tiles_x = Common::DivCeil(width, s.tile_width);
+ for (u32 ty = rect.y / s.tile_height; ty <= (rect.y + rect.height - 1) / s.tile_height;
+ ++ty) {
+ for (u32 tx = rect.x / s.tile_width; tx <= (rect.x + rect.width - 1) / s.tile_width;
+ ++tx) {
+ VmaAllocation& allocation = s.level_tiles[level][size_t{ty} * tiles_x + tx];
+ if (allocation != VK_NULL_HANDLE) {
+ continue;
+ }
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ allocation = s.allocator->AllocateSparseMemory(tile_requirements, memory, offset);
+ if (allocation == VK_NULL_HANDLE) {
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory",
+ gpu_addr);
+ roll_back();
+ return false;
+ }
+ fresh.push_back(&allocation);
+ s.bound_bytes += tile;
+ const u32 x = tx * s.tile_width;
+ const u32 y = ty * s.tile_height;
+ image_binds.push_back(VkSparseImageMemoryBind{
+ .subresource{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = level,
+ .arrayLayer = 0,
+ },
+ .offset{static_cast<s32>(x), static_cast<s32>(y), 0},
+ .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y),
+ 1},
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ }
+ }
+ if (needs_tail && s.tail_level < s.levels && s.tail_memory == VK_NULL_HANDLE) {
+ 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) {
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory for the mip "
+ "tail",
+ gpu_addr);
+ roll_back();
+ return false;
+ }
+ fresh_tail = true;
+ s.bound_bytes += requirements.size;
+ 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 true;
+ }
+ 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 blocks is submitted (as BindLayers).
+ 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 tiled image {:#x}: binding failed ({})", gpu_addr,
+ static_cast<int>(result));
+ roll_back();
+ return false;
+ }
+ fence.Wait();
+ return true;
+}
+
std::vector<u32> Image::BindLayers(std::span<const u32> layers) {
SparseLayerMemory& s = sparse_layers;
std::vector<u32> bound;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 3944e183fd..89eb8424eb 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -389,6 +389,21 @@ public:
/// layers that have memory afterwards (a cap per image can leave some out).
std::vector<u32> BindLayers(std::span<const u32> layers);
+ /// SparseTiles images: gives the host sparse blocks under `rects` (and the mip tail, for
+ /// levels in it) device memory where they have none yet. False if that failed.
+ bool BindTiles(std::span<const VideoCommon::LevelRect> rects);
+
+ /// SparseTiles images: the host sparse block, in texels, and the first level of the mip tail.
+ u32 SparseTileWidth() const noexcept {
+ return sparse_layers.tile_width;
+ }
+ u32 SparseTileHeight() const noexcept {
+ return sparse_layers.tile_height;
+ }
+ s32 SparseTailLevel() const noexcept {
+ return static_cast<s32>(sparse_layers.tail_level);
+ }
+
/// Device memory BindLayers has bound to the image's layers (an unbacked image has no other).
u64 SparseBoundBytes() const noexcept {
return sparse_layers.bound_bytes;
@@ -434,6 +449,9 @@ private:
layer_bytes = other.layer_bytes;
layer_memory = std::move(other.layer_memory);
other.layer_memory.clear();
+ level_tiles = std::move(other.level_tiles);
+ other.level_tiles.clear();
+ tail_level = other.tail_level;
tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE);
bound_bytes = other.bound_bytes;
warned = other.warned;
@@ -449,8 +467,14 @@ private:
for (const VmaAllocation allocation : layer_memory) {
allocator->FreeSparseMemory(allocation);
}
+ for (const auto& tiles : level_tiles) {
+ for (const VmaAllocation allocation : tiles) {
+ allocator->FreeSparseMemory(allocation);
+ }
+ }
allocator->FreeSparseMemory(tail_memory);
layer_memory.clear();
+ level_tiles.clear();
tail_memory = VK_NULL_HANDLE;
}
@@ -464,6 +488,9 @@ private:
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
+ /// SparseTiles: per level before the mip tail, per sparse block (rows), null until bound
+ std::vector<std::vector<VmaAllocation>> level_tiles;
+ u32 tail_level{}; ///< SparseTiles: first level of the mip tail
VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one
VkDeviceSize bound_bytes{};
bool warned{};
@@ -473,6 +500,9 @@ private:
/// Reads the image's sparse requirements; false when it can't be bound by layer.
bool InitSparseLayers();
+ /// Reads the image's sparse requirements; false when it can't be bound by sparse block.
+ bool InitSparseTiles();
+
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 04254fb417..878c229953 100644
--- a/src/video_core/texture_cache/image_base.h
+++ b/src/video_core/texture_cache/image_base.h
@@ -56,6 +56,10 @@ enum class ImageFlagBits : u32 {
/// 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,
+ /// A large single image over Switch 2 sparse blocks: unbacked, each host sparse block gets
+ /// device memory when the guest memory under it can hold data (Image::BindTiles), and only
+ /// the parts written are uploaded.
+ SparseTiles = 1 << 23,
};
DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits)
@@ -108,6 +112,9 @@ struct ImageBase {
/// Large layered images uploaded by layer: one bit per layer written since the last upload
/// while CpuModified. Empty while CpuModified means the whole image.
std::vector<u64> dirty_layers;
+ /// SparseTiles images: the guest byte ranges (offset, size) written since the last upload while
+ /// CpuModified. Empty while CpuModified means all of it that can hold data.
+ std::vector<std::pair<u64, u64>> dirty_ranges;
u64 scale_tick = 0;
bool has_scaled = false;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 3b783fc6c6..4b93cd135b 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -729,6 +729,10 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped;
return;
}
+ if (True(image.flags & ImageFlagBits::SparseTiles)) {
+ MarkRangeWritten(image, cpu_addr, size);
+ return;
+ }
if (UploadsByLayer(image)) {
MarkLayersWritten(image, cpu_addr, size);
return;
@@ -1316,6 +1320,10 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
RefreshSparseLayers(image);
return;
}
+ if (True(image.flags & ImageFlagBits::SparseTiles)) {
+ RefreshSparseTiles(image);
+ return;
+ }
if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) {
runtime.InsertUploadMemoryBarrier();
return;
@@ -1401,6 +1409,63 @@ void TextureCache<P>::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t siz
}
template <class P>
+void TextureCache<P>::MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size) {
+ if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_ranges.empty()) {
+ return; // all of it is dirty already
+ }
+ const DAddr begin = (std::max<DAddr>)(cpu_addr, image.cpu_addr);
+ const DAddr end = (std::min<DAddr>)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes);
+ if (end <= begin) {
+ return;
+ }
+ image.flags |= ImageFlagBits::CpuModified;
+ image.dirty_ranges.emplace_back(begin - image.cpu_addr, end - begin);
+}
+
+template <class P>
+void TextureCache<P>::RefreshSparseTiles(Image& image) {
+ if constexpr (requires { &Image::BindTiles; }) {
+ // The ranges written since the last upload, or, for a new image, all of it that can hold
+ // data (the tiles the game maps there, or the blocks written).
+ std::vector<std::pair<u64, u64>> ranges = std::move(image.dirty_ranges);
+ image.dirty_ranges.clear();
+ if (ranges.empty()) {
+ ranges = Tegra::Nx2SparseDataRanges(image.gpu_addr, image.guest_size_bytes);
+ }
+ const std::vector<LevelRect> rects =
+ SparseTileRects(image.info, ranges, image.SparseTileWidth(), image.SparseTileHeight(),
+ image.SparseTailLevel());
+ if (rects.empty() || !BindSparseTiles(image, rects)) {
+ runtime.TransitionImageLayout(image);
+ return;
+ }
+ const size_t bytes = UnswizzledRectsBytes(image.info, rects);
+ auto staging = runtime.UploadStagingBuffer(bytes);
+ const std::vector<BufferImageCopy> copies =
+ UnswizzleImageRects(*gpu_memory, image.gpu_addr, image.info, rects, staging.mapped_span);
+ image.UploadMemory(staging, copies);
+ runtime.InsertUploadMemoryBarrier();
+ ++images_uploaded;
+ uploaded_bytes += bytes;
+ }
+}
+
+template <class P>
+bool TextureCache<P>::BindSparseTiles(Image& image, std::span<const LevelRect> rects) {
+ if constexpr (requires { &Image::BindTiles; }) {
+ const u64 bound_bytes = ImageMemoryBytes(image);
+ const bool bound = image.BindTiles(rects);
+ // RegisterImage counts what an image has bound by then; later binds are added here.
+ if (True(image.flags & ImageFlagBits::Registered)) {
+ total_used_memory += ImageMemoryBytes(image) - bound_bytes;
+ }
+ return bound;
+ } else {
+ return false;
+ }
+}
+
+template <class P>
bool TextureCache<P>::UploadWrittenLayers(Image& image) {
if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) {
const u32 num_layers = static_cast<u32>(image.info.resources.layers);
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index 94102cd261..f27e237cfa 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -164,6 +164,15 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
/// Records the layers a write of [cpu_addr, +size) touches.
void MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size);
+ /// SparseTiles images: notes a written range, uploaded with the next refresh.
+ void MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size);
+
+ /// SparseTiles images: binds and uploads the parts written since the last upload.
+ void RefreshSparseTiles(Image& image);
+
+ /// Image::BindTiles, counting what it binds in total_used_memory.
+ bool BindSparseTiles(Image& image, std::span<const LevelRect> rects);
+
/// Uploads the image's dirty layers. False when it needs a full upload instead.
bool UploadWrittenLayers(Image& image);
diff --git a/src/video_core/texture_cache/types.h b/src/video_core/texture_cache/types.h
index ecacd8c6bd..9e541d6afc 100644
--- a/src/video_core/texture_cache/types.h
+++ b/src/video_core/texture_cache/types.h
@@ -132,6 +132,15 @@ struct ImageCopy {
Extent3D extent;
};
+/// A rectangle of one mip level, in texels.
+struct LevelRect {
+ s32 level;
+ u32 x;
+ u32 y;
+ u32 width;
+ u32 height;
+};
+
struct BufferImageCopy {
size_t buffer_offset;
size_t buffer_size;
diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp
index ef34019a08..0e1acba813 100644
--- a/src/video_core/texture_cache/util.cpp
+++ b/src/video_core/texture_cache/util.cpp
@@ -929,6 +929,210 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory
return copies;
}
+namespace {
+// One level of a single-layer 2D block-linear image of 1x1-texel blocks, laid out as
+// UnswizzleImage reads it: rows of blocks, each block one GOB wide and 2^block_height GOBs tall.
+struct BlockLinearLevel {
+ u32 offset; ///< Into the image
+ u32 width; ///< Texels
+ u32 height;
+ u32 block_height; ///< Log2 of the GOBs a block is tall
+ u32 block_texels; ///< Texels across a block
+ u32 block_rows; ///< Texel rows in a block
+ u32 block_bytes;
+ u32 blocks_per_row;
+};
+
+std::vector<BlockLinearLevel> BlockLinearLevels(const ImageInfo& info) {
+ const LevelInfo level_info = MakeLevelInfo(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ const Extent2D gob = GobSize(bpp_log2, info.block.height, info.tile_width_spacing);
+ std::vector<BlockLinearLevel> levels;
+ u32 offset = 0;
+ for (s32 level = 0; level < info.resources.levels; ++level) {
+ const Extent3D level_size = AdjustMipSize(info.size, level);
+ const Extent3D num_tiles = AdjustTileSize(level_size, level_info.tile_size);
+ const Extent3D block =
+ AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
+ const u32 stride_alignment = StrideAlignment(num_tiles, info.block, gob, bpp_log2);
+ const u32 stride = Common::AlignUpLog2(num_tiles.width, stride_alignment) << bpp_log2;
+ levels.push_back({
+ .offset = offset,
+ .width = level_size.width,
+ .height = level_size.height,
+ .block_height = block.height,
+ .block_texels = GOB_SIZE_X >> bpp_log2,
+ .block_rows = GOB_SIZE_Y << block.height,
+ .block_bytes = GOB_SIZE << block.height,
+ .blocks_per_row = Common::DivCeilLog2(stride, GOB_SIZE_X_SHIFT),
+ });
+ offset += CalculateLevelSize(level_info, level);
+ }
+ return levels;
+}
+
+// Rows of blocks a rectangle covers, and its blocks across.
+struct RectBlocks {
+ u32 first_row;
+ u32 last_row;
+ u32 first_block;
+ u32 num_blocks;
+};
+
+RectBlocks BlocksOf(const BlockLinearLevel& level, const LevelRect& rect) {
+ const u32 first_block = rect.x / level.block_texels;
+ return {
+ .first_row = rect.y / level.block_rows,
+ .last_row = (rect.y + rect.height - 1) / level.block_rows,
+ .first_block = first_block,
+ .num_blocks = (std::min)(Common::DivCeil(rect.width, level.block_texels),
+ level.blocks_per_row - first_block),
+ };
+}
+
+u32 BlockRowTexelRows(const BlockLinearLevel& level, u32 row) {
+ return (std::min)(level.block_rows, level.height - row * level.block_rows);
+}
+} // Anonymous namespace
+
+std::vector<LevelRect> SparseTileRects(const ImageInfo& info,
+ std::span<const std::pair<u64, u64>> ranges, u32 tile_width,
+ u32 tile_height, s32 tail_level) {
+ std::vector<LevelRect> rects;
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u64 image_end = CalculateLevelBytes(
+ CalculateLevelSizes(MakeLevelInfo(info), info.resources.levels), info.resources.levels);
+ for (s32 index = 0; index < static_cast<s32>(levels.size()); ++index) {
+ const BlockLinearLevel& level = levels[index];
+ const u64 level_end =
+ index + 1 < static_cast<s32>(levels.size()) ? levels[index + 1].offset : image_end;
+ const auto overlaps = [&](const std::pair<u64, u64>& range) {
+ return range.first < level_end && range.first + range.second > level.offset;
+ };
+ if (index >= tail_level) {
+ if (std::ranges::any_of(ranges, overlaps)) {
+ rects.push_back({index, 0, 0, level.width, level.height});
+ }
+ continue;
+ }
+ const u32 tiles_x = Common::DivCeil(level.width, tile_width);
+ const u32 tiles_y = Common::DivCeil(level.height, tile_height);
+ std::vector<bool> touched(size_t{tiles_x} * tiles_y);
+ for (const auto& range : ranges) {
+ if (!overlaps(range)) {
+ continue;
+ }
+ const u64 begin = (std::max<u64>)(range.first, level.offset) - level.offset;
+ const u64 end = (std::min<u64>)(range.first + range.second, level_end) - level.offset;
+ const u64 first = begin / level.block_bytes;
+ const u64 last = (end - 1) / level.block_bytes;
+ for (u64 row = first / level.blocks_per_row; row <= last / level.blocks_per_row; ++row) {
+ const u64 row_start = row * level.blocks_per_row;
+ const u64 low = (std::max)(first, row_start) - row_start;
+ const u64 high = (std::min)(last, row_start + level.blocks_per_row - 1) - row_start;
+ const u64 x0 = low * level.block_texels;
+ const u64 y0 = row * level.block_rows;
+ if (x0 >= level.width || y0 >= level.height) {
+ continue;
+ }
+ const u64 x1 = (std::min<u64>)((high + 1) * level.block_texels, level.width);
+ const u64 y1 = (std::min<u64>)(y0 + level.block_rows, level.height);
+ for (u64 ty = y0 / tile_height; ty <= (y1 - 1) / tile_height; ++ty) {
+ for (u64 tx = x0 / tile_width; tx <= (x1 - 1) / tile_width; ++tx) {
+ touched[ty * tiles_x + tx] = true;
+ }
+ }
+ }
+ }
+ for (u32 ty = 0; ty < tiles_y; ++ty) {
+ for (u32 tx = 0; tx < tiles_x;) {
+ if (!touched[size_t{ty} * tiles_x + tx]) {
+ ++tx;
+ continue;
+ }
+ const u32 first_tile = tx;
+ while (tx < tiles_x && touched[size_t{ty} * tiles_x + tx]) {
+ ++tx;
+ }
+ const u32 x = first_tile * tile_width;
+ const u32 y = ty * tile_height;
+ rects.push_back({
+ .level = index,
+ .x = x,
+ .y = y,
+ .width = (std::min)(tx * tile_width, level.width) - x,
+ .height = (std::min)(y + tile_height, level.height) - y,
+ });
+ }
+ }
+ }
+ return rects;
+}
+
+size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects) {
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ size_t bytes = 0;
+ for (const LevelRect& rect : rects) {
+ const BlockLinearLevel& level = levels[rect.level];
+ const RectBlocks blocks = BlocksOf(level, rect);
+ for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) {
+ bytes += (size_t{blocks.num_blocks * level.block_texels} << bpp_log2) *
+ BlockRowTexelRows(level, row);
+ }
+ }
+ return bytes;
+}
+
+std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory,
+ GPUVAddr gpu_addr, const ImageInfo& info,
+ std::span<const LevelRect> rects,
+ std::span<u8> output) {
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ std::vector<BufferImageCopy> copies;
+ std::vector<u8> swizzled;
+ size_t output_offset = 0;
+ for (const LevelRect& rect : rects) {
+ const BlockLinearLevel& level = levels[rect.level];
+ const RectBlocks blocks = BlocksOf(level, rect);
+ const u32 width = blocks.num_blocks * level.block_texels;
+ const size_t row_bytes = size_t{blocks.num_blocks} * level.block_bytes;
+ for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) {
+ // A row of whole blocks is laid out like an image of that width on its own.
+ const u32 texel_rows = BlockRowTexelRows(level, row);
+ const size_t bytes = (size_t{width} << bpp_log2) * texel_rows;
+ const GPUVAddr row_addr = gpu_addr + level.offset +
+ u64{row} * level.blocks_per_row * level.block_bytes +
+ u64{blocks.first_block} * level.block_bytes;
+ swizzled.assign(row_bytes, 0);
+ gpu_memory.ReadBlockUnsafe(row_addr, swizzled.data(), row_bytes);
+ UnswizzleTexture(output.subspan(output_offset, bytes), swizzled, 1U << bpp_log2, width,
+ texel_rows, 1, level.block_height, 0, 0,
+ gpu_memory.UsesNativeTiling());
+ const u32 row_y = row * level.block_rows;
+ const u32 y0 = (std::max)(rect.y, row_y);
+ const u32 y1 = (std::min)(rect.y + rect.height, row_y + texel_rows);
+ copies.push_back(BufferImageCopy{
+ .buffer_offset = output_offset + ((size_t{y0 - row_y} * width) << bpp_log2),
+ .buffer_size = bytes,
+ .buffer_row_length = width,
+ .buffer_image_height = texel_rows,
+ .image_subresource =
+ {
+ .base_level = rect.level,
+ .base_layer = 0,
+ .num_layers = 1,
+ },
+ .image_offset = {static_cast<s32>(rect.x), static_cast<s32>(y0), 0},
+ .image_extent = {rect.width, y1 - y0, 1},
+ });
+ output_offset += bytes;
+ }
+ }
+ return copies;
+}
+
void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies) {
// Same offsets and sizes as ConvertImage below.
const auto recompression = Settings::GetAstcRecompression();
diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h
index 0381d2d24b..74da0f90f9 100644
--- a/src/video_core/texture_cache/util.h
+++ b/src/video_core/texture_cache/util.h
@@ -8,6 +8,8 @@
#include <optional>
#include <span>
+#include <utility>
+#include <vector>
#include <boost/container/small_vector.hpp>
#include "common/common_types.h"
@@ -72,6 +74,25 @@ struct OverlapResult {
Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info,
std::span<const u8> input, std::span<u8> output);
+/// For a single-layer 2D block-linear image of 1x1-texel blocks: the host sparse blocks
+/// (tile_width x tile_height texels) that the guest byte ranges (offset, size) touch, merged into
+/// runs along x. Levels from tail_level on (the host's mip tail) come whole.
+[[nodiscard]] std::vector<LevelRect> SparseTileRects(const ImageInfo& info,
+ std::span<const std::pair<u64, u64>> ranges,
+ u32 tile_width, u32 tile_height,
+ s32 tail_level);
+
+/// Bytes UnswizzleImageRects writes for `rects`.
+[[nodiscard]] size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects);
+
+/// Unswizzles `rects` (from SparseTileRects: on GOB columns) of a single-layer 2D block-linear
+/// image into output, reading only the guest blocks under them.
+[[nodiscard]] std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory,
+ GPUVAddr gpu_addr,
+ const ImageInfo& info,
+ std::span<const LevelRect> rects,
+ std::span<u8> output);
+
/// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting.
void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies);