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/buffer_cache/buffer_cache.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src/video_core/buffer_cache/buffer_cache.h') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index f30c85911f..126e73c9b6 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "common/range_sets.inc" #include "video_core/buffer_cache/buffer_cache_base.h" @@ -1372,7 +1373,12 @@ void BufferCache

::UpdateVertexBuffer(u32 index) { const auto& array = maxwell3d->regs.vertex_streams[index]; const auto& limit = maxwell3d->regs.vertex_stream_limits[index]; const GPUVAddr gpu_addr_begin = array.Address(); - const GPUVAddr gpu_addr_end = limit.Address() + 1; + const bool native = maxwell3d->GetNativeMacroEngine().native_turing; + const u64 native_size = native ? maxwell3d->regs.NativeVertexStreamSize(index) : 0; + if (native && array.enable != 0 && native_size > (std::numeric_limits::max)()) { + throw std::runtime_error("Native vertex stream exceeds supported 32-bit buffer size"); + } + const GPUVAddr gpu_addr_end = native ? gpu_addr_begin + native_size : limit.Address() + 1; const std::optional device_addr = gpu_memory->GpuToCpuAddress(gpu_addr_begin); const u32 address_size = static_cast(gpu_addr_end - gpu_addr_begin); u32 size = address_size; // TODO: Analyze stride and number of vertices -- 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/buffer_cache/buffer_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 c01459347405591446d50858d7b075d617a58be9 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:26:52 -0400 Subject: video_core: cheaper small GPU reads, buffer usage tracking and compute uniforms - MemoryManager::ReadBlockUnsafe reads within one 4 KiB page through a single translation (as Read does) instead of the page walk: texture and sampler descriptors, several per draw (2.6% -> 0.1% of title 1's GPU thread). - UsageTracker skips ranges it tracked just before (until Reset): draws bind the same multi-megabyte storage buffers again and again, and each Track wrote a word per 4 KiB (2.0% -> 0.7%). - Small compute uniform buffers the GPU has not written are streamed, as graphics ones are; synchronizing them re-protected pages an inline upload had just unprotected, two VirtualProtect calls per dispatch (1.9% -> 0.7%, page protection 3.2% -> 2.3%). --- src/video_core/buffer_cache/buffer_cache.h | 9 ++++++++- src/video_core/buffer_cache/usage_tracker.h | 17 +++++++++++++++++ src/video_core/memory_manager.cpp | 10 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) (limited to 'src/video_core/buffer_cache/buffer_cache.h') diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 4df855c0fe..ad2e200438 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -1211,8 +1211,15 @@ void BufferCache

::BindHostComputeUniformBuffers() { return alignment > 1 && (offset % alignment) != 0; } }(); + // Small buffers the GPU has not written are streamed, as graphics ones are: synchronizing + // them made every dispatch re-protect pages an inline upload had just unprotected (two + // VirtualProtect calls per dispatch, ~4,000 dispatches a second in title 1). + const bool use_fast_buffer = + needs_alignment_stream || + (has_host_buffer && size <= channel_state->uniform_buffer_skip_cache_size && + !memory_tracker.IsRegionGpuModified(binding.device_addr, size)); if constexpr (!IS_OPENGL) { - if (needs_alignment_stream) { + if (use_fast_buffer) { const std::span span = runtime.BindMappedUniformBuffer(0, binding_index, size); device_memory.ReadBlockUnsafe(binding.device_addr, span.data(), size); diff --git a/src/video_core/buffer_cache/usage_tracker.h b/src/video_core/buffer_cache/usage_tracker.h index 6f84e463e3..3e2da8bc0b 100644 --- a/src/video_core/buffer_cache/usage_tracker.h +++ b/src/video_core/buffer_cache/usage_tracker.h @@ -6,6 +6,10 @@ #pragma once +#include +#include +#include + #include "common/alignment.h" #include "common/common_types.h" @@ -24,9 +28,19 @@ public: void Reset() noexcept { std::ranges::fill(pages, 0ULL); + recent.fill({~u64{0}, 0}); } void Track(u64 offset, u64 size) noexcept { + // Tracking is idempotent until Reset: skip ranges tracked just before. Draws bind the + // same (often multi-megabyte) storage buffers again and again, and each Track wrote a + // word per 4 KiB of the range. + for (const auto& [recent_offset, recent_size] : recent) { + if (recent_offset == offset && recent_size == size) { + return; + } + } + recent[recent_cursor++ % recent.size()] = {offset, size}; const size_t page = offset >> BUFFER_PAGE_SHIFT; const size_t page_end = (offset + size) >> BUFFER_PAGE_SHIFT; if (page_end < page || page_end >= pages.size()) { @@ -83,6 +97,9 @@ private: private: std::vector pages; + /// Ranges tracked last, (offset, size). + std::array, 4> recent{{{~u64{0}, 0}, {~u64{0}, 0}, {~u64{0}, 0}, {~u64{0}, 0}}}; + size_t recent_cursor{}; }; } // namespace VideoCommon diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 9a9fdab7bc..0eae8fac24 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -410,6 +410,16 @@ void MemoryManager::ReadBlock(GPUVAddr gpu_src_addr, void* dest_buffer, std::siz } void MemoryManager::ReadBlockUnsafe(GPUVAddr gpu_src_addr, void* dest_buffer, const std::size_t size) const { + // Small reads within one 4 KiB page (texture and sampler descriptors, several per draw): one + // translation and a copy, as Read does, instead of the page walk (~3% of the GPU thread + // in title 1). Anything else, unmapped or sparse pages included, takes the full path. + constexpr std::size_t SmallPage = 0x1000; + if (size <= SmallPage && (gpu_src_addr & (SmallPage - 1)) + size <= SmallPage) { + if (const u8* const pointer = GetPointer(gpu_src_addr)) { + std::memcpy(dest_buffer, pointer, size); + return; + } + } ReadBlockImpl(gpu_src_addr, dest_buffer, size, VideoCommon::CacheType::None, true); } -- cgit v1.2.3