diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-30 04:39:44 -0400 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-10-30 17:27:04 +0100 |
| commit | 97ce27ab35153884d2fd05c278433928e429cd91 (patch) | |
| tree | dd5a6cbd9274931e44860182e91a78cfe1c2e6f7 /src/video_core/texture_cache | |
| parent | 6380b6687ded27892404c576becd78b7161ba68a (diff) | |
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.
Diffstat (limited to 'src/video_core/texture_cache')
| -rw-r--r-- | src/video_core/texture_cache/image_base.h | 3 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache.h | 108 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache_base.h | 16 |
3 files changed, 125 insertions, 2 deletions
diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 47bab9617a..20d3883130 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -99,6 +99,9 @@ struct ImageBase { u32 converted_size_bytes = 0; u32 scale_rating = 0; u32 host_level_offset = 0; ///< Guest mip level held by host mip 0 (see TrimmedLevels). + /// 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; 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 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<P>::ApplyThresholdOverrides(bool log) { template <class P> void TextureCache<P>::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<P>::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<std::pair<u32, u64>> shapes; for (const auto& [key, value] : created_shapes) shapes.emplace_back(value.first, key); @@ -706,7 +716,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](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<P>::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<P>::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,12 +1335,92 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) { QueueAsyncUnswizzle(image, image_id); return; } + ++images_uploaded; + uploaded_bytes += image.guest_size_bytes; + largest_upload = (std::max<u64>)(largest_upload, image.guest_size_bytes); auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image)); UploadImageContents(image, staging); runtime.InsertUploadMemoryBarrier(); } template <class P> +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; }) { + 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 <class P> +void TextureCache<P>::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<u32>(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<DAddr>)(cpu_addr, image.cpu_addr) - image.cpu_addr; + const DAddr end = (std::min<DAddr>)(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<u32>((end - 1) / stride), layers - 1); + for (u32 layer = static_cast<u32>(begin / stride); layer <= last; ++layer) { + image.dirty_layers[layer / 64] |= u64{1} << (layer % 64); + } +} + +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); + std::vector<u32> 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 <class P> template <typename StagingBuffer> void TextureCache<P>::UploadImageContents(Image& image, StagingBuffer& staging) { const std::span<u8> mapped_span = staging.mapped_span; diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 2e375595de..f4cc89f2ba 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -128,6 +128,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI u64 gc_download_deletes{}; ///< Evictions that downloaded the image first (a full GPU wait) u64 async_decodes_queued{}; u64 images_created{}; + u64 images_uploaded{}; ///< Image contents uploaded from guest memory (last 600 frames) + u64 uploaded_bytes{}; ///< Guest bytes those uploads read + u64 largest_upload{}; ///< Largest of them + u64 layer_uploads{}; ///< Of them, uploads of only the written layers bool native_thresholds_applied{}; u64 native_critical_memory{}; u64 native_expected_memory{}; @@ -151,6 +155,18 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI using AsyncBuffer = typename P::AsyncBuffer; using BufferType = typename P::BufferType; + static constexpr u32 LayerUploadMinSize = 64U << 20; + static constexpr size_t LargeScratchCapacity = 64U << 20; + + /// Large layered images whose refresh can decode only the layers written since the last one. + [[nodiscard]] bool UploadsByLayer(const ImageBase& image) const noexcept; + + /// Records the layers a write of [cpu_addr, +size) touches. + void MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size); + + /// Uploads the image's dirty layers. False when it needs a full upload instead. + bool UploadWrittenLayers(Image& image); + struct PendingUnswizzle { ImageId image_id; VideoCommon::ImageInfo info; |
