From 6e7edd7462325ee18a0db6fdc29ff7698ac108cc Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:30:09 +0100 Subject: fix: Improve QMD cache management by removing completed entries to prevent stalling and memory leakage --- src/video_core/native_qmd.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/video_core/native_qmd.h b/src/video_core/native_qmd.h index 15c59cb7c6..924267e406 100644 --- a/src/video_core/native_qmd.h +++ b/src/video_core/native_qmd.h @@ -32,7 +32,13 @@ public: pending[0] = {address, argument & 15, ((argument >> 14) & 0x3ff) + 1}; for (std::size_t i = 0; i < count; ++i) { const auto request = pending[i]; - if (states.size() >= 4096 && !states.contains(request.address)) return "QMD cache limit"; + if (states.size() >= 4096 && !states.contains(request.address)) { + // Finished QMDs with nothing deferred are only history, drop them instead of stalling. + std::erase_if(states, [](const auto& entry) { + return entry.second.completed && entry.second.deferred == 0; + }); + if (states.size() >= 4096) return "QMD cache limit"; + } auto& state = states[request.address]; if (request.action == 3 || request.action == 2) { if (request.action == 3 || !state.loaded) { -- cgit v1.2.3 From 9e4e58c8e54add0e9909de8a9cba2444d8314ee7 Mon Sep 17 00:00:00 2001 From: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:54:54 -0300 Subject: yuzu_cmd: raise the open file limit on macOS and Linux Loose RomFS folders keep thousands of files open through the VFS cache. macOS shells default to 256, so title 1 stopped loading at "Too many open files". Raise it to 8192 like the Qt frontend and like the Windows path here already does. Assisted-by: Claude (Anthropic) --- src/yuzu_cmd/yuzu.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src') diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 3d9f50d4e5..70fd9090e3 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -56,6 +57,10 @@ #include "yuzu_cmd/emu_window/emu_window_sdl3_vk.h" #include "yuzu_cmd/nx2_sampler.h" +#if defined(__unix__) || defined(__APPLE__) +#include +#endif + #ifdef _WIN32 // windows.h needs to be included before shellapi.h #include @@ -262,6 +267,12 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { // Raise the CRT's open-file limit like the Qt frontend: loose RomFS folders keep thousands of // files open through the VFS cache. _setmaxstdio(8192); +#elif defined(__unix__) || defined(__APPLE__) + // Same for the open file limit (macOS shells default to 256). + if (struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl) == 0) { + rl.rlim_cur = std::min(8192, rl.rlim_max); + setrlimit(RLIMIT_NOFILE, &rl); + } #endif Common::Log::Initialize(); -- cgit v1.2.3 From adf02d0f1e6316a7b4a84949e716dcec15880224 Mon Sep 17 00:00:00 2001 From: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:54:54 -0300 Subject: vulkan: clamp compute shared memory on MoltenVK Switch 2 compute shaders request 48 KiB; Apple GPUs allow 32 KiB of threadgroup memory and Metal rejected the pipelines. Assisted-by: Claude (Anthropic) --- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 17f57983e3..ac878ddfef 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1089,7 +1089,7 @@ std::unique_ptr PipelineCache::CreateComputePipeline( const VkDriverIdKHR driver_id = device.GetDriverID(); const bool needs_shared_mem_clamp = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY || - driver_id == VK_DRIVER_ID_ARM_PROPRIETARY; + driver_id == VK_DRIVER_ID_ARM_PROPRIETARY || driver_id == VK_DRIVER_ID_MOLTENVK; const u32 max_shared_memory = device.GetMaxComputeSharedMemorySize(); if (needs_shared_mem_clamp && program.shared_memory_size > max_shared_memory) { LOG_WARNING(Render_Vulkan, -- cgit v1.2.3 From 487d04f8edf641288c8023009600d5d306cad586 Mon Sep 17 00:00:00 2001 From: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:54:55 -0300 Subject: yuzu_cmd: give MoltenVK the view's CAMetalLayer VkMetalSurfaceCreateInfoEXT needs the CAMetalLayer, not the NSView that SDL_Metal_CreateView returns. Assisted-by: Claude (Anthropic) --- src/yuzu_cmd/emu_window/emu_window_sdl3_vk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3_vk.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3_vk.cpp index afbe67716f..08e0db10da 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3_vk.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3_vk.cpp @@ -73,7 +73,7 @@ EmuWindow_SDL3_VK::EmuWindow_SDL3_VK(InputCommon::InputSubsystem* input_subsyste } else if (SDL_GetPointerProperty(window_props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr) != nullptr) { window_info.type = Core::Frontend::WindowSystemType::Cocoa; - window_info.render_surface = SDL_Metal_CreateView(render_window); + window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(render_window)); } else if (void* android_window = SDL_GetPointerProperty(window_props, SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, nullptr); -- cgit v1.2.3 From 9ad01b60c8cefe0bb2d91eabf67cf54a1eeeb92c Mon Sep 17 00:00:00 2001 From: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:58:10 -0300 Subject: video_core: arm64 spin hint in the GPU split Pause() fell back to a sched_yield system call on arm64; use the yield instruction like the x86 PAUSE. Mark the Windows-only back thread id unused for clang. Assisted-by: Claude (Anthropic) --- src/video_core/gpu_split.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp index f913ca82ce..2bc8e29470 100644 --- a/src/video_core/gpu_split.cpp +++ b/src/video_core/gpu_split.cpp @@ -34,12 +34,14 @@ void Pause() { _mm_pause(); #elif defined(__x86_64__) || defined(__i386__) __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield"); #else std::this_thread::yield(); #endif } -std::atomic g_back_thread_id{}; +[[maybe_unused]] std::atomic g_back_thread_id{}; std::atomic g_back_busy{}; } // Anonymous namespace -- cgit v1.2.3 From dfb3241838c17bd0e6c392cc29f24d085185dd73 Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:57:05 +0100 Subject: nx2: apply GPU accuracy changes mid-game by stopping and restarting the GPU split The split starts only with unsafe command reads, decided once at start-up. Switching to accurate afterwards made the front thread flush caches the back thread owns, which raced on Scheduler::chunk and crashed. MaybeStart now re-checks the settings between commands: it drains and stops the split when they no longer allow it and starts it again when they do. DmaPusher keeps unsafe reads while the split is active to cover the gap until then. --- src/video_core/dma_pusher.cpp | 5 ++++- src/video_core/gpu_split.cpp | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp index 62f7e6bfdb..88d571ae5f 100644 --- a/src/video_core/dma_pusher.cpp +++ b/src/video_core/dma_pusher.cpp @@ -151,7 +151,10 @@ bool DmaPusher::Step() { } } // A resumed entry is read again: the words after the acquire may be new by now. - const bool use_safe = Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe(); + // A safe read flushes the caches, which belong to the split's back thread while it runs + // (the split only starts with unsafe reads; the settings can change mid-game). + const bool use_safe = !VideoCommon::GpuSplit::Get().Active() && + (Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe()); if (use_safe) { Tegra::Memory::GpuGuestMemoryheaders(memory_manager, dma_state.dma_get, header.size, &command_headers); const std::span commands{headers.data(), headers.size()}; diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp index f913ca82ce..3997ea4565 100644 --- a/src/video_core/gpu_split.cpp +++ b/src/video_core/gpu_split.cpp @@ -88,15 +88,23 @@ void GpuSplit::MaybeStart(Core::System& system_) { return value != nullptr ? value[0] : '2'; }(); const bool wanted = mode == '1' || mode == '2'; - if (active || !wanted || !Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) { + if (!wanted || !Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) { return; } // The front reads command lists straight from guest memory; a read that flushes the caches - // first (safe DMA, accurate GPU) would touch back-owned state. + // first (safe DMA, accurate GPU) would touch back-owned state. The settings can change + // mid-game: hand the engines back to the front (and start again when they allow it). const bool safe_reads = Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe(); - if (safe_reads || Settings::IsGPULevelHigh() || - Settings::values.renderer_backend.GetValue() != Settings::RendererBackend::Vulkan) { + const bool allowed = !safe_reads && !Settings::IsGPULevelHigh() && + Settings::values.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan; + if (active && !allowed) { + Drain(); + Stop(); + LOG_INFO(HW_GPU, "GPU thread split: stopped, the accuracy settings need the caches on the front"); + return; + } + if (active || !allowed) { return; } system = &system_; -- cgit v1.2.3 From 24e3cb462e47b22edda334939123694ff6920e95 Mon Sep 17 00:00:00 2001 From: givethesourceplox Date: Fri, 30 Oct 2026 11:09:38 -0300 Subject: core: return empty perf stats before the system is running (#30) The SDL frontend's title timer can fire before perf_stats exists and crashed on its null mutex during boot. Assisted-by: Claude (Anthropic) Co-authored-by: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> --- src/core/core.cpp | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/core/core.cpp b/src/core/core.cpp index 3728ccf8f7..64e1d8de06 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -496,6 +496,9 @@ struct System::Impl { } PerfStatsResults GetAndResetPerfStats() { + if (!perf_stats) { + return {}; + } return perf_stats->GetAndResetStats(core_timing.GetGlobalTimeUs()); } -- cgit v1.2.3 From 75b707a90ac8610e3a7da277e9291fd2ba4a6b52 Mon Sep 17 00:00:00 2001 From: ZnOw01 Date: Fri, 30 Oct 2026 09:35:24 -0500 Subject: yuzu_cmd: skip kernel teardown when nothing was booted SDL_AppQuit runs on every exit path, including the ones where SDL_AppInit returned early: no ROM given, --help, or --version. The kernel only exists once a title is loaded, so Pause() and ShutdownMainProcess() walk into an uninitialized KernelCore and the process dies with SIGSEGV. The crash lands before spdlog flushes, which is why --version printed nothing instead of a version line. Guard the teardown on System::IsPoweredOn(), and ignore the null appstate SDL passes when SDL_AppInit never stored one. Assisted-by: Claude (Anthropic) (diagnosed the crash and wrote the guard) --- src/yuzu_cmd/yuzu.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 70fd9090e3..22abe6a759 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -491,9 +491,17 @@ extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { } extern "C" void SDL_AppQuit(void *appstate, SDL_AppResult result) { SdlState *state = (SdlState *)appstate; + if (!state) { + return; + } state->system.DetachDebugger(); - void(state->system.Pause()); - state->system.ShutdownMainProcess(); + // SDL calls this even when SDL_AppInit never powered the system on (no ROM given, --help or + // --version): the kernel is uninitialised then, and Pause()/ShutdownMainProcess() would + // dereference it. + if (state->system.IsPoweredOn()) { + void(state->system.Pause()); + state->system.ShutdownMainProcess(); + } delete state; } -- cgit v1.2.3 From f9fc1e10fd7ed0a1c4664d48d2f095ce8826d87b Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:39:42 -0400 Subject: memory_manager: notify the caches once per changed range PageTableOp and BigPageTableOp called ModifyGPUMemory for every page whose entry changed, and each call scans the texture and buffer caches. Mapping one of Mario Kart World's 1.45 GB sparse regions made ~370,000 of those scans. Runs of changed pages are now reported as one range. --- src/video_core/memory_manager.cpp | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 0eae8fac24..2dd8b04656 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -120,11 +120,26 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de page_table.ReserveRange(gpu_addr, size); } for (u64 offset{}; offset < size; offset += page_size) { + // The caches hear about each run of pages whose entry changed in one call, not page by page + // (a 1.45 GB Switch 2 region made ~370,000 texture cache scans). + GPUVAddr modified_start{}; + u64 modified_size{}; + const auto flush_modified = [&] { + if (modified_size != 0) { + rasterizer->ModifyGPUMemory(unique_identifier, modified_start, modified_size); + modified_size = 0; + } + }; const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, false); SetEntry(current_gpu_addr, entry_type, false); if (current_entry_type != entry_type) { - rasterizer->ModifyGPUMemory(unique_identifier, current_gpu_addr, page_size); + if (modified_size == 0) { + modified_start = current_gpu_addr; + } + modified_size += page_size; + } else { + flush_modified(); } if (entry_type == EntryType::Mapped) { const DAddr current_dev_addr = dev_addr + offset; @@ -135,17 +150,31 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de remaining_size -= page_size; } kind_map.Map(gpu_addr, gpu_addr + size, kind); + flush_modified(); return gpu_addr; } GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr dev_addr, size_t size, PTEKind kind, MemoryManager::EntryType entry_type) { [[maybe_unused]] u64 remaining_size{size}; for (u64 offset{}; offset < size; offset += big_page_size) { + GPUVAddr modified_start{}; + u64 modified_size{}; + const auto flush_modified = [&] { + if (modified_size != 0) { + rasterizer->ModifyGPUMemory(unique_identifier, modified_start, modified_size); + modified_size = 0; + } + }; const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, true); SetEntry(current_gpu_addr, entry_type, true); if (current_entry_type != entry_type) { - rasterizer->ModifyGPUMemory(unique_identifier, current_gpu_addr, big_page_size); + if (modified_size == 0) { + modified_start = current_gpu_addr; + } + modified_size += big_page_size; + } else { + flush_modified(); } if (entry_type == EntryType::Mapped) { const DAddr current_dev_addr = dev_addr + offset; @@ -173,6 +202,7 @@ GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr remaining_size -= big_page_size; } { + flush_modified(); std::unique_lock lock(guard); kind_map.Map(gpu_addr, gpu_addr + size, kind); } -- cgit v1.2.3 From 6380b6687ded27892404c576becd78b7161ba68a Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:39:43 -0400 Subject: nx2: give sparse regions memory only where the GPU writes nx2:gpu 214 regions with the sparse flag (word 7 bit 0x200) are Mario Kart World's terrain texture arrays and layer pools, 170 MB to 5.8 GB, mostly empty. Backed whole they ran the graphics pool dry (26 "could not allocate" per session, later regions without memory), and backing a block at a time made the texture cache rebuild the 1.45 GB arrays each time a block was mapped. A sparse region now gets a device (SMMU) range of its own when it is created, mapped for the GPU at once, so its GPU mapping never changes. Before a GPU-side write lands in it (WriteBlock, Write), the 4 MiB blocks it touches get memory, cleared, behind their device pages. Unbacked device pages read zero and drop writes instead of crashing, and the unmapped-device-access errors are logged 16 times, not once per page. nx2:gpu 215 releases the device range and the blocks after the GPU is done with them. The log reports GPU-only memory in use and its peak. Mario Kart World, 180 s: no allocation failures; ~2.8 GB of terrain blocks get memory instead of 6.5 GB of whole regions, next to 3 GB of ordinary GPU-only regions. Terrain renders as before. --- src/core/device_memory_manager.inc | 44 +++--- .../hle/service/nvdrv/devices/nvhost_as_gpu.cpp | 60 +++++++- src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h | 6 + .../hle/service/nvdrv/devices/nx2_stub_device.h | 53 ++++++- src/core/hle/service/nx2_gsv.h | 156 +++++++++++++++++++-- src/video_core/memory_manager.cpp | 30 ++-- src/video_core/nx2.h | 40 ++++++ 7 files changed, 349 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index d70444ef8c..d78a229e1f 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -425,10 +425,13 @@ void DeviceMemoryManager::ReadBlock(DAddr address, void* dest_pointer, s WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } std::memset(dest_pointer, 0, copy_amount); }, [&](size_t copy_amount, const u8* const src_ptr) { @@ -444,10 +447,13 @@ void DeviceMemoryManager::WriteBlock(DAddr address, const void* src_poin WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } }, [&](size_t copy_amount, u8* const dst_ptr) { std::memcpy(dst_ptr, src_pointer, copy_amount); @@ -484,10 +490,13 @@ void DeviceMemoryManager::ReadBlockUnsafe(DAddr address, void* dest_poin WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device ReadBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } std::memset(dest_pointer, 0, copy_amount); }, [&](size_t copy_amount, const u8* const src_ptr) { @@ -504,10 +513,13 @@ void DeviceMemoryManager::WriteBlockUnsafe(DAddr address, const void* sr WalkBlock( address, size, [&](size_t copy_amount, DAddr current_vaddr) { - LOG_ERROR( - HW_Memory, - "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", - current_vaddr, address, size); + static std::atomic reported{}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 16) { + LOG_ERROR( + HW_Memory, + "Unmapped Device WriteBlock @ {:#016x} (start address = {:#016x}, size = {})", + current_vaddr, address, size); + } }, [&](size_t copy_amount, u8* const dst_ptr) { std::memcpy(dst_ptr, src_pointer, copy_amount); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index 2634902641..c4119b2ab1 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -63,7 +63,13 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span i Nx2Map(gpu_address, cpu_address, size); }, [this](u64 gpu_address, u64 size) { Nx2Unmap(gpu_address, size); }, - [this](u64 cpu_address, u64 size) { Nx2Release(cpu_address, size); }); + [this](u64 cpu_address, u64 size) { Nx2Release(cpu_address, size); }, + [this](u64 gpu_address, u64 size, bool create) { + Nx2Sparse(gpu_address, size, create); + }, + [this](u64 gpu_address, u64 cpu_address, u64 size) { + Nx2BackSparse(gpu_address, cpu_address, size); + }); } return result; } @@ -185,6 +191,58 @@ void nvhost_as_gpu::Nx2Release(u64 cpu_address, u64 size) { LOG_DEBUG(Service_NVDRV, "Switch 2 released backing {:#x}+{:#x}", cpu_address, size); } +void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) { + std::scoped_lock lock(mutex); + if (!gmmu) { + return; + } + auto& smmu = system.Host1x().MemoryManager(); + const u64 device_size = Common::AlignUp(size, 0x20000); + if (!create) { + const auto it = nx2_sparse_ranges.find(gpu_address); + if (it == nx2_sparse_ranges.end()) { + return; + } + const DAddr device_address = it->second.second; + nx2_sparse_ranges.erase(it); + // After the command lists that may still use it. + system.GPU().RunInOrder([gmmu = gmmu.get(), &smmu, gpu_address, size, device_address, + device_size] { + gmmu->Unmap(gpu_address, size); + smmu.Unmap(device_address, size); + smmu.Free(device_address, device_size); + }); + return; + } + const DAddr device_address = smmu.Allocate(device_size); + if (device_address == 0) { + LOG_CRITICAL(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x}: out of SMMU space", + gpu_address, size); + return; + } + nx2_sparse_ranges[gpu_address] = {size, device_address}; + gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false); + LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address, + size, device_address); +} + +void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lock(mutex); + auto it = nx2_sparse_ranges.upper_bound(gpu_address); + auto* session = container.GetSession(nx2_session); + if (it == nx2_sparse_ranges.begin() || session == nullptr) { + return; + } + --it; + const auto& [start, range] = *it; + const auto& [region_size, device_address] = range; + if (gpu_address + size > start + region_size) { + return; + } + system.Host1x().MemoryManager().Map(device_address + (gpu_address - start), cpu_address, size, + session->asid, true); +} + void nvhost_as_gpu::OnClose(DeviceFD fd) {} // Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h index d64ec85dec..69050bb677 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h @@ -241,6 +241,12 @@ private: }; std::map, Nx2DeviceRange> nx2_device_ranges; void Nx2Release(u64 cpu_address, u64 size); + + // Switch 2 sparse regions: a device range per region, mapped for the GPU when it's created; + // blocks get memory behind their device pages as the GPU writes them. + void Nx2Sparse(u64 gpu_address, u64 size, bool create); + void Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size); + std::map> nx2_sparse_ranges; ///< GPU address -> (size, device) }; } // namespace Service::Nvidia::Devices diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h index 08c21f09e3..989bf61682 100644 --- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h +++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h @@ -40,6 +40,10 @@ public: using Listener = std::function; using UnmapListener = std::function; using ReleaseListener = std::function; + /// A sparse region is created (true) or destroyed (false). + using SparseListener = std::function; + /// A block of a sparse region gets memory [cpu_address, +size). + using BackListener = std::function; static Nx2GpuMemoryRegistry& Get() { static Nx2GpuMemoryRegistry registry; @@ -83,14 +87,55 @@ public: } } + // Sparse regions (nx2:gpu 214 with the sparse flag): the GPU range reaches device memory of its + // own from the start, and each block gets memory when the GPU first writes it (BackSparse). + // The GPU mapping never changes, so the texture cache doesn't rebuild the images over it as + // blocks get memory; unbacked blocks read zero. + void CreateSparse(u64 gpu_address, u64 size) { + std::scoped_lock lk{mutex}; + sparse_regions[gpu_address] = size; + for (auto& [owner, listener] : listeners) { + if (listener.sparse) { + listener.sparse(gpu_address, size, true); + } + } + } + + void DestroySparse(u64 gpu_address, u64 size) { + std::scoped_lock lk{mutex}; + sparse_regions.erase(gpu_address); + for (auto& [owner, listener] : listeners) { + if (listener.sparse) { + listener.sparse(gpu_address, size, false); + } + } + } + + void BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lk{mutex}; + for (auto& [owner, listener] : listeners) { + if (listener.back) { + listener.back(gpu_address, cpu_address, size); + } + } + } + void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener, - ReleaseListener release_listener = {}) { + ReleaseListener release_listener = {}, SparseListener sparse_listener = {}, + BackListener back_listener = {}) { std::scoped_lock lk{mutex}; for (const auto& m : mappings) { listener(m.gpu_address, m.cpu_address, m.size); } + if (sparse_listener) { + for (const auto& [gpu_address, size] : sparse_regions) { + sparse_listener(gpu_address, size, true); + } + } listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener), - std::move(release_listener)}); + std::move(release_listener), + std::move(sparse_listener), + std::move(back_listener)}); } void RemoveListener(const void* owner) { @@ -107,6 +152,7 @@ public: } std::scoped_lock lk{mutex}; mappings.clear(); + sparse_regions.clear(); } // Device (SMMU) address of a CPU range an address space mapped; used to present images. @@ -144,6 +190,8 @@ private: Listener map; UnmapListener unmap; ReleaseListener release; + SparseListener sparse; + BackListener back; }; // Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it. @@ -169,6 +217,7 @@ private: std::mutex mutex; std::vector mappings; + std::map sparse_regions; ///< GPU address -> size std::map listeners; }; diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 3f1f25b6bf..f3cc87ac4d 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -24,9 +24,11 @@ #include #include "common/alignment.h" +#include "common/div_ceil.h" #include "common/settings.h" #include "common/thread.h" #include "core/core.h" +#include "core/device_memory.h" #include "video_core/rasterizer_interface.h" #include "video_core/renderer_base.h" #include "core/frontend/framebuffer_layout.h" @@ -314,9 +316,15 @@ public: {214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"}, {215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"}, }); + if (LazySparseRegions()) { + Tegra::Nx2SetSparseBacking( + [this](u64 gpu_address, u64 size) { BackSparseWrite(gpu_address, size); }); + } } ~INx2GpuSession() override { + // Waits for a backing the GPU thread may be doing. + Tegra::Nx2SetSparseBacking({}); for (auto* event : m_events) { if (event != nullptr) { m_context.CloseEvent(event); @@ -532,11 +540,23 @@ private: w[1], w[2], w[3], w[4], w[5], w[6], w[7], w[8], w[9], w[10], w[11], w[12]); if (last > start && ((last + 1 - start) % Core::Memory::YUZU_PAGESIZE) == 0) { const u64 size = last + 1 - start; + std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); - if (const auto address = BackGpuOnlyRegion(start, size)) { + if ((w[7] & SparseRegionFlag) != 0 && LazySparseRegions()) { + // Title 1's terrain texture arrays and layer pools (170 MB-5.8 GB, mostly empty): + // backed whole, they ran the pool dry and the later ones got no memory at all. + // The region gets a device range of its own, unbacked pages read zero, and + // BackSparseWrite gives a block memory before the GPU writes into it. + m_sparse_regions[start] = {size, std::vector(Common::DivCeil(size, SparseBlockSize))}; + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size); + Tegra::Nx2AddSparseSpan(start, size); + LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it", + start, last); + } else if (const auto address = BackGpuOnlyRegion(start, size)) { Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(start, *address, size); - LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x}", - start, last, *address); + LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x} " + "({} MiB of GPU-only memory in use, peak {} MiB)", + start, last, *address, m_gpu_only_bytes >> 20, m_gpu_only_peak >> 20); } } else { LOG_WARNING(Service, "nx2:gpu 214 unexpected region [{:#x}, {:#x}]", start, last); @@ -556,17 +576,30 @@ private: const u64 last = u64{w[5]} | (u64{w[6]} << 32); LOG_DEBUG(Service, "nx2:gpu 215 destroy sparse region [{:#x}, {:#x}] words {:#x} {:#x} {:#x} {:#x}", first, last, w[1], w[2], w[7], w[8]); + std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); if (last > first) { - Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1); - const auto region = m_gpu_only_regions.find(first); - if (region != m_gpu_only_regions.end() && region->second.size == last - first + 1) { + if (m_sparse_regions.erase(first) != 0) { + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1); + } else { + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1); + } + // The region's backing: all of it, or the blocks a sparse region got as it was written. + std::shared_ptr> done; + for (auto region = m_gpu_only_regions.lower_bound(first); + region != m_gpu_only_regions.end() && region->first <= last;) { + if (region->first + region->second.size - 1 > last) { + ++region; + continue; + } Nvidia::Devices::Nx2GpuMemoryRegistry::Get().ReleaseBacking( region->second.cpu_address, region->second.size); - auto done = std::make_shared>(false); - system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); - m_released_regions.push_back({std::move(region->second), std::move(done)}); - m_gpu_only_regions.erase(region); + if (!done) { + done = std::make_shared>(false); + system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); + } + m_released_regions.push_back({std::move(region->second), done}); + region = m_gpu_only_regions.erase(region); } } SignalAfterGpu(w[8]); @@ -590,11 +623,13 @@ private: return true; // still mapped: keep its pages } region.pages->Close(system.Kernel()); + m_gpu_only_bytes -= region.size; return true; }); } - std::optional BackGpuOnlyRegion(u64 gpu_address, u64 size) { + std::optional BackGpuOnlyRegion(u64 gpu_address, u64 size, bool log_failure = true, + bool clear = false) { using Core::Memory::YUZU_PAGESIZE; auto& kernel_core = system.Kernel(); auto* const process = system.ApplicationProcess(); @@ -611,18 +646,36 @@ private: pg.get(), size / YUZU_PAGESIZE, Kernel::KMemoryManager::EncodeOption(Kernel::KMemoryManager::Pool::SystemNonSecure, Kernel::KMemoryManager::Direction::FromBack)))) { - LOG_ERROR(Service, "nx2:gpu 214 could not allocate {:#x} bytes", size); + if (log_failure) { + LOG_ERROR(Service, "nx2:gpu 214 could not allocate {:#x} bytes", size); + } return std::nullopt; } + if (clear) { + // Cheap for memory that's still zero (most of it): only reused pages get cleared. + for (const auto& block : *pg) { + system.DeviceMemory().buffer.ClearBackingRegion( + GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base, block.GetSize(), 0); + } + } auto& page_table = process->GetPageTable(); const u64 region_start = GetInteger(page_table.GetAliasCodeRegionStart()); const u64 region_size = page_table.GetAliasCodeRegionSize(); constexpr u64 Alignment = 0x200000; // 2 MiB - for (u64 offset = 0; offset + size <= region_size; offset += Alignment) { + // From where the last backing went (sparse regions back hundreds of blocks), then wrapping. + const u64 slots = region_size / Alignment; + for (u64 i = 0; i < slots; ++i) { + const u64 offset = ((m_alias_hint + i) % slots) * Alignment; + if (offset + size > region_size) { + continue; + } const Common::ProcessAddress address{region_start + offset}; if (R_SUCCEEDED(page_table.MapPageGroup(address, *pg, Kernel::KMemoryState::IoMemory, Kernel::KMemoryPermission::UserReadWrite))) { + m_alias_hint = (offset + Common::AlignUp(size, Alignment)) / Alignment; m_gpu_only_regions[gpu_address] = {GetInteger(address), size, std::move(pg)}; + m_gpu_only_bytes += size; + m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); return GetInteger(address); } } @@ -632,6 +685,83 @@ private: return std::nullopt; } + // Runs before a GPU-side write that may land in a sparse region (Tegra::Nx2BackSparseWrite, + // usually on the GPU thread): the blocks the write touches get memory, cleared, behind the + // region's device pages. The GPU mapping stays as it is, so the images over the region keep + // their storage and only see the write. + void BackSparseWrite(u64 gpu_address, u64 size) { + std::scoped_lock lk{m_region_mutex}; + const u64 end = gpu_address + size; + auto it = m_sparse_regions.upper_bound(gpu_address); + if (it != m_sparse_regions.begin()) { + --it; + } + for (; it != m_sparse_regions.end() && it->first < end; ++it) { + auto& [start, region] = *it; + if (start + region.size <= gpu_address) { + continue; + } + const u64 first_block = ((std::max)(gpu_address, start) - start) / SparseBlockSize; + const u64 last_block = ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize; + for (u64 block = first_block; block <= last_block; ++block) { + if (region.blocks[block] != BlockEmpty) { + continue; + } + const u64 block_address = start + block * SparseBlockSize; + const u64 block_size = (std::min)(SparseBlockSize, region.size - block * SparseBlockSize); + const auto address = BackGpuOnlyRegion(block_address, block_size, false, true); + if (!address) { + region.blocks[block] = BlockFailed; // out of memory: not again for every write + if (!region.warned) { + region.warned = true; + LOG_WARNING(Service, "nx2:gpu sparse region at {:#x}: pool exhausted, GPU writes " + "to its unbacked blocks are dropped", + start); + } + continue; + } + region.blocks[block] = BlockBacked; + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address, + block_size); + m_sparse_backed_bytes += block_size; + if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { + LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " + "memory in use, peak {} MiB", + m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, + m_gpu_only_peak >> 20); + } + } + } + } + + // EDEN_NX2_SPARSE_LAZY=0 backs sparse regions whole, as before. + static bool LazySparseRegions() { + static const bool lazy = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_LAZY"); + return value == nullptr || value[0] != '0'; + }(); + return lazy; + } + + static constexpr u32 SparseRegionFlag = 0x200; ///< Word 7 of 214 (0x60c arrays, 0xa0c pools) + static constexpr u64 SparseBlockSize = 4ULL << 20; + static constexpr u8 BlockEmpty = 0; + static constexpr u8 BlockBacked = 1; + static constexpr u8 BlockFailed = 2; + struct SparseRegion { + u64 size; + std::vector blocks; ///< BlockEmpty / BlockBacked / BlockFailed per SparseBlockSize + bool warned{}; ///< Logged that the pool ran out for its blocks + }; + std::map m_sparse_regions; ///< By GPU address + u64 m_sparse_blocks{}; ///< Sparse blocks backed so far + u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far + u64 m_gpu_only_bytes{}; ///< Backing of GPU-only regions and sparse blocks right now + u64 m_gpu_only_peak{}; + u64 m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region + /// Regions are created and destroyed on the service thread and backed on the GPU thread. + std::recursive_mutex m_region_mutex; + struct GpuOnlyRegion { u64 cpu_address; u64 size; diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp index 2dd8b04656..9111ba10d2 100644 --- a/src/video_core/memory_manager.cpp +++ b/src/video_core/memory_manager.cpp @@ -14,6 +14,7 @@ #include "core/hle/kernel/k_process.h" #include "memory_manager.h" #include "video_core/guest_memory.h" +#include "video_core/nx2.h" #include "video_core/host1x/host1x.h" #include "video_core/invalidation_accumulator.h" #include "video_core/memory_manager.h" @@ -119,7 +120,6 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de if (entry_type == EntryType::Mapped) { page_table.ReserveRange(gpu_addr, size); } - for (u64 offset{}; offset < size; offset += page_size) { // The caches hear about each run of pages whose entry changed in one call, not page by page // (a 1.45 GB Switch 2 region made ~370,000 texture cache scans). GPUVAddr modified_start{}; @@ -130,6 +130,7 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de modified_size = 0; } }; + for (u64 offset{}; offset < size; offset += page_size) { const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, false); SetEntry(current_gpu_addr, entry_type, false); @@ -149,14 +150,13 @@ GPUVAddr MemoryManager::PageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr de } remaining_size -= page_size; } - kind_map.Map(gpu_addr, gpu_addr + size, kind); flush_modified(); + kind_map.Map(gpu_addr, gpu_addr + size, kind); return gpu_addr; } GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr dev_addr, size_t size, PTEKind kind, MemoryManager::EntryType entry_type) { [[maybe_unused]] u64 remaining_size{size}; - for (u64 offset{}; offset < size; offset += big_page_size) { GPUVAddr modified_start{}; u64 modified_size{}; const auto flush_modified = [&] { @@ -165,6 +165,7 @@ GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr modified_size = 0; } }; + for (u64 offset{}; offset < size; offset += big_page_size) { const GPUVAddr current_gpu_addr = gpu_addr + offset; [[maybe_unused]] const auto current_entry_type = GetEntry(current_gpu_addr, true); SetEntry(current_gpu_addr, entry_type, true); @@ -201,8 +202,8 @@ GPUVAddr MemoryManager::BigPageTableOp(GPUVAddr gpu_addr, [[maybe_unused]] DAddr } remaining_size -= big_page_size; } - { flush_modified(); + { std::unique_lock lock(guard); kind_map.Map(gpu_addr, gpu_addr + size, kind); } @@ -300,6 +301,12 @@ void MemoryManager::Write(GPUVAddr addr, T data) { std::memcpy(page_pointer, &data, sizeof(T)); return; } + // A Switch 2 sparse block nobody wrote yet gets its memory now. + Tegra::Nx2BackSparseWrite(addr, sizeof(T)); + if (auto page_pointer{GetPointer(addr)}; page_pointer) { + std::memcpy(page_pointer, &data, sizeof(T)); + return; + } ASSERT(false); } @@ -411,8 +418,12 @@ void MemoryManager::ReadBlockImpl(GPUVAddr gpu_src_addr, void* dest_buffer, std: if (!unsafe) { rasterizer->FlushRegion(dev_addr_base, copy_amount, which); } - u8* physical = memory.GetPointer(dev_addr_base); - std::memcpy(dest_buffer, physical, copy_amount); + // A device page without memory (a sparse block nobody wrote yet) reads zero. + if (const u8* physical = memory.GetPointer(dev_addr_base)) [[likely]] { + std::memcpy(dest_buffer, physical, copy_amount); + } else { + std::memset(dest_buffer, 0, copy_amount); + } dest_buffer = static_cast(dest_buffer) + copy_amount; }; auto mapped_big = [&](std::size_t page_index, std::size_t offset, std::size_t copy_amount) { @@ -454,6 +465,8 @@ void MemoryManager::ReadBlockUnsafe(GPUVAddr gpu_src_addr, void* dest_buffer, co } void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffer, std::size_t size, [[maybe_unused]] VideoCommon::CacheType which, bool unsafe) { + // Give Switch 2 sparse pages the write lands on their memory first (otherwise it is dropped). + Tegra::Nx2BackSparseWrite(gpu_dest_addr, size); auto just_advance = [&]([[maybe_unused]] std::size_t page_index, [[maybe_unused]] std::size_t offset, std::size_t copy_amount) { src_buffer = static_cast(src_buffer) + copy_amount; }; @@ -462,8 +475,9 @@ void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffe if (!unsafe) { rasterizer->InvalidateRegion(dev_addr_base, copy_amount, which); } - u8* physical = memory.GetPointer(dev_addr_base); - std::memcpy(physical, src_buffer, copy_amount); + if (u8* physical = memory.GetPointer(dev_addr_base)) [[likely]] { + std::memcpy(physical, src_buffer, copy_amount); + } src_buffer = static_cast(src_buffer) + copy_amount; }; auto mapped_big = [&](std::size_t page_index, std::size_t offset, std::size_t copy_amount) { diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 04a01e5314..58c9b17bcf 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,45 @@ inline void Nx2ClearSemaphoreRegions() { } } +// Switch 2 sparse regions (nx2:gpu 214, word 7 bit 0x200: title 1's terrain texture arrays and +// layer pools, 170 MB-5.8 GB each, of which the game fills a little) get memory per block when the +// GPU first writes there, not in full. nx2:gpu installs the handler and the span its regions cover; +// the GPU memory manager calls Nx2BackSparseWrite before each write. +using Nx2SparseBackingHandler = std::function; +inline std::recursive_mutex g_nx2_sparse_backing_mutex; +inline Nx2SparseBackingHandler g_nx2_sparse_backing; +inline std::atomic g_nx2_sparse_low{~u64{0}}; +inline std::atomic g_nx2_sparse_high{0}; + +inline void Nx2SetSparseBacking(Nx2SparseBackingHandler handler) { + std::scoped_lock lk{g_nx2_sparse_backing_mutex}; + g_nx2_sparse_backing = std::move(handler); + g_nx2_sparse_low.store(~u64{0}); + g_nx2_sparse_high.store(0); +} + +/// Widens the span that writes are checked against (a new lazily backed region). +inline void Nx2AddSparseSpan(u64 gpu_address, u64 size) { + u64 low = g_nx2_sparse_low.load(); + while (gpu_address < low && !g_nx2_sparse_low.compare_exchange_weak(low, gpu_address)) { + } + u64 high = g_nx2_sparse_high.load(); + while (gpu_address + size > high && + !g_nx2_sparse_high.compare_exchange_weak(high, gpu_address + size)) { + } +} + +inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) { + if (gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || + gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { + return; + } + std::scoped_lock lk{g_nx2_sparse_backing_mutex}; + if (g_nx2_sparse_backing) { + g_nx2_sparse_backing(gpu_address, size); + } +} + /// GPU VA in the first page that mirrors a release at `address`, or 0. inline u64 Nx2SemaphoreMirror(u64 address) { for (const auto& region : g_nx2_semaphore_regions) { -- cgit v1.2.3 From 97ce27ab35153884d2fd05c278433928e429cd91 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:39:44 -0400 Subject: texture_cache: re-decode only the written layers of large ASTC arrays Any write into an image marked all of it CPU-modified, so each terrain tile Mario Kart World streams into its 8192x8192x68 ASTC arrays re-read and re-decoded the whole 1.45 GB array: a 2 GiB staging buffer and a full GPU wait each time, 15-16 GB of uploads per 600 frames. Large layered images on the accelerated ASTC path (64 MiB and up) now record the layers written since their last upload and stay tracked; a refresh reads and decodes only those layers, one dispatch per layer and level into a single-layer storage view. The texture cache's 600-frame report counts uploads from guest memory, their size and how many went by layer. Upload scratch buffers grown past 64 MiB for one huge image (341 MiB for the terrain layer pools) are freed at the next frame instead of kept for the session. Mario Kart World, 180 s: after loading, uploads drop from 15-16 GB to 0.2-0.5 GB per 600 frames, the largest from 1453 MiB to 2-15 MiB. --- src/common/scratch_buffer.h | 7 ++ src/video_core/renderer_vulkan/vk_compute_pass.cpp | 55 ++++++++++- src/video_core/renderer_vulkan/vk_compute_pass.h | 9 ++ .../renderer_vulkan/vk_texture_cache.cpp | 26 ++++- src/video_core/renderer_vulkan/vk_texture_cache.h | 9 ++ src/video_core/texture_cache/image_base.h | 3 + src/video_core/texture_cache/texture_cache.h | 108 ++++++++++++++++++++- src/video_core/texture_cache/texture_cache_base.h | 16 +++ 8 files changed, 227 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/common/scratch_buffer.h b/src/common/scratch_buffer.h index 929a65609a..cad1506922 100644 --- a/src/common/scratch_buffer.h +++ b/src/common/scratch_buffer.h @@ -122,6 +122,13 @@ public: return buffer_capacity; } + /// Frees the buffer; the next resize allocates it again. + void release() noexcept { + buffer.reset(); + buffer_capacity = 0; + last_requested_size = 0; + } + void swap(ScratchBuffer& other) noexcept { std::swap(last_requested_size, other.last_requested_size); std::swap(buffer_capacity, other.buffer_capacity); diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index b3ebf7b3f6..8cd8abb599 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -583,6 +583,24 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, swizzles); } +void ASTCDecoderPass::AssembleLayers(Image& image, const StagingBufferRef& map, + std::span swizzles, + std::span layers) { + 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, + .layers = layers, + .layer_view = [&image](s32 host_level, u32 layer) { + return image.StorageImageLayerView(host_level, layer); + }, + }; + 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, @@ -631,7 +649,42 @@ void ASTCDecoderPass::AssembleInto(const VideoCommon::ImageInfo& info, u32 skip, : 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) { + // Only some layers: one dispatch per layer and level, into that layer's own view. + for (size_t i = 0; i < target.layers.size(); ++i) { + for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { + const size_t layer_offset = i * input_layer_stride; + compute_pass_descriptor_queue.Acquire(scheduler, 2); + compute_pass_descriptor_queue.AddBuffer(map.buffer, + map.offset + layer_offset + swizzle.buffer_offset, + input_layer_stride - swizzle.buffer_offset); + compute_pass_descriptor_queue.AddImage( + target.layer_view(swizzle.level - static_cast(skip), target.layers[i])); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + auto params = MakeBlockLinearSwizzle2DParams(swizzle, info); + params.layer_stride = input_layer_stride; + const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 8U); + const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 8U); + scheduler.Record([this, num_dispatches_x, num_dispatches_y, block_dims, params, + descriptor_data](vk::CommandBuffer cmdbuf) { + const AstcPushConstants uniforms{ + .blocks_dims = block_dims, + .layer_stride = params.layer_stride, + .block_size = params.block_size, + .x_shift = params.x_shift, + .block_height = params.block_height, + .block_height_mask = params.block_height_mask, + .native_tiling = params.native_tiling, + }; + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, uniforms); + cmdbuf.Dispatch(num_dispatches_x, num_dispatches_y, 1); + }); + } + } + for (const VideoCommon::SwizzleParameters& swizzle : target.layers.empty() ? swizzles + : std::span{}) { 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); diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index 5daf4eb3de..9f2e8e9f1c 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -131,6 +131,11 @@ public: void Assemble(Image& image, const StagingBufferRef& map, std::span swizzles); + /// Decodes only `layers` of `image` (an initialized array); `map` holds them in order. + void AssembleLayers(Image& image, const StagingBufferRef& map, + std::span swizzles, + std::span layers); + /// Where a decode writes: an RGBA8 image with a storage view per (host) level. struct Target { VkImage image; @@ -138,6 +143,10 @@ public: bool initialized; std::function level_view; bool wait; ///< Wait for the GPU afterwards (the decoded image itself is sampled). + /// When set, the input holds only these layers (in order), each decoded into + /// layer_view(host level, layer). + std::span layers{}; + std::function layer_view{}; }; /// Decodes `info`'s ASTC data in `map` into `target`. `skip` leading levels are absent on the diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index f5fcdd1380..27f0e58321 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -342,7 +342,8 @@ constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM } [[nodiscard]] vk::ImageView MakeStorageView(const vk::Device& device, u32 level, VkImage image, - VkFormat format) { + VkFormat format, u32 base_layer = 0, + u32 layer_count = VK_REMAINING_ARRAY_LAYERS) { static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, .pNext = nullptr, @@ -365,8 +366,8 @@ constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = level, .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, + .baseArrayLayer = base_layer, + .layerCount = layer_count, }, }); } @@ -2493,6 +2494,19 @@ VkImageView Image::StorageImageView(s32 level) noexcept { return *view; } +VkImageView Image::StorageImageLayerView(s32 level, u32 layer) { + const u32 layers = static_cast(info.resources.layers); + if (storage_layer_views.empty()) { + storage_layer_views.resize(storage_image_views.size() * layers); + } + auto& view = storage_layer_views[static_cast(level) * layers + layer]; + if (!view) { + view = MakeStorageView(runtime->device.GetLogical(), level, *(this->*current_image), + VK_FORMAT_A8B8G8R8_UNORM_PACK32, layer, 1); + } + return *view; +} + bool Image::IsRescaled() const noexcept { return True(flags & ImageFlagBits::Rescaled); } @@ -3326,6 +3340,12 @@ void TextureCacheRuntime::AccelerateImageUpload( ASSERT(false); } +void TextureCacheRuntime::AccelerateImageUploadLayers( + Image& image, const StagingBufferRef& map, + std::span swizzles, std::span layers) { + astc_decoder_pass->AssembleLayers(image, map, swizzles, layers); +} + void TextureCacheRuntime::TranscodeAstcToBcn( Image& image, const StagingBufferRef& map, std::span swizzles) { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 1d92d439f4..59f2936e06 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -109,6 +109,11 @@ public: std::span, u32 z_start, u32 z_count); + /// Decodes only `layers` of an ASTC array; the staging buffer holds those layers in order. + void AccelerateImageUploadLayers(Image&, const StagingBufferRef&, + std::span, + std::span layers); + void InsertUploadMemoryBarrier() {} void TransitionImageLayout(Image& image); @@ -377,6 +382,9 @@ public: VkImageView StorageImageView(s32 level) noexcept; + /// A storage view of one layer of a host level (decoding only some layers of an array). + VkImageView StorageImageLayerView(s32 level, u32 layer); + bool IsRescaled() const noexcept; bool ScaleUp(bool ignore = false); @@ -409,6 +417,7 @@ private: vk::Image Image::*current_image{}; std::vector storage_image_views; + std::vector storage_layer_views; ///< By host level * layers + layer VkImageAspectFlags aspect_mask = 0; bool initialized = false; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 47bab9617a..20d3883130 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -99,6 +99,9 @@ struct ImageBase { u32 converted_size_bytes = 0; u32 scale_rating = 0; u32 host_level_offset = 0; ///< Guest mip level held by host mip 0 (see TrimmedLevels). + /// Large layered images uploaded by layer: one bit per layer written since the last upload + /// while CpuModified. Empty while CpuModified means the whole image. + std::vector dirty_layers; u64 scale_tick = 0; bool has_scaled = false; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 62cd523073..d582f520f7 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -198,6 +198,13 @@ void TextureCache

::ApplyThresholdOverrides(bool log) { template void TextureCache

::TickFrame() { + // Upload buffers grown for one huge image (title 1's 341 MiB terrain layer pools) are freed + // rather than kept for the rest of the session. + for (auto* const scratch : {&swizzle_data_buffer, &unswizzle_data_buffer}) { + if (scratch->capacity() > LargeScratchCapacity) { + scratch->release(); + } + } if constexpr (HAS_DEVICE_MEMORY_INFO) { // Switch 2 titles (native tiling) stream far more texture data than Eden's thresholds // were tuned for: title 1's attract loop alone keeps ~5.3 GB resident, so the collector @@ -280,6 +287,9 @@ void TextureCache

::TickFrame() { images_created, images_deleted, gc_deletes + gc_costly_deletes, gc_costly_deletes, gc_download_deletes, async_decodes_queued, astc_opaque_images, astc_alpha_images); + LOG_INFO(HW_GPU, "Texture cache last 600 frames: {} uploads from guest memory, {} MiB (largest {} MiB, {} by layer)", + images_uploaded, uploaded_bytes >> 20, largest_upload >> 20, layer_uploads); + images_uploaded = uploaded_bytes = largest_upload = layer_uploads = 0; astc_opaque_images = astc_alpha_images = 0; std::vector> shapes; for (const auto& [key, value] : created_shapes) shapes.emplace_back(value.first, key); @@ -706,7 +716,11 @@ FramebufferId TextureCache

::GetFramebufferId(const RenderTargets& key) { template void TextureCache

::WriteMemory(DAddr cpu_addr, size_t size) { - ForEachImageInRegion(cpu_addr, size, [this](ImageId image_id, Image& image) { + ForEachImageInRegion(cpu_addr, size, [this, cpu_addr, size](ImageId image_id, Image& image) { + if (UploadsByLayer(image)) { + MarkLayersWritten(image, cpu_addr, size); + return; + } if (True(image.flags & ImageFlagBits::CpuModified)) { return; } @@ -812,6 +826,7 @@ void TextureCache

::UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t siz UntrackImage(image, id); } } + image.dirty_layers.clear(); // all of it if (True(image.flags & ImageFlagBits::Remapped)) { continue; } @@ -1280,7 +1295,16 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { image.flags &= ~ImageFlagBits::CpuModified; - TrackImage(image, image_id); + // Images uploaded by layer stay tracked while some of their layers are dirty. + if (False(image.flags & ImageFlagBits::Tracked)) { + TrackImage(image, image_id); + } + + if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) { + runtime.InsertUploadMemoryBarrier(); + return; + } + image.dirty_layers.clear(); if (True(image.flags & ImageFlagBits::Unbacked)) { // Nothing to upload into (and staging it could take tens of GB). @@ -1311,11 +1335,91 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { QueueAsyncUnswizzle(image, image_id); return; } + ++images_uploaded; + uploaded_bytes += image.guest_size_bytes; + largest_upload = (std::max)(largest_upload, image.guest_size_bytes); auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image)); UploadImageContents(image, staging); runtime.InsertUploadMemoryBarrier(); } +template +bool TextureCache

::UploadsByLayer(const ImageBase& image) const noexcept { + // Title 1's terrain arrays (8192x8192x68 ASTC, 1.45 GB each) get a few layers streamed in at a + // time; re-decoding all of them for each took a 2 GiB staging buffer and stalled the GPU. + if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { + return True(image.flags & ImageFlagBits::AcceleratedUpload) && + image.info.type == ImageType::e2D && image.info.resources.layers > 1 && + image.guest_size_bytes >= LayerUploadMinSize && + False(image.flags & (ImageFlagBits::Unbacked | ImageFlagBits::Sparse | + ImageFlagBits::Remapped)); + } else { + return false; + } +} + +template +void TextureCache

::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size) { + if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_layers.empty()) { + return; // all of it is dirty already + } + const u32 layers = static_cast(image.info.resources.layers); + if (False(image.flags & ImageFlagBits::CpuModified)) { + image.flags |= ImageFlagBits::CpuModified; + image.dirty_layers.assign(Common::DivCeil(layers, 64U), 0); + } + const DAddr begin = (std::max)(cpu_addr, image.cpu_addr) - image.cpu_addr; + const DAddr end = (std::min)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes) - + image.cpu_addr; + const u32 stride = image.info.layer_stride; + if (end <= begin || stride == 0) { + return; + } + const u32 last = (std::min)(static_cast((end - 1) / stride), layers - 1); + for (u32 layer = static_cast(begin / stride); layer <= last; ++layer) { + image.dirty_layers[layer / 64] |= u64{1} << (layer % 64); + } +} + +template +bool TextureCache

::UploadWrittenLayers(Image& image) { + if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { + const u32 num_layers = static_cast(image.info.resources.layers); + std::vector layers; + for (u32 layer = 0; layer < num_layers; ++layer) { + if ((image.dirty_layers[layer / 64] >> (layer % 64)) & 1) { + layers.push_back(layer); + } + } + image.dirty_layers.clear(); + if (layers.empty() || layers.size() == num_layers) { + return false; + } + // Each layer's mip chain from the first level the host keeps on (as UploadImageContents). + auto uploads = FullUploadSwizzles(image.info); + const size_t level_offset = uploads[image.host_level_offset].buffer_offset; + const size_t tail = image.info.layer_stride - level_offset; + auto staging = runtime.UploadStagingBuffer(tail * layers.size()); + for (size_t i = 0; i < layers.size(); ++i) { + gpu_memory->ReadBlock(image.gpu_addr + u64{layers[i]} * image.info.layer_stride + + level_offset, + staging.mapped_span.data() + i * tail, tail, + VideoCommon::CacheType::NoTextureCache); + } + uploads.erase(uploads.begin(), uploads.begin() + image.host_level_offset); + for (auto& upload : uploads) { + upload.buffer_offset -= level_offset; + } + runtime.AccelerateImageUploadLayers(image, staging, uploads, layers); + ++images_uploaded; + uploaded_bytes += tail * layers.size(); + ++layer_uploads; + return true; + } else { + return false; + } +} + template template void TextureCache

::UploadImageContents(Image& image, StagingBuffer& staging) { diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 2e375595de..f4cc89f2ba 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -128,6 +128,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches Date: Fri, 30 Oct 2026 05:16:34 -0400 Subject: texture_cache: give images over unwritten sparse blocks no device memory Mario Kart World creates nine terrain layer pools in sparse regions (four R8 16384x16384, four RG16 8192x8192, one R16 8192x8192: 2.9 GB of device memory). In 180 s of title and attract demo not one of their blocks is written and nothing renders into them, so they hold zeros; on the console only the few tiles the game maps have memory. nx2:gpu now publishes which 4 MiB blocks of each sparse region have been written (nx2.h, a leaf lock the texture cache may take). An image of 32 MiB or more lying in one sparse region with no written block is created as a sparse resident image with nothing bound: reads return zero, as an unmapped tile does. The first write into its range, or the first time the GPU renders into it (that range then counts as written), marks it remapped, so a backed image replaces it at its next use. Mario Kart World, 180 s: the collector deletes 0-80 images per 600 frames instead of thousands, frames over 100 ms per 600 presents drop from 1-9 to 0-1, peak working set 14.7 GB. One render target in a small sparse region gets replaced once (logged). --- src/core/hle/service/nx2_gsv.h | 3 + src/video_core/nx2.h | 76 ++++++++++++++++++++++ .../renderer_vulkan/vk_texture_cache.cpp | 30 ++++++++- src/video_core/texture_cache/image_base.h | 3 + src/video_core/texture_cache/texture_cache.h | 14 ++++ .../vulkan_common/vulkan_memory_allocator.cpp | 29 ++++++--- .../vulkan_common/vulkan_memory_allocator.h | 4 ++ 7 files changed, 147 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index f3cc87ac4d..9d00b81368 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -549,6 +549,7 @@ private: // BackSparseWrite gives a block memory before the GPU writes into it. m_sparse_regions[start] = {size, std::vector(Common::DivCeil(size, SparseBlockSize))}; Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size); + Tegra::Nx2SparseRegionCreated(start, size, SparseBlockSize); Tegra::Nx2AddSparseSpan(start, size); LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it", start, last); @@ -580,6 +581,7 @@ private: FreeReleasedRegions(); if (last > first) { if (m_sparse_regions.erase(first) != 0) { + Tegra::Nx2SparseRegionDestroyed(first); Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1); } else { Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Unmap(first, last - first + 1); @@ -723,6 +725,7 @@ private: region.blocks[block] = BlockBacked; Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address, block_size); + Tegra::Nx2SparseBlockWritten(start, block); m_sparse_backed_bytes += block_size; if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 58c9b17bcf..1b565e73d4 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -93,6 +94,81 @@ inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) { } } +// Which blocks of each sparse region have been written (got memory), for the texture cache: an +// image over blocks nobody wrote is all zeros and needs no device memory. Its own lock, taken last: +// the texture cache asks with its lock held, and nx2:gpu updates it holding none of the GPU's. +struct Nx2SparseRegionState { + u64 size; + u64 block_size; + std::vector written; +}; +inline std::mutex g_nx2_sparse_state_mutex; +inline std::map g_nx2_sparse_state; + +inline void Nx2SparseRegionCreated(u64 gpu_address, u64 size, u64 block_size) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + g_nx2_sparse_state[gpu_address] = {size, block_size, + std::vector((size + block_size - 1) / block_size)}; +} + +inline void Nx2SparseRegionDestroyed(u64 gpu_address) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + g_nx2_sparse_state.erase(gpu_address); +} + +inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + if (const auto it = g_nx2_sparse_state.find(region_address); it != g_nx2_sparse_state.end()) { + it->second.written[block] = true; + } +} + +/// The GPU renders into [gpu_address, +size): count its blocks as written, so images made there +/// later get memory (their contents live in the host image, not in guest memory). +inline void Nx2SparseMarkWritten(u64 gpu_address, u64 size) { + if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || + gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { + return; + } + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + for (auto& [start, region] : g_nx2_sparse_state) { + const u64 end = start + region.size; + if (gpu_address >= end || gpu_address + size <= start) { + continue; + } + const u64 first = ((std::max)(gpu_address, start) - start) / region.block_size; + const u64 last = ((std::min)(gpu_address + size, end) - 1 - start) / region.block_size; + for (u64 block = first; block <= last; ++block) { + region.written[block] = true; + } + } +} + +/// True when [gpu_address, +size) lies in one sparse region and none of it has been written. +inline bool Nx2SparseUnwritten(u64 gpu_address, u64 size) { + if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || + gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { + return false; + } + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + auto it = g_nx2_sparse_state.upper_bound(gpu_address); + if (it == g_nx2_sparse_state.begin()) { + return false; + } + --it; + const auto& [start, region] = *it; + if (gpu_address + size > start + region.size) { + return false; + } + const u64 last = (gpu_address + size - 1 - start) / region.block_size; + for (u64 block = (gpu_address - start) / region.block_size; block <= last; ++block) { + if (region.written[block]) { + return false; + } + } + return true; +} + /// GPU VA in the first page that mirrors a release at `address`, or 0. inline u64 Nx2SemaphoreMirror(u64 address) { for (const auto& region : g_nx2_semaphore_regions) { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 27f0e58321..08a471cfd6 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -20,6 +20,7 @@ #include "common/settings.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" +#include "video_core/nx2.h" #include "video_core/gpu_logging/gpu_logging.h" #include "video_core/engines/fermi_2d.h" @@ -165,6 +166,16 @@ constexpr VkBorderColor ConvertBorderColor(const std::array& color) { return bytes * info.resources.layers > (u64{1} << 30); } +// A large image over Switch 2 sparse blocks nobody has written (title 1's terrain layer pools, 2.9 +// GB of R8/RG16/R16 that stay empty in the attract demo) is all zeros: it gets no device memory +// until a write makes the texture cache replace it. +[[nodiscard]] bool IsUnwrittenSparse(const ImageInfo& info, GPUVAddr gpu_addr) { + constexpr u32 MinSize = 32U << 20; + const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); + return size >= MinSize && info.type != ImageType::e1D && info.type != ImageType::Buffer && + info.type != ImageType::Linear && Tegra::Nx2SparseUnwritten(gpu_addr, size); +} + // 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)) { @@ -310,7 +321,8 @@ constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM [[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator, const ImageInfo& info, std::span view_formats, - std::optional format_override = {}) { + std::optional format_override = {}, + bool unbacked = false) { if (info.type == ImageType::Buffer) { return vk::Image{}; } @@ -338,6 +350,11 @@ constexpr std::array OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM image_ci.pNext = &image_format_list; } } + if (unbacked) { + if (vk::Image image = allocator.CreateUnbackedImage(image_ci)) { + return image; + } + } return allocator.CreateImage(image_ci); } @@ -2019,10 +2036,19 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu : IsOpaqueBc1Astc(runtime_.device, info) ? std::span{OpaqueAstcViewFormats} : runtime->ViewFormats(info.format), - AstcHostFormat(runtime_.device, info))), + AstcHostFormat(runtime_.device, info), + IsUnwrittenSparse(info, gpu_addr_))), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; + // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing was + // written there yet, so the first write gets it replaced. + const ImageInfo host_info = TrimmedHostInfo(runtime_.device, info, host_level_offset); + const u64 texels = u64{host_info.size.width} * host_info.size.height * + host_info.size.depth * static_cast(host_info.resources.layers); + if (texels <= (u64{1} << 28)) { + flags |= VideoCommon::ImageFlagBits::UnwrittenSparse; + } } if (host_level_offset != 0) { flags |= VideoCommon::ImageFlagBits::TrimmedLevels; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 20d3883130..cfb839d17e 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -50,6 +50,9 @@ enum class ImageFlagBits : u32 { /// 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. + /// Over Switch 2 sparse blocks nobody wrote: all zeros, so the host image has no memory + /// (Unbacked) until the first write, which makes a backed image replace it. + UnwrittenSparse = 1 << 21, }; 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 d582f520f7..168df50c8b 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.h" #include "video_core/nx2_profile.h" #include "video_core/textures/decoders.h" @@ -717,6 +718,11 @@ FramebufferId TextureCache

::GetFramebufferId(const RenderTargets& key) { template void TextureCache

::WriteMemory(DAddr cpu_addr, size_t size) { ForEachImageInRegion(cpu_addr, size, [this, cpu_addr, size](ImageId image_id, Image& image) { + if (True(image.flags & ImageFlagBits::UnwrittenSparse)) { + // Written at last: a backed image replaces it where it is used next. + image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped; + return; + } if (UploadsByLayer(image)) { MarkLayersWritten(image, cpu_addr, size); return; @@ -2918,6 +2924,14 @@ void TextureCache

::PrepareImage(ImageId image_id, bool is_modification, bool } } if (is_modification) { + if (True(image.flags & ImageFlagBits::UnwrittenSparse) && + False(image.flags & ImageFlagBits::Remapped)) { + // The GPU renders into it: these writes are lost, later uses get a backed image. + LOG_WARNING(HW_GPU, "GPU write into unwritten sparse image {:#x}; replacing it", + image.gpu_addr); + Tegra::Nx2SparseMarkWritten(image.gpu_addr, image.guest_size_bytes); + image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped; + } MarkModification(image); } lru_cache.Touch(image.lru_index, frame_tick); diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 8bf692b00e..92f537aa3c 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -248,21 +248,15 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat } 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) { + // 68 layers, ~23 GB): on the console only resident tiles have memory. Use an image with + // nothing bound, so views and copies still work, instead of aborting. + if (vk::Image sparse = CreateUnbackedImage(ci)) { 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()); + return sparse; } } if (result != VK_SUCCESS) { @@ -286,6 +280,21 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat device.GetDispatchLoader()); } +vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) const { + if (ci.imageType == VK_IMAGE_TYPE_1D) { + return vk::Image{}; + } + 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) { + return vk::Image{}; + } + return vk::Image(sparse, ci.usage, *device.GetLogical(), allocator, VK_NULL_HANDLE, + device.GetDispatchLoader()); +} + vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const { // MESA will do memcpy() if not marked as host cached, so just force mark it for most buffers auto const anv_flags = (usage == MemoryUsage::Stream diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index fa7d4b41c3..99649b9aef 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -107,6 +107,10 @@ namespace Vulkan { /// images would otherwise leave holes in the shared blocks). vk::Image CreateImage(const VkImageCreateInfo &ci, bool dedicated = false) const; + /// A sparse resident image with nothing bound (reads return zero, writes are dropped), or + /// a null image when the device can't make one. + vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const; + vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const; vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage, -- cgit v1.2.3 From d4ee7fcd5129e7ee32ff38a5aef9b449d5a756de Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:28:53 -0400 Subject: vulkan: bind memory to sparse terrain arrays layer by layer The written part of Mario Kart World's sparse terrain arrays is small: 13-26% of their blocks. The two 1.45 GB ASTC arrays were backed in full on the host, and the two 5.8 GB BC6H arrays, too large to back, stayed unbacked, so what the game streamed into them never showed. A color array of 32 MiB or more lying in one sparse region, in a format uploaded layer by layer (huge ASTC arrays decoded on the GPU, or formats sampled as they are), is now a sparse resident image (SparseLayers). When it is refreshed, the layers written since (or, for a new image, the layers any of whose blocks was written) get device memory bound with vkQueueBindSparse (under the submit lock, waited on, as the multi-range buffer cache does) and only those layers are uploaded. Unwritten layers read zero. One image binds at most 768 MiB (384 MiB on GPUs with less than 7 GiB); rendering into one binds all its layers. Mario Kart World, 180 s: the ASTC arrays hold 1.4 MiB per written layer, four BC6H layers (341 MiB) now have their data, device memory in use 4.8-5.0 GB instead of 6.0-7.1 GB with the texture cache below its collection threshold, 16.8-17.2 ms average frames, peak working set 13.4 GB. --- src/video_core/nx2.h | 15 ++ .../renderer_vulkan/vk_texture_cache.cpp | 231 ++++++++++++++++++++- src/video_core/renderer_vulkan/vk_texture_cache.h | 65 ++++++ src/video_core/texture_cache/image_base.h | 3 + src/video_core/texture_cache/texture_cache.h | 86 +++++++- src/video_core/texture_cache/texture_cache_base.h | 6 + src/video_core/vulkan_common/vulkan_device.h | 5 + .../vulkan_common/vulkan_memory_allocator.cpp | 29 +++ .../vulkan_common/vulkan_memory_allocator.h | 6 + src/video_core/vulkan_common/vulkan_wrapper.cpp | 11 + src/video_core/vulkan_common/vulkan_wrapper.h | 4 + 11 files changed, 453 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 1b565e73d4..1a3bae93ec 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -123,6 +123,21 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { } } +/// True when [gpu_address, +size) lies in one sparse region. +inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) { + if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || + gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] { + return false; + } + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + auto it = g_nx2_sparse_state.upper_bound(gpu_address); + if (it == g_nx2_sparse_state.begin()) { + return false; + } + --it; + return gpu_address + size <= it->first + it->second.size; +} + /// The GPU renders into [gpu_address, +size): count its blocks as written, so images made there /// later get memory (their contents live in the host image, not in guest memory). inline void Nx2SparseMarkWritten(u64 gpu_address, u64 size) { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 08a471cfd6..b77f9af5f8 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -16,7 +16,9 @@ #include #include #include +#include "common/alignment.h" #include "common/bit_util.h" +#include "common/div_ceil.h" #include "common/settings.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" @@ -176,6 +178,31 @@ constexpr VkBorderColor ConvertBorderColor(const std::array& color) { info.type != ImageType::Linear && Tegra::Nx2SparseUnwritten(gpu_addr, size); } +// A large color array in a Switch 2 sparse region (title 1's terrain arrays: 13-26% of their +// blocks written) gets memory by layer, as its layers are written: unbacked at first, then +// Image::BindLayers. Only formats uploaded layer by layer: huge ASTC arrays (GPU decode) and +// formats the host samples as they are. +[[nodiscard]] bool UsesSparseLayers(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { + if (!device.IsSparseResidencyImage2DSupported() || info.type != ImageType::e2D || + info.resources.layers <= 1 || info.num_samples != 1 || + VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) { + return false; + } + const bool layer_uploads = + IsHugeAstcArray(device, info) || + (!IsPixelFormatASTC(info.format) && + (!IsPixelFormatBCn(info.format) || device.IsOptimalBcnSupported())); + const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); + return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size); +} + +// The most device memory one SparseLayers image binds: 768 MiB, 384 MiB on GPUs with less than +// 7 GiB (title 1's BC6H arrays are 85 MB a layer). +[[nodiscard]] VkDeviceSize SparseLayersCap(const Device& device) { + return device.GetDeviceLocalMemory() < (u64{7} << 30) ? (VkDeviceSize{384} << 20) + : (VkDeviceSize{768} << 20); +} + // 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)) { @@ -2037,16 +2064,19 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu ? std::span{OpaqueAstcViewFormats} : runtime->ViewFormats(info.format), AstcHostFormat(runtime_.device, info), - IsUnwrittenSparse(info, gpu_addr_))), + UsesSparseLayers(runtime_.device, info, gpu_addr_) || + IsUnwrittenSparse(info, gpu_addr_))), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; - // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing was - // written there yet, so the first write gets it replaced. const ImageInfo host_info = TrimmedHostInfo(runtime_.device, info, host_level_offset); const u64 texels = u64{host_info.size.width} * host_info.size.height * host_info.size.depth * static_cast(host_info.resources.layers); - if (texels <= (u64{1} << 28)) { + if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) { + flags |= VideoCommon::ImageFlagBits::SparseLayers; + } else if (texels <= (u64{1} << 28)) { + // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing + // was written there yet, so the first write gets it replaced. flags |= VideoCommon::ImageFlagBits::UnwrittenSparse; } } @@ -2520,6 +2550,199 @@ VkImageView Image::StorageImageView(s32 level) noexcept { return *view; } +bool Image::InitSparseLayers() { + const Device& device = runtime->device; + const VkImage handle = *original_image; + const auto requirements = device.GetLogical().GetImageSparseMemoryRequirements(handle); + const auto it = std::ranges::find_if(requirements, [](const auto& r) { + return (r.formatProperties.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0; + }); + if (it == requirements.end()) { + return false; + } + SparseLayerMemory& s = sparse_layers; + s.allocator = &runtime->memory_allocator; + s.requirements = device.GetLogical().GetImageMemoryRequirements(handle); + s.sparse = *it; + u32 offset = host_level_offset; + const ImageInfo host = TrimmedHostInfo(device, info, offset); + s.width = host.size.width; + s.height = host.size.height; + s.levels = static_cast(host.resources.levels); + // Sparse block size in texels (drivers give it in texels or in compressed blocks). + const bool huge_astc = IsHugeAstcArray(device, info); + const u32 block_width = huge_astc ? 1 : VideoCore::Surface::DefaultBlockWidth(info.format); + const u32 block_height = huge_astc ? 1 : VideoCore::Surface::DefaultBlockHeight(info.format); + const u64 block_bytes = huge_astc ? 4 : VideoCore::Surface::BytesPerBlock(info.format); + const VkExtent3D granularity = it->formatProperties.imageGranularity; + s.tile_width = granularity.width; + s.tile_height = granularity.height; + const u64 tile = s.requirements.alignment; + if (u64{granularity.width / block_width} * (granularity.height / block_height) * block_bytes != + tile && + u64{granularity.width} * granularity.height * block_bytes == tile) { + s.tile_width *= block_width; + s.tile_height *= block_height; + } + const u32 tail_lod = (std::min)(it->imageMipTailFirstLod, s.levels); + VkDeviceSize bytes = 0; + for (u32 level = 0; level < tail_lod; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + bytes += u64{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height) * + tile; + } + if (tail_lod < s.levels && + (it->formatProperties.flags & VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT) == 0) { + bytes += it->imageMipTailSize; + } + s.layer_bytes = Common::AlignUp(bytes, tile); + s.layer_memory.assign(static_cast(info.resources.layers), VK_NULL_HANDLE); + LOG_INFO(Render_Vulkan, + "Sparse layered image {:#x}: {}x{} {} levels {} layers, {} KiB a layer, block {}x{}", + gpu_addr, s.width, s.height, s.levels, info.resources.layers, s.layer_bytes >> 10, + s.tile_width, s.tile_height); + return true; +} + +std::vector Image::BindLayers(std::span layers) { + SparseLayerMemory& s = sparse_layers; + std::vector bound; + if (s.layer_memory.empty()) { + return bound; + } + const Device& device = runtime->device; + const u64 tile = s.requirements.alignment; + const u32 tail_lod = (std::min)(s.sparse.imageMipTailFirstLod, s.levels); + const bool has_tail = tail_lod < s.levels; + const bool single_tail = + (s.sparse.formatProperties.flags & VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT) != 0; + const VkDeviceSize cap = SparseLayersCap(device); + std::vector image_binds; + std::vector opaque_binds; + for (const u32 layer : layers) { + if (s.layer_memory[layer] != VK_NULL_HANDLE) { + bound.push_back(layer); + continue; + } + if (s.bound_bytes + s.layer_bytes > cap) { + if (!std::exchange(s.warned, true)) { + LOG_WARNING(Render_Vulkan, "Sparse layered image {:#x}: {} MiB bound, more layers " + "stay without memory", + gpu_addr, s.bound_bytes >> 20); + } + continue; + } + const VkMemoryRequirements requirements{ + .size = s.layer_bytes, + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + const VmaAllocation allocation = s.allocator->AllocateSparseMemory(requirements, memory, offset); + if (allocation == VK_NULL_HANDLE) { + LOG_WARNING(Render_Vulkan, "Sparse layered image {:#x}: out of device memory", gpu_addr); + break; + } + s.layer_memory[layer] = allocation; + s.bound_bytes += s.layer_bytes; + for (u32 level = 0; level < tail_lod; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + image_binds.push_back(VkSparseImageMemoryBind{ + .subresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = level, + .arrayLayer = layer, + }, + .offset{0, 0, 0}, + .extent{width, height, 1}, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + offset += u64{Common::DivCeil(width, s.tile_width)} * + Common::DivCeil(height, s.tile_height) * tile; + } + if (has_tail && !single_tail) { + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset + layer * s.sparse.imageMipTailStride, + .size = s.sparse.imageMipTailSize, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + bound.push_back(layer); + } + if (has_tail && single_tail && s.tail_memory == VK_NULL_HANDLE && !bound.empty()) { + const VkMemoryRequirements requirements{ + .size = Common::AlignUp(s.sparse.imageMipTailSize, tile), + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + s.tail_memory = s.allocator->AllocateSparseMemory(requirements, memory, offset); + if (s.tail_memory != VK_NULL_HANDLE) { + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset, + .size = s.sparse.imageMipTailSize, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + } + if (image_binds.empty() && opaque_binds.empty()) { + return bound; + } + const VkSparseImageMemoryBindInfo image_bind_info{ + .image = *original_image, + .bindCount = static_cast(image_binds.size()), + .pBinds = image_binds.data(), + }; + const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{ + .image = *original_image, + .bindCount = static_cast(opaque_binds.size()), + .pBinds = opaque_binds.data(), + }; + const VkBindSparseInfo bind_info{ + .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .bufferBindCount = 0, + .pBufferBinds = nullptr, + .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U, + .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info, + .imageBindCount = image_binds.empty() ? 0U : 1U, + .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + // Bound before the command buffer that uploads the layers is submitted (as the multi-range + // buffer cache does). + vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }); + VkResult result; + { + std::scoped_lock lock{scheduler->submit_mutex}; + result = device.GetGraphicsQueue().BindSparse(bind_info, *fence); + } + if (result != VK_SUCCESS) { + LOG_ERROR(Render_Vulkan, "Sparse layered image {:#x}: binding failed ({})", gpu_addr, + static_cast(result)); + return {}; + } + fence.Wait(); + return bound; +} + VkImageView Image::StorageImageLayerView(s32 level, u32 layer) { const u32 layers = static_cast(info.resources.layers); if (storage_layer_views.empty()) { diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 59f2936e06..13919f1270 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -385,6 +385,10 @@ public: /// A storage view of one layer of a host level (decoding only some layers of an array). VkImageView StorageImageLayerView(s32 level, u32 layer); + /// SparseLayers images: gives `layers` device memory where they have none yet. Returns the + /// layers that have memory afterwards (a cap per image can leave some out). + std::vector BindLayers(std::span layers); + bool IsRescaled() const noexcept; bool ScaleUp(bool ignore = false); @@ -403,6 +407,67 @@ private: Scheduler* scheduler{}; TextureCacheRuntime* runtime{}; + /// Memory bound to the layers of a SparseLayers image. Declared before the images, so it is + /// released after them. + struct SparseLayerMemory { + SparseLayerMemory() = default; + SparseLayerMemory(const SparseLayerMemory&) = delete; + SparseLayerMemory& operator=(const SparseLayerMemory&) = delete; + SparseLayerMemory(SparseLayerMemory&& other) noexcept { + *this = std::move(other); + } + SparseLayerMemory& operator=(SparseLayerMemory&& other) noexcept { + Release(); + allocator = std::exchange(other.allocator, nullptr); + requirements = other.requirements; + sparse = other.sparse; + width = other.width; + height = other.height; + levels = other.levels; + tile_width = other.tile_width; + tile_height = other.tile_height; + layer_bytes = other.layer_bytes; + layer_memory = std::move(other.layer_memory); + other.layer_memory.clear(); + tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE); + bound_bytes = other.bound_bytes; + warned = other.warned; + return *this; + } + ~SparseLayerMemory() { + Release(); + } + void Release() noexcept { + if (allocator == nullptr) { + return; + } + for (const VmaAllocation allocation : layer_memory) { + allocator->FreeSparseMemory(allocation); + } + allocator->FreeSparseMemory(tail_memory); + layer_memory.clear(); + tail_memory = VK_NULL_HANDLE; + } + + const MemoryAllocator* allocator{}; + VkMemoryRequirements requirements{}; + VkSparseImageMemoryRequirements sparse{}; + u32 width{}; ///< Host level 0 + u32 height{}; + u32 levels{}; ///< Host levels + u32 tile_width{}; ///< Sparse block, in texels + u32 tile_height{}; + VkDeviceSize layer_bytes{}; ///< One layer's levels before the mip tail, and its own tail + std::vector layer_memory; ///< Per layer, null until bound + VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one + VkDeviceSize bound_bytes{}; + bool warned{}; + }; + SparseLayerMemory sparse_layers; + + /// Reads the image's sparse requirements; false when it can't be bound by layer. + bool InitSparseLayers(); + vk::Image original_image; vk::Image scaled_image; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index cfb839d17e..04254fb417 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -53,6 +53,9 @@ enum class ImageFlagBits : u32 { /// Over Switch 2 sparse blocks nobody wrote: all zeros, so the host image has no memory /// (Unbacked) until the first write, which makes a backed image replace it. UnwrittenSparse = 1 << 21, + /// A large array over Switch 2 sparse blocks: unbacked, each layer gets device memory when + /// something is written to it (Image::BindLayers) and only those layers are uploaded. + SparseLayers = 1 << 22, }; 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 168df50c8b..3de82d2eb4 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 @@ -1306,6 +1307,10 @@ void TextureCache

::RefreshContents(Image& image, ImageId image_id) { TrackImage(image, image_id); } + if (True(image.flags & ImageFlagBits::SparseLayers)) { + RefreshSparseLayers(image); + return; + } if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) { runtime.InsertUploadMemoryBarrier(); return; @@ -1354,6 +1359,9 @@ bool TextureCache

::UploadsByLayer(const ImageBase& image) const noexcept { // Title 1's terrain arrays (8192x8192x68 ASTC, 1.45 GB each) get a few layers streamed in at a // time; re-decoding all of them for each took a 2 GiB staging buffer and stalled the GPU. if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { + if (True(image.flags & ImageFlagBits::SparseLayers)) { + return true; + } return True(image.flags & ImageFlagBits::AcceleratedUpload) && image.info.type == ImageType::e2D && image.info.resources.layers > 1 && image.guest_size_bytes >= LayerUploadMinSize && @@ -1401,6 +1409,71 @@ bool TextureCache

::UploadWrittenLayers(Image& image) { if (layers.empty() || layers.size() == num_layers) { return false; } + UploadLayers(image, layers); + return true; + } else { + return false; + } +} + +template +void TextureCache

::RefreshSparseLayers(Image& image) { + if constexpr (requires { &Image::BindLayers; }) { + // The layers written since the last upload, or, for a new image, every layer any of whose + // blocks has been written. + const u32 num_layers = static_cast(image.info.resources.layers); + const u64 stride = image.info.layer_stride; + std::vector layers; + for (u32 layer = 0; layer < num_layers; ++layer) { + const bool written = + image.dirty_layers.empty() + ? !Tegra::Nx2SparseUnwritten(image.gpu_addr + layer * stride, stride) + : ((image.dirty_layers[layer / 64] >> (layer % 64)) & 1) != 0; + if (written) { + layers.push_back(layer); + } + } + image.dirty_layers.clear(); + const std::vector bound = layers.empty() ? std::vector{} : image.BindLayers(layers); + if (bound.empty()) { + runtime.TransitionImageLayout(image); + return; + } + UploadLayers(image, bound); + runtime.InsertUploadMemoryBarrier(); + } +} + +template +void TextureCache

::UploadLayers(Image& image, std::span layers) { + if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { + if (False(image.flags & ImageFlagBits::AcceleratedUpload)) { + // Plain formats: each layer unswizzled on its own into its layer of the image. + ImageInfo layer_info = image.info; + layer_info.resources.layers = 1; + const size_t layer_size = CalculateUnswizzledSizeBytes(layer_info); + auto staging = runtime.UploadStagingBuffer(layer_size * layers.size()); + std::vector copies; + for (size_t i = 0; i < layers.size(); ++i) { + const GPUVAddr layer_addr = image.gpu_addr + u64{layers[i]} * image.info.layer_stride; + Tegra::Memory::GpuGuestMemory + swizzle_data(*gpu_memory, layer_addr, image.info.layer_stride, + &swizzle_data_buffer); + auto layer_copies = + UnswizzleImage(*gpu_memory, layer_addr, layer_info, swizzle_data, + staging.mapped_span.subspan(i * layer_size, layer_size)); + for (BufferImageCopy copy : layer_copies) { + copy.buffer_offset += i * layer_size; + copy.image_subresource.base_layer = static_cast(layers[i]); + copies.push_back(copy); + } + } + image.UploadMemory(staging, copies); + ++images_uploaded; + uploaded_bytes += u64{image.info.layer_stride} * layers.size(); + ++layer_uploads; + return; + } // Each layer's mip chain from the first level the host keeps on (as UploadImageContents). auto uploads = FullUploadSwizzles(image.info); const size_t level_offset = uploads[image.host_level_offset].buffer_offset; @@ -1420,9 +1493,6 @@ bool TextureCache

::UploadWrittenLayers(Image& image) { ++images_uploaded; uploaded_bytes += tail * layers.size(); ++layer_uploads; - return true; - } else { - return false; } } @@ -2524,7 +2594,8 @@ 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)) { + if (True(image.flags & ImageFlagBits::Unbacked) && + False(image.flags & ImageFlagBits::SparseLayers)) { // 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), @@ -2924,6 +2995,13 @@ void TextureCache

::PrepareImage(ImageId image_id, bool is_modification, bool } } if (is_modification) { + if constexpr (requires { &Image::BindLayers; }) { + if (True(image.flags & ImageFlagBits::SparseLayers)) { + std::vector layers(static_cast(image.info.resources.layers)); + std::iota(layers.begin(), layers.end(), 0U); + image.BindLayers(layers); + } + } if (True(image.flags & ImageFlagBits::UnwrittenSparse) && False(image.flags & ImageFlagBits::Remapped)) { // The GPU renders into it: these writes are lost, later uses get a backed image. diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index f4cc89f2ba..11e95f98b3 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -167,6 +167,12 @@ class TextureCache : public VideoCommon::ChannelSetupCaches layers); + struct PendingUnswizzle { ImageId image_id; VideoCommon::ImageInfo info; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index b1fa8db183..39ae25f43a 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -321,6 +321,11 @@ public: return features.features.sparseBinding && graphics_family_sparse_binding; } + /// 2D images whose memory can be bound in parts, on the graphics queue. + bool IsSparseResidencyImage2DSupported() const { + return IsSparseBindingSupported() && features.features.sparseResidencyImage2D; + } + /// Returns true for tile-based deferred renderers. bool IsTiler() const { switch (GetDriverID()) { diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 92f537aa3c..a0c96d1b45 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -295,6 +295,35 @@ vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) cons device.GetDispatchLoader()); } +VmaAllocation MemoryAllocator::AllocateSparseMemory(const VkMemoryRequirements &requirements, + VkDeviceMemory &memory, + VkDeviceSize &offset) const { + const VmaAllocationCreateInfo alloc_ci{ + .flags = 0, + .usage = VMA_MEMORY_USAGE_UNKNOWN, + .requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + .preferredFlags = 0, + .memoryTypeBits = requirements.memoryTypeBits, + .pool = VK_NULL_HANDLE, + .pUserData = nullptr, + .priority = 0.f, + }; + VmaAllocation allocation{}; + VmaAllocationInfo info{}; + if (vmaAllocateMemory(allocator, &requirements, &alloc_ci, &allocation, &info) != VK_SUCCESS) { + return VK_NULL_HANDLE; + } + memory = info.deviceMemory; + offset = info.offset; + return allocation; +} + +void MemoryAllocator::FreeSparseMemory(VmaAllocation allocation) const { + if (allocation != VK_NULL_HANDLE) { + vmaFreeMemory(allocator, allocation); + } +} + vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const { // MESA will do memcpy() if not marked as host cached, so just force mark it for most buffers auto const anv_flags = (usage == MemoryUsage::Stream diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index 99649b9aef..f3c6c177a2 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -111,6 +111,12 @@ namespace Vulkan { /// a null image when the device can't make one. vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const; + /// Device memory to bind to part of a sparse resource. Null when there is none left. + VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements, + VkDeviceMemory &memory, VkDeviceSize &offset) const; + + void FreeSparseMemory(VmaAllocation allocation) const; + vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const; vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage, diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index bb43fe399f..dbaaf450a0 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -219,6 +219,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkGetEventStatus); X(vkGetFenceStatus); X(vkGetImageMemoryRequirements); + X(vkGetImageSparseMemoryRequirements); X(vkGetPipelineCacheData); X(vkGetMemoryFdKHR); #ifdef _WIN32 @@ -866,6 +867,16 @@ VkMemoryRequirements Device::GetImageMemoryRequirements(VkImage image) const noe return requirements; } +std::vector Device::GetImageSparseMemoryRequirements( + VkImage image) const { + u32 count = 0; + dld->vkGetImageSparseMemoryRequirements(handle, image, &count, nullptr); + std::vector requirements(count); + dld->vkGetImageSparseMemoryRequirements(handle, image, &count, requirements.data()); + requirements.resize(count); + return requirements; +} + std::vector Device::GetPipelineExecutablePropertiesKHR( VkPipeline pipeline) const { const VkPipelineInfoKHR info{ diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 947150648d..3010368973 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -335,6 +335,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkGetEventStatus vkGetEventStatus{}; PFN_vkGetFenceStatus vkGetFenceStatus{}; PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements{}; + PFN_vkGetImageSparseMemoryRequirements vkGetImageSparseMemoryRequirements{}; PFN_vkGetPipelineCacheData vkGetPipelineCacheData{}; PFN_vkGetMemoryFdKHR vkGetMemoryFdKHR{}; #ifdef _WIN32 @@ -1111,6 +1112,9 @@ public: VkMemoryRequirements GetImageMemoryRequirements(VkImage image) const noexcept; + std::vector GetImageSparseMemoryRequirements( + VkImage image) const; + std::vector GetPipelineExecutablePropertiesKHR( VkPipeline pipeline) const; -- cgit v1.2.3 From 6f5e137ec4eb83bbff9ea15f1ad1a8407cfc0cba Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:14:52 +0100 Subject: texture_cache: log unbacked layers over the image's memory cap --- src/video_core/nx2.h | 2 +- .../renderer_vulkan/vk_texture_cache.cpp | 22 ++++++++++++++++++++++ src/video_core/texture_cache/texture_cache.h | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 1a3bae93ec..4b0d3d5f62 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -96,7 +96,7 @@ inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) { // Which blocks of each sparse region have been written (got memory), for the texture cache: an // image over blocks nobody wrote is all zeros and needs no device memory. Its own lock, taken last: -// the texture cache asks with its lock held, and nx2:gpu updates it holding none of the GPU's. +// the texture cache asks with its lock held, and nx2:gpu updates it holding only its region mutex. struct Nx2SparseRegionState { u64 size; u64 block_size; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index b77f9af5f8..9204ade9eb 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -2620,6 +2620,20 @@ std::vector Image::BindLayers(std::span layers) { const VkDeviceSize cap = SparseLayersCap(device); std::vector image_binds; std::vector opaque_binds; + const bool tail_was_bound = s.tail_memory != VK_NULL_HANDLE; + std::vector fresh; // Layers given memory by this call + // A failed bind leaves the layers as they were, so the next call tries them again. + const auto roll_back = [&] { + for (const u32 layer : fresh) { + s.allocator->FreeSparseMemory(s.layer_memory[layer]); + s.layer_memory[layer] = VK_NULL_HANDLE; + s.bound_bytes -= s.layer_bytes; + } + if (s.tail_memory != VK_NULL_HANDLE && !tail_was_bound) { + s.allocator->FreeSparseMemory(s.tail_memory); + s.tail_memory = VK_NULL_HANDLE; + } + }; for (const u32 layer : layers) { if (s.layer_memory[layer] != VK_NULL_HANDLE) { bound.push_back(layer); @@ -2647,6 +2661,7 @@ std::vector Image::BindLayers(std::span layers) { } s.layer_memory[layer] = allocation; s.bound_bytes += s.layer_bytes; + fresh.push_back(layer); for (u32 level = 0; level < tail_lod; ++level) { const u32 width = (std::max)(s.width >> level, 1U); const u32 height = (std::max)(s.height >> level, 1U); @@ -2693,6 +2708,12 @@ std::vector Image::BindLayers(std::span layers) { .memoryOffset = offset, .flags = 0, }); + } else { + LOG_WARNING(Render_Vulkan, "Sparse layered image {:#x}: out of device memory for the " + "mip tail", + gpu_addr); + roll_back(); + return {}; } } if (image_binds.empty() && opaque_binds.empty()) { @@ -2737,6 +2758,7 @@ std::vector Image::BindLayers(std::span layers) { if (result != VK_SUCCESS) { LOG_ERROR(Render_Vulkan, "Sparse layered image {:#x}: binding failed ({})", gpu_addr, static_cast(result)); + roll_back(); return {}; } fence.Wait(); diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 3de82d2eb4..63abffed0f 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -2999,6 +2999,7 @@ void TextureCache

::PrepareImage(ImageId image_id, bool is_modification, bool if (True(image.flags & ImageFlagBits::SparseLayers)) { std::vector layers(static_cast(image.info.resources.layers)); std::iota(layers.begin(), layers.end(), 0U); + // Layers over the image's memory cap stay unbacked (logged once). image.BindLayers(layers); } } -- cgit v1.2.3 From ea33ad1865970ee731e5f74dbcc26ca472798afe Mon Sep 17 00:00:00 2001 From: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:45:53 -0300 Subject: vulkan: write the driver pipeline cache atomically; reject truncated ones The emulator stopped while saving vulkan_pipelines.bin left it empty, and every later boot terminated: the header read throws ios_base::failure, and macOS's libc++ throws one the handler's type doesn't match. Write the cache to a temporary file and rename it over the old one, and treat a file shorter than its header as invalid without reading it. Assisted-by: Claude (Anthropic) --- .../renderer_vulkan/vk_pipeline_cache.cpp | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index ac878ddfef..2c64f3375b 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1138,7 +1138,11 @@ std::unique_ptr PipelineCache::CreateComputePipeline( void PipelineCache::SerializeVulkanPipelineCache(const std::filesystem::path& filename, const vk::PipelineCache& pipeline_cache, u32 cache_version) try { - std::ofstream file(filename, std::ios::binary); + // Written beside the cache and renamed over it: the emulator stopped mid-write left the cache + // empty, and every later boot failed to read it. + auto temporary = filename; + temporary += ".tmp"; + std::ofstream file(temporary, std::ios::binary); file.exceptions(std::ifstream::failbit); if (!file.is_open()) { LOG_ERROR(Common_Filesystem, "Failed to open Vulkan driver pipeline cache file {}", @@ -1156,6 +1160,15 @@ void PipelineCache::SerializeVulkanPipelineCache(const std::filesystem::path& fi pipeline_cache.Read(&cache_size, cache_data.data()); } file.write(cache_data.data(), cache_size); + file.close(); + std::error_code error; + std::filesystem::rename(temporary, filename, error); + if (error) { + LOG_ERROR(Common_Filesystem, "Failed to replace Vulkan driver pipeline cache file {}: {}", + Common::FS::PathToUTF8String(filename), error.message()); + std::filesystem::remove(temporary, error); + return; + } LOG_INFO(Render_Vulkan, "Vulkan driver pipelines cached at: {}", Common::FS::PathToUTF8String(filename)); @@ -1190,6 +1203,14 @@ vk::PipelineCache PipelineCache::LoadVulkanPipelineCache(const std::filesystem:: std::array magic_number; u32 cache_version; + // A file cut short (the emulator stopped while writing it) would throw from the reads + // below, and macOS's libc++ throws an ios_base::failure the handler doesn't match. + if (static_cast(end) < magic_number.size() + sizeof(cache_version)) { + file.close(); + LOG_ERROR(Common_Filesystem, "Truncated Vulkan driver pipeline cache file, deleting it"); + Common::FS::RemoveFile(filename); + return create_pipeline_cache(0, nullptr); + } file.read(magic_number.data(), magic_number.size()) .read(reinterpret_cast(&cache_version), sizeof(cache_version)); if (magic_number != VULKAN_CACHE_MAGIC_NUMBER || cache_version != expected_cache_version) { -- cgit v1.2.3