From 97ce27ab35153884d2fd05c278433928e429cd91 Mon Sep 17 00:00:00 2001
From: Takoroka <137927095+Takoroka@users.noreply.github.com>
Date: Fri, 30 Oct 2026 04:39:44 -0400
Subject: texture_cache: re-decode only the written layers of large ASTC arrays
Any write into an image marked all of it CPU-modified, so each terrain
tile Mario Kart World streams into its 8192x8192x68 ASTC arrays re-read
and re-decoded the whole 1.45 GB array: a 2 GiB staging buffer and a
full GPU wait each time, 15-16 GB of uploads per 600 frames.
Large layered images on the accelerated ASTC path (64 MiB and up) now
record the layers written since their last upload and stay tracked; a
refresh reads and decodes only those layers, one dispatch per layer and
level into a single-layer storage view. The texture cache's 600-frame
report counts uploads from guest memory, their size and how many went by
layer. Upload scratch buffers grown past 64 MiB for one huge image (341
MiB for the terrain layer pools) are freed at the next frame instead of
kept for the session.
Mario Kart World, 180 s: after loading, uploads drop from 15-16 GB to
0.2-0.5 GB per 600 frames, the largest from 1453 MiB to 2-15 MiB.
---
src/video_core/texture_cache/texture_cache.h | 108 ++++++++++++++++++++++++++-
1 file changed, 106 insertions(+), 2 deletions(-)
(limited to 'src/video_core/texture_cache/texture_cache.h')
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 62cd523073..d582f520f7 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -198,6 +198,13 @@ void TextureCache
::ApplyThresholdOverrides(bool log) {
template
void TextureCache::TickFrame() {
+ // Upload buffers grown for one huge image (title 1's 341 MiB terrain layer pools) are freed
+ // rather than kept for the rest of the session.
+ for (auto* const scratch : {&swizzle_data_buffer, &unswizzle_data_buffer}) {
+ if (scratch->capacity() > LargeScratchCapacity) {
+ scratch->release();
+ }
+ }
if constexpr (HAS_DEVICE_MEMORY_INFO) {
// Switch 2 titles (native tiling) stream far more texture data than Eden's thresholds
// were tuned for: title 1's attract loop alone keeps ~5.3 GB resident, so the collector
@@ -280,6 +287,9 @@ void TextureCache
::TickFrame() {
images_created, images_deleted, gc_deletes + gc_costly_deletes, gc_costly_deletes,
gc_download_deletes,
async_decodes_queued, astc_opaque_images, astc_alpha_images);
+ LOG_INFO(HW_GPU, "Texture cache last 600 frames: {} uploads from guest memory, {} MiB (largest {} MiB, {} by layer)",
+ images_uploaded, uploaded_bytes >> 20, largest_upload >> 20, layer_uploads);
+ images_uploaded = uploaded_bytes = largest_upload = layer_uploads = 0;
astc_opaque_images = astc_alpha_images = 0;
std::vector> shapes;
for (const auto& [key, value] : created_shapes) shapes.emplace_back(value.first, key);
@@ -706,7 +716,11 @@ FramebufferId TextureCache::GetFramebufferId(const RenderTargets& key) {
template
void TextureCache::WriteMemory(DAddr cpu_addr, size_t size) {
- ForEachImageInRegion(cpu_addr, size, [this](ImageId image_id, Image& image) {
+ ForEachImageInRegion(cpu_addr, size, [this, cpu_addr, size](ImageId image_id, Image& image) {
+ if (UploadsByLayer(image)) {
+ MarkLayersWritten(image, cpu_addr, size);
+ return;
+ }
if (True(image.flags & ImageFlagBits::CpuModified)) {
return;
}
@@ -812,6 +826,7 @@ void TextureCache
::UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t siz
UntrackImage(image, id);
}
}
+ image.dirty_layers.clear(); // all of it
if (True(image.flags & ImageFlagBits::Remapped)) {
continue;
}
@@ -1280,7 +1295,16 @@ void TextureCache
::RefreshContents(Image& image, ImageId image_id) {
image.flags &= ~ImageFlagBits::CpuModified;
- TrackImage(image, image_id);
+ // Images uploaded by layer stay tracked while some of their layers are dirty.
+ if (False(image.flags & ImageFlagBits::Tracked)) {
+ TrackImage(image, image_id);
+ }
+
+ if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) {
+ runtime.InsertUploadMemoryBarrier();
+ return;
+ }
+ image.dirty_layers.clear();
if (True(image.flags & ImageFlagBits::Unbacked)) {
// Nothing to upload into (and staging it could take tens of GB).
@@ -1311,11 +1335,91 @@ void TextureCache
::RefreshContents(Image& image, ImageId image_id) {
QueueAsyncUnswizzle(image, image_id);
return;
}
+ ++images_uploaded;
+ uploaded_bytes += image.guest_size_bytes;
+ largest_upload = (std::max)(largest_upload, image.guest_size_bytes);
auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image));
UploadImageContents(image, staging);
runtime.InsertUploadMemoryBarrier();
}
+template
+bool TextureCache::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; }) {
+ return True(image.flags & ImageFlagBits::AcceleratedUpload) &&
+ image.info.type == ImageType::e2D && image.info.resources.layers > 1 &&
+ image.guest_size_bytes >= LayerUploadMinSize &&
+ False(image.flags & (ImageFlagBits::Unbacked | ImageFlagBits::Sparse |
+ ImageFlagBits::Remapped));
+ } else {
+ return false;
+ }
+}
+
+template
+void TextureCache::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size) {
+ if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_layers.empty()) {
+ return; // all of it is dirty already
+ }
+ const u32 layers = static_cast(image.info.resources.layers);
+ if (False(image.flags & ImageFlagBits::CpuModified)) {
+ image.flags |= ImageFlagBits::CpuModified;
+ image.dirty_layers.assign(Common::DivCeil(layers, 64U), 0);
+ }
+ const DAddr begin = (std::max)(cpu_addr, image.cpu_addr) - image.cpu_addr;
+ const DAddr end = (std::min)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes) -
+ image.cpu_addr;
+ const u32 stride = image.info.layer_stride;
+ if (end <= begin || stride == 0) {
+ return;
+ }
+ const u32 last = (std::min)(static_cast((end - 1) / stride), layers - 1);
+ for (u32 layer = static_cast(begin / stride); layer <= last; ++layer) {
+ image.dirty_layers[layer / 64] |= u64{1} << (layer % 64);
+ }
+}
+
+template
+bool TextureCache::UploadWrittenLayers(Image& image) {
+ if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) {
+ const u32 num_layers = static_cast(image.info.resources.layers);
+ std::vector layers;
+ for (u32 layer = 0; layer < num_layers; ++layer) {
+ if ((image.dirty_layers[layer / 64] >> (layer % 64)) & 1) {
+ layers.push_back(layer);
+ }
+ }
+ image.dirty_layers.clear();
+ if (layers.empty() || layers.size() == num_layers) {
+ return false;
+ }
+ // 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;
+ const size_t tail = image.info.layer_stride - level_offset;
+ auto staging = runtime.UploadStagingBuffer(tail * layers.size());
+ for (size_t i = 0; i < layers.size(); ++i) {
+ gpu_memory->ReadBlock(image.gpu_addr + u64{layers[i]} * image.info.layer_stride +
+ level_offset,
+ staging.mapped_span.data() + i * tail, tail,
+ VideoCommon::CacheType::NoTextureCache);
+ }
+ uploads.erase(uploads.begin(), uploads.begin() + image.host_level_offset);
+ for (auto& upload : uploads) {
+ upload.buffer_offset -= level_offset;
+ }
+ runtime.AccelerateImageUploadLayers(image, staging, uploads, layers);
+ ++images_uploaded;
+ uploaded_bytes += tail * layers.size();
+ ++layer_uploads;
+ return true;
+ } else {
+ return false;
+ }
+}
+
template
template
void TextureCache::UploadImageContents(Image& image, StagingBuffer& staging) {
--
cgit v1.2.3
From 940d44ca25628288f1b5f64c25c022b3a4ae5ce2 Mon Sep 17 00:00:00 2001
From: Takoroka <137927095+Takoroka@users.noreply.github.com>
Date: Fri, 30 Oct 2026 05:16:34 -0400
Subject: texture_cache: give images over unwritten sparse blocks no device
memory
Mario Kart World creates nine terrain layer pools in sparse regions (four
R8 16384x16384, four RG16 8192x8192, one R16 8192x8192: 2.9 GB of device
memory). In 180 s of title and attract demo not one of their blocks is
written and nothing renders into them, so they hold zeros; on the console
only the few tiles the game maps have memory.
nx2:gpu now publishes which 4 MiB blocks of each sparse region have been
written (nx2.h, a leaf lock the texture cache may take). An image of 32
MiB or more lying in one sparse region with no written block is created
as a sparse resident image with nothing bound: reads return zero, as an
unmapped tile does. The first write into its range, or the first time the
GPU renders into it (that range then counts as written), marks it
remapped, so a backed image replaces it at its next use.
Mario Kart World, 180 s: the collector deletes 0-80 images per 600 frames
instead of thousands, frames over 100 ms per 600 presents drop from 1-9 to
0-1, peak working set 14.7 GB. One render target in a small sparse region
gets replaced once (logged).
---
src/core/hle/service/nx2_gsv.h | 3 +
src/video_core/nx2.h | 76 ++++++++++++++++++++++
.../renderer_vulkan/vk_texture_cache.cpp | 30 ++++++++-
src/video_core/texture_cache/image_base.h | 3 +
src/video_core/texture_cache/texture_cache.h | 14 ++++
.../vulkan_common/vulkan_memory_allocator.cpp | 29 ++++++---
.../vulkan_common/vulkan_memory_allocator.h | 4 ++
7 files changed, 147 insertions(+), 12 deletions(-)
(limited to 'src/video_core/texture_cache/texture_cache.h')
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(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
#include
#include
+#include