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