From 2bd0285a383652b086f51ef779f083f0bce62670 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:32:48 -0400 Subject: Texture cache: stop the churn, the per-frame image leak and garbage textures - Unmapped GPU addresses get a stand-in CPU address fixed by the GPU address. A fresh one per lookup meant FindImage never found an image there again: title 1's bloom chain at 0xf0000-0x15b600 was re-created on every use (~5,400 images per 600 frames), filling VRAM. - ASTC whose blocks carry no alpha (exact endpoint-mode scan reusing the decoder's block parsing) is re-compressed to BC1 instead of BC3, half the size; about 70% of title 1's ASTC qualifies. Copies between BC1 and BC3 hosts are skipped (their guest data is uploaded instead). EDEN_NX2_ASTC_BC1=0 turns it off. - Deleting an image cancels every decode still queued for it. An image re-decoded mid-decode has two; the first to finish cleared IsDecoding, the second was not cancelled and was uploaded into the next image to take the slot: garbage textures (the title logo, road patches) and a staging overrun when that image was smaller. - Diagnostics: images created and deleted per 600 frames by shape, collector deletions, async decodes, BC1/BC3 split; collector thresholds overridable (EDEN_NX2_TC_EXPECTED_MIB/CRITICAL_MIB); GPU ASTC decode wait timing (EDEN_NX2_ASTC_FLUSH=1 submits without it). Title 1 attract loop: device memory 4.8-5.1 GB instead of 5.9-6.1 GB, no decoded textures evicted, ~17 -> 22-23 fps. --- src/video_core/texture_cache/image_info.h | 2 + src/video_core/texture_cache/texture_cache.h | 124 +++++++++++++++++----- src/video_core/texture_cache/texture_cache_base.h | 15 +++ src/video_core/texture_cache/util.cpp | 20 ++-- src/video_core/texture_cache/util.h | 3 + 5 files changed, 134 insertions(+), 30 deletions(-) (limited to 'src/video_core/texture_cache') diff --git a/src/video_core/texture_cache/image_info.h b/src/video_core/texture_cache/image_info.h index 7407062d19..79237aa482 100644 --- a/src/video_core/texture_cache/image_info.h +++ b/src/video_core/texture_cache/image_info.h @@ -54,6 +54,8 @@ struct ImageInfo { bool forced_flushed = false; bool dma_downloaded = false; bool is_sparse = false; + // ASTC data whose blocks carry no alpha: re-compressed to BC1 instead of BC3 (half the size). + bool astc_opaque = false; }; } // namespace VideoCommon diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 7382ef5e4f..fbcd3eefbb 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -79,6 +80,15 @@ TextureCache

::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag critical_memory = DEFAULT_CRITICAL_MEMORY + 1_GiB; minimum_memory = 0; } + // EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB override the collector's thresholds. + for (const auto& [name, target] : {std::pair{"EDEN_NX2_TC_EXPECTED_MIB", &expected_memory}, + std::pair{"EDEN_NX2_TC_CRITICAL_MIB", &critical_memory}}) { + if (const char* value = std::getenv(name)) { + *target = std::strtoull(value, nullptr, 10) << 20; + } + } + LOG_INFO(HW_GPU, "Texture cache thresholds: expected {} MiB, critical {} MiB, minimum {} MiB", + expected_memory >> 20, critical_memory >> 20, minimum_memory >> 20); const bool gpu_unswizzle_enabled = Settings::values.gpu_unswizzle_enabled.GetValue(); @@ -151,6 +161,7 @@ void TextureCache

::RunGarbageCollector() { if (True(image.flags & ImageFlagBits::Tracked)) { UntrackImage(image, image_id); } + ++(True(image.flags & ImageFlagBits::CostlyLoad) ? gc_costly_deletes : gc_deletes); UnregisterImage(image_id); DeleteImage(image_id, image.scale_tick > frame_tick + 5); if (aggressive_mode && total_used_memory < critical_memory) { @@ -215,6 +226,23 @@ void TextureCache

::TickFrame() { if constexpr (requires { runtime.LogMemoryStatistics(); }) { runtime.LogMemoryStatistics(); } + LOG_INFO(HW_GPU, "Texture cache last 600 frames: created {} images, deleted {} (collector {}, {} costly), {} async decodes, ASTC {} opaque / {} alpha", + images_created, images_deleted, gc_deletes + gc_costly_deletes, gc_costly_deletes, + async_decodes_queued, astc_opaque_images, astc_alpha_images); + astc_opaque_images = astc_alpha_images = 0; + std::vector> shapes; + for (const auto& [key, value] : created_shapes) shapes.emplace_back(value.first, key); + std::sort(shapes.rbegin(), shapes.rend()); + for (size_t i = 0; i < std::min(10, shapes.size()); ++i) { + const u64 key = shapes[i].second; + LOG_INFO(HW_GPU, " created {}x: format {} type {} levels {} layers {} {}x{} (last at {:#x})", + shapes[i].first, key >> 56, (key >> 52) & 0xF, (key >> 48) & 0xF, + (key >> 36) & 0xFFF, (key >> 18) & 0x3FFFF, key & 0x3FFFF, + created_shapes[key].second); + } + created_shapes.clear(); + gc_deletes = gc_costly_deletes = async_decodes_queued = 0; + images_created = images_deleted = 0; } if (total_used_memory > minimum_memory) { RunGarbageCollector(); @@ -1323,12 +1351,9 @@ ImageId TextureCache

::FindOrInsertImage(const ImageInfo& info, GPUVAddr gpu_a template ImageId TextureCache

::FindImage(const ImageInfo& info, GPUVAddr gpu_addr, RelaxedOptions options) { - std::optional cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr); + const std::optional cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info)); if (!cpu_addr) { - cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info)); - if (!cpu_addr) { - return ImageId{}; - } + return ImageId{}; } const bool broken_views = runtime.HasBrokenTextureViewFormats() || True(options & RelaxedOptions::ForceBrokenViews); @@ -1457,6 +1482,7 @@ void TextureCache

::QueueAsyncDecode(Image& image, ImageId image_id) { LOG_DEBUG(HW_GPU, "Queuing async texture decode {:#x}", image.gpu_addr); image.flags |= ImageFlagBits::IsDecoding; + ++async_decodes_queued; auto decode = std::make_unique(); auto* decode_ptr = decode.get(); decode->image_id = image_id; @@ -1516,6 +1542,12 @@ void TextureCache

::TickAsyncDecode() { continue; } Image& image = slot_images[async_decode->image_id]; + if (async_decode->decoded_data.size() > MapSizeBytes(image)) { + LOG_ERROR(HW_GPU, "Async decode of {} bytes for a {}-byte image at {:#x}, dropped", + async_decode->decoded_data.size(), MapSizeBytes(image), image.gpu_addr); + i = async_decodes.erase(i); + continue; + } auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image)); std::memcpy(staging.mapped_span.data(), async_decode->decoded_data.data(), async_decode->decoded_data.size()); @@ -1630,18 +1662,32 @@ bool TextureCache

::ScaleDown(Image& image) { return true; } +template +std::optional TextureCache

::ImageCpuAddress(GPUVAddr gpu_addr, size_t size) const { + if (const std::optional cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr)) { + return cpu_addr; + } + if (const std::optional cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, size)) { + return cpu_addr; + } + // No CPU backing (title 1 renders a bloom chain at GPU 0xf0000-0x15b600): a stand-in address + // fixed by the GPU address, at or above ~(1 << 40) so TrackImage skips it. A fresh address per + // lookup made every use create another image (thousands a minute, never found again). + constexpr GPUVAddr StandInRange = 1ULL << 40; + if (gpu_addr >= StandInRange || size > StandInRange - gpu_addr - 1) { + return std::nullopt; + } + return ~(1ULL << 40ULL) + gpu_addr; +} + template ImageId TextureCache

::InsertImage(const ImageInfo& info, GPUVAddr gpu_addr, RelaxedOptions options) { - std::optional cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr); + std::optional cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info)); if (!cpu_addr) { - const auto size = CalculateGuestSizeInBytes(info); - cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, size); - if (!cpu_addr) { - const DAddr fake_addr = ~(1ULL << 40ULL) + virtual_invalid_space; - virtual_invalid_space += Common::AlignUp(size, 32); - cpu_addr = std::optional(fake_addr); - } + const DAddr fake_addr = ~(1ULL << 40ULL) + virtual_invalid_space; + virtual_invalid_space += Common::AlignUp(CalculateGuestSizeInBytes(info), 32); + cpu_addr = std::optional(fake_addr); } ASSERT_MSG(cpu_addr, "Tried to insert an image to an invalid gpu_addr={:#x}", gpu_addr); ImageInfo storage_info = info; @@ -1760,10 +1806,36 @@ ImageId TextureCache

::JoinImages(const ImageInfo& info, GPUVAddr gpu_addr, DA } } + if constexpr (requires { runtime.UseOpaqueAstcFormat(); }) { + // Opaque ASTC goes to BC1 rather than BC3 when it is re-compressed (the Vulkan runtime + // decides). The guest data is already there when an image is first used. + constexpr size_t MaxScanBytes = 64_MiB; + if (IsPixelFormatASTC(new_info.format) && runtime.UseOpaqueAstcFormat()) { + const size_t scan_bytes = CalculateGuestSizeInBytes(new_info); + if (scan_bytes <= MaxScanBytes) { + astc_scan_buffer.resize(scan_bytes); + gpu_memory->ReadBlockUnsafe(gpu_addr, astc_scan_buffer.data(), scan_bytes); + new_info.astc_opaque = AstcDataLooksOpaque(astc_scan_buffer); + ++(new_info.astc_opaque ? astc_opaque_images : astc_alpha_images); + } + } + } const ImageId new_image_id = slot_images.insert(runtime, new_info, gpu_addr, cpu_addr); Image& new_image = slot_images[new_image_id]; new_image.allocation_tick = frame_tick; + { + // Which shapes are being created (periodic log): format, type, levels, layers, size. + const u64 key = (u64{static_cast(new_info.format)} << 56) | + (u64{static_cast(new_info.type)} << 52) | + (u64{static_cast(new_info.resources.levels) & 0xF} << 48) | + (u64{static_cast(new_info.resources.layers) & 0xFFF} << 36) | + (u64{new_info.size.width & 0x3FFFF} << 18) | (new_info.size.height & 0x3FFFF); + auto& shape = created_shapes[key]; + ++shape.first; + shape.second = gpu_addr; + ++images_created; + } if (!gpu_memory->IsContinuousRange(new_image.gpu_addr, new_image.guest_size_bytes) && new_info.is_sparse) { @@ -1959,12 +2031,9 @@ std::optional::BlitImages> TextureCache

::GetBlitImag template ImageId TextureCache

::FindDMAImage(const ImageInfo& info, GPUVAddr gpu_addr) { - std::optional cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr); + const std::optional cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info)); if (!cpu_addr) { - cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info)); - if (!cpu_addr) { - return ImageId{}; - } + return ImageId{}; } ImageId image_id{}; boost::container::small_vector image_ids; @@ -2463,12 +2532,14 @@ void TextureCache

::UntrackImage(ImageBase& image, ImageId image_id) { template void TextureCache

::DeleteImage(ImageId image_id, bool immediate_delete) { ImageBase& image = slot_images[image_id]; - if (True(image.flags & ImageFlagBits::IsDecoding)) { - // A worker is still decoding it: keep its context alive but drop the result. - for (auto& async_decode : async_decodes) { - if (async_decode->image_id == image_id) { - async_decode->cancelled = true; - } + ++images_deleted; + // Drop every decode still queued for it, whatever IsDecoding says: an image re-decoded while + // a decode was in flight has two, and the first to finish clears the flag. A decode left + // behind was uploaded into the next image to take this slot (another texture's data: garbage + // textures, and a staging overrun when that image was smaller). + for (auto& async_decode : async_decodes) { + if (async_decode->image_id == image_id) { + async_decode->cancelled = true; } } if (image.HasScaled()) { @@ -2696,6 +2767,11 @@ template void TextureCache

::CopyImage(ImageId dst_id, ImageId src_id, std::vector copies) { Image& dst = slot_images[dst_id]; Image& src = slot_images[src_id]; + if (dst.info.astc_opaque != src.info.astc_opaque) { + // BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the + // same guest data, which is uploaded from guest memory instead. + return; + } const bool is_rescaled = True(src.flags & ImageFlagBits::Rescaled); if (is_rescaled) { ASSERT(True(dst.flags & ImageFlagBits::Rescaled)); diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index ed9758fdee..f6f083e10a 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -120,6 +121,17 @@ class TextureCache : public VideoCommon::ChannelSetupCaches astc_scan_buffer; + u64 images_deleted{}; + std::map> created_shapes; + static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB; static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB; static constexpr size_t GC_EMERGENCY_COUNTS = 2; @@ -450,6 +462,9 @@ private: DAddr virtual_invalid_space{}; + /// The CPU address an image at gpu_addr is keyed by: its backing, or a fixed stand-in. + [[nodiscard]] std::optional ImageCpuAddress(GPUVAddr gpu_addr, size_t size) const; + bool has_deleted_images = false; bool is_rescaling = false; u64 total_used_memory = 0; diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index aae2c7e4fd..7a1a1d89c0 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -603,6 +604,10 @@ u32 CalculateUnswizzledSizeBytes(const ImageInfo& info) noexcept { return NumBlocksPerLayer(info, tile_size) * info.resources.layers * BytesPerBlock(info.format); } +bool AstcDataLooksOpaque(std::span data) noexcept { + return Tegra::Texture::ASTC::IsOpaque(data); +} + u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept { if (info.type == ImageType::Buffer) { return info.size.width * BytesPerBlock(info.format); @@ -611,8 +616,10 @@ u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept { if (IsPixelFormatASTC(info.format) && Settings::values.astc_recompression.GetValue() != Settings::AstcRecompression::Uncompressed) { const u32 bpp_div = - Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 ? 2 - : 1; + Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 || + info.astc_opaque + ? 2 + : 1; // NumBlocksPerLayer doesn't account for this correctly, so we have to do it manually. u32 output_size = 0; for (s32 i = 0; i < info.resources.levels; i++) { @@ -955,10 +962,11 @@ void ConvertImage(std::span input, const ImageInfo& info, std::span data) noexcept; + [[nodiscard]] u32 CalculateLayerStride(const ImageInfo& info) noexcept; [[nodiscard]] u32 CalculateLayerSize(const ImageInfo& info) noexcept; -- cgit v1.2.3