From 51207c73c889dbdc4a586a5da23314f6d33e4eb9 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:26:16 -0400 Subject: Switch 2: Turing macros, native compute, Ampere shader lowering, native tiling From changes.patch: the TU104/GA10x MME interpreter (shared macro context for the process's channels, also on the compute class), native compute with QMD 3.0 dependency scheduling, the Ampere-to-Maxwell shader lowering, native vertex stream sizes, and the console's GOB swizzle for textures, DMA copies and the host unswizzle shaders. Switch 2 channels turn all of it on at init. The lowering is widened for title 4 (all 32 of its programs lower): general TEX (any dimension/LOD/mask, operand split 1+1/2+1/2+2/4+n, any but consistent descriptor buffer), cube/array, SULD channel masks, 128-bit STG, indexed IPA, VOTE, SHFL with any immediate or predicate output, R2P/P2R byte select, uniform ULOP3/ULEA/UBREV/UFLO.SH/UISETP signed/UMOV URZ, IMNMX cbuf, IADD3 and FFMA uniform operands, FMUL /4 /8 x8, and BSSY/BSYNC/BREAK/WARPSYNC as no-ops. The capture stops at unmapped memory. EDEN_NX2_SHADER_DUMP dumps every native program; tools in C:\dev\nx2work test the lowering offline. --- src/video_core/texture_cache/texture_cache.h | 81 +++++++++++++++++++++------- 1 file changed, 61 insertions(+), 20 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 e92e54f8ce..cd4ef82dbe 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -803,6 +803,17 @@ std::pair TextureCache

::TryFindFramebufferImage if (it == page_table.end()) { return {}; } + const auto view_format = [&]() { + switch (config.pixel_format) { + case Service::android::PixelFormat::Rgb565: + return PixelFormat::R5G6B5_UNORM; + case Service::android::PixelFormat::Bgra8888: + return PixelFormat::B8G8R8A8_UNORM; + default: + return PixelFormat::A8B8G8R8_UNORM; + } + }(); + const auto& image_map_ids = it->second; boost::container::small_vector valid_image_ids; for (const ImageMapId map_id : image_map_ids) { @@ -814,20 +825,17 @@ std::pair TextureCache

::TryFindFramebufferImage if (image.image_view_ids.empty()) { continue; } + if (!VideoCore::Surface::IsViewCompatible(image.info.format, view_format, false, true)) { + static u32 rejected_alias_count{}; + if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") && rejected_alias_count++ < 8) { + LOG_WARNING(HW_GPU, "Ignoring incompatible framebuffer alias: address={:#x}, image={}, view={}", + cpu_addr, image.info.format, view_format); + } + continue; + } valid_image_ids.push_back(map.image_id); } - const auto view_format = [&]() { - switch (config.pixel_format) { - case Service::android::PixelFormat::Rgb565: - return PixelFormat::R5G6B5_UNORM; - case Service::android::PixelFormat::Bgra8888: - return PixelFormat::B8G8R8A8_UNORM; - default: - return PixelFormat::A8B8G8R8_UNORM; - } - }(); - const auto GetImageViewForFramebuffer = [&](ImageId image_id) { ImageViewInfo info{ImageViewType::e2D, view_format}; if (config.blending == Tegra::BlendMode::Opaque) { @@ -869,6 +877,7 @@ void TextureCache

::CommitAsyncFlushes() { // This is intentionally passing the value by copy if constexpr (IMPLEMENTS_ASYNC_DOWNLOADS) { auto& download_ids = uncommitted_downloads; + const size_t redundant_images = DeduplicatePendingImageDownloads(download_ids, slot_images); if (download_ids.empty()) { committed_downloads.emplace_back(std::move(uncommitted_downloads)); uncommitted_downloads.clear(); @@ -882,17 +891,32 @@ void TextureCache

::CommitAsyncFlushes() { for (PendingDownload& download_info : download_ids) { if (download_info.is_swizzle) { total_size_bytes += - Common::AlignUp(slot_images[download_info.object_id].unswizzled_size_bytes, 64); + Common::AlignUp(download_info.GetImage(slot_images).unswizzled_size_bytes, 64); any_none_dma = true; download_info.async_buffer_id = last_async_buffer_id; } } if (any_none_dma) { + if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS") && total_size_bytes >= 64_MiB) { + LOG_WARNING(HW_GPU, "Large image readback batch: bytes={}, entries={}, duplicates_removed={}, submitted_batches={}, retained_buffers={}", + total_size_bytes, download_ids.size(), redundant_images, + committed_downloads.size(), async_buffers_death_ring.size()); + size_t traced_images = 0; + for (const auto& download : download_ids) { + if (!download.is_swizzle || traced_images++ >= 8) continue; + const auto& image = download.GetImage(slot_images); + LOG_WARNING(HW_GPU, "Readback image: gpu={:#x}, cpu={:#x}, format={}, size={}x{}x{}, levels={}, layers={}, layer_stride={}, guest_bytes={}, download_bytes={}", + image.gpu_addr, image.cpu_addr, image.info.format, + image.info.size.width, image.info.size.height, image.info.size.depth, + image.info.resources.levels, image.info.resources.layers, + image.info.layer_stride, image.guest_size_bytes, image.unswizzled_size_bytes); + } + } auto download_map = runtime.DownloadStagingBuffer(total_size_bytes, true); for (const PendingDownload& download_info : download_ids) { if (download_info.is_swizzle) { - Image& image = slot_images[download_info.object_id]; + Image& image = download_info.GetImage(slot_images); const auto copies = FullDownloadCopies(image.info); image.DownloadMemory(download_map, copies); download_map.offset += Common::AlignUp(image.unswizzled_size_bytes, 64); @@ -925,7 +949,7 @@ void TextureCache

::PopAsyncFlushes() { auto& download_info = download_ids[i - 1]; auto& download_buffer = download_map[download_info.async_buffer_id]; if (download_info.is_swizzle) { - const ImageBase& image = slot_images[download_info.object_id]; + const ImageBase& image = download_info.GetImage(slot_images); const auto copies = FullDownloadCopies(image.info); download_buffer.offset -= Common::AlignUp(image.unswizzled_size_bytes, 64); std::span download_span = @@ -954,7 +978,7 @@ void TextureCache

::PopAsyncFlushes() { size_t total_size_bytes = 0; for (const PendingDownload& download_info : download_ids) { if (download_info.is_swizzle) { - total_size_bytes += slot_images[download_info.object_id].unswizzled_size_bytes; + total_size_bytes += download_info.GetImage(slot_images).unswizzled_size_bytes; } } auto download_map = runtime.DownloadStagingBuffer(total_size_bytes); @@ -963,7 +987,7 @@ void TextureCache

::PopAsyncFlushes() { if (!download_info.is_swizzle) { continue; } - Image& image = slot_images[download_info.object_id]; + Image& image = download_info.GetImage(slot_images); const auto copies = FullDownloadCopies(image.info); image.DownloadMemory(download_map, copies); download_map.offset += image.unswizzled_size_bytes; @@ -976,7 +1000,7 @@ void TextureCache

::PopAsyncFlushes() { if (!download_info.is_swizzle) { continue; } - const ImageBase& image = slot_images[download_info.object_id]; + const ImageBase& image = download_info.GetImage(slot_images); const auto copies = FullDownloadCopies(image.info); SwizzleImage(*gpu_memory, image.gpu_addr, image.info, copies, download_span, swizzle_data_buffer); download_map.offset += image.unswizzled_size_bytes; @@ -1178,7 +1202,18 @@ void TextureCache

::UploadImageContents(Image& image, StagingBuffer& staging) } template -ImageViewId TextureCache

::CreateImageView(const TICEntry& config) { +ImageViewId TextureCache

::CreateImageView(const TICEntry& raw_config) { + const bool native = gpu_memory->UsesNativeTiling(); + if (native && raw_config.BaseLayer() && std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { + static u32 texture_view_traces{}; + if (texture_view_traces++ < 8) { + LOG_WARNING(HW_GPU, "Native texture view header: address={:#x}, dimensions={}x{}x{}, type={}, legacy_layer={}, raw={:016x}/{:016x}/{:016x}/{:016x}", + raw_config.Address(), raw_config.Width(), raw_config.Height(), raw_config.Depth(), + static_cast(raw_config.texture_type.Value()), raw_config.BaseLayer(), + raw_config.raw[0], raw_config.raw[1], raw_config.raw[2], raw_config.raw[3]); + } + } + const TICEntry config = TextureViewHeader(raw_config, native); const ImageInfo info(config); if (info.type == ImageType::Buffer) { const ImageViewInfo view_info(config, 0); @@ -1526,7 +1561,9 @@ ImageId TextureCache

::InsertImage(const ImageInfo& info, GPUVAddr gpu_addr, } } ASSERT_MSG(cpu_addr, "Tried to insert an image to an invalid gpu_addr={:#x}", gpu_addr); - const ImageId image_id = JoinImages(info, gpu_addr, *cpu_addr); + ImageInfo storage_info = info; + storage_info.native_tiling = gpu_memory->UsesNativeTiling(); + const ImageId image_id = JoinImages(storage_info, gpu_addr, *cpu_addr); const Image& image = slot_images[image_id]; // Using "image.gpu_addr" instead of "gpu_addr" is important because it might be different const auto [it, is_new] = image_allocs_table.try_emplace(image.gpu_addr); @@ -2385,7 +2422,11 @@ void TextureCache

::DeleteImage(ImageId image_id, bool immediate_delete) { } slot_image_views.erase(image_view_id); } - if (!immediate_delete) { + const bool retained = RetainPendingImageDownloads(image_id, slot_images[image_id], + uncommitted_downloads, committed_downloads); + if (retained) { + LOG_INFO(Render_Vulkan, "Retained image for queued download: id={}", image_id.Value()); + } else if (!immediate_delete) { sentenced_images.Push(std::move(slot_images[image_id])); } slot_images.erase(image_id); -- cgit v1.2.3 From 2a35b19166ab577674339a514bb7eb1657409443 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:55:29 -0400 Subject: Vulkan: unbacked sparse images for textures too large to back Title 1 samples an 8192x8192, 12-level, 68-layer array (a sparse virtual texture, ~23 GB): vkCreateImage failed and Eden aborted, or VMA put it in system memory and staged a full upload (34 GB private). Images over 2^28 texels are now created sparse-resident with no memory bound; the texture cache neither uploads nor downloads them. Images merely over the VRAM budget are allocated without the budget limit instead of failing. --- .../renderer_vulkan/vk_texture_cache.cpp | 3 ++ src/video_core/texture_cache/image_base.h | 1 + src/video_core/texture_cache/texture_cache.h | 11 ++++- .../vulkan_common/vulkan_memory_allocator.cpp | 48 +++++++++++++++++++++- src/video_core/vulkan_common/vulkan_wrapper.h | 5 +++ 5 files changed, 65 insertions(+), 3 deletions(-) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 312617475d..7e2420200e 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1879,6 +1879,9 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info, runtime->ViewFormats(info.format))), aspect_mask(ImageAspectMask(info.format)) { + if (original_image.IsUnbacked()) { + flags |= VideoCommon::ImageFlagBits::Unbacked; + } if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { switch (Settings::values.accelerate_astc.GetValue()) { case Settings::AstcDecodeMode::Gpu: diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 79fc8505d6..846a6f7fc1 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -45,6 +45,7 @@ enum class ImageFlagBits : u32 { AsynchronousDecode = 1 << 16, IsDecoding = 1 << 17, ///< Is currently being decoded asynchronously. + Unbacked = 1 << 18, ///< Host image has no memory (too big to back); never upload/download. }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index cd4ef82dbe..f3da5684a5 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -597,7 +597,7 @@ void TextureCache

::WriteMemory(DAddr cpu_addr, size_t size) { template bool TextureCache

::IsDownloadable(const ImageBase& image) const noexcept { - if (!image.IsSafeGpuCopy()) { + if (!image.IsSafeGpuCopy() || True(image.flags & ImageFlagBits::Unbacked)) { return false; } if (image.info.num_samples == 1) { @@ -1146,6 +1146,12 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { TrackImage(image, image_id); + if (True(image.flags & ImageFlagBits::Unbacked)) { + // Nothing to upload into (and staging it could take tens of GB). + runtime.TransitionImageLayout(image); + return; + } + if (image.info.num_samples > 1 && !runtime.CanUploadMSAA()) { LOG_WARNING(HW_GPU, "MSAA image uploads are not implemented"); runtime.TransitionImageLayout(image); @@ -2669,7 +2675,8 @@ void TextureCache

::BindRenderTarget(ImageViewId* old_id, ImageViewId new_id) } if (new_id) { const ImageViewBase& old_view = slot_image_views[new_id]; - if (True(old_view.flags & ImageViewFlagBits::PreemtiveDownload)) { + if (True(old_view.flags & ImageViewFlagBits::PreemtiveDownload) && + False(slot_images[old_view.image_id].flags & ImageFlagBits::Unbacked)) { const PendingDownload new_download{true, 0, old_view.image_id}; uncommitted_downloads.emplace_back(new_download); } diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index f4721543ae..aafb3183b6 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -221,7 +221,53 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci) const VkImage handle{}; VmaAllocation allocation{}; VmaAllocationInfo alloc_info{}; - vk::Check(vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info)); + // Images this large (title 1's sparse texture arrays reach 4.5G texels, ~23 GB) are never + // backed: VMA would quietly place them in system memory once VRAM is full. They take the + // unbacked sparse fallback below. + const u64 texels = u64{ci.extent.width} * ci.extent.height * ci.extent.depth * ci.arrayLayers; + const bool huge = texels > (u64{1} << 28); + VkResult result = huge ? VK_ERROR_OUT_OF_DEVICE_MEMORY + : vmaCreateImage(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info); + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && !huge) { + // Over the device memory budget (Switch 2 titles fill 8 GB cards): let the driver page + // it out rather than failing the draw and aborting. + static u32 logged{}; + if (logged++ < 32) { + LOG_WARNING(Render_Vulkan, + "Image {}x{}x{} ({} levels, {} layers, format {}) is over the memory " + "budget; allocating it anyway", + ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels, + ci.arrayLayers, static_cast(ci.format)); + } + VmaAllocationCreateInfo relaxed = alloc_ci; + relaxed.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; + result = vmaCreateImage(allocator, &ci, &relaxed, &handle, &allocation, &alloc_info); + } + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && ci.imageType != VK_IMAGE_TYPE_1D) { + // Too big to back at all, e.g. title 1's sparse virtual texture arrays (8192x8192, + // 68 layers, ~23 GB): on the console only resident tiles have memory. Create a sparse + // resident image with nothing bound, so views and copies still work; reads of it return + // zero and writes are dropped, instead of aborting. + VkImageCreateInfo sparse_ci = ci; + sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT; + VkImage sparse{}; + if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr, + &sparse) == VK_SUCCESS) { + LOG_WARNING(Render_Vulkan, + "Image {}x{}x{} ({} levels, {} layers, format {}) does not fit; using an " + "unbacked sparse image", + ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels, + ci.arrayLayers, static_cast(ci.format)); + return vk::Image(sparse, ci.usage, *device.GetLogical(), allocator, VK_NULL_HANDLE, + device.GetDispatchLoader()); + } + } + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "Image {}x{}x{} ({} levels, {} layers, format {}) failed: {}", + ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels, + ci.arrayLayers, static_cast(ci.format), static_cast(result)); + } + vk::Check(result); // Log GPU memory allocation for images if (GPU::Logging::IsActive() && diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 97d2ba3700..947150648d 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -689,6 +689,11 @@ public: allocation{allocation_}, dld{&dld_} {} Image() = default; + /// True for an image created without memory (an unbacked sparse image). + bool IsUnbacked() const noexcept { + return handle != VK_NULL_HANDLE && allocation == VK_NULL_HANDLE; + } + Image(const Image&) = delete; Image& operator=(const Image&) = delete; -- cgit v1.2.3 From 8393e2912616ce475619900be8cb29e551ff2997 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:19:44 -0400 Subject: Fix the P6 scratch save in R2UR/MATCH/HFMA2.RELU; GPU memory dump tool The colour cast on title 1 (magenta/cyan, then red/green, changing per course) came from the sky/atmosphere volume shader (a pixel shader drawing a 32^3 RGBA16F table slice by slice), which starts with an unguarded R2UR. R2UR, MATCH and HFMA2.RELU use P6 as scratch and saved/restored it with the uniform-guard mask, 1 << (6 - 7) for an unguarded instruction: undefined, in practice bit 31, which shifted into the opcode bits and turned the save into another instruction. They now always save and restore P6. Debugging aids used to find it: - EDEN_NX2_DUMP=::,...: flush GPU memory ranges at a present (on the GPU thread) and save them next to the frame captures; nx2work/vol3d.py untiles Switch 2 3D textures from such dumps. - GPU::RunOnGpuThread. - Logs of Switch 2 3D images and descriptors. --- src/core/hle/service/nx2_gsv.h | 42 ++++++++++++++++++++++ .../frontend/ampere/lower_to_maxwell.h | 16 +++++---- src/video_core/gpu.cpp | 4 +++ src/video_core/gpu.h | 3 ++ src/video_core/texture_cache/image_info.cpp | 15 +++++++- src/video_core/texture_cache/texture_cache.h | 6 ++++ 6 files changed, 78 insertions(+), 8 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 b7f0e9261a..fc7de1fae7 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -21,6 +21,7 @@ #include "common/alignment.h" #include "common/settings.h" #include "core/core.h" +#include "video_core/rasterizer_interface.h" #include "video_core/renderer_base.h" #include "core/frontend/framebuffer_layout.h" #include "core/perf_stats.h" @@ -758,6 +759,7 @@ private: } system.GPU().RequestComposite(std::move(layers), std::move(fences)); CaptureFrame(); + DumpRanges(); system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs()); system.GetPerfStats().EndSystemFrame(); system.GetPerfStats().BeginSystemFrame(); @@ -797,6 +799,46 @@ private: layout); } + // EDEN_NX2_DUMP=::[,:...]: at that + // present, flush those GPU ranges back from the host GPU and save their bytes as + // nx2-dump-

.bin in the EDEN_NX2_FRAME_CAPTURE directory (to inspect what a pass + // rendered, e.g. a 3D volume, offline). + void DumpRanges() { + static const std::string spec = [] { + const char* text = std::getenv("EDEN_NX2_DUMP"); + return text ? std::string{text} : std::string{}; + }(); + if (spec.empty() || m_presents != std::strtoull(spec.c_str(), nullptr, 10)) { + return; + } + const char* const directory = std::getenv("EDEN_NX2_FRAME_CAPTURE"); + size_t position = spec.find(':'); + while (position != std::string::npos) { + char* end{}; + const u64 address = std::strtoull(spec.c_str() + position + 1, &end, 16); + const u64 size = *end == ':' ? std::strtoull(end + 1, &end, 16) : 0; + position = spec.find(',', position + 1); // the next "address:size" + const std::string path = fmt::format("{}/nx2-dump-{:x}.bin", directory ? directory : ".", address); + // On the GPU thread: waiting for it here can deadlock with a GPU wait on this present. + static_cast(system.GPU().RunOnGpuThread([this, address, size, path] { + std::vector bytes(size); + for (u64 current = address; current < address + size;) { + const u64 chunk = std::min((current | 0xfff) + 1, address + size) - current; + if (const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(current)) { + system.GPU().Renderer().ReadRasterizer()->FlushRegion(*device, chunk); + system.Host1x().MemoryManager().ReadBlock(*device, bytes.data() + (current - address), chunk); + } + current += chunk; + } + if (std::FILE* file = std::fopen(path.c_str(), "wb")) { + std::fwrite(bytes.data(), 1, bytes.size(), file); + std::fclose(file); + } + LOG_INFO(Service, "nx2: dumped {:#x} bytes at {:#x} to {}", size, address, path); + })); + } + } + // 228(u64 token, u32 slot): return a buffer without presenting it. void CancelClientBuffer(HLERequestContext& ctx) { constexpr u64 AvailableSlots = 0x7fff; diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 0cebaa4363..1390951ef5 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -161,15 +161,17 @@ inline LoweredProgram LowerToMaxwell(std::span code) { }; std::string_view operand_error; const auto emit = [&](u64 word) { result.instructions.push_back(word); }; - const u32 guard_mask = 1U << guard_slot; - const auto save_predicate = [&] { - emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{guard_mask} << 20)); + // P(guard slot) while a uniform guard is swapped in; P6 for lowerings that use it as + // scratch (R2UR, MATCH, HFMA2.RELU), which save and restore it with the defaults below. + const u32 guard_mask = guard_slot < 7 ? 1U << guard_slot : 0U; + const auto save_predicate = [&](u32 mask = 1U << 6) { + emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{mask} << 20)); }; - const auto restore_predicate = [&] { - emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{guard_mask} << 20)); + const auto restore_predicate = [&](u32 mask = 1U << 6) { + emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20)); }; if (guarded_uniform) { - save_predicate(); + save_predicate(guard_mask); emit(0x38f0000000070000ULL | (u64{239} << 8) | (u64{guard_mask} << 20)); } const u32 output_mask = uniform_output == 7 ? 0U : 1U << (6 - uniform_output); @@ -2229,7 +2231,7 @@ inline LoweredProgram LowerToMaxwell(std::span code) { emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{output_mask} << 20)); emit(0x38f0000000070000ULL | (u64{254} << 8) | (u64{output_mask} << 20)); } - if (guarded_uniform) restore_predicate(); + if (guarded_uniform) restore_predicate(guard_mask); } return fail(static_cast(code.size()), main_exited ? "native program lacks captured library return" : "native program has no unconditional EXIT"); } diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 2a61a66d82..6441b9511a 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -411,6 +411,10 @@ u64 GPU::RequestFlush(DAddr addr, std::size_t size) { }); } +u64 GPU::RunOnGpuThread(std::function&& action) { + return impl->RequestSyncOperation(std::move(action)); +} + u64 GPU::CurrentSyncRequestFence() const { return impl->CurrentSyncRequestFence(); } diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 41ba186cd3..3a6546b41b 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -164,6 +164,9 @@ public: /// Request a host GPU memory flush from the CPU. [[nodiscard]] u64 RequestFlush(DAddr addr, std::size_t size); + /// Runs an action on the GPU thread between command lists (returns its sync fence). + u64 RunOnGpuThread(std::function&& action); + /// Obtains current flush request fence id. [[nodiscard]] u64 CurrentSyncRequestFence() const; diff --git a/src/video_core/texture_cache/image_info.cpp b/src/video_core/texture_cache/image_info.cpp index 8b59eb6841..7e86bf00cf 100644 --- a/src/video_core/texture_cache/image_info.cpp +++ b/src/video_core/texture_cache/image_info.cpp @@ -4,9 +4,13 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include #include #include "common/assert.h" +#include "common/logging.h" #include "common/settings.h" #include "video_core/surface.h" #include "video_core/texture_cache/format_lookup_table.h" @@ -109,14 +113,23 @@ ImageInfo::ImageInfo(const TICEntry& config) noexcept { size.height = config.Height(); resources.layers = config.BaseLayer() + config.Depth() * 6; break; - case TextureType::Texture3D: + case TextureType::Texture3D: { ASSERT(config.BaseLayer() == 0); type = ImageType::e3D; size.width = config.Width(); size.height = config.Height(); size.depth = config.Depth(); resources.layers = 1; + // A few distinct 3D descriptors, raw (to check Switch 2 volume fields). + static std::vector> logged; + if (logged.size() < 16 && std::find(logged.begin(), logged.end(), config.raw) == logged.end()) { + logged.push_back(config.raw); + LOG_INFO(HW_GPU, "3D TIC {:016x} {:016x} {:016x} {:016x}: {}x{}x{} block_depth {} levels {}", + config.raw[0], config.raw[1], config.raw[2], config.raw[3], size.width, size.height, + size.depth, config.block_depth.Value(), config.max_mip_level.Value() + 1); + } break; + } case TextureType::Texture1DBuffer: type = ImageType::Buffer; size.width = config.Width(); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index f3da5684a5..75005f0731 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1569,6 +1569,12 @@ ImageId TextureCache

::InsertImage(const ImageInfo& info, GPUVAddr gpu_addr, ASSERT_MSG(cpu_addr, "Tried to insert an image to an invalid gpu_addr={:#x}", gpu_addr); ImageInfo storage_info = info; storage_info.native_tiling = gpu_memory->UsesNativeTiling(); + if (storage_info.native_tiling && info.type == ImageType::e3D) { + // Switch 2 3D images (lighting/fog volumes) are rare: log each one's shape. + LOG_INFO(HW_GPU, "3D image at {:#x}: {}x{}x{} levels {} format {} block {}x{}x{}", + gpu_addr, info.size.width, info.size.height, info.size.depth, info.resources.levels, + static_cast(info.format), info.block.width, info.block.height, info.block.depth); + } const ImageId image_id = JoinImages(storage_info, gpu_addr, *cpu_addr); const Image& image = slot_images[image_id]; // Using "image.gpu_addr" instead of "gpu_addr" is important because it might be different -- cgit v1.2.3 From 934ad3b97c12e1d40cf4e1f6094c756873b6af82 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:37:34 -0400 Subject: Keep huge ASTC arrays from a lower mip on the host (title 1 terrain) Title 1's terrain virtual textures are 8192x8192x68 ASTC 8x8 sRGB arrays with 12 levels: 1.5 GB in guest memory (fully backed) but 23 GB each as the RGBA8 Eden decodes ASTC to, so they were created unbacked and the near terrain sampled black. A GPU-decoded ASTC 2D array over 1 GiB decoded now keeps only the levels from the first one that fits in 512 MiB (here 3: 1024x1024, 380 MiB); host mip 0 is that guest level. Uploads read each layer's tail of the mip chain into a compact buffer and decode only those levels, views shift their level range, and these images are never downloaded. Normalized coordinates and derivative LOD address the smaller image correctly; the nearest terrain is lower resolution than on the console. Also logs how unbacked images are used (views, render targets, blits, DMA). --- src/video_core/renderer_vulkan/vk_compute_pass.cpp | 18 +++++-- .../renderer_vulkan/vk_texture_cache.cpp | 61 ++++++++++++++++++++-- src/video_core/texture_cache/image_base.h | 4 ++ src/video_core/texture_cache/texture_cache.h | 51 +++++++++++++++++- src/video_core/texture_cache/util.cpp | 7 +++ 5 files changed, 132 insertions(+), 9 deletions(-) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index 2eac00103e..65e0d5f145 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -31,6 +31,7 @@ #include "video_core/renderer_vulkan/vk_update_descriptor.h" #include "video_core/texture_cache/accelerated_swizzle.h" #include "video_core/texture_cache/types.h" +#include "video_core/texture_cache/util.h" #include "video_core/textures/decoders.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -558,6 +559,15 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, image_barrier); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, vk_pipeline); }); + // With trimmed host levels the input holds each layer's mip chain from host_level_offset on + // (see TextureCache::UploadImageContents), so its layer stride is that tail's size. + const u32 skip = image.host_level_offset; + const u32 input_layer_stride = + skip != 0 ? image.info.layer_stride - + static_cast(VideoCommon::FullUploadSwizzles(image.info)[skip].buffer_offset) + : image.info.layer_stride; + const size_t input_size = skip != 0 ? size_t{input_layer_stride} * image.info.resources.layers + : image.guest_size_bytes; for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 8U); @@ -566,12 +576,14 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, compute_pass_descriptor_queue.Acquire(scheduler, 2); compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset, - image.guest_size_bytes - swizzle.buffer_offset); - compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level)); + input_size - swizzle.buffer_offset); + compute_pass_descriptor_queue.AddImage( + image.StorageImageView(swizzle.level - static_cast(skip))); const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; // To unswizzle the ASTC data - const auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info); + auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info); + params.layer_stride = input_layer_stride; ASSERT(params.origin == (std::array{0, 0, 0})); ASSERT(params.destination == (std::array{0, 0, 0})); ASSERT(params.bytes_per_block_log2 == 4); diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 7e2420200e..d3f255f05d 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -160,6 +160,40 @@ constexpr VkBorderColor ConvertBorderColor(const std::array& color) { info.size.depth == 1; } +// The host shape of an image whose decoded copy would not fit in VRAM: a GPU-decoded ASTC array +// over 1 GiB as RGBA8 (title 1's 8192x8192x68 ASTC 8x8 virtual textures are 24 GB each) keeps only +// its mip levels from `level_offset` on, so host mip 0 is guest mip `level_offset`. Normalized +// coordinates and derivative LOD still address it correctly; explicit LODs sample one level lower. +[[nodiscard]] ImageInfo TrimmedHostInfo(const Device& device, const ImageInfo& info, u32& level_offset) { + level_offset = 0; + if (!WillUseAcceleratedAstcDecode(device, info) || info.type != ImageType::e2D) { + return info; + } + const auto decoded_bytes = [&](u32 skip) { + u64 bytes = 0; + for (s32 level = static_cast(skip); level < info.resources.levels; ++level) { + bytes += u64{(std::max)(info.size.width >> level, 1U)} * + (std::max)(info.size.height >> level, 1U) * 4; + } + return bytes * info.resources.layers; + }; + if (decoded_bytes(0) <= (u64{1} << 30)) { + return info; + } + while (level_offset + 1 < static_cast(info.resources.levels) && + decoded_bytes(level_offset) > (u64{512} << 20)) { + ++level_offset; + } + ImageInfo host = info; + host.size.width = (std::max)(info.size.width >> level_offset, 1U); + host.size.height = (std::max)(info.size.height >> level_offset, 1U); + host.resources.levels -= static_cast(level_offset); + LOG_WARNING(Render_Vulkan, "{}x{}x{} ASTC image would be {} MiB decoded; keeping mip levels {}+ ({}x{})", + info.size.width, info.size.height, info.resources.layers, decoded_bytes(0) >> 20, + level_offset, host.size.width, host.size.height); + return host; +} + [[nodiscard]] VkImageCreateInfo MakeImageCreateInfo(const Device& device, const ImageInfo& info, std::optional format_override = {}) { auto format_info = @@ -527,6 +561,20 @@ TransformBufferCopies(std::span copies, size_t bu }; } +// A guest view's levels on a host image that keeps only levels >= host_level_offset. +[[nodiscard]] SubresourceRange HostRange(const Image& image, SubresourceRange range) { + const s32 skip = static_cast(image.host_level_offset); + if (skip == 0) { + return range; + } + const s32 host_levels = image.info.resources.levels - skip; + const s32 base = std::clamp(range.base.level - skip, 0, host_levels - 1); + const s32 end = std::clamp(range.base.level + range.extent.levels - skip, base + 1, host_levels); + range.base.level = base; + range.extent.levels = end - base; + return range; +} + [[nodiscard]] VkImageSubresourceRange MakeSubresourceRange(const ImageView* image_view) { SubresourceRange range = image_view->range; if (True(image_view->flags & VideoCommon::ImageViewFlagBits::Slice)) { @@ -1876,12 +1924,16 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu VAddr cpu_addr_) : VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler}, runtime{&runtime_}, - original_image(MakeImage(runtime_.device, runtime_.memory_allocator, info, + original_image(MakeImage(runtime_.device, runtime_.memory_allocator, + TrimmedHostInfo(runtime_.device, info, host_level_offset), runtime->ViewFormats(info.format))), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; } + if (host_level_offset != 0) { + flags |= VideoCommon::ImageFlagBits::TrimmedLevels; + } if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { switch (Settings::values.accelerate_astc.GetValue()) { case Settings::AstcDecodeMode::Gpu: @@ -1906,11 +1958,12 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str()); } current_image = &Image::original_image; - storage_image_views.resize(info.resources.levels); + const s32 host_levels = info.resources.levels - static_cast(host_level_offset); + storage_image_views.resize(host_levels); if (WillUseAcceleratedAstcDecode(runtime->device, info)) { const auto& device = runtime->device.GetLogical(); const VkFormat storage_format = VK_FORMAT_A8B8G8R8_UNORM_PACK32; - for (s32 level = 0; level < info.resources.levels; ++level) { + for (s32 level = 0; level < host_levels; ++level) { storage_image_views[level] = MakeStorageView(device, level, *original_image, storage_format); } @@ -2575,7 +2628,7 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI .viewType = VkImageViewType{}, .format = format_info.format, .components = swizzle_mapping, - .subresourceRange = MakeSubresourceRange(aspect_mask, info.range), + .subresourceRange = MakeSubresourceRange(aspect_mask, HostRange(image, info.range)), }; const auto create = [&](TextureType tex_type, std::optional num_layers) { VkImageViewCreateInfo ci{create_info}; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 846a6f7fc1..d0af53631d 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -46,6 +46,9 @@ enum class ImageFlagBits : u32 { AsynchronousDecode = 1 << 16, IsDecoding = 1 << 17, ///< Is currently being decoded asynchronously. Unbacked = 1 << 18, ///< Host image has no memory (too big to back); never upload/download. + /// The host image keeps only mip levels >= host_level_offset (its decoded copy would not fit + /// in VRAM); uploads skip the rest, downloads are never done. + TrimmedLevels = 1 << 19, }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) @@ -94,6 +97,7 @@ struct ImageBase { u32 unswizzled_size_bytes = 0; u32 converted_size_bytes = 0; u32 scale_rating = 0; + u32 host_level_offset = 0; ///< Guest mip level held by host mip 0 (see TrimmedLevels). 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 75005f0731..e1cb493697 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -597,7 +597,7 @@ void TextureCache

::WriteMemory(DAddr cpu_addr, size_t size) { template bool TextureCache

::IsDownloadable(const ImageBase& image) const noexcept { - if (!image.IsSafeGpuCopy() || True(image.flags & ImageFlagBits::Unbacked)) { + if (!image.IsSafeGpuCopy() || True(image.flags & (ImageFlagBits::Unbacked | ImageFlagBits::TrimmedLevels))) { return false; } if (image.info.num_samples == 1) { @@ -714,6 +714,14 @@ bool TextureCache

::BlitImage(const Tegra::Engines::Fermi2D::Surface& dst, Image& dst_image = slot_images[dst_id]; Image& src_image = slot_images[src_id]; + if (True(dst_image.flags & ImageFlagBits::Unbacked)) { + static u32 logged{}; + if (logged++ < 64) { + LOG_INFO(HW_GPU, "2D blit into unbacked image {:#x}: dst {:#x} {}x{} from {:#x} {}x{}", + dst_image.gpu_addr, dst.Address(), dst.width, dst.height, src.Address(), + src.width, src.height); + } + } bool is_src_rescaled = True(src_image.flags & ImageFlagBits::Rescaled); bool is_dst_rescaled = True(dst_image.flags & ImageFlagBits::Rescaled); @@ -1186,6 +1194,24 @@ void TextureCache

::UploadImageContents(Image& image, StagingBuffer& staging) const std::span mapped_span = staging.mapped_span; const GPUVAddr gpu_addr = image.gpu_addr; + if (True(image.flags & ImageFlagBits::AcceleratedUpload) && image.host_level_offset != 0) { + // Only levels >= host_level_offset exist on the host: read each layer's tail of the mip + // chain into a compact buffer (layer stride = that tail's size) and decode those levels. + 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; + for (s32 layer = 0; layer < image.info.resources.layers; ++layer) { + gpu_memory->ReadBlock(gpu_addr + layer * image.info.layer_stride + level_offset, + mapped_span.data() + layer * 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.AccelerateImageUpload(image, staging, uploads, 0, 0); + return; + } if (True(image.flags & ImageFlagBits::AcceleratedUpload)) { gpu_memory->ReadBlock(gpu_addr, mapped_span.data(), mapped_span.size_bytes(), VideoCommon::CacheType::NoTextureCache); @@ -1908,6 +1934,13 @@ ImageId TextureCache

::FindDMAImage(const ImageInfo& info, GPUVAddr gpu_addr) return true; } } else if (IsSubCopy(info, existing_image, gpu_addr)) { + if (True(existing_image.flags & ImageFlagBits::Unbacked)) { + static u32 logged{}; + if (logged++ < 64) { + LOG_INFO(HW_GPU, "DMA copy at {:#x} ({}x{}) into unbacked image {:#x}", gpu_addr, + info.size.width, info.size.height, existing_image.gpu_addr); + } + } image_id = existing_image_id; image_ids.push_back(existing_image_id); return true; @@ -2003,6 +2036,13 @@ ImageViewId TextureCache

::FindRenderTargetView(const ImageInfo& info, GPUVAdd base = image.TryFindBase(gpu_addr).value(); } const s32 layers = image.info.type == ImageType::e3D ? info.size.depth : info.resources.layers; + if (True(image.flags & ImageFlagBits::Unbacked)) { + static u32 logged{}; + if (logged++ < 64) { + LOG_INFO(HW_GPU, "Render target in unbacked image {:#x} at {:#x}: {}x{} level {} layer {}", + image.gpu_addr, gpu_addr, info.size.width, info.size.height, base.level, base.layer); + } + } const SubresourceRange range{ .base = base, .extent = {.levels = 1, .layers = layers}, @@ -2185,6 +2225,13 @@ ImageViewId TextureCache

::FindOrEmplaceImageView(ImageId image_id, const Imag if (const ImageViewId image_view_id = image.FindView(info); image_view_id) { return image_view_id; } + if (True(image.flags & ImageFlagBits::Unbacked)) { + // How the game uses an image too large to back (e.g. a sparse virtual texture). + LOG_INFO(HW_GPU, "View of unbacked image {:#x}: type {} format {} levels {}+{} layers {}+{}", + image.gpu_addr, static_cast(info.type), static_cast(info.format), + info.range.base.level, info.range.extent.levels, info.range.base.layer, + info.range.extent.layers); + } const ImageViewId image_view_id = slot_image_views.insert(runtime, info, image_id, image, slot_images); image.InsertView(info, image_view_id); return image_view_id; @@ -2682,7 +2729,7 @@ void TextureCache

::BindRenderTarget(ImageViewId* old_id, ImageViewId new_id) if (new_id) { const ImageViewBase& old_view = slot_image_views[new_id]; if (True(old_view.flags & ImageViewFlagBits::PreemtiveDownload) && - False(slot_images[old_view.image_id].flags & ImageFlagBits::Unbacked)) { + False(slot_images[old_view.image_id].flags & (ImageFlagBits::Unbacked | ImageFlagBits::TrimmedLevels))) { const PendingDownload new_download{true, 0, old_view.image_id}; uncommitted_downloads.emplace_back(new_download); } diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index 21070b2d25..aae2c7e4fd 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -1298,6 +1298,13 @@ void DeduceBlitImages(ImageInfo& dst_info, ImageInfo& src_info, const ImageBase* } u32 MapSizeBytes(const ImageBase& image) { + if (True(image.flags & ImageFlagBits::AcceleratedUpload) && image.host_level_offset != 0) { + // Each layer's mip chain from host_level_offset on (see UploadImageContents). + const auto uploads = FullUploadSwizzles(image.info); + const u32 tail = image.info.layer_stride - + static_cast(uploads[image.host_level_offset].buffer_offset); + return tail * image.info.resources.layers; + } if (True(image.flags & ImageFlagBits::AcceleratedUpload)) { return image.guest_size_bytes; } else if (True(image.flags & ImageFlagBits::Converted)) { -- cgit v1.2.3 From 55d315c644fdf36405daaf71f75942553a267a6e Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:08:43 -0400 Subject: ASTC: re-compress to BC3 except huge arrays, which stay RGBA8 and trimmed Title 1 has ~700 ASTC 8x8 textures (~280 MB as stored); decoded to RGBA8 they took ~4.5 GB, pushing Eden past the 8 GB of an RTX 4070 Laptop (8.4-9.8 GB in use) and the texture cache into permanent aggressive collection. With astc_recompression=Bc3 (CPU, asynchronous) they fit (4.6-5.6 GB). The two 8192x8192x68 terrain arrays would still not fit as BC3, so ASTC 2D arrays over 1 GiB as RGBA8 always use the GPU decoder with trimmed levels (RGBA8 host image, UNORM/sRGB views), and the decoder pass now exists whenever the GPU lacks native ASTC. Also logs every 600 frames: device memory against the collector thresholds, the largest images, totals by format, and buffer cache totals. Title screen 65-150 s: 10.1 -> 16.0 fps. --- src/video_core/buffer_cache/buffer_cache.h | 16 +++++++ .../renderer_vulkan/vk_texture_cache.cpp | 51 ++++++++++++++++++++-- src/video_core/texture_cache/texture_cache.h | 40 +++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 126e73c9b6..4df855c0fe 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "common/range_sets.inc" #include "video_core/buffer_cache/buffer_cache_base.h" @@ -79,6 +81,20 @@ void BufferCache

::TickFrame() { return; } runtime.TickFrame(slot_buffers); + if (static u64 frames{}; ++frames % 600 == 0) { + u64 total{}; + size_t count{}; + std::vector> largest; + for (const auto [buffer_id, buffer] : slot_buffers) { + total += buffer->SizeBytes(); + ++count; + largest.emplace_back(buffer->SizeBytes(), buffer->CpuAddr()); + } + std::partial_sort(largest.begin(), largest.begin() + std::min(6, largest.size()), + largest.end(), std::greater<>{}); + LOG_INFO(HW_GPU, "Buffer cache: {} buffers {} MiB; largest {}", count, total >> 20, + fmt::join(largest.begin(), largest.begin() + std::min(6, largest.size()), ", ")); + } // Calculate hits and shots and move hit bits to the right const u32 hits = std::reduce(channel_state->uniform_cache_hits.begin(), diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index d3f255f05d..12d985fb12 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -148,10 +148,41 @@ constexpr VkBorderColor ConvertBorderColor(const std::array& color) { return usage; } +// An ASTC 2D array whose RGBA8 decode exceeds 1 GiB (title 1's 8192x8192x68 terrain textures). +// These stay on the GPU decoder as RGBA8 with trimmed levels (TrimmedHostInfo) even when other +// ASTC images are re-compressed to BC1/BC3 on the CPU: re-compressing them would not fit either. +[[nodiscard]] bool IsHugeAstcArray(const Device& device, const ImageInfo& info) { + if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported() || + info.type != ImageType::e2D || info.size.depth != 1) { + return false; + } + u64 bytes = 0; + for (s32 level = 0; level < info.resources.levels; ++level) { + bytes += u64{(std::max)(info.size.width >> level, 1U)} * + (std::max)(info.size.height >> level, 1U) * 4; + } + return bytes * info.resources.layers > (u64{1} << 30); +} + +// The host format of a huge ASTC array: the GPU decoder's RGBA8 (sRGB kept for sampling). +[[nodiscard]] std::optional HugeAstcHostFormat(const Device& device, const ImageInfo& info) { + if (!IsHugeAstcArray(device, info)) { + return std::nullopt; + } + return VideoCore::Surface::IsPixelFormatSRGB(info.format) ? VK_FORMAT_A8B8G8R8_SRGB_PACK32 + : VK_FORMAT_A8B8G8R8_UNORM_PACK32; +} + +constexpr std::array HugeAstcViewFormats{VK_FORMAT_A8B8G8R8_UNORM_PACK32, + VK_FORMAT_A8B8G8R8_SRGB_PACK32}; + [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) { return false; } + if (IsHugeAstcArray(device, info)) { + return true; + } if (Settings::values.accelerate_astc.GetValue() != Settings::AstcDecodeMode::Gpu) { return false; } @@ -990,7 +1021,9 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_}, staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_}, render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} { - if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu) { + // Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting. + if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu || + !device.IsOptimalAstcSupported()) { astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); } @@ -1926,7 +1959,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu runtime{&runtime_}, original_image(MakeImage(runtime_.device, runtime_.memory_allocator, TrimmedHostInfo(runtime_.device, info, host_level_offset), - runtime->ViewFormats(info.format))), + IsHugeAstcArray(runtime_.device, info) + ? std::span{HugeAstcViewFormats} + : runtime->ViewFormats(info.format), + HugeAstcHostFormat(runtime_.device, info))), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; @@ -1934,7 +1970,11 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu if (host_level_offset != 0) { flags |= VideoCommon::ImageFlagBits::TrimmedLevels; } - if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { + if (IsHugeAstcArray(runtime->device, info)) { + flags |= VideoCommon::ImageFlagBits::AcceleratedUpload; + flags |= VideoCommon::ImageFlagBits::Converted; + flags |= VideoCommon::ImageFlagBits::CostlyLoad; + } else if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { switch (Settings::values.accelerate_astc.GetValue()) { case Settings::AstcDecodeMode::Gpu: if (WillUseAcceleratedAstcDecode(runtime->device, info)) { @@ -2580,6 +2620,11 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI } } auto format_info = MaxwellToVK::SurfaceFormat(*device, FormatType::Optimal, true, format); + if (IsHugeAstcArray(*device, image.info)) { + format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format) + ? VK_FORMAT_A8B8G8R8_SRGB_PACK32 + : VK_FORMAT_A8B8G8R8_UNORM_PACK32; + } if (device->ApiVersion() >= VK_API_VERSION_1_3) { const VkFormatProperties3 properties3 = device->GetPhysical().GetFormatProperties3(format_info.format); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index e1cb493697..11de37eb6b 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -6,7 +6,10 @@ #pragma once +#include #include +#include +#include #include #include #include "common/container/unordered_map.h" @@ -173,6 +176,43 @@ void TextureCache

::TickFrame() { if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); } + // Every 600 frames: device memory in use against the collector's thresholds, and what the + // images themselves account for (VRAM pressure makes the collector thrash). + if (static u64 frames{}; ++frames % 600 == 0) { + u64 image_bytes{}; + size_t images{}; + std::vector> largest; + std::map> by_format; + for (const auto [image_id, image] : slot_images) { + const u64 bytes = GetScaledImageSizeBytes(*image); + image_bytes += bytes; + ++images; + largest.emplace_back(bytes, image); + auto& format = by_format[static_cast(image->info.format)]; + format.first += bytes; + ++format.second; + } + LOG_INFO(HW_GPU, "Texture cache: device memory {} MiB (expected {} MiB, critical {} MiB), {} images {} MiB", + total_used_memory >> 20, expected_memory >> 20, critical_memory >> 20, images, + image_bytes >> 20); + std::partial_sort(largest.begin(), largest.begin() + std::min(12, largest.size()), + largest.end(), [](const auto& a, const auto& b) { return a.first > b.first; }); + for (size_t i = 0; i < std::min(12, largest.size()); ++i) { + const Image& image = *largest[i].second; + LOG_INFO(HW_GPU, " {} MiB: {:#x} {}x{}x{} layers {} levels {} format {} type {} flags {:#x}", + largest[i].first >> 20, image.gpu_addr, image.info.size.width, image.info.size.height, + image.info.size.depth, image.info.resources.layers, image.info.resources.levels, + static_cast(image.info.format), static_cast(image.info.type), + static_cast(image.flags)); + } + std::vector> formats; + for (const auto& [format, total] : by_format) formats.emplace_back(total.first, format); + std::sort(formats.rbegin(), formats.rend()); + for (size_t i = 0; i < std::min(8, formats.size()); ++i) { + LOG_INFO(HW_GPU, " format {}: {} MiB in {} images", formats[i].second, formats[i].first >> 20, + by_format[formats[i].second].second); + } + } if (total_used_memory > minimum_memory) { RunGarbageCollector(); } -- cgit v1.2.3 From a6083a697311fe9cfa48489e03f50c14f38a3aff Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:34:48 -0400 Subject: Texture cache: fix use-after-free in async texture decode TickAsyncDecode locked a finished decode's mutex, erased the context (freeing the mutex) and only then let the lock go out of scope, unlocking freed memory. With ASTC re-compressed on the CPU (accelerate_astc=CpuAsynchronous) title 1 hit this a few minutes in: cdb reported "Free Heap block modified after it was freed" at the mutex offset, and a run without the debugger crashed. The lock is now released before the erase. Also, deleting an image while it is being decoded (deletions other than the collector, which already skips them) cancels the decode instead of uploading into a freed slot. Verified under cdb past 330 s (failed at 243 s twice before). --- src/video_core/texture_cache/texture_cache.h | 22 +++++++++++++++++++--- src/video_core/texture_cache/texture_cache_base.h | 1 + 2 files changed, 20 insertions(+), 3 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 11de37eb6b..de15e13da3 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1499,9 +1499,17 @@ void TextureCache

::TickAsyncDecode() { auto i = async_decodes.begin(); while (i != async_decodes.end()) { auto* async_decode = i->get(); - std::unique_lock lock{async_decode->mutex}; - if (!async_decode->complete) { - ++i; + { + // Release the lock before the context is erased below: unlocking it afterwards wrote + // into freed memory (heap corruption, crashes minutes into title 1). + std::unique_lock lock{async_decode->mutex}; + if (!async_decode->complete) { + ++i; + continue; + } + } + if (async_decode->cancelled) { + i = async_decodes.erase(i); continue; } Image& image = slot_images[async_decode->image_id]; @@ -2452,6 +2460,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; + } + } + } if (image.HasScaled()) { total_used_memory -= GetScaledImageSizeBytes(image); } diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index d103d66189..ed9758fdee 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -68,6 +68,7 @@ struct AsyncDecodeContext { boost::container::small_vector copies; std::mutex mutex; std::atomic_bool complete; + bool cancelled{}; ///< The image was deleted meanwhile: drop the result (GPU thread only). }; using TextureCacheGPUMap = ::Common::unordered_map, Common::IdentityHash>; -- cgit v1.2.3 From 5220e4aa539617b83b0b4f2a1666dd068616830e Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:36:20 -0400 Subject: Texture cache: log VMA blocks per memory type with the periodic memory log Device memory use is ~3 GB above what the images account for; this shows where. The per-decode log drops to debug level. --- .../renderer_vulkan/vk_texture_cache.cpp | 4 ++++ src/video_core/renderer_vulkan/vk_texture_cache.h | 2 ++ src/video_core/texture_cache/texture_cache.h | 5 ++++- .../vulkan_common/vulkan_memory_allocator.cpp | 26 ++++++++++++++++++++++ .../vulkan_common/vulkan_memory_allocator.h | 3 +++ 5 files changed, 39 insertions(+), 1 deletion(-) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 12d985fb12..1d5fa2ced7 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1895,6 +1895,10 @@ bool TextureCacheRuntime::CanReportMemoryUsage() const { return device.CanReportMemoryUsage(); } +void TextureCacheRuntime::LogMemoryStatistics() const { + Vulkan::LogMemoryStatistics(device); +} + void TextureCacheRuntime::FlushDeferredClear() { scheduler.FlushDeferredClear(); } diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 2e9736f707..4b03c80a5a 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -68,6 +68,8 @@ public: bool CanReportMemoryUsage() const; + void LogMemoryStatistics() const; + bool CanDownloadMsaa(const VideoCommon::ImageInfo& info) const; [[nodiscard]] VkImage AcquireMsaaScratchImage(const VkImageCreateInfo& image_ci); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index de15e13da3..7382ef5e4f 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -212,6 +212,9 @@ void TextureCache

::TickFrame() { LOG_INFO(HW_GPU, " format {}: {} MiB in {} images", formats[i].second, formats[i].first >> 20, by_format[formats[i].second].second); } + if constexpr (requires { runtime.LogMemoryStatistics(); }) { + runtime.LogMemoryStatistics(); + } } if (total_used_memory > minimum_memory) { RunGarbageCollector(); @@ -1451,7 +1454,7 @@ u64 TextureCache

::GetScaledImageSizeBytes(const ImageBase& image) { template void TextureCache

::QueueAsyncDecode(Image& image, ImageId image_id) { UNIMPLEMENTED_IF(False(image.flags & ImageFlagBits::Converted)); - LOG_INFO(HW_GPU, "Queuing async texture decode"); + LOG_DEBUG(HW_GPU, "Queuing async texture decode {:#x}", image.gpu_addr); image.flags |= ImageFlagBits::IsDecoding; auto decode = std::make_unique(); diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index aafb3183b6..23f1b0a798 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -451,4 +452,29 @@ MemoryCommit MemoryAllocator::Commit(const vk::Buffer &buffer, MemoryUsage usage return MemoryCommit(allocator, a, info); } +void LogMemoryStatistics(const Device& device) { + const VmaAllocator allocator = device.GetAllocator(); + const VkPhysicalDeviceMemoryProperties* properties{}; + vmaGetMemoryProperties(allocator, &properties); + VmaTotalStatistics stats{}; + vmaCalculateStatistics(allocator, &stats); + for (u32 type = 0; type < properties->memoryTypeCount; ++type) { + const VmaStatistics& s = stats.memoryType[type].statistics; + if (s.blockCount == 0) { + continue; + } + LOG_INFO(Render_Vulkan, "VMA type {} (heap {}, flags {:#x}): {} blocks {} MiB, {} allocations {} MiB", + type, properties->memoryTypes[type].heapIndex, + static_cast(properties->memoryTypes[type].propertyFlags), s.blockCount, + s.blockBytes >> 20, s.allocationCount, s.allocationBytes >> 20); + } + std::array budgets{}; + vmaGetHeapBudgets(allocator, budgets.data()); + for (u32 heap = 0; heap < properties->memoryHeapCount; ++heap) { + LOG_INFO(Render_Vulkan, "VMA heap {}: usage {} MiB of budget {} MiB, VMA blocks {} MiB", + heap, budgets[heap].usage >> 20, budgets[heap].budget >> 20, + budgets[heap].statistics.blockBytes >> 20); + } +} + } // namespace Vulkan diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index a0f9081fc0..7467e929d3 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -142,4 +142,7 @@ namespace Vulkan { u32 valid_memory_types{~0u}; }; + /// Logs VMA's blocks and allocations per memory type, and each heap's budget. + void LogMemoryStatistics(const Device& device); + } // namespace Vulkan -- cgit v1.2.3 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. --- NX2_NOTES.md | 9 +- src/video_core/renderer_vulkan/vk_compute_pass.cpp | 31 +++++- .../renderer_vulkan/vk_texture_cache.cpp | 35 +++++- src/video_core/renderer_vulkan/vk_texture_cache.h | 3 + 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 + src/video_core/textures/astc.cpp | 65 +++++++++++ src/video_core/textures/astc.h | 3 + 11 files changed, 276 insertions(+), 34 deletions(-) (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/NX2_NOTES.md b/NX2_NOTES.md index bbabc71ce8..fcbbadb349 100644 --- a/NX2_NOTES.md +++ b/NX2_NOTES.md @@ -151,16 +151,21 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in | 93 | title 1 colour cast (magenta/cyan, red/green, changing per course) | the sky/atmosphere 32^3 volume shader starts with an unguarded R2UR; R2UR/MATCH/HFMA2.RELU saved P6 with the uniform-guard mask `1 << (6 - 7)` (undefined), which shifted into the opcode bits | always save/restore P6 (2ed0c48). Found with `EDEN_NX2_DUMP` (flush GPU ranges at a present to files) and `nx2work/vol3d.py` / `volslices.py` (untile Switch 2 3D textures) | | 94 | near terrain black | 8192x8192x68 ASTC 8x8 virtual textures: 1.5 GB in guest memory, 23 GB each as Eden's RGBA8 decode | huge GPU-decoded ASTC arrays keep levels from the first that fits 512 MiB (here 3) on the host (60c4791) | | 95 | wind/foliage shards | SM70+ MUFU.SIN/COS take turns (sin(2pi x)); Eden's Maxwell MUFU takes radians | scale by 2pi (245e6ad); jungle canopy not re-checked yet | -| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player reads "video-frame-count" (MediaProperty, main+0x19e5860) and requires < 9; the game itself sets it from a count of registered entries (main+0x7107c0 adds, main+0x70f620 removes) that only grows: in-game movies never finish (no NVDEC/H.264 decode activity in the log) | open: Switch 2 movie/H.264 decode path. `nx2work/nso_plt.py` names PLT imports | +| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player (main+0x19e5860) lays out its decoder frames with nn::codec::CalculateDecoderImagePoolBuffer, which sends nx2:gsv 991; auto-stubbed, the reply kept the request words, so the frame size read as 0x167027f'00000001 and the budget check at main+0x19e5b58 failed. (Not the "video-frame-count" < 9 check: the game sets that from a two-slot registry, at most 2.) | gsv 991 answers a block-linear NV12 size (640x360 -> 0x64000) | | 97 | pop-in, objects flicker in and out, low-quality meshes | high-detail LODs drawn through task/mesh programs (bound in stage slot 1, header type VS, ISBERD/ISBEWR), which are rejected and skipped | open: task/mesh support | | 98 | ~12 fps at the title | host GPU ~20% busy, emulated cores ~0.9 host cores, GPU thread 45-70% busy, much of it waiting on the host GPU: semaphore acquires wait for host fences under GPU accuracy High; ASTC uploads end in Finish() (a Flush() there crashed at boot); a 1 ms poll sleep while channels are suspended | gpu_accuracy=Low: ~16 fps; SDL main loop capped at 240 Hz (88f04ad). Open: ASTC upload without a full stall (and per-layer re-upload of the trimmed virtual textures), native MME interpreter cost, draw path | | 99 | VRAM over the card (8.4-9.8 GB used on an 8 GB RTX 4070 Laptop), texture cache always collecting | ~700 ASTC 8x8 textures decoded to RGBA8 (16x their size, ~4.5 GB) | astc_recompression=Bc3 + accelerate_astc=CpuAsynchronous in the config; huge ASTC arrays stay on the GPU decoder as trimmed RGBA8 (ed0a3c9): 4.6-5.6 GB, ~10 -> 16 fps | | 100 | 8 ImageTranscode threads at ~44% each (3.5 cores) | Eden's default pool is half the hardware threads | an eighth, at least 2 (`EDEN_NX2_TRANSCODE_THREADS` overrides): ~19 fps on the attract loop | | 101 | Medal's capture hook inside Eden (`medal_hook64!OBS_Negotiate` in GPU-thread stacks) | Medal registers a per-user Vulkan implicit layer that loads into every Vulkan app | launches set `DISABLE_VULKAN_MEDAL_OBS_CAPTURE=1` (the layer's own off switch) | | 102 | host crash (log stops, segfault) at 113 s in one run; under cdb "Free Heap block modified after it was freed" at 243 s, twice | Eden's TickAsyncDecode erased a finished decode while still holding its mutex (unlocked freed memory); only reached with accelerate_astc=CpuAsynchronous (the BC3 setup) | release the lock first, cancel decodes of deleted images (8766a72); clean past 330 s. Also made nx2 service callbacks (vsync timer, syncpt actions, capture/dump) independent of their sessions' lifetime (190747a) | +| 103 | (title 1) texture churn: the collector evicted ~1,250 decoded ASTC textures per 600 frames and they were decoded again (lag and pop-in turning the camera) | device memory sat above the collector's critical line (5.4 GB of a 7.0 GB budget): ASTC 8x8 is 4x larger as BC3, and a bloom chain at unmapped GPU 0xf0000-0x15b600 was re-created on every use (each lookup got a fresh stand-in CPU address), ~5,400 images per 600 frames on the map | unmapped GPU addresses get a stand-in address fixed by the GPU address, so images are found again; ASTC whose blocks carry no alpha (exact endpoint-mode scan, ~70% of them) goes to BC1 (`EDEN_NX2_ASTC_BC1=0` turns it off). Attract loop: 4.8-5.1 GB, no costly evictions, ~17 -> 22-23 fps | +| 104 | garbage textures (title logo, road patches) and a host crash 90-120 s in that skipped the crash handler (fail-fast) | an image re-decoded while a decode was in flight has two; the first to finish cleared IsDecoding, so deleting the image did not cancel the second, which was then uploaded into the next image to take the slot (another texture's data; a staging overrun when that image was smaller) | every pending decode of a deleted image is cancelled; a decode larger than its image is dropped and logged. Frames clean; crash: verifying with long runs | +| 105 | (title 1) movie abort moved on: `nn::codec::InitializeVp9Decoder` fails with 2001-0114 (InvalidHandle) | gsv 901/902/1001/1002 are auto-stubbed and hand back handle 0 | open: the Switch 2 video decoder service (VP9) | Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4` -(12 GB). Debug knobs: `EDEN_NX2_AUTO_STUB`, `EDEN_NX2_IPC_TRACE`, `EDEN_NX2_SVC_TRACE`, +(12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds), +`EDEN_NX2_ASTC_FLUSH=1` (GPU ASTC decode without the full wait), `EDEN_NX2_CRASH_DUMP=

` (prints the +faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_NX2_IPC_TRACE`, `EDEN_NX2_SVC_TRACE`, `EDEN_NX2_PULLER_TRACE`, `EDEN_NX2_METHOD_TRACE`, `EDEN_NX2_HIDE_WINDOW`, `EDEN_NX2_FRAME_CAPTURE=` (every 120th presented frame as TGA), `EDEN_NX2_PB_CHECK` (pushbuffer entries changed between submit and execution), `EDEN_NX2_QUERY_TRACE` (every GPU diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index 65e0d5f145..ba521520a0 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -5,6 +5,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include "common/assert.h" #include "common/common_types.h" +#include "common/logging.h" #include "common/div_ceil.h" #include "common/vector_math.h" #include "video_core/host_shaders/astc_decoder_comp_spv.h" @@ -627,7 +630,33 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, 0, image_barrier); }); - scheduler.Finish(); + // Each decode ends with the GPU thread waiting for the host GPU to go idle. Measured here + // (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=1 submits without waiting instead. + static const bool flush_only = [] { + const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH"); + return value != nullptr && value[0] == '1'; + }(); + const auto start = std::chrono::steady_clock::now(); + if (flush_only) { + scheduler.Flush(); + } else { + scheduler.Finish(); + } + const auto end = std::chrono::steady_clock::now(); + static u32 decodes{}; + static std::chrono::nanoseconds waited{}; + static auto window_start = end; + ++decodes; + waited += end - start; + if (end - window_start >= std::chrono::seconds{5}) { + LOG_INFO(Render_Vulkan, "ASTC GPU decodes: {} in {} ms, {} ms waiting ({})", decodes, + std::chrono::duration_cast(end - window_start).count(), + std::chrono::duration_cast(waited).count(), + flush_only ? "flush" : "finish"); + decodes = 0; + waited = {}; + window_start = end; + } } constexpr u32 BL3D_BINDING_INPUT_BUFFER = 0; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 1d5fa2ced7..cfc6483a04 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -176,6 +177,22 @@ constexpr VkBorderColor ConvertBorderColor(const std::array& color) { constexpr std::array HugeAstcViewFormats{VK_FORMAT_A8B8G8R8_UNORM_PACK32, VK_FORMAT_A8B8G8R8_SRGB_PACK32}; +// Opaque ASTC (ImageInfo::astc_opaque) re-compressed to BC1 instead of BC3. +[[nodiscard]] bool IsOpaqueBc1Astc(const Device& device, const ImageInfo& info) { + return info.astc_opaque && IsPixelFormatASTC(info.format) && !IsHugeAstcArray(device, info); +} + +constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM_BLOCK, + VK_FORMAT_BC1_RGBA_SRGB_BLOCK}; + +[[nodiscard]] std::optional AstcHostFormat(const Device& device, const ImageInfo& info) { + if (IsOpaqueBc1Astc(device, info)) { + return VideoCore::Surface::IsPixelFormatSRGB(info.format) ? VK_FORMAT_BC1_RGBA_SRGB_BLOCK + : VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + } + return HugeAstcHostFormat(device, info); +} + [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) { return false; @@ -1899,6 +1916,16 @@ void TextureCacheRuntime::LogMemoryStatistics() const { Vulkan::LogMemoryStatistics(device); } +bool TextureCacheRuntime::UseOpaqueAstcFormat() const { + // EDEN_NX2_ASTC_BC1=0 keeps every re-compressed ASTC image in BC3. + static const bool enabled = [] { + const char* value = std::getenv("EDEN_NX2_ASTC_BC1"); + return value == nullptr || value[0] != '0'; + }(); + return enabled && !device.IsOptimalAstcSupported() && + Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3; +} + void TextureCacheRuntime::FlushDeferredClear() { scheduler.FlushDeferredClear(); } @@ -1965,8 +1992,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu TrimmedHostInfo(runtime_.device, info, host_level_offset), IsHugeAstcArray(runtime_.device, info) ? std::span{HugeAstcViewFormats} + : IsOpaqueBc1Astc(runtime_.device, info) + ? std::span{OpaqueAstcViewFormats} : runtime->ViewFormats(info.format), - HugeAstcHostFormat(runtime_.device, info))), + AstcHostFormat(runtime_.device, info))), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; @@ -2628,6 +2657,10 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format) ? VK_FORMAT_A8B8G8R8_SRGB_PACK32 : VK_FORMAT_A8B8G8R8_UNORM_PACK32; + } else if (IsOpaqueBc1Astc(*device, image.info) && IsPixelFormatASTC(format)) { + format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format) + ? VK_FORMAT_BC1_RGBA_SRGB_BLOCK + : VK_FORMAT_BC1_RGBA_UNORM_BLOCK; } if (device->ApiVersion() >= VK_API_VERSION_1_3) { const VkFormatProperties3 properties3 = diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 4b03c80a5a..1d171b576f 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -70,6 +70,9 @@ public: void LogMemoryStatistics() const; + /// True when ASTC is re-compressed to BC3 and opaque images may use BC1 instead. + bool UseOpaqueAstcFormat() const; + bool CanDownloadMsaa(const VideoCommon::ImageInfo& info) const; [[nodiscard]] VkImage AcquireMsaaScratchImage(const VkImageCreateInfo& image_ci); 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; diff --git a/src/video_core/textures/astc.cpp b/src/video_core/textures/astc.cpp index 0b6d06c49a..950ce28d6b 100644 --- a/src/video_core/textures/astc.cpp +++ b/src/video_core/textures/astc.cpp @@ -2038,4 +2038,69 @@ void Decompress(std::span data, uint32_t width, uint32_t height, } } +bool IsOpaque(std::span data) { + // Endpoint modes with an alpha channel: LDR luminance+alpha (4, 5), RGB scale + two alpha + // (10), LDR RGBA (12, 13) and HDR RGBA (14, 15). + constexpr u32 AlphaModes = (1U << 4) | (1U << 5) | (1U << 10) | (1U << 12) | (1U << 13) | + (1U << 14) | (1U << 15); + for (size_t offset = 0; offset + 16 <= data.size(); offset += 16) { + const std::span block = data.subspan(offset, 16); + InputBitStream strm(block); + const TexelWeightParams params = DecodeBlockInfo(strm); + if (params.m_bError) { + continue; // decodes to the opaque error colour + } + if (params.m_bVoidExtentLDR || params.m_bVoidExtentHDR) { + // One RGBA16 colour in bits 64-127: UNORM16, or FP16 for HDR. + const u32 alpha = block[14] | (u32{block[15]} << 8); + if (alpha != (params.m_bVoidExtentHDR ? 0x3C00U : 0xFFFFU)) { + return false; + } + continue; + } + const u32 partitions = strm.ReadBits<2>() + 1; + if (partitions == 4 && params.m_bDualPlane) { + continue; + } + if (partitions == 1) { + if ((AlphaModes >> strm.ReadBits<4>()) & 1) { + return false; + } + continue; + } + strm.ReadBits<10>(); // partition index + const u32 base_cem = strm.ReadBits<6>(); + const u32 base_mode = base_cem & 3; + if (base_mode == 0) { + if ((AlphaModes >> (base_cem >> 2)) & 1) { + return false; + } + continue; + } + // The rest of the per-partition modes sit just below the weights (DecompressBlock). + const u32 weight_bits = params.GetPackedBitSize(); + if (weight_bits < 24 || weight_bits > 96) { + continue; + } + const u32 extra_bits = partitions == 2 ? 2 : partitions == 3 ? 5 : 8; + const u32 extra_offset = 128 - weight_bits - extra_bits; + u32 extra = 0; + for (u32 bit = 0; bit < extra_bits; ++bit) { + const u32 position = extra_offset + bit; + extra |= ((block[position / 8] >> (position % 8)) & 1U) << bit; + } + u32 cem = ((extra << 6) | base_cem) >> 2; + const u32 classes = cem; + cem >>= partitions; + for (u32 i = 0; i < partitions; ++i) { + const u32 mode = ((base_mode - (((classes >> i) & 1) != 0 ? 0 : 1)) << 2) | + ((cem >> (2 * i)) & 3); + if ((AlphaModes >> mode) & 1) { + return false; + } + } + } + return true; +} + } // namespace Tegra::Texture::ASTC diff --git a/src/video_core/textures/astc.h b/src/video_core/textures/astc.h index afd3933c3e..95c2c7ffea 100644 --- a/src/video_core/textures/astc.h +++ b/src/video_core/textures/astc.h @@ -8,4 +8,7 @@ namespace Tegra::Texture::ASTC { void Decompress(std::span data, uint32_t width, uint32_t height, uint32_t depth, uint32_t block_width, uint32_t block_height, std::span output); +/// True if no 16-byte block in `data` decodes to alpha below 1. +bool IsOpaque(std::span data); + } // namespace Tegra::Texture::ASTC -- cgit v1.2.3 From 0d5534a2883329183ae8aa89b3fe1c7b67291ece Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:43:01 -0400 Subject: ASTC: decode and re-compress to BC1/BC3 on the GPU; texture cache budget - accelerate_astc=Gpu with astc_recompression=Bc1/Bc3 now decodes ASTC with the GPU decoder into a temporary RGBA8 image (dedicated memory, freed once the GPU is past it) and compresses it with a new compute shader (bc_encoder.comp: bounding-box endpoints along the colours' covariance, inset 1/16, nearest palette entry; BC3 alpha ramp; BC1 punch-through), then copies the blocks into the image. No CPU decode, so streamed textures are ready the frame they are first used instead of showing placeholders for seconds; the decode pass no longer waits for the GPU on this path. - The CPU path (CpuAsynchronous) fills a new image with zero blocks before its first decode instead of showing stale VRAM, and the transcode pool is a quarter of the hardware threads. - Switch 2 titles: collector thresholds near the device budget, re-read every 60 frames from VK_EXT_memory_budget (another game shrank it from 7.0 to 5.4 GB). - EDEN_NX2_HIDE_WINDOW=0 now leaves the window visible. --- src/video_core/host_shaders/CMakeLists.txt | 1 + src/video_core/host_shaders/bc_encoder.comp | 183 +++++++++++++++++++++ src/video_core/renderer_vulkan/vk_compute_pass.cpp | 129 +++++++++++++-- src/video_core/renderer_vulkan/vk_compute_pass.h | 34 ++++ .../renderer_vulkan/vk_texture_cache.cpp | 123 +++++++++++++- src/video_core/renderer_vulkan/vk_texture_cache.h | 17 ++ src/video_core/texture_cache/image_base.h | 1 + src/video_core/texture_cache/texture_cache.h | 61 ++++++- src/video_core/texture_cache/texture_cache_base.h | 7 + src/video_core/texture_cache/util.cpp | 29 ++++ src/video_core/texture_cache/util.h | 3 + src/video_core/textures/workers.cpp | 9 +- src/video_core/vulkan_common/vulkan_device.cpp | 14 ++ src/video_core/vulkan_common/vulkan_device.h | 3 + .../vulkan_common/vulkan_memory_allocator.cpp | 6 +- .../vulkan_common/vulkan_memory_allocator.h | 4 +- src/yuzu_cmd/emu_window/emu_window_sdl3.h | 5 +- 17 files changed, 596 insertions(+), 33 deletions(-) create mode 100644 src/video_core/host_shaders/bc_encoder.comp (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index c4e7ac3f6f..8082e830d6 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -15,6 +15,7 @@ set(GLSL_INCLUDES set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp + ${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag ${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag diff --git a/src/video_core/host_shaders/bc_encoder.comp b/src/video_core/host_shaders/bc_encoder.comp new file mode 100644 index 0000000000..3a87e92dcb --- /dev/null +++ b/src/video_core/host_shaders/bc_encoder.comp @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Compresses one level of an RGBA8 image (the ASTC decoder's output) to BC1 or BC3, one 4x4 block +// per invocation, so re-compressed ASTC never round-trips through the CPU. Colour endpoints are +// the block's bounding box, oriented along the colours' covariance and inset by 1/16; each texel +// takes the nearest palette entry. Values are encoded as stored (sRGB data stays sRGB, like the +// CPU path). + +#version 460 core + +layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout (set = 0, binding = 0, rgba8) uniform readonly image2DArray source; + +layout (std430, set = 0, binding = 1) writeonly buffer Output { + uvec2 blocks[]; +}; + +layout (push_constant) uniform PushConstants { + uint blocks_x; + uint blocks_y; + uint width; + uint height; + uint bc3; + uint out_offset; // in 8-byte units +}; + +uint Pack565(vec3 color) { + const uvec3 q = uvec3(round(clamp(color, 0.0, 1.0) * vec3(31.0, 63.0, 31.0))); + return (q.r << 11) | (q.g << 5) | q.b; +} + +vec3 Unpack565(uint value) { + const uvec3 q = uvec3((value >> 11) & 31u, (value >> 5) & 63u, value & 31u); + const uvec3 e = uvec3((q.r << 3) | (q.r >> 2), (q.g << 2) | (q.g >> 4), (q.b << 3) | (q.b >> 2)); + return vec3(e) / 255.0; +} + +float Distance2(vec3 a, vec3 b) { + const vec3 d = a - b; + return dot(d, d); +} + +// BC1 colour block. With punch_through (BC1 only), texels with alpha < 0.5 use the transparent +// index of the three-colour mode; otherwise the four-colour mode is used (BC3 always decodes so). +uvec2 EncodeColor(in vec4 texels[16], bool punch_through) { + vec3 lo = vec3(1.0); + vec3 hi = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + lo = min(lo, texels[i].rgb); + hi = max(hi, texels[i].rgb); + } + if (any(greaterThan(lo, hi))) { + // Every texel transparent: colour0 == colour1 selects three-colour mode, index 3. + return uvec2(0u, 0xFFFFFFFFu); + } + const vec3 center = (lo + hi) * 0.5; + const vec3 extent = hi - lo; + const int major = extent.g >= extent.r && extent.g >= extent.b ? 1 : (extent.r >= extent.b ? 0 : 2); + vec3 covariance = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + const vec3 d = texels[i].rgb - center; + covariance += d * d[major]; + } + vec3 e0 = hi; + vec3 e1 = lo; + for (int c = 0; c < 3; ++c) { + if (covariance[c] < 0.0) { + e0[c] = lo[c]; + e1[c] = hi[c]; + } + } + const vec3 inset = (e0 - e1) / 16.0; + e0 -= inset; + e1 += inset; + + uint q0 = Pack565(e0); + uint q1 = Pack565(e1); + // Four-colour mode needs colour0 > colour1, three-colour mode colour0 <= colour1. + if (punch_through ? q0 > q1 : q0 < q1) { + const uint t = q0; + q0 = q1; + q1 = t; + } + if (!punch_through && q0 == q1) { + return uvec2(q0 | (q1 << 16), 0u); + } + vec3 palette[4]; + palette[0] = Unpack565(q0); + palette[1] = Unpack565(q1); + if (punch_through) { + palette[2] = (palette[0] + palette[1]) * 0.5; + palette[3] = vec3(0.0); + } else { + palette[2] = (2.0 * palette[0] + palette[1]) / 3.0; + palette[3] = (palette[0] + 2.0 * palette[1]) / 3.0; + } + const int entries = punch_through ? 3 : 4; + uint indices = 0u; + for (int i = 0; i < 16; ++i) { + uint best = 0u; + if (punch_through && texels[i].a < 0.5) { + best = 3u; + } else { + float best_distance = Distance2(texels[i].rgb, palette[0]); + for (int p = 1; p < entries; ++p) { + const float distance = Distance2(texels[i].rgb, palette[p]); + if (distance < best_distance) { + best_distance = distance; + best = uint(p); + } + } + } + indices |= best << (2 * i); + } + return uvec2(q0 | (q1 << 16), indices); +} + +// BC3 alpha block: eight-value mode (alpha0 > alpha1), indices 0 = alpha0, 1 = alpha1, 2..7 the +// ramp from alpha0 to alpha1. +uvec2 EncodeAlpha(in vec4 texels[16]) { + float lo = 1.0; + float hi = 0.0; + for (int i = 0; i < 16; ++i) { + lo = min(lo, texels[i].a); + hi = max(hi, texels[i].a); + } + const uint a0 = uint(round(hi * 255.0)); + const uint a1 = uint(round(lo * 255.0)); + uint bits_lo = a0 | (a1 << 8); + uint bits_hi = 0u; + if (a0 == a1) { + return uvec2(bits_lo, bits_hi); + } + for (uint i = 0u; i < 16u; ++i) { + const float t = (texels[i].a * 255.0 - float(a1)) / float(a0 - a1); + const uint q = uint(clamp(round(t * 7.0), 0.0, 7.0)); + const uint index = q == 7u ? 0u : (q == 0u ? 1u : 8u - q); + const uint bit = 16u + 3u * i; + if (bit < 32u) { + bits_lo |= index << bit; + if (bit > 29u) { + bits_hi |= index >> (32u - bit); + } + } else { + bits_hi |= index << (bit - 32u); + } + } + return uvec2(bits_lo, bits_hi); +} + +void main() { + const uvec3 id = gl_GlobalInvocationID; + if (id.x >= blocks_x || id.y >= blocks_y) { + return; + } + vec4 texels[16]; + const ivec2 base = ivec2(id.xy) * 4; + const ivec2 last = ivec2(width, height) - 1; + bool any_transparent = false; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + // Edge blocks repeat the last row/column so padding doesn't widen the endpoints. + const ivec2 coord = min(base + ivec2(x, y), last); + texels[y * 4 + x] = imageLoad(source, ivec3(coord, int(id.z))); + any_transparent = any_transparent || texels[y * 4 + x].a < 0.5; + } + } + const uint block = (id.z * blocks_y + id.y) * blocks_x + id.x; + if (bc3 != 0u) { + blocks[out_offset + block * 2u] = EncodeAlpha(texels); + blocks[out_offset + block * 2u + 1u] = EncodeColor(texels, false); + } else { + blocks[out_offset + block] = EncodeColor(texels, any_transparent); + } +} diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index ba521520a0..b3ebf7b3f6 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -20,6 +20,7 @@ #include "common/div_ceil.h" #include "common/vector_math.h" #include "video_core/host_shaders/astc_decoder_comp_spv.h" +#include "video_core/host_shaders/bc_encoder_comp_spv.h" #include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h" #include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h" #include "video_core/host_shaders/resolve_conditional_render_comp_spv.h" @@ -190,6 +191,51 @@ struct AstcPushConstants { u32 native_tiling; }; +constexpr std::array BC_ENCODER_DESCRIPTOR_SET_BINDINGS{{ + { + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, + { + .binding = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, +}}; + +constexpr std::array BC_ENCODER_DESCRIPTOR_UPDATE_TEMPLATE{{ + { + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .offset = 0, + .stride = sizeof(DescriptorUpdateEntry), + }, + { + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .offset = sizeof(DescriptorUpdateEntry), + .stride = sizeof(DescriptorUpdateEntry), + }, +}}; + +struct BcEncoderPushConstants { + u32 blocks_x; + u32 blocks_y; + u32 width; + u32 height; + u32 bc3; + u32 out_offset; // in 8-byte units +}; + struct QueriesPrefixScanPushConstants { u32 min_accumulation_base; u32 max_accumulation_base; @@ -526,16 +572,31 @@ ASTCDecoderPass::~ASTCDecoderPass() = default; void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, std::span swizzles) { + const Target target{ + .image = image.Handle(), + .aspect_mask = image.AspectMask(), + .initialized = image.ExchangeInitialization(), + .level_view = [&image](s32 host_level) { return image.StorageImageView(host_level); }, + .wait = true, + }; + AssembleInto(image.info, image.host_level_offset, image.guest_size_bytes, target, map, + swizzles); +} + +void ASTCDecoderPass::AssembleInto(const VideoCommon::ImageInfo& info, u32 skip, + size_t guest_size_bytes, const Target& target, + const StagingBufferRef& map, + std::span swizzles) { using namespace VideoCommon::Accelerated; const std::array block_dims{ - VideoCore::Surface::DefaultBlockWidth(image.info.format), - VideoCore::Surface::DefaultBlockHeight(image.info.format), + VideoCore::Surface::DefaultBlockWidth(info.format), + VideoCore::Surface::DefaultBlockHeight(info.format), }; scheduler.RequestOutsideRenderPassOperationContext(); const VkPipeline vk_pipeline = *pipeline; - const VkImageAspectFlags aspect_mask = image.AspectMask(); - const VkImage vk_image = image.Handle(); - const bool is_initialized = image.ExchangeInitialization(); + const VkImageAspectFlags aspect_mask = target.aspect_mask; + const VkImage vk_image = target.image; + const bool is_initialized = target.initialized; scheduler.Record([vk_pipeline, vk_image, aspect_mask, is_initialized](vk::CommandBuffer cmdbuf) { const VkImageMemoryBarrier image_barrier{ @@ -564,28 +625,27 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, }); // With trimmed host levels the input holds each layer's mip chain from host_level_offset on // (see TextureCache::UploadImageContents), so its layer stride is that tail's size. - const u32 skip = image.host_level_offset; const u32 input_layer_stride = - skip != 0 ? image.info.layer_stride - - static_cast(VideoCommon::FullUploadSwizzles(image.info)[skip].buffer_offset) - : image.info.layer_stride; - const size_t input_size = skip != 0 ? size_t{input_layer_stride} * image.info.resources.layers - : image.guest_size_bytes; + skip != 0 ? info.layer_stride - + static_cast(VideoCommon::FullUploadSwizzles(info)[skip].buffer_offset) + : info.layer_stride; + const size_t input_size = skip != 0 ? size_t{input_layer_stride} * info.resources.layers + : guest_size_bytes; for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 8U); const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 8U); - const u32 num_dispatches_z = image.info.resources.layers; + const u32 num_dispatches_z = info.resources.layers; compute_pass_descriptor_queue.Acquire(scheduler, 2); compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset, input_size - swizzle.buffer_offset); compute_pass_descriptor_queue.AddImage( - image.StorageImageView(swizzle.level - static_cast(skip))); + target.level_view(swizzle.level - static_cast(skip))); const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; // To unswizzle the ASTC data - auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info); + auto params = MakeBlockLinearSwizzle2DParams(swizzle, info); params.layer_stride = input_layer_stride; ASSERT(params.origin == (std::array{0, 0, 0})); ASSERT(params.destination == (std::array{0, 0, 0})); @@ -636,6 +696,9 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH"); return value != nullptr && value[0] == '1'; }(); + if (!target.wait) { + return; + } const auto start = std::chrono::steady_clock::now(); if (flush_only) { scheduler.Flush(); @@ -659,6 +722,44 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, } } +BcEncoderPass::BcEncoderPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) + : ComputePass(device_, scheduler_, descriptor_pool_, BC_ENCODER_DESCRIPTOR_SET_BINDINGS, + BC_ENCODER_DESCRIPTOR_UPDATE_TEMPLATE, ASTC_BANK_INFO, + COMPUTE_PUSH_CONSTANT_RANGE, BC_ENCODER_COMP_SPV), + scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {} + +BcEncoderPass::~BcEncoderPass() = default; + +void BcEncoderPass::Encode(VkImageView source, u32 width, u32 height, u32 layers, bool bc3, + VkBuffer buffer, VkDeviceSize buffer_offset, VkDeviceSize buffer_size, + VkDeviceSize out_offset) { + scheduler.RequestOutsideRenderPassOperationContext(); + compute_pass_descriptor_queue.Acquire(scheduler, 2); + compute_pass_descriptor_queue.AddImage(source); + compute_pass_descriptor_queue.AddBuffer(buffer, buffer_offset, buffer_size); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + const BcEncoderPushConstants uniforms{ + .blocks_x = Common::DivCeil(width, 4U), + .blocks_y = Common::DivCeil(height, 4U), + .width = width, + .height = height, + .bc3 = bc3 ? 1U : 0U, + .out_offset = static_cast(out_offset / 8), + }; + const VkPipeline vk_pipeline = *pipeline; + scheduler.Record([this, vk_pipeline, uniforms, layers, descriptor_data](vk::CommandBuffer cmdbuf) { + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, vk_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, uniforms); + cmdbuf.Dispatch(Common::DivCeil(uniforms.blocks_x, 8U), Common::DivCeil(uniforms.blocks_y, 8U), + layers); + }); +} + constexpr u32 BL3D_BINDING_INPUT_BUFFER = 0; constexpr u32 BL3D_BINDING_OUTPUT_BUFFER = 1; diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index 3d30aa6bdc..5daf4eb3de 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -130,6 +131,21 @@ public: void Assemble(Image& image, const StagingBufferRef& map, std::span swizzles); + /// Where a decode writes: an RGBA8 image with a storage view per (host) level. + struct Target { + VkImage image; + VkImageAspectFlags aspect_mask; + bool initialized; + std::function level_view; + bool wait; ///< Wait for the GPU afterwards (the decoded image itself is sampled). + }; + + /// Decodes `info`'s ASTC data in `map` into `target`. `skip` leading levels are absent on the + /// host (TrimmedLevels) and `guest_size_bytes` is the full guest size. + void AssembleInto(const VideoCommon::ImageInfo& info, u32 skip, size_t guest_size_bytes, + const Target& target, const StagingBufferRef& map, + std::span swizzles); + private: Scheduler& scheduler; StagingBufferPool& staging_buffer_pool; @@ -137,6 +153,24 @@ private: MemoryAllocator& memory_allocator; }; +/// Compresses an RGBA8 image level to BC1 or BC3 blocks in a buffer (bc_encoder.comp). +class BcEncoderPass final : public ComputePass { +public: + explicit BcEncoderPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_); + ~BcEncoderPass(); + + /// Encodes every layer of one level (`source` is its RGBA8 storage view) into `buffer`, + /// `out_offset` bytes past `buffer_offset`; blocks are row-major, layer after layer. + void Encode(VkImageView source, u32 width, u32 height, u32 layers, bool bc3, VkBuffer buffer, + VkDeviceSize buffer_offset, VkDeviceSize buffer_size, VkDeviceSize out_offset); + +private: + Scheduler& scheduler; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; +}; + class BlockLinearUnswizzle3DPass final : public ComputePass { public: explicit BlockLinearUnswizzle3DPass(const Device& device_, Scheduler& scheduler_, diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index cfc6483a04..f5fcdd1380 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -193,6 +193,17 @@ constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM return HugeAstcHostFormat(device, info); } +// ASTC that the GPU decodes and re-compresses to BC1/BC3 itself (TranscodeAstcToBcn): no CPU +// decode, so streamed textures are ready the frame they are first used instead of seconds later. +[[nodiscard]] bool WillUseGpuBcnTranscode(const Device& device, const ImageInfo& info) { + if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported() || + IsHugeAstcArray(device, info) || info.type != ImageType::e2D || info.size.depth != 1) { + return false; + } + return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && + Settings::values.astc_recompression.GetValue() != Settings::AstcRecompression::Uncompressed; +} + [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) { return false; @@ -1043,6 +1054,11 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched !device.IsOptimalAstcSupported()) { astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); + if (Settings::values.astc_recompression.GetValue() != + Settings::AstcRecompression::Uncompressed) { + bc_encoder_pass.emplace(device, scheduler, descriptor_pool, + compute_pass_descriptor_queue); + } } if (!device.IsKhrImageFormatListSupported()) { return; @@ -1894,6 +1910,10 @@ u64 TextureCacheRuntime::GetDeviceMemoryUsage() const { return device.GetDeviceMemoryUsage(); } +u64 TextureCacheRuntime::GetDeviceMemoryBudget() const { + return device.GetDeviceMemoryBudget(); +} + bool TextureCacheRuntime::CanDownloadMsaa(const VideoCommon::ImageInfo& info) const { const VkImageAspectFlags aspect_mask = ImageAspectMask(info.format); if ((aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { @@ -1971,6 +1991,9 @@ void TextureCacheRuntime::ReleaseMsaaScratchImage(VkImage image) { } void TextureCacheRuntime::TickFrame() { + while (!pending_transcodes.empty() && scheduler.IsFree(pending_transcodes.front().tick)) { + pending_transcodes.pop_front(); + } static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60; std::erase_if(msaa_scratch_images, [this](MsaaScratchImage& scratch) { if (!scheduler.IsFree(scratch.tick)) { @@ -2010,7 +2033,8 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu } else if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { switch (Settings::values.accelerate_astc.GetValue()) { case Settings::AstcDecodeMode::Gpu: - if (WillUseAcceleratedAstcDecode(runtime->device, info)) { + if (WillUseAcceleratedAstcDecode(runtime->device, info) || + WillUseGpuBcnTranscode(runtime->device, info)) { flags |= VideoCommon::ImageFlagBits::AcceleratedUpload; } break; @@ -3281,6 +3305,9 @@ void TextureCacheRuntime::AccelerateImageUpload( u32 z_start, u32 z_count) { if (IsPixelFormatASTC(image.info.format)) { + if (bc_encoder_pass && WillUseGpuBcnTranscode(device, image.info)) { + return TranscodeAstcToBcn(image, map, swizzles); + } return astc_decoder_pass->Assemble(image, map, swizzles); } @@ -3299,6 +3326,100 @@ void TextureCacheRuntime::AccelerateImageUpload( ASSERT(false); } +void TextureCacheRuntime::TranscodeAstcToBcn( + Image& image, const StagingBufferRef& map, + std::span swizzles) { + const ImageInfo& info = image.info; + const u32 width = info.size.width; + const u32 height = info.size.height; + const u32 levels = static_cast(info.resources.levels); + const u32 layers = static_cast(info.resources.layers); + constexpr VkFormat DecodeFormat = VK_FORMAT_A8B8G8R8_UNORM_PACK32; + PendingTranscode pending{ + .tick = 0, + .image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = DecodeFormat, + .extent = {width, height, 1}, + .mipLevels = levels, + .arrayLayers = layers, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_STORAGE_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }, true), + .views = {}, + }; + for (u32 level = 0; level < levels; ++level) { + pending.views.push_back( + MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat)); + } + astc_decoder_pass->AssembleInto( + info, 0, image.guest_size_bytes, + ASTCDecoderPass::Target{ + .image = *pending.image, + .aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT, + .initialized = false, + .level_view = [&pending](s32 level) { return *pending.views[level]; }, + .wait = false, + }, + map, swizzles); + + // Encode every level into one buffer, then copy it into the image. + const bool bc3 = !IsOpaqueBc1Astc(device, info) && + Settings::values.astc_recompression.GetValue() != + Settings::AstcRecompression::Bc1; + const u64 block_bytes = bc3 ? 16 : 8; + boost::container::small_vector copies; + u64 total = 0; + for (u32 level = 0; level < levels; ++level) { + const u32 level_width = (std::max)(width >> level, 1U); + const u32 level_height = (std::max)(height >> level, 1U); + const u32 blocks_x = Common::DivCeil(level_width, 4U); + const u32 blocks_y = Common::DivCeil(level_height, 4U); + const u64 size = u64{blocks_x} * blocks_y * layers * block_bytes; + copies.push_back({ + .buffer_offset = total, + .buffer_size = size, + .buffer_row_length = blocks_x * 4, + .buffer_image_height = blocks_y * 4, + .image_subresource{ + .base_level = static_cast(level), + .base_layer = 0, + .num_layers = static_cast(layers), + }, + .image_offset{0, 0, 0}, + .image_extent{level_width, level_height, 1}, + }); + total += size; + } + const auto output = staging_buffer_pool.Request(total, MemoryUsage::DeviceLocal); + for (u32 level = 0; level < levels; ++level) { + bc_encoder_pass->Encode(*pending.views[level], copies[level].image_extent.width, + copies[level].image_extent.height, layers, bc3, output.buffer, + output.offset, total, copies[level].buffer_offset); + } + scheduler.Record([](vk::CommandBuffer cmdbuf) { + const VkMemoryBarrier barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, barrier); + }); + image.UploadMemory(output.buffer, output.offset, copies); + pending.tick = scheduler.CurrentTick(); + pending_transcodes.push_back(std::move(pending)); +} + void TextureCacheRuntime::TransitionImageLayout(Image& image) { if (!image.ExchangeInitialization()) { VkImageMemoryBarrier barrier{ diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 1d171b576f..1d92d439f4 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include "video_core/texture_cache/texture_cache_base.h" @@ -66,6 +67,8 @@ public: u64 GetDeviceMemoryUsage() const; + u64 GetDeviceMemoryBudget() const; + bool CanReportMemoryUsage() const; void LogMemoryStatistics() const; @@ -162,6 +165,20 @@ public: BlitImageHelper& blit_image_helper; RenderPassCache& render_pass_cache; std::optional astc_decoder_pass; + std::optional bc_encoder_pass; + + /// ASTC decoded on the GPU and re-compressed to BC1/BC3 there (accelerate_astc=Gpu with + /// astc_recompression=Bc1/Bc3): decode into a temporary RGBA8 image, encode, copy. + void TranscodeAstcToBcn(Image& image, const StagingBufferRef& map, + std::span swizzles); + + /// Temporary decode images, freed once the GPU is past `tick`. + struct PendingTranscode { + u64 tick; + vk::Image image; + std::vector views; + }; + std::deque pending_transcodes; std::optional bl3d_unswizzle_pass; const Settings::ResolutionScalingInfo& resolution; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index d0af53631d..47bab9617a 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -49,6 +49,7 @@ enum class ImageFlagBits : u32 { /// The host image keeps only mip levels >= host_level_offset (its decoded copy would not fit /// in VRAM); uploads skip the rest, downloads are never done. TrimmedLevels = 1 << 19, + ZeroFilled = 1 << 20, ///< Cleared to zero blocks before its first asynchronous decode. }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index fbcd3eefbb..75df31dc27 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -80,15 +80,7 @@ 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); + ApplyThresholdOverrides(); const bool gpu_unswizzle_enabled = Settings::values.gpu_unswizzle_enabled.GetValue(); @@ -181,8 +173,47 @@ void TextureCache

::RunGarbageCollector() { } } +template +void TextureCache

::ApplyThresholdOverrides() { + // 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); +} + template void TextureCache

::TickFrame() { + 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 + // sat in aggressive mode evicting decoded ASTC (and decoding it again). Total use is + // measured from the driver, so collect only near the device budget instead. + if (!native_thresholds_applied && gpu_memory != nullptr && gpu_memory->UsesNativeTiling()) { + native_thresholds_applied = true; + const u64 device_local = runtime.GetDeviceLocalMemory(); + critical_memory = (std::max)(critical_memory, device_local - 256_MiB); + expected_memory = (std::max)(expected_memory, device_local - 1280_MiB); + ApplyThresholdOverrides(); + native_critical_memory = critical_memory; + native_expected_memory = expected_memory; + } + // The budget shrinks when other programs use VRAM (another game: 7.0 -> 6.4 GB), and + // collecting only past it means the driver pages textures out. Re-read it now and then. + if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) { + if (native_thresholds_applied && ++budget_check_frames % 60 == 0) { + const u64 budget = runtime.GetDeviceMemoryBudget(); + if (budget > 2_GiB) { + critical_memory = (std::min)(native_critical_memory, budget - 512_MiB); + expected_memory = (std::min)(native_expected_memory, budget - 1280_MiB); + } + } + } + } // If we can obtain the memory info, use it instead of the estimate. if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); @@ -1492,6 +1523,18 @@ void TextureCache

::QueueAsyncDecode(Image& image, ImageId image_id) { Tegra::Memory::GpuGuestMemory swizzle_data(*gpu_memory, image.gpu_addr, image.guest_size_bytes, &swizzle_data_buffer); auto copies = UnswizzleImage(*gpu_memory, image.gpu_addr, image.info, swizzle_data, local_unswizzle_data_buffer); const size_t out_size = MapSizeBytes(image); + if (False(image.flags & ImageFlagBits::ZeroFilled)) { + // Until the decode lands (a second or more while title 1 streams ~40 textures a second) + // a new image showed whatever its memory last held, often other textures. Zero blocks + // read as opaque black (BC1) or transparent (BC3) instead. + image.flags |= ImageFlagBits::ZeroFilled; + auto zero_copies = copies; + ConvertedLayout(image.info, zero_copies); + auto staging = runtime.UploadStagingBuffer(out_size); + std::memset(staging.mapped_span.data(), 0, out_size); + image.UploadMemory(staging, zero_copies); + runtime.InsertUploadMemoryBarrier(); + } auto func = [out_size, copies, info = image.info, input = std::move(local_unswizzle_data_buffer), diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index f6f083e10a..ce1de8e867 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -126,6 +126,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches astc_scan_buffer; @@ -462,6 +466,9 @@ private: DAddr virtual_invalid_space{}; + /// Applies EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB and logs the thresholds. + void ApplyThresholdOverrides(); + /// 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; diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index 7a1a1d89c0..ffc1d59ec3 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -929,6 +929,35 @@ boost::container::small_vector UnswizzleImage(Tegra::Memory return copies; } +void ConvertedLayout(const ImageInfo& info, std::span copies) { + // Same offsets and sizes as ConvertImage below. + const auto recompression = Settings::values.astc_recompression.GetValue(); + const bool astc = IsPixelFormatASTC(info.format); + u64 output_offset = 0; + for (BufferImageCopy& copy : copies) { + const Extent3D mip_size = AdjustMipSize(info.size, copy.image_subresource.base_level); + const u64 layers = static_cast(copy.image_subresource.num_layers) * copy.image_extent.depth; + u64 size; + if (astc && recompression == Settings::AstcRecompression::Uncompressed) { + size = u64{copy.image_extent.width} * copy.image_extent.height * layers * + BytesPerBlock(PixelFormat::A8B8G8R8_UNORM); + } else if (astc) { + const u64 bpp_div = + recompression == Settings::AstcRecompression::Bc1 || info.astc_opaque ? 2 : 1; + size = u64{Common::AlignUp(copy.image_extent.width, 4U)} * + Common::AlignUp(copy.image_extent.height, 4U) * layers / bpp_div; + } else { + size = u64{copy.image_extent.width} * copy.image_extent.height * layers * + ConvertedBytesPerBlock(info.format); + } + copy.buffer_offset = output_offset; + copy.buffer_size = size; + copy.buffer_row_length = mip_size.width; + copy.buffer_image_height = mip_size.height; + output_offset += size; + } +} + void ConvertImage(std::span input, const ImageInfo& info, std::span output, std::span copies) { u32 output_offset = 0; diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h index 89f85e4e8a..0381d2d24b 100644 --- a/src/video_core/texture_cache/util.h +++ b/src/video_core/texture_cache/util.h @@ -72,6 +72,9 @@ struct OverlapResult { Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info, std::span input, std::span output); +/// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting. +void ConvertedLayout(const ImageInfo& info, std::span copies); + void ConvertImage(std::span input, const ImageInfo& info, std::span output, std::span copies); diff --git a/src/video_core/textures/workers.cpp b/src/video_core/textures/workers.cpp index 6922634e30..e3a0f26cf2 100644 --- a/src/video_core/textures/workers.cpp +++ b/src/video_core/textures/workers.cpp @@ -11,14 +11,15 @@ namespace Tegra::Texture { Common::ThreadWorker& GetThreadWorkers() { - // An eighth of the hardware threads (at least 2): with half, re-compressing streamed ASTC - // textures took ~3.5 cores from the emulated CPU cores and the GPU thread on an 8-core - // laptop. EDEN_NX2_TRANSCODE_THREADS= overrides. + // A quarter of the hardware threads (at least 2). Half took ~3.5 cores from the emulated + // CPU cores and the GPU thread on an 8-core laptop when textures were thrashing; with an + // eighth (2) both threads stayed saturated and streamed textures arrived seconds late (shown + // as placeholders), while 4 cost no frame rate. EDEN_NX2_TRANSCODE_THREADS= overrides. static Common::ThreadWorker workers{[] { if (const char* text = std::getenv("EDEN_NX2_TRANSCODE_THREADS")) { return (std::max)(static_cast(std::strtoul(text, nullptr, 10)), 1U); } - return (std::max)(std::thread::hardware_concurrency() / 8, 2U); + return (std::max)(std::thread::hardware_concurrency() / 4, 2U); }(), "ImageTranscode"}; return workers; } diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index be09bca8d3..454e5d4e02 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1612,6 +1612,20 @@ u64 Device::GetDeviceMemoryUsage() const { return result; } +u64 Device::GetDeviceMemoryBudget() const { + if (!extensions.memory_budget) { + return 0; + } + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; + budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; + physical.GetMemoryProperties(&budget); + u64 result{}; + for (const size_t heap : valid_heap_memory) { + result += budget.heapBudget[heap]; + } + return result; +} + void Device::CollectPhysicalMemoryInfo() { // Calculate limits using memory budget VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index bbf3754e39..34ee63a275 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -963,6 +963,9 @@ FN_MAX_LIMIT_LIST u64 GetDeviceMemoryUsage() const; + /// The driver's current memory budget for this process (VK_EXT_memory_budget), 0 without it. + u64 GetDeviceMemoryBudget() const; + u32 GetSetsPerPool() const { return sets_per_pool; } diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 23f1b0a798..27f8fa6b4c 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -206,10 +206,12 @@ MemoryAllocator::MemoryAllocator(const Device &device_) MemoryAllocator::~MemoryAllocator() = default; -vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci) const +vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicated) const { const VmaAllocationCreateInfo alloc_ci = { - .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT, + .flags = static_cast(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) | + (dedicated ? VmaAllocationCreateFlags{VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT} + : VmaAllocationCreateFlags{0}), .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, .requiredFlags = 0, .preferredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index 7467e929d3..fa7d4b41c3 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -103,7 +103,9 @@ namespace Vulkan { MemoryAllocator(const MemoryAllocator &) = delete; - vk::Image CreateImage(const VkImageCreateInfo &ci) const; + /// `dedicated`: own device memory, returned to the driver on destruction (short-lived + /// images would otherwise leave holes in the shared blocks). + vk::Image CreateImage(const VkImageCreateInfo &ci, bool dedicated = false) const; vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const; diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h index bcac169849..0dc2a4983a 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h @@ -25,10 +25,11 @@ class InputSubsystem; enum class MouseButton; } // namespace InputCommon -// EDEN_NX2_HIDE_WINDOW=1 creates the render window hidden and without focus, for unattended +// EDEN_NX2_HIDE_WINDOW=1 (anything but 0) creates the render window hidden and without focus, for unattended // test runs that must not cover or interrupt the desktop. inline SDL_WindowFlags Nx2HiddenWindowFlags() { - return std::getenv("EDEN_NX2_HIDE_WINDOW") != nullptr ? SDL_WINDOW_HIDDEN | SDL_WINDOW_NOT_FOCUSABLE + const char* hide = std::getenv("EDEN_NX2_HIDE_WINDOW"); + return hide != nullptr && hide[0] != '0' ? SDL_WINDOW_HIDDEN | SDL_WINDOW_NOT_FOCUSABLE : SDL_WindowFlags{0}; } -- cgit v1.2.3 From 571f9b58eb8a9c102a91a5d26cd32ebae4330232 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:07:49 -0400 Subject: nx2: GPU-thread zone timers and an in-process sampling profiler EDEN_NX2_GPU_PROFILE=1 times the GPU thread's stages (command parsing, macros, draw setup by step, compute, texture uploads) and logs each one's share of every second. EDEN_NX2_SAMPLE= (Windows) pauses the named threads about 600 times a second for EDEN_NX2_SAMPLE_SECONDS, walks their stacks and writes per-function counts plus folded stacks (EDEN_NX2_SAMPLE_OUT). --- src/video_core/CMakeLists.txt | 1 + src/video_core/gpu_thread.cpp | 15 +- src/video_core/nx2_profile.h | 149 +++++++++ .../renderer_vulkan/vk_graphics_pipeline.cpp | 109 ++++--- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 30 +- src/video_core/texture_cache/texture_cache.h | 2 + src/yuzu_cmd/CMakeLists.txt | 2 + src/yuzu_cmd/nx2_sampler.cpp | 333 +++++++++++++++++++++ src/yuzu_cmd/nx2_sampler.h | 16 + src/yuzu_cmd/yuzu.cpp | 2 + 10 files changed, 613 insertions(+), 46 deletions(-) create mode 100644 src/video_core/nx2_profile.h create mode 100644 src/yuzu_cmd/nx2_sampler.cpp create mode 100644 src/yuzu_cmd/nx2_sampler.h (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index d17deacacd..513a644166 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -103,6 +103,7 @@ add_library(video_core STATIC native_qmd.h native_query_trace.h nx2.h + nx2_profile.h present.h pte_kind.h query_cache/bank_base.h diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 793e8e9eeb..cf4e159806 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -17,6 +17,7 @@ #include "video_core/dma_pusher.h" #include "video_core/gpu.h" #include "video_core/gpu_thread.h" +#include "video_core/nx2_profile.h" #include "video_core/host1x/host1x.h" #include "video_core/renderer_base.h" @@ -38,29 +39,41 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten auto current_context = context.Acquire(); CommandDataContainer next; + namespace Profile = VideoCommon::Nx2Profile; while (!stop_token.stop_requested()) { + Profile::Report(); if (scheduler.HasPendingChannels()) { // Suspended channels wait on semaphores the CPU releases: poll them meanwhile. - scheduler.ResumePending(system.GPU()); + { + Profile::Scope scope{Profile::Zone::ResumePending}; + scheduler.ResumePending(system.GPU()); + } if (!state.queue.TryPop(next)) { + Profile::Scope scope{Profile::Zone::PendingSleep}; std::this_thread::sleep_for(std::chrono::milliseconds{1}); continue; } } else { + Profile::Scope scope{Profile::Zone::Idle}; state.queue.PopWait(next, stop_token); } if (stop_token.stop_requested()) { break; } if (auto* submit_list = std::get_if(&next.data)) { + Profile::Scope scope{Profile::Zone::Push}; scheduler.Push(system.GPU(), submit_list->channel, std::move(submit_list->entries)); } else if (std::holds_alternative(next.data)) { + Profile::Scope scope{Profile::Zone::Tick}; system.GPU().TickWork(); } else if (auto* run = std::get_if(&next.data)) { + Profile::Scope scope{Profile::Zone::Run}; run->func(); } else if (const auto* flush = std::get_if(&next.data)) { + Profile::Scope scope{Profile::Zone::FlushInvalidate}; renderer.ReadRasterizer()->FlushRegion(flush->addr, flush->size); } else if (const auto* invalidate = std::get_if(&next.data)) { + Profile::Scope scope{Profile::Zone::FlushInvalidate}; renderer.ReadRasterizer()->OnCacheInvalidation(invalidate->addr, invalidate->size); } else { ASSERT(false); diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h new file mode 100644 index 0000000000..051403c982 --- /dev/null +++ b/src/video_core/nx2_profile.h @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/common_types.h" +#include "common/logging.h" + +// EDEN_NX2_GPU_PROFILE: time where the GPU thread spends its time. Each zone counts its own +// (exclusive) time, nested zones excluded; every 10 s the thread logs each zone's share. +namespace VideoCommon::Nx2Profile { + +enum class Zone : u32 { + Other, ///< GPU thread time outside every zone below (register writes, command parsing) + Idle, + PendingSleep, + ResumePending, + Push, + Tick, + Run, + FlushInvalidate, + Macro, + Draw, + Clear, + Compute, + FlushWork, + PipelineLookup, + Configure, + SyncDescriptors, + ReadHandles, + FillImageViews, + TextureBuffers, + UpdateBuffers, + DescriptorAcquire, + BindStageBuffers, + PushDescriptors, + RenderTargets, + ConfigureDraw, + DynamicState, + DrawRecord, + TextureUpload, + Count, +}; + +inline constexpr std::array(Zone::Count)> zone_names{ + "other", "idle", "pending sleep", "resume pending", "push", + "tick", "run", "flush/inval", "macro", "draw", + "clear", "compute", "flush work", "pipeline lookup", "configure", + "sync descriptors", "read handles", "fill image views", "texture buffers", "update buffers", + "descriptor acquire", "bind stage buffers", "push descriptors", "render targets", + "configure draw", "dynamic state", "draw record", "texture upload", +}; + +inline bool Enabled() { + static const bool enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr; + return enabled; +} + +inline u64 Now() { + return static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count()); +} + +struct ThreadData { + std::array(Zone::Count)> exclusive_ns{}; + std::array(Zone::Count)> count{}; + u64 child_ns{}; ///< Time of the zones nested in the current one + u64 window_start{}; +}; + +inline thread_local ThreadData t_data; + +class Scope { +public: + explicit Scope(Zone zone_) : zone{zone_}, enabled{Enabled()} { + if (!enabled) { + return; + } + parent_child_ns = t_data.child_ns; + t_data.child_ns = 0; + start = Now(); + } + + ~Scope() { + if (!enabled) { + return; + } + const u64 elapsed = Now() - start; + auto& data = t_data; + const auto index = static_cast(zone); + data.exclusive_ns[index] += elapsed - data.child_ns; + ++data.count[index]; + data.child_ns = parent_child_ns + elapsed; + } + + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + +private: + Zone zone; + bool enabled; + u64 start{}; + u64 parent_child_ns{}; +}; + +/// Called by the GPU thread between commands: logs and resets the totals every 10 s. +inline void Report() { + if (!Enabled()) { + return; + } + auto& data = t_data; + const u64 now = Now(); + if (data.window_start == 0) { + data.window_start = now; + data.child_ns = 0; + return; + } + const u64 window = now - data.window_start; + if (window < 10'000'000'000ULL) { + return; + } + // Time in no zone at all is the wall time minus the zones' totals. + u64 zoned{}; + for (size_t i = 1; i < data.exclusive_ns.size(); ++i) { + zoned += data.exclusive_ns[i]; + } + data.exclusive_ns[0] = window > zoned ? window - zoned : 0; + std::string line; + for (size_t i = 0; i < data.exclusive_ns.size(); ++i) { + if (data.exclusive_ns[i] == 0 && data.count[i] == 0) { + continue; + } + // Per second of wall time: milliseconds spent and calls made. + const double ms = static_cast(data.exclusive_ns[i]) * 1000.0 / window; + const double calls = static_cast(data.count[i]) * 1e9 / window; + line += fmt::format(" | {} {:.1f} ms ({:.0f}/s)", zone_names[i], ms, calls); + } + LOG_INFO(HW_GPU, "GPU thread per second:{}", line); + data = {}; + data.window_start = now; +} + +} // namespace VideoCommon::Nx2Profile diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 172ee4dcaa..f737ad9f70 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -29,6 +29,7 @@ #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/gpu_logging/gpu_logging.h" #include "common/settings.h" +#include "video_core/nx2_profile.h" #if defined(_MSC_VER) && defined(NDEBUG) #define LAMBDA_FORCEINLINE [[msvc::forceinline]] @@ -355,7 +356,11 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { views.reserve(num_image_elements); samplers.reserve(num_textures); - texture_cache.SynchronizeDescriptors(false); + namespace Profile = VideoCommon::Nx2Profile; + { + Profile::Scope scope{Profile::Zone::SyncDescriptors}; + texture_cache.SynchronizeDescriptors(false); + } buffer_cache.SetUniformBuffersState(enabled_uniform_buffer_masks, &uniform_buffer_sizes); @@ -432,24 +437,31 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { return true; }}; - if constexpr (Spec::enabled_stages[0]) { - config_stage(0); - } - if constexpr (Spec::enabled_stages[1]) { - config_stage(1); - } - if constexpr (Spec::enabled_stages[2]) { - config_stage(2); - } - if constexpr (Spec::enabled_stages[3]) { - config_stage(3); - } - if constexpr (Spec::enabled_stages[4]) { - config_stage(4); + { + Profile::Scope scope{Profile::Zone::ReadHandles}; + if constexpr (Spec::enabled_stages[0]) { + config_stage(0); + } + if constexpr (Spec::enabled_stages[1]) { + config_stage(1); + } + if constexpr (Spec::enabled_stages[2]) { + config_stage(2); + } + if constexpr (Spec::enabled_stages[3]) { + config_stage(3); + } + if constexpr (Spec::enabled_stages[4]) { + config_stage(4); + } } ASSERT(views.size() == num_image_elements); ASSERT(samplers.size() == num_textures); - texture_cache.FillImageViews(std::span(views.data(), views.size()), false, Spec::has_images); + { + Profile::Scope scope{Profile::Zone::FillImageViews}; + texture_cache.FillImageViews(std::span(views.data(), views.size()), false, + Spec::has_images); + } VideoCommon::ImageViewInOut* texture_buffer_it{views.data()}; const auto bind_stage_info{[&](size_t stage) LAMBDA_FORCEINLINE { @@ -493,39 +505,54 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { texture_buffer_it += Shader::NumDescriptors(info.image_descriptors); } }}; - if constexpr (Spec::enabled_stages[0]) { - bind_stage_info(0); - } - if constexpr (Spec::enabled_stages[1]) { - bind_stage_info(1); - } - if constexpr (Spec::enabled_stages[2]) { - bind_stage_info(2); - } - if constexpr (Spec::enabled_stages[3]) { - bind_stage_info(3); - } - if constexpr (Spec::enabled_stages[4]) { - bind_stage_info(4); + { + Profile::Scope scope{Profile::Zone::TextureBuffers}; + if constexpr (Spec::enabled_stages[0]) { + bind_stage_info(0); + } + if constexpr (Spec::enabled_stages[1]) { + bind_stage_info(1); + } + if constexpr (Spec::enabled_stages[2]) { + bind_stage_info(2); + } + if constexpr (Spec::enabled_stages[3]) { + bind_stage_info(3); + } + if constexpr (Spec::enabled_stages[4]) { + bind_stage_info(4); + } } if (regs.transform_feedback_enabled != 0) { scheduler.RequestOutsideRenderPassOperationContext(); } - buffer_cache.UpdateGraphicsBuffers(is_indexed); - buffer_cache.BindHostGeometryBuffers(is_indexed); + { + Profile::Scope scope{Profile::Zone::UpdateBuffers}; + buffer_cache.UpdateGraphicsBuffers(is_indexed); + buffer_cache.BindHostGeometryBuffers(is_indexed); + } - guest_descriptor_queue.Acquire(scheduler, num_descriptor_entries, uses_descriptor_buffer); + { + Profile::Scope scope{Profile::Zone::DescriptorAcquire}; + guest_descriptor_queue.Acquire(scheduler, num_descriptor_entries, uses_descriptor_buffer); + } RescalingPushConstant rescaling; RenderAreaPushConstant render_area; const VideoCommon::SamplerId* samplers_it{samplers.data()}; const VideoCommon::ImageViewInOut* views_it{views.data()}; const auto prepare_stage{[&](size_t stage) LAMBDA_FORCEINLINE { - buffer_cache.BindHostStageBuffers(stage); - PushImageDescriptors(texture_cache, guest_descriptor_queue, stage_infos[stage], rescaling, - samplers_it, views_it); + { + Profile::Scope scope{Profile::Zone::BindStageBuffers}; + buffer_cache.BindHostStageBuffers(stage); + } + { + Profile::Scope scope{Profile::Zone::PushDescriptors}; + PushImageDescriptors(texture_cache, guest_descriptor_queue, stage_infos[stage], + rescaling, samplers_it, views_it); + } const auto& info{stage_infos[stage]}; if (info.uses_render_area) { render_area.uses_render_area = true; @@ -555,12 +582,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { buffer_cache.runtime.PostCopyBarrier(); buffer_cache.any_buffer_uploaded = false; } - texture_cache.UpdateRenderTargets(false); - texture_cache.CheckFeedbackLoop(std::span{views.data(), - views.size()}); + { + Profile::Scope scope{Profile::Zone::RenderTargets}; + texture_cache.UpdateRenderTargets(false); + texture_cache.CheckFeedbackLoop( + std::span{views.data(), views.size()}); + } if (IsBuilt() && !pipeline) { return false; } + Profile::Scope scope{Profile::Zone::ConfigureDraw}; return ConfigureDraw(rescaling, render_area); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 609ac12fb4..decfb0d136 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -49,6 +49,7 @@ #include "video_core/texture_cache/texture_cache_base.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#include "video_core/nx2_profile.h" namespace Vulkan { @@ -253,8 +254,12 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { LOG_DEBUG(HW_GPU, "Native graphics draw failed; refusing submission completion"); } }; - FlushWork(); - gpu_memory->FlushCaching(); + namespace Profile = VideoCommon::Nx2Profile; + { + Profile::Scope scope{Profile::Zone::FlushWork}; + FlushWork(); + gpu_memory->FlushCaching(); + } const auto report_failure = [&](std::string_view reason) { static std::set> seen; @@ -266,7 +271,10 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { regs.rt[0].Address(), regs.rt[0].width, regs.rt[0].height); } }; - GraphicsPipeline* const pipeline{pipeline_cache.CurrentGraphicsPipeline()}; + GraphicsPipeline* const pipeline{[&] { + Profile::Scope scope{Profile::Zone::PipelineLookup}; + return pipeline_cache.CurrentGraphicsPipeline(); + }()}; if (!pipeline) { report_failure("no pipeline"); return; @@ -274,17 +282,23 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; // update engine as channel may be different. pipeline->SetEngine(maxwell3d, gpu_memory); - if (!pipeline->Configure(is_indexed)) { + if (Profile::Scope scope{Profile::Zone::Configure}; !pipeline->Configure(is_indexed)) { report_failure("configure failed"); return; } - UpdateDynamicStates(); + { + Profile::Scope scope{Profile::Zone::DynamicState}; + UpdateDynamicStates(); + } query_cache.NotifySegment(true); HandleTransformFeedback(); query_cache.CounterEnable(VideoCommon::QueryType::ZPassPixelCount64, maxwell3d->regs.zpass_pixel_count_enable); - draw_func(); + { + Profile::Scope scope{Profile::Zone::DrawRecord}; + draw_func(); + } submitted = true; static u32 native_capture_count{}; if (maxwell3d->GetNativeMacroEngine().native_turing && @@ -412,6 +426,7 @@ void TraceWindowTarget(const Tegra::Engines::Maxwell3D& maxwell3d, Tegra::Memory } // Anonymous namespace void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Draw}; TraceWindowTarget(*maxwell3d, *gpu_memory, is_indexed ? "draw-indexed" : "draw", is_indexed ? maxwell3d->draw_manager.draw_state.index_buffer.count : maxwell3d->draw_manager.draw_state.vertex_buffer.count); @@ -554,6 +569,7 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { } void RasterizerVulkan::DrawIndirect() { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Draw}; const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); PrepareDraw(params.is_indexed, [this, ¶ms] { @@ -659,6 +675,7 @@ void RasterizerVulkan::DrawTexture() { } void RasterizerVulkan::Clear(u32 layer_count) { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Clear}; if (Tegra::Engines::g_nx2_trace_draws_after_upscale > 0) { const auto& regs = maxwell3d->regs; LOG_INFO(HW_GPU, "after upscale: clear rt0 {:#x} {}x{} fmt {:#x} surface {:#x} colour {} {} {} {}", @@ -852,6 +869,7 @@ void RasterizerVulkan::Clear(u32 layer_count) { } void RasterizerVulkan::DispatchCompute() { + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::Compute}; FlushWork(); gpu_memory->FlushCaching(); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 75df31dc27..ad1804877a 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -28,6 +28,7 @@ #include "video_core/texture_cache/samples_helper.h" #include "video_core/texture_cache/texture_cache_base.h" #include "video_core/texture_cache/util.h" +#include "video_core/nx2_profile.h" #include "video_core/textures/decoders.h" namespace VideoCommon { @@ -1251,6 +1252,7 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { // Only upload modified images return; } + VideoCommon::Nx2Profile::Scope profile_scope{VideoCommon::Nx2Profile::Zone::TextureUpload}; image.flags &= ~ImageFlagBits::CpuModified; diff --git a/src/yuzu_cmd/CMakeLists.txt b/src/yuzu_cmd/CMakeLists.txt index 17a1545167..a87b4e5288 100644 --- a/src/yuzu_cmd/CMakeLists.txt +++ b/src/yuzu_cmd/CMakeLists.txt @@ -31,6 +31,8 @@ add_executable(yuzu-cmd emu_window/emu_window_sdl3_null.h emu_window/emu_window_sdl3_vk.cpp emu_window/emu_window_sdl3_vk.h + nx2_sampler.cpp + nx2_sampler.h sdl_config.cpp sdl_config.h yuzu.cpp diff --git a/src/yuzu_cmd/nx2_sampler.cpp b/src/yuzu_cmd/nx2_sampler.cpp new file mode 100644 index 0000000000..c01b812650 --- /dev/null +++ b/src/yuzu_cmd/nx2_sampler.cpp @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "yuzu_cmd/nx2_sampler.h" + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Nx2Sampler { +namespace { + +constexpr size_t MaxDepth = 40; + +struct Sample { + unsigned thread; + unsigned depth; + std::array frames; +}; + +struct Target { + HANDLE handle; + std::string name; +}; + +double EnvSeconds(const char* name, double fallback) { + const char* text = std::getenv(name); + return text != nullptr ? std::strtod(text, nullptr) : fallback; +} + +std::vector SplitNames(const char* text) { + std::vector names; + std::string current; + for (const char* c = text; *c != '\0'; ++c) { + if (*c == ',') { + names.push_back(current); + current.clear(); + } else { + current += *c; + } + } + if (!current.empty()) { + names.push_back(current); + } + return names; +} + +bool Matches(const std::string& thread_name, const std::vector& patterns) { + for (const auto& pattern : patterns) { + if (!pattern.empty() && pattern.back() == '*') { + if (thread_name.compare(0, pattern.size() - 1, pattern, 0, pattern.size() - 1) == 0) { + return true; + } + } else if (thread_name == pattern) { + return true; + } + } + return false; +} + +std::vector FindThreads(const std::vector& patterns) { + using GetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PWSTR*); + const auto get_description = reinterpret_cast( + GetProcAddress(GetModuleHandleA("KernelBase.dll"), "GetThreadDescription")); + std::vector targets; + if (get_description == nullptr) { + return targets; + } + const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + return targets; + } + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + const DWORD self = GetCurrentThreadId(); + for (BOOL ok = Thread32First(snapshot, &entry); ok; ok = Thread32Next(snapshot, &entry)) { + if (entry.th32OwnerProcessID != GetCurrentProcessId() || entry.th32ThreadID == self) { + continue; + } + const HANDLE handle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | + THREAD_QUERY_LIMITED_INFORMATION, + FALSE, entry.th32ThreadID); + if (handle == nullptr) { + continue; + } + PWSTR wide{}; + std::string name; + if (SUCCEEDED(get_description(handle, &wide)) && wide != nullptr) { + char buffer[256]{}; + WideCharToMultiByte(CP_UTF8, 0, wide, -1, buffer, sizeof(buffer) - 1, nullptr, + nullptr); + name = buffer; + LocalFree(wide); + } + if (Matches(name, patterns)) { + targets.push_back({handle, name}); + } else { + CloseHandle(handle); + } + } + CloseHandle(snapshot); + return targets; +} + +// Walks a suspended thread's stack from its context. Nothing here may allocate: the thread +// could be paused holding the heap lock. +unsigned Walk(CONTEXT context, std::array& frames) { + unsigned depth = 0; + while (depth < MaxDepth && context.Rip != 0) { + frames[depth++] = context.Rip; + DWORD64 image_base{}; + const PRUNTIME_FUNCTION function = RtlLookupFunctionEntry(context.Rip, &image_base, nullptr); + if (function == nullptr) { + // A leaf function (no unwind data): its return address is on top of the stack. JIT + // code has none either; past the first frame, stop there. + if (depth != 1) { + break; + } + context.Rip = *reinterpret_cast(context.Rsp); + context.Rsp += 8; + continue; + } + PVOID handler_data{}; + DWORD64 establisher_frame{}; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, context.Rip, function, &context, + &handler_data, &establisher_frame, nullptr); + } + return depth; +} + +struct Symbolizer { + using SymInitializeFn = BOOL(WINAPI*)(HANDLE, PCSTR, BOOL); + using SymSetOptionsFn = DWORD(WINAPI*)(DWORD); + using SymFromAddrFn = BOOL(WINAPI*)(HANDLE, DWORD64, PDWORD64, PSYMBOL_INFO); + + Symbolizer() { + const HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); + if (dbghelp == nullptr) { + return; + } + const auto set_options = + reinterpret_cast(GetProcAddress(dbghelp, "SymSetOptions")); + const auto initialize = + reinterpret_cast(GetProcAddress(dbghelp, "SymInitialize")); + from_address = reinterpret_cast(GetProcAddress(dbghelp, "SymFromAddr")); + if (set_options == nullptr || initialize == nullptr || from_address == nullptr) { + from_address = nullptr; + return; + } + set_options(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + if (!initialize(GetCurrentProcess(), nullptr, TRUE)) { + from_address = nullptr; + } + } + + const std::string& Name(DWORD64 address) { + const auto [it, is_new] = cache.try_emplace(address); + if (!is_new) { + return it->second; + } + if (from_address != nullptr) { + alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 1024]{}; + auto* const symbol = reinterpret_cast(buffer); + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = 1024; + DWORD64 displacement{}; + if (from_address(GetCurrentProcess(), address, &displacement, symbol)) { + it->second.assign(symbol->Name, std::min(symbol->NameLen, 160)); + return it->second; + } + } + HMODULE module{}; + if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &module)) { + char path[MAX_PATH] = "?"; + GetModuleFileNameA(module, path, sizeof(path)); + const char* base = std::strrchr(path, '\\'); + it->second = std::string{base != nullptr ? base + 1 : path} + "!?"; + } else { + it->second = "(jit)"; + } + return it->second; + } + + SymFromAddrFn from_address{}; + std::unordered_map cache; +}; + +void Report(const std::vector& targets, const std::vector& samples, + const char* path) { + Symbolizer symbolizer; + std::FILE* const file = std::fopen(path, "w"); + if (file == nullptr) { + return; + } + for (unsigned thread = 0; thread < targets.size(); ++thread) { + std::map self; + std::map inclusive; + std::map self_caller; + unsigned total = 0; + for (const Sample& sample : samples) { + if (sample.thread != thread || sample.depth == 0) { + continue; + } + ++total; + const std::string& top = symbolizer.Name(sample.frames[0]); + ++self[top]; + if (sample.depth > 1) { + ++self_caller[top + " <- " + symbolizer.Name(sample.frames[1])]; + } + std::set seen; + for (unsigned i = 0; i < sample.depth; ++i) { + const std::string& name = symbolizer.Name(sample.frames[i]); + if (seen.insert(name).second) { + ++inclusive[name]; + } + } + } + const auto print = [&](const char* title, const std::map& counts, + size_t limit) { + std::vector> sorted; + for (const auto& [name, count] : counts) { + sorted.emplace_back(count, name); + } + std::sort(sorted.rbegin(), sorted.rend()); + std::fprintf(file, "-- %s\n", title); + for (size_t i = 0; i < std::min(limit, sorted.size()); ++i) { + std::fprintf(file, "%6u %5.1f%% %s\n", sorted[i].first, + 100.0 * sorted[i].first / std::max(total, 1U), + sorted[i].second.c_str()); + } + }; + std::fprintf(file, "=== thread \"%s\": %u samples\n", targets[thread].name.c_str(), total); + print("self (where the thread is)", self, 60); + print("self <- caller", self_caller, 50); + print("inclusive (on the stack)", inclusive, 90); + std::fprintf(file, "\n"); + } + std::fclose(file); + + // .folded: every distinct stack with its count, root first ("thread;a;b;c count"). + std::map folded; + for (const Sample& sample : samples) { + std::string line = targets[sample.thread].name; + for (unsigned i = sample.depth; i-- > 0;) { + line += ';'; + line += symbolizer.Name(sample.frames[i]); + } + ++folded[line]; + } + if (std::FILE* const out = std::fopen((std::string{path} + ".folded").c_str(), "w")) { + for (const auto& [line, count] : folded) { + std::fprintf(out, "%s %u\n", line.c_str(), count); + } + std::fclose(out); + } +} + +void Run(std::vector patterns, double start, double seconds, std::string out) { + std::this_thread::sleep_for(std::chrono::duration(start)); + const std::vector targets = FindThreads(patterns); + std::fprintf(stderr, "nx2 sampler: %zu threads match, sampling %.0f s\n", targets.size(), + seconds); + if (targets.empty()) { + return; + } + std::vector samples; + samples.reserve(static_cast(seconds * 1100.0) * targets.size() + 16); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration(seconds); + while (std::chrono::steady_clock::now() < deadline && + samples.size() + targets.size() <= samples.capacity()) { + for (unsigned i = 0; i < targets.size(); ++i) { + const HANDLE handle = targets[i].handle; + if (SuspendThread(handle) == static_cast(-1)) { + continue; + } + CONTEXT context{}; + context.ContextFlags = CONTEXT_FULL; + if (GetThreadContext(handle, &context)) { + Sample& sample = samples.emplace_back(); + sample.thread = i; + sample.depth = Walk(context, sample.frames); + } + ResumeThread(handle); + } + Sleep(1); + } + Report(targets, samples, out.c_str()); + std::fprintf(stderr, "nx2 sampler: %zu samples written to %s\n", samples.size(), out.c_str()); + for (const auto& target : targets) { + CloseHandle(target.handle); + } +} + +} // Anonymous namespace + +void StartFromEnvironment() { + const char* names = std::getenv("EDEN_NX2_SAMPLE"); + if (names == nullptr || *names == '\0') { + return; + } + const char* out = std::getenv("EDEN_NX2_SAMPLE_OUT"); + std::thread{Run, SplitNames(names), EnvSeconds("EDEN_NX2_SAMPLE_START", 60.0), + EnvSeconds("EDEN_NX2_SAMPLE_SECONDS", 30.0), + std::string{out != nullptr ? out : "nx2_samples.txt"}} + .detach(); +} + +} // namespace Nx2Sampler + +#else + +namespace Nx2Sampler { +void StartFromEnvironment() {} +} // namespace Nx2Sampler + +#endif diff --git a/src/yuzu_cmd/nx2_sampler.h b/src/yuzu_cmd/nx2_sampler.h new file mode 100644 index 0000000000..a73c8fa302 --- /dev/null +++ b/src/yuzu_cmd/nx2_sampler.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace Nx2Sampler { + +// EDEN_NX2_SAMPLE=: a sampling profiler for named host threads (Windows only). +// Names are comma separated; a name ending in '*' matches by prefix (CPUCore_*). From +// EDEN_NX2_SAMPLE_START seconds (default 60) for EDEN_NX2_SAMPLE_SECONDS (default 30), each +// thread is paused about 1000 times a second and its call stack recorded; the counts per +// function (where the thread is, and everything on its stack) go to EDEN_NX2_SAMPLE_OUT +// (default nx2_samples.txt). +void StartFromEnvironment(); + +} // namespace Nx2Sampler diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index bf256766fb..ffa1876c4f 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -54,6 +54,7 @@ #endif #include "yuzu_cmd/emu_window/emu_window_sdl3_null.h" #include "yuzu_cmd/emu_window/emu_window_sdl3_vk.h" +#include "yuzu_cmd/nx2_sampler.h" #ifdef _WIN32 // windows.h needs to be included before shellapi.h @@ -246,6 +247,7 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { #ifdef _WIN32 SetUnhandledExceptionFilter(WriteCrashDump); #endif + Nx2Sampler::StartFromEnvironment(); // SDL calls SDL_AppIterate back to back by default, which kept a host core at 100% polling // input; 240 Hz is plenty for input and the key script. SDL_SetHint(SDL_HINT_MAIN_CALLBACK_RATE, "240"); -- cgit v1.2.3 From 2bb0f8acdf79816ae1c474c42e2b63dbaa4500e8 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:54:03 -0400 Subject: texture_cache: Switch 2 collector thresholds follow the device memory budget Native titles' thresholds came from Eden's device memory figure, which is capped near 7 GB: on a tester's RTX 4070 SUPER (11.2 GB budget) title 1 kept evicting 800-1,600 textures per 600 frames with 3 GB unused. They now follow the budget (critical at budget - 768 MiB, expected at budget - 1536 MiB, re-read every 60 frames); the EDEN_NX2_TC_* overrides still win, and the thresholds are logged only when they move by 256 MiB. --- src/video_core/texture_cache/texture_cache.h | 27 ++++++++++++++++------- src/video_core/texture_cache/texture_cache_base.h | 2 +- 2 files changed, 20 insertions(+), 9 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 ad1804877a..13dfbba20e 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -175,7 +175,7 @@ void TextureCache

::RunGarbageCollector() { } template -void TextureCache

::ApplyThresholdOverrides() { +void TextureCache

::ApplyThresholdOverrides(bool log) { // 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}}) { @@ -183,8 +183,10 @@ void TextureCache

::ApplyThresholdOverrides() { *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); + if (log) { + LOG_INFO(HW_GPU, "Texture cache thresholds: expected {} MiB, critical {} MiB, minimum {} MiB", + expected_memory >> 20, critical_memory >> 20, minimum_memory >> 20); + } } template @@ -203,14 +205,23 @@ void TextureCache

::TickFrame() { native_critical_memory = critical_memory; native_expected_memory = expected_memory; } - // The budget shrinks when other programs use VRAM (another game: 7.0 -> 6.4 GB), and - // collecting only past it means the driver pages textures out. Re-read it now and then. + // Follow the driver's budget (re-read now and then): it shrinks when other programs use + // VRAM (another game: 7.0 -> 6.4 GB), and collecting only past it means the driver pages + // textures out. It is also the room to use: Eden's device memory figure is capped near + // 7 GB, which on a 12 GB card (11.2 GB budget) kept title 1 evicting textures with 3 GB + // unused. EDEN_NX2_TC_*_MIB overrides still win. if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) { - if (native_thresholds_applied && ++budget_check_frames % 60 == 0) { + if (native_thresholds_applied && budget_check_frames++ % 60 == 0) { const u64 budget = runtime.GetDeviceMemoryBudget(); if (budget > 2_GiB) { - critical_memory = (std::min)(native_critical_memory, budget - 512_MiB); - expected_memory = (std::min)(native_expected_memory, budget - 1280_MiB); + const u64 old_critical = critical_memory; + critical_memory = budget - 768_MiB; + expected_memory = budget - 1536_MiB; + // Log only real changes (the budget moves by a few MiB all the time). + const u64 change = critical_memory > old_critical + ? critical_memory - old_critical + : old_critical - critical_memory; + ApplyThresholdOverrides(change >= 256_MiB); } } } diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index ce1de8e867..3c96c91a93 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -467,7 +467,7 @@ private: DAddr virtual_invalid_space{}; /// Applies EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB and logs the thresholds. - void ApplyThresholdOverrides(); + void ApplyThresholdOverrides(bool log = true); /// 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; -- cgit v1.2.3 From e08b40252790eef96337409d7dd606aa7bfa8428 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:51:42 -0400 Subject: video_core: less waiting on the GPU caches' locks - The texture and buffer caches' mutexes spin briefly before sleeping (Common::SpinRecursiveMutex): the GPU thread, the fencing thread and CPU write traps take them tens of thousands of times a second for microseconds, and a sleeping waiter's wake-up took longer than that. Title 1's GPU thread slept 7.7% of its time there, now 3.7%. - Fences commit and pop texture and buffer downloads separately, and only when there are any (one fence per compute dispatch in Switch 2 titles): the fencing thread no longer holds both caches while it copies buffer downloads. - Inline uploads and pre-draw invalidation take the caches one at a time again: holding one while std::lock waited for the other waited longer. --- src/common/CMakeLists.txt | 1 + src/common/spin_recursive_mutex.h | 98 +++++++++++++++++++++++ src/video_core/buffer_cache/buffer_cache_base.h | 3 +- src/video_core/fence_manager.h | 49 +++++++++--- src/video_core/renderer_opengl/gl_rasterizer.cpp | 2 +- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 14 +++- src/video_core/texture_cache/texture_cache.h | 5 ++ src/video_core/texture_cache/texture_cache_base.h | 6 +- 8 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 src/common/spin_recursive_mutex.h (limited to 'src/video_core/texture_cache/texture_cache.h') diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index aae4afd180..d3e7b03cd2 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -96,6 +96,7 @@ add_library( ${CMAKE_CURRENT_BINARY_DIR}/scm_rev.cpp scm_rev.h scope_exit.h + spin_recursive_mutex.h scratch_buffer.h settings.cpp settings.h diff --git a/src/common/spin_recursive_mutex.h b/src/common/spin_recursive_mutex.h new file mode 100644 index 0000000000..291dd4e41e --- /dev/null +++ b/src/common/spin_recursive_mutex.h @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__) +#include +#endif + +namespace Common { + +/// A recursive mutex that spins briefly before sleeping. The GPU caches' locks are taken tens of +/// thousands of times a second by the GPU thread, the fencing thread and CPU write traps, and +/// held for microseconds: std::recursive_mutex put a waiting thread to sleep at once, and its +/// wake-up took longer than the critical section (title 1's GPU thread spent ~8% asleep there). +class SpinRecursiveMutex { +public: + void lock() { + const std::thread::id self = std::this_thread::get_id(); + if (owner.load(std::memory_order_relaxed) == self) { + ++depth; + return; + } + std::uint32_t expected = 0; + if (!state.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + LockContended(); + } + owner.store(self, std::memory_order_relaxed); + depth = 1; + } + + bool try_lock() { + const std::thread::id self = std::this_thread::get_id(); + if (owner.load(std::memory_order_relaxed) == self) { + ++depth; + return true; + } + std::uint32_t expected = 0; + if (!state.compare_exchange_strong(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + return false; + } + owner.store(self, std::memory_order_relaxed); + depth = 1; + return true; + } + + void unlock() { + if (--depth != 0) { + return; + } + owner.store(std::thread::id{}, std::memory_order_relaxed); + if (state.exchange(0, std::memory_order_release) == 2) { + state.notify_one(); + } + } + +private: + // About 10 microseconds of spinning before sleeping. + static constexpr int SpinCount = 1024; + + static void Pause() { +#if defined(_M_X64) || defined(_M_IX86) || defined(__x86_64__) || defined(__i386__) + _mm_pause(); +#elif defined(__aarch64__) + asm volatile("yield"); +#endif + } + + void LockContended() { + for (int spin = 0; spin < SpinCount; ++spin) { + Pause(); + if (state.load(std::memory_order_relaxed) == 0) { + std::uint32_t expected = 0; + if (state.compare_exchange_weak(expected, 1, std::memory_order_acquire, + std::memory_order_relaxed)) { + return; + } + } + } + // 2: held with (possible) sleepers; taken this way it stays marked, so its unlock wakes + // the next sleeper. + while (state.exchange(2, std::memory_order_acquire) != 0) { + state.wait(2, std::memory_order_relaxed); + } + } + + std::atomic state{0}; ///< 0 free, 1 held, 2 held with sleepers + std::atomic owner{}; + std::uint32_t depth{}; ///< Recursion depth, touched only by the owner +}; + +} // namespace Common diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index e4aa5896e5..57011752cc 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -21,6 +21,7 @@ #include #include "common/common_types.h" +#include "common/spin_recursive_mutex.h" #include "common/div_ceil.h" #include "common/literals.h" #include "common/lru_cache.h" @@ -339,7 +340,7 @@ public: } while (channel_state->has_deleted_buffers); } - std::recursive_mutex mutex; + Common::SpinRecursiveMutex mutex; Runtime& runtime; bool any_buffer_uploaded = false; diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h index 65d8f24aae..eb72529da0 100644 --- a/src/video_core/fence_manager.h +++ b/src/video_core/fence_manager.h @@ -81,7 +81,7 @@ public: if constexpr (!can_async_check) { TryReleasePendingFences(); } - CommitAsyncFlushes(); + const u8 committed_flushes = CommitAsyncFlushes(); TFence new_fence = CreateFence(!should_flush); if constexpr (can_async_check) { guard.lock(); @@ -94,6 +94,7 @@ public: pending_operations.emplace_back(std::move(uncommitted_operations)); uncommitted_operations.clear(); // moved-from: make it empty explicitly } + pending_flushes.push_back(committed_flushes); QueueFence(new_fence); if (!delay_fence) { func(); @@ -186,7 +187,8 @@ private: return; } } - PopAsyncFlushes(); + PopAsyncFlushes(pending_flushes.front()); + pending_flushes.pop_front(); auto operations = std::move(pending_operations.front()); pending_operations.pop_front(); for (auto& operation : operations) { @@ -207,6 +209,7 @@ private: TFence current_fence; std::deque> current_operations; + u8 current_flushes{}; while (!stop_token.stop_requested()) { { std::unique_lock lock(guard); @@ -216,13 +219,15 @@ private: } current_fence = std::move(fences.front()); current_operations = std::move(pending_operations.front()); + current_flushes = pending_flushes.front(); fences.pop(); pending_operations.pop_front(); + pending_flushes.pop_front(); } if (!current_fence->IsStubbed()) { WaitFence(current_fence); } - PopAsyncFlushes(); + PopAsyncFlushes(current_flushes); for (auto& operation : current_operations) { operation(); } @@ -245,27 +250,51 @@ private: query_cache.HasUncommittedFlushes(); } - void PopAsyncFlushes() { - { - std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; + static constexpr u8 TextureFlushes = 1; + static constexpr u8 BufferFlushes = 2; + + /// committed: what CommitAsyncFlushes returned for this fence. + void PopAsyncFlushes(u8 committed) { + if (committed & TextureFlushes) { + std::scoped_lock lock{texture_cache.mutex}; texture_cache.PopAsyncFlushes(); + } + if (committed & BufferFlushes) { + std::scoped_lock lock{buffer_cache.mutex}; buffer_cache.PopAsyncFlushes(); } query_cache.PopAsyncFlushes(); } - void CommitAsyncFlushes() { + /// Returns which caches committed downloads for this fence (TextureFlushes, BufferFlushes). + /// Switch 2 titles signal a fence per compute dispatch (thousands a second), mostly with + /// nothing or only buffer data to download: the fencing thread then takes only the lock of + /// the cache with work, instead of holding both while it copies (the GPU thread's draws and + /// uploads waited for it). + u8 CommitAsyncFlushes() { + u8 committed = 0; { - std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; - texture_cache.CommitAsyncFlushes(); - buffer_cache.CommitAsyncFlushes(); + std::scoped_lock lock{texture_cache.mutex}; + if (texture_cache.HasAsyncFlushWork()) { + texture_cache.CommitAsyncFlushes(); + committed |= TextureFlushes; + } + } + { + std::scoped_lock lock{buffer_cache.mutex}; + if (buffer_cache.HasUncommittedFlushes()) { + buffer_cache.CommitAsyncFlushes(); + committed |= BufferFlushes; + } } query_cache.CommitAsyncFlushes(); + return committed; } std::queue fences; std::deque> uncommitted_operations; std::deque>> pending_operations; + std::deque pending_flushes; ///< Per fence: the caches that committed downloads std::mutex guard; std::mutex ring_guard; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 1d77d28c46..4e0972d1d2 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -716,7 +716,7 @@ void RasterizerOpenGL::AccelerateInlineToMemory(GPUVAddr address, size_t copy_si } gpu_memory->WriteBlockUnsafe(address, memory.data(), copy_size); { - std::unique_lock lock{buffer_cache.mutex}; + std::unique_lock lock{buffer_cache.mutex}; if (!buffer_cache.InlineMemory(*cpu_addr, copy_size, memory)) { buffer_cache.WriteMemory(*cpu_addr, copy_size); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 886c5dbfe4..429b2d63ef 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1036,11 +1036,13 @@ void RasterizerVulkan::InvalidateRegion(DAddr addr, u64 size, VideoCommon::Cache void RasterizerVulkan::InnerInvalidation(std::span> sequences) { { - // Both caches under one lock: this runs before most draws (constant buffer updates). - std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; + std::scoped_lock lock{texture_cache.mutex}; for (const auto& [addr, size] : sequences) { texture_cache.WriteMemory(addr, size); } + } + { + std::scoped_lock lock{buffer_cache.mutex}; for (const auto& [addr, size] : sequences) { buffer_cache.WriteMemory(addr, size); } @@ -1248,12 +1250,16 @@ void RasterizerVulkan::AccelerateInlineToMemory(GPUVAddr address, size_t copy_si return; } gpu_memory->WriteBlockUnsafe(address, memory.data(), copy_size); + // One cache at a time: holding both while waiting for either (std::lock) waited longer, the + // fencing thread and CPU write traps holding one of them most of the time. { - // Both caches under one lock (title 1 makes thousands of inline uploads a second). - std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; + std::scoped_lock lock{buffer_cache.mutex}; if (!buffer_cache.InlineMemory(*cpu_addr, copy_size, memory)) { buffer_cache.WriteMemory(*cpu_addr, copy_size); } + } + { + std::scoped_lock lock{texture_cache.mutex}; texture_cache.WriteMemory(*cpu_addr, copy_size); } pipeline_cache.InvalidateRegion(*cpu_addr, copy_size); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 13dfbba20e..fb8adef161 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -989,6 +989,11 @@ bool TextureCache

::HasUncommittedFlushes() const noexcept { return !uncommitted_downloads.empty(); } +template +bool TextureCache

::HasAsyncFlushWork() const noexcept { + return !uncommitted_downloads.empty() || !uncommitted_async_buffers.empty(); +} + template bool TextureCache

::ShouldWaitAsyncFlushes() const noexcept { return !committed_downloads.empty() && !committed_downloads.front().empty(); diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 3c96c91a93..11ac8a2421 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -23,6 +23,7 @@ #include #include "common/common_types.h" +#include "common/spin_recursive_mutex.h" #include "common/hash.h" #include "common/literals.h" #include "common/lru_cache.h" @@ -244,6 +245,9 @@ public: /// Return true when there are uncommitted images to be downloaded [[nodiscard]] bool HasUncommittedFlushes() const noexcept; + /// Whether CommitAsyncFlushes would commit anything (downloads or their buffers). + [[nodiscard]] bool HasAsyncFlushWork() const noexcept; + /// Return true when the caller should wait for async downloads [[nodiscard]] bool ShouldWaitAsyncFlushes() const noexcept; @@ -276,7 +280,7 @@ public: /// Prepare an image to be used void PrepareImage(ImageId image_id, bool is_modification, bool invalidate); - std::recursive_mutex mutex; + Common::SpinRecursiveMutex mutex; private: /// Iterate over all page indices in a range -- cgit v1.2.3